Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

org billing email, restrict plan downgrades, card expiry check #5312

Merged
merged 18 commits into from
Aug 12, 2024

Conversation

pjain1
Copy link
Member

@pjain1 pjain1 commented Jul 18, 2024

No description provided.

}

func comparableInt(v *int) int {
if v == nil || *v < 0 {
Copy link
Member Author

@pjain1 pjain1 Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a quota is not set (nil) the we should use the default value instead of unlimited ? can just change the plan creation logic to use default value instead of nil in case a quota is not set

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but I think I'm fine with having unlimited when a quota is not explicitly set – it reduces user pain if we mess up, and should help in dev and testing. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense but current logic uses default value during org creation if any quota is not set and during plan change it uses the persisted quota in database if not present in plan. Should I change this logic then ?

Comment on lines 59 to 67
// very basic check if the customer has a payment method and if it's a card then is not expired
for i.Next() {
hasPaymentMethod = true
pm := i.PaymentMethod()
if pm.Type == stripe.PaymentMethodTypeCard {
isCardValid = new(bool)
*isCardValid = int(pm.Card.ExpYear) > time.Now().Year() || (int(pm.Card.ExpYear) == time.Now().Year() && int(pm.Card.ExpMonth) >= int(time.Now().Month()))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Stripe have an API for this? There are other factors than just expiry, and Stripe might know about them (like probably they track cards they have detected to be blocked)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find any such API but added specifically this check because Eric O mentioned that we want to show a banner if the card expires.

Comment on lines 97 to 106
var isCardValid *bool
// very basic check if the customer has a payment method and if it's a card then is not expired
for it.Next() {
hasPaymentMethod = true
pm := it.PaymentMethod()
isCardValid = new(bool)
if pm.Type == stripe.PaymentMethodTypeCard {
*isCardValid = int(pm.Card.ExpYear) > time.Now().Year() || (int(pm.Card.ExpYear) == time.Now().Year() && int(pm.Card.ExpMonth) >= int(time.Now().Month()))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is duplicated twice and non-trivial, can we pull it into a util func?

Comment on lines 333 to 336
// don't allow plan downgrades, only superuser can downgrade
if planDowngrade(plan, org) && !claims.Superuser(ctx) {
return nil, status.Errorf(codes.FailedPrecondition, "plan downgrade not allowed")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not allow downgrades? What if someone doesn't use a bigger plan and wants to switch to a smaller one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishantmonu51 suggested we should not allow downgrades, for this they should contact us. Downgrades can be tricky if they are already over some quotas.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be better to let people downgrade, and then in the beginning just have a job that checks/alerts if they don't get within the lower quotas within a day or so. Then we can just contact or hibernate non-compliant projects without causing trouble for people who want to downgrade for legitimate reasons.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishantmonu51 any thoughts here ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the restriction, just logging in case of downgrade

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we intentionally wanted to keep the scope limited here and not allow downgrade, as downgrade would require checking if the quotas for the lower plan are met and only allow to downgrade. The expected flow here for a user would be to create a support ticket, FE team will manually verify the quotas and then downgrade directly in orb.

Other reasons for not allowing downgrade is for enterprise users who have signed a multi-year contract and we don't want them to downgrade unless explicitly approved. Since its not going to be a common case and in many such cases we want to have manual intervention. We do not allow downgrade at present.

Comment on lines 1847 to 1851
enum PaymentCardStatus {
PAYMENT_CARD_STATUS_UNSPECIFIED = 0;
PAYMENT_CARD_STATUS_OK = 1;
PAYMENT_CARD_STATUS_EXPIRED = 2;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the goal of exposing this? Just wondering if we could just track whether they have a valid payment method or not, and direct them to the Stripe portal if they want more details?

As stated elsewhere, I'm also guessing there are many other conditions that could cause a card to be invalid than just expiry.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the api we can just check if there exists a payment method or not for the customer, does explicitly say if its a valid or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is Eric O mentioned that we want to show a banner if the card expires.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, maybe not completely related, but do you think it would make sense to have generic billing_warning and billing_error fields on an org, and then show those in banners instead? Then we could populate billing_warning if it looks like the card will expire within the next 3 months or so, and set billing_error if we get a webhook from Stripe that tells us that a payment actually failed or a card has become invalid for some reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure that makes sense, however we will need some background jobs to clear out these warnings/errors once they are resolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or we just rely on webhooks to clear these warnings like payment card updated for expired card warning or payment succeeded for payment failed error. Looks fragile though because webhook delivery may fail or our service may be unavailable at that point etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I see webhooks are retried upto 3 days by stripe but we may need async queue to process the webhooks internally and prevent processing duplicate webhooks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should webhook support be added in separate PR ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can add it in a separate PR, but I think we should do it sooner rather than later.

Webhooks should be very stable, as you say they are usually retried until ack'ed, but if you worry about inconsistencies you can also have a background job once per day or so that checks and syncs statuses in case a webhook was dropped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed checking of card expiry, just keeping check of presence of a payment method so that we can disallow plan change if no payment is present. This is all internal and not exposed in any API so that once we have billing warnings and errors, will just make sure no billing errors present instead of this payment method check.

admin/database/database.go Show resolved Hide resolved
@pjain1 pjain1 merged commit 2d4833f into main Aug 12, 2024
7 checks passed
@pjain1 pjain1 deleted the self_serve_followups branch August 12, 2024 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants