123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714 |
- package controller
- import (
- "database/sql"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "strconv"
- "time"
- "github.com/ente-io/museum/pkg/controller/commonbilling"
- "github.com/ente-io/museum/pkg/controller/discord"
- "github.com/ente-io/museum/pkg/controller/offer"
- "github.com/ente-io/museum/pkg/repo/storagebonus"
- "github.com/ente-io/museum/ente"
- emailCtrl "github.com/ente-io/museum/pkg/controller/email"
- "github.com/ente-io/museum/pkg/repo"
- "github.com/ente-io/museum/pkg/utils/billing"
- "github.com/ente-io/museum/pkg/utils/email"
- "github.com/ente-io/stacktrace"
- log "github.com/sirupsen/logrus"
- "github.com/spf13/viper"
- "github.com/stripe/stripe-go/v72"
- "github.com/stripe/stripe-go/v72/client"
- "github.com/stripe/stripe-go/v72/webhook"
- "golang.org/x/text/currency"
- )
- // StripeController provides abstractions for handling billing on Stripe
- type StripeController struct {
- StripeClients ente.StripeClientPerAccount
- BillingPlansPerAccount ente.BillingPlansPerAccount
- BillingRepo *repo.BillingRepository
- FileRepo *repo.FileRepository
- UserRepo *repo.UserRepository
- StorageBonusRepo *storagebonus.Repository
- DiscordController *discord.DiscordController
- EmailNotificationCtrl *emailCtrl.EmailNotificationController
- OfferController *offer.OfferController
- CommonBillCtrl *commonbilling.Controller
- }
- const BufferPeriodOnPaymentFailureInDays = 7
- // Return a new instance of StripeController
- func NewStripeController(plans ente.BillingPlansPerAccount, stripeClients ente.StripeClientPerAccount, billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, storageBonusRepo *storagebonus.Repository, discordController *discord.DiscordController, emailNotificationController *emailCtrl.EmailNotificationController, offerController *offer.OfferController, commonBillCtrl *commonbilling.Controller) *StripeController {
- return &StripeController{
- StripeClients: stripeClients,
- BillingRepo: billingRepo,
- FileRepo: fileRepo,
- UserRepo: userRepo,
- BillingPlansPerAccount: plans,
- StorageBonusRepo: storageBonusRepo,
- DiscordController: discordController,
- EmailNotificationCtrl: emailNotificationController,
- OfferController: offerController,
- CommonBillCtrl: commonBillCtrl,
- }
- }
- // GetCheckoutSession handles the creation of stripe checkout session for subscription purchase
- func (c *StripeController) GetCheckoutSession(productID string, userID int64, redirectRootURL string) (string, error) {
- if productID == "" {
- return "", stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- // error sql.ErrNoRows not possible as user must at least have a free subscription
- return "", stacktrace.Propagate(err, "")
- }
- hasActivePaidSubscription := billing.IsActivePaidPlan(subscription)
- hasStripeSubscription := subscription.PaymentProvider == ente.Stripe
- if hasActivePaidSubscription {
- if hasStripeSubscription {
- return "", stacktrace.Propagate(ente.ErrBadRequest, "")
- } else if !subscription.Attributes.IsCancelled {
- return "", stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- }
- if hasStripeSubscription {
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, nil)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- if stripeSubscription.Status != stripe.SubscriptionStatusCanceled {
- return "", stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- }
- stripeSuccessURL := redirectRootURL + viper.GetString("stripe.path.success")
- stripeCancelURL := redirectRootURL + viper.GetString("stripe.path.cancel")
- allowPromotionCodes := true
- params := &stripe.CheckoutSessionParams{
- ClientReferenceID: stripe.String(strconv.FormatInt(userID, 10)),
- SuccessURL: stripe.String(stripeSuccessURL),
- CancelURL: stripe.String(stripeCancelURL),
- Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
- LineItems: []*stripe.CheckoutSessionLineItemParams{
- {
- Price: stripe.String(productID),
- Quantity: stripe.Int64(1),
- },
- },
- AllowPromotionCodes: &allowPromotionCodes,
- }
- var stripeClient *client.API
- if subscription.PaymentProvider == ente.Stripe {
- stripeClient = c.StripeClients[subscription.Attributes.StripeAccountCountry]
- // attach the subscription to existing customerID
- params.Customer = stripe.String(subscription.Attributes.CustomerID)
- } else {
- stripeClient = c.StripeClients[ente.DefaultStripeAccountCountry]
- user, err := c.UserRepo.Get(userID)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- // attach user's emailID to the checkout session and subsequent subscription bought
- params.CustomerEmail = stripe.String(user.Email)
- }
- s, err := stripeClient.CheckoutSessions.New(params)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- return s.ID, nil
- }
- // GetVerifiedSubscription verifies and returns the verified subscription
- func (c *StripeController) GetVerifiedSubscription(userID int64, sessionID string) (ente.Subscription, error) {
- var stripeSubscription stripe.Subscription
- var err error
- if sessionID != "" {
- log.Info("Received session ID: " + sessionID)
- // Get verified subscription request was received from success redirect page
- stripeSubscription, err = c.getStripeSubscriptionFromSession(userID, sessionID)
- } else {
- log.Info("Did not receive a session ID")
- // Get verified subscription request for a subscription update
- stripeSubscription, err = c.getUserStripeSubscription(userID)
- }
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- log.Info("Received stripe subscription with ID: " + stripeSubscription.ID)
- subscription, err := c.getEnteSubscriptionFromStripeSubscription(userID, stripeSubscription)
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- log.Info("Returning ente subscription with ID: " + strconv.FormatInt(subscription.ID, 10))
- return subscription, nil
- }
- func (c *StripeController) HandleUSNotification(payload []byte, header string) error {
- event, err := webhook.ConstructEvent(payload, header, viper.GetString("stripe.us.webhook-secret"))
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- return c.handleWebhookEvent(event, ente.StripeUS)
- }
- func (c *StripeController) HandleINNotification(payload []byte, header string) error {
- event, err := webhook.ConstructEvent(payload, header, viper.GetString("stripe.in.webhook-secret"))
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- return c.handleWebhookEvent(event, ente.StripeIN)
- }
- func (c *StripeController) handleWebhookEvent(event stripe.Event, country ente.StripeAccountCountry) error {
- // The event body would already have been logged by the upper layers by the
- // time we get here, so we can only handle the events that we care about. In
- // case we receive an unexpected event, we do log an error though.
- handler := c.findHandlerForEvent(event)
- if handler == nil {
- log.Error("Received an unexpected webhook from stripe:", event.Type)
- return nil
- }
- eventLog, err := handler(event, country)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- if eventLog.UserID == 0 {
- // Do not try to log if we do not have an associated user. This can
- // happen, e.g. with out of order webhooks.
- // Or in case of offer application, where events are logged by the Storage Bonus Repo
- //
- // See: Ignore webhooks received before user has been created
- return nil
- }
- err = c.BillingRepo.LogStripePush(eventLog)
- return stacktrace.Propagate(err, "")
- }
- func (c *StripeController) findHandlerForEvent(event stripe.Event) func(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) {
- switch event.Type {
- case "checkout.session.completed":
- return c.handleCheckoutSessionCompleted
- case "customer.subscription.updated":
- return c.handleCustomerSubscriptionUpdated
- case "invoice.paid":
- return c.handleInvoicePaid
- case "payment_intent.payment_failed":
- return c.handlePaymentIntentFailed
- default:
- return nil
- }
- }
- // Payment is successful and the subscription is created.
- // You should provision the subscription.
- func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) {
- var session stripe.CheckoutSession
- json.Unmarshal(event.Data.Raw, &session)
- if session.ClientReferenceID != "" { // via payments.ente.io, where we inserted the userID
- userID, _ := strconv.ParseInt(session.ClientReferenceID, 10, 64)
- newSubscription, err := c.GetVerifiedSubscription(userID, session.ID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- stripeSubscription, err := c.getStripeSubscriptionFromSession(userID, session.ID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- currentSubscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- if currentSubscription.ExpiryTime >= newSubscription.ExpiryTime &&
- currentSubscription.ProductID != ente.FreePlanProductID {
- log.Warn("Webhook is reporting an outdated purchase that was already verified stripeSubscription:", stripeSubscription.ID)
- return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
- }
- err = c.BillingRepo.ReplaceSubscription(
- currentSubscription.ID,
- newSubscription,
- )
- isUpgradingFromFreePlan := currentSubscription.ProductID == ente.FreePlanProductID
- if isUpgradingFromFreePlan {
- go func() {
- cur := currency.MustParseISO(string(session.Currency))
- amount := fmt.Sprintf("%v%v", currency.Symbol(cur), float64(session.AmountTotal)/float64(100))
- c.DiscordController.NotifyNewSub(userID, "stripe", amount)
- }()
- go func() {
- c.EmailNotificationCtrl.OnAccountUpgrade(userID)
- }()
- }
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
- } else {
- priceID, err := c.getPriceIDFromSession(session.ID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- email := session.CustomerDetails.Email
- err = c.OfferController.ApplyOffer(email, priceID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- }
- return ente.StripeEventLog{}, nil
- }
- // Stripe fires this when a subscription starts or changes. For example,
- // renewing a subscription, adding a coupon, applying a discount, adding an
- // invoice item, and changing plans all trigger this event. In our case, we use
- // this only to track plan changes or subscriptions going past due. The rest
- // (subscription creations, deletions, renewals and failures) are tracked by
- // individual events.
- func (c *StripeController) handleCustomerSubscriptionUpdated(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) {
- var stripeSubscription stripe.Subscription
- json.Unmarshal(event.Data.Raw, &stripeSubscription)
- currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscription.ID, ente.Stripe)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- // See: Ignore webhooks received before user has been created
- log.Warn("Webhook is reporting an event for un-verified subscription stripeSubscriptionID:", stripeSubscription.ID)
- return ente.StripeEventLog{}, nil
- }
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- userID := currentSubscription.UserID
- if stripeSubscription.Status == stripe.SubscriptionStatusPastDue {
- user, err := c.UserRepo.Get(userID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io",
- ente.AccountOnHoldEmailSubject, ente.OnHoldTemplate, map[string]interface{}{
- "PaymentProvider": "Stripe",
- }, nil)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- }
- newSubscription, err := c.getEnteSubscriptionFromStripeSubscription(userID, stripeSubscription)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- // If the customer has changed the plan, we update state in the database. If
- // the plan has not changed, we will ignore this webhook and rely on other
- // events to update the state
- if currentSubscription.ProductID != newSubscription.ProductID {
- c.BillingRepo.ReplaceSubscription(currentSubscription.ID, newSubscription)
- }
- return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
- }
- // Continue to provision the subscription as payments continue to be made.
- func (c *StripeController) handleInvoicePaid(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) {
- var invoice stripe.Invoice
- json.Unmarshal(event.Data.Raw, &invoice)
- stripeSubscriptionID := invoice.Subscription.ID
- currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscriptionID, ente.Stripe)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- // See: Ignore webhooks received before user has been created
- log.Warn("Webhook is reporting an event for un-verified subscription stripeSubscriptionID:", stripeSubscriptionID)
- return ente.StripeEventLog{}, nil
- }
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- userID := currentSubscription.UserID
- client := c.StripeClients[currentSubscription.Attributes.StripeAccountCountry]
- stripeSubscription, err := client.Subscriptions.Get(stripeSubscriptionID, nil)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- newExpiryTime := stripeSubscription.CurrentPeriodEnd * 1000 * 1000
- if currentSubscription.ExpiryTime == newExpiryTime {
- //outdated invoice
- log.Warn("Webhook is reporting an outdated purchase that was already verified stripeSubscriptionID:", stripeSubscription.ID)
- return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil
- }
- err = c.BillingRepo.UpdateSubscriptionExpiryTime(
- currentSubscription.ID, newExpiryTime)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil
- }
- // Event used to ONLY handle failures to SEPA payments, since we set
- // SubscriptionPaymentBehaviorAllowIncomplete only for SEPA. Other payment modes
- // will fail and will be handled synchronously
- func (c *StripeController) handlePaymentIntentFailed(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) {
- var paymentIntent stripe.PaymentIntent
- json.Unmarshal(event.Data.Raw, &paymentIntent)
- isSEPA := paymentIntent.LastPaymentError.PaymentMethod.Type == stripe.PaymentMethodTypeSepaDebit
- if !isSEPA {
- // Ignore events for other payment methods, since they will be handled
- // synchronously
- log.Info("Ignoring payment intent failed event for non-SEPA payment method")
- return ente.StripeEventLog{}, nil
- }
- client := c.StripeClients[country]
- invoiceID := paymentIntent.Invoice.ID
- invoice, err := client.Invoices.Get(invoiceID, nil)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- stripeSubscriptionID := invoice.Subscription.ID
- currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscriptionID, ente.Stripe)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- // See: Ignore webhooks received before user has been created
- log.Warn("Webhook is reporting an event for un-verified subscription stripeSubscriptionID:", stripeSubscriptionID)
- }
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- userID := currentSubscription.UserID
- stripeSubscription, err := client.Subscriptions.Get(stripeSubscriptionID, nil)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- productID := stripeSubscription.Items.Data[0].Price.ID
- // If the current subscription is not the same as the one in the webhook,
- // then ignore
- fmt.Printf("productID: %s, currentSubscription.ProductID: %s\n", productID, currentSubscription.ProductID)
- if currentSubscription.ProductID != productID {
- // no-op
- log.Warn("Webhook is reporting un-verified subscription update", stripeSubscription.ID, "invoiceID:", invoiceID)
- return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil
- }
- // If the current subscription is the same as the one in the webhook, then
- // we need to expire the subscription, and send an email to the user.
- newExpiryTime := time.Now().UnixMicro()
- err = c.BillingRepo.UpdateSubscriptionExpiryTime(
- currentSubscription.ID, newExpiryTime)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- // Send an email to the user
- user, err := c.UserRepo.Get(userID)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- // TODO: Inform customer that payment_failed.html with invoice.HostedInvoiceURL
- err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io",
- ente.AccountOnHoldEmailSubject, ente.OnHoldTemplate, map[string]interface{}{
- "PaymentProvider": "Stripe",
- "InvoiceURL": invoice.HostedInvoiceURL,
- }, nil)
- if err != nil {
- return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
- }
- return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil
- }
- func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (ente.SubscriptionUpdateResponse, error) {
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "")
- }
- newPlan, newStripeAccountCountry, err := c.getPlanAndAccount(stripeID)
- if err != nil {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "")
- }
- if subscription.PaymentProvider != ente.Stripe || subscription.ProductID == stripeID || subscription.Attributes.StripeAccountCountry != newStripeAccountCountry {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- if newPlan.Storage < subscription.Storage { // Downgrade
- canDowngrade, canDowngradeErr := c.CommonBillCtrl.CanDowngradeToGivenStorage(newPlan.Storage, userID)
- if canDowngradeErr != nil {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(canDowngradeErr, "")
- }
- if !canDowngrade {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrCannotDowngrade, "")
- }
- log.Info("Usage is good")
- }
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- params := stripe.SubscriptionParams{}
- params.AddExpand("default_payment_method")
- stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, ¶ms)
- if err != nil {
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "")
- }
- isSEPA := false
- if stripeSubscription.DefaultPaymentMethod != nil {
- isSEPA = stripeSubscription.DefaultPaymentMethod.Type == stripe.PaymentMethodTypeSepaDebit
- } else {
- log.Info("No default payment method found")
- }
- var paymentBehavior stripe.SubscriptionPaymentBehavior
- if isSEPA {
- paymentBehavior = stripe.SubscriptionPaymentBehaviorAllowIncomplete
- } else {
- paymentBehavior = stripe.SubscriptionPaymentBehaviorPendingIfIncomplete
- }
- params = stripe.SubscriptionParams{
- ProrationBehavior: stripe.String(string(stripe.SubscriptionProrationBehaviorAlwaysInvoice)),
- Items: []*stripe.SubscriptionItemsParams{
- {
- ID: stripe.String(stripeSubscription.Items.Data[0].ID),
- Price: stripe.String(stripeID),
- },
- },
- PaymentBehavior: stripe.String(string(paymentBehavior)),
- }
- params.AddExpand("latest_invoice.payment_intent")
- newStripeSubscription, err := client.Subscriptions.Update(subscription.OriginalTransactionID, ¶ms)
- if err != nil {
- stripeError := err.(*stripe.Error)
- switch stripeError.Type {
- case stripe.ErrorTypeCard:
- return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil
- default:
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "")
- }
- }
- if isSEPA {
- if newStripeSubscription.Status == stripe.SubscriptionStatusPastDue {
- if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusRequiresAction {
- return ente.SubscriptionUpdateResponse{Status: "requires_action", ClientSecret: newStripeSubscription.LatestInvoice.PaymentIntent.ClientSecret}, nil
- } else if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusRequiresPaymentMethod {
- return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil
- } else if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusProcessing {
- return ente.SubscriptionUpdateResponse{Status: "success"}, nil
- }
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- } else {
- if newStripeSubscription.PendingUpdate != nil {
- switch newStripeSubscription.LatestInvoice.PaymentIntent.Status {
- case stripe.PaymentIntentStatusRequiresAction:
- return ente.SubscriptionUpdateResponse{Status: "requires_action", ClientSecret: newStripeSubscription.LatestInvoice.PaymentIntent.ClientSecret}, nil
- case stripe.PaymentIntentStatusRequiresPaymentMethod:
- inv := newStripeSubscription.LatestInvoice
- client.Invoices.VoidInvoice(inv.ID, nil)
- return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil
- }
- return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- }
- return ente.SubscriptionUpdateResponse{Status: "success"}, nil
- }
- func (c *StripeController) UpdateSubscriptionCancellationStatus(userID int64, status bool) (ente.Subscription, error) {
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- // error sql.ErrNoRows not possible as user must at least have a free subscription
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- if subscription.PaymentProvider != ente.Stripe {
- return ente.Subscription{}, stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- if subscription.Attributes.IsCancelled == status {
- // no-op
- return subscription, nil
- }
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- params := &stripe.SubscriptionParams{
- CancelAtPeriodEnd: stripe.Bool(status),
- }
- _, err = client.Subscriptions.Update(subscription.OriginalTransactionID, params)
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, status)
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- subscription.Attributes.IsCancelled = status
- return subscription, nil
- }
- func (c *StripeController) GetStripeCustomerPortal(userID int64, redirectRootURL string) (string, error) {
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- if subscription.PaymentProvider != ente.Stripe {
- return "", stacktrace.Propagate(ente.ErrBadRequest, "")
- }
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- params := &stripe.BillingPortalSessionParams{
- Customer: stripe.String(subscription.Attributes.CustomerID),
- ReturnURL: stripe.String(redirectRootURL),
- }
- ps, err := client.BillingPortalSessions.New(params)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- return ps.URL, nil
- }
- func (c *StripeController) getStripeSubscriptionFromSession(userID int64, checkoutSessionID string) (stripe.Subscription, error) {
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- return stripe.Subscription{}, stacktrace.Propagate(err, "")
- }
- var stripeClient *client.API
- if subscription.PaymentProvider == ente.Stripe {
- stripeClient = c.StripeClients[subscription.Attributes.StripeAccountCountry]
- } else {
- stripeClient = c.StripeClients[ente.DefaultStripeAccountCountry]
- }
- params := &stripe.CheckoutSessionParams{}
- params.AddExpand("subscription")
- checkoutSession, err := stripeClient.CheckoutSessions.Get(checkoutSessionID, params)
- if err != nil {
- return stripe.Subscription{}, stacktrace.Propagate(err, "")
- }
- if (*checkoutSession.Subscription).Status != stripe.SubscriptionStatusActive {
- return stripe.Subscription{}, stacktrace.Propagate(&stripe.InvalidRequestError{}, "")
- }
- return *checkoutSession.Subscription, nil
- }
- func (c *StripeController) getPriceIDFromSession(sessionID string) (string, error) {
- stripeClient := c.StripeClients[ente.DefaultStripeAccountCountry]
- params := &stripe.CheckoutSessionListLineItemsParams{}
- params.AddExpand("data.price")
- items := stripeClient.CheckoutSessions.ListLineItems(sessionID, params)
- for items.Next() { // Return the first PriceID that has been fetched
- return items.LineItem().Price.ID, nil
- }
- return "", stacktrace.Propagate(ente.ErrNotFound, "")
- }
- func (c *StripeController) getUserStripeSubscription(userID int64) (stripe.Subscription, error) {
- subscription, err := c.BillingRepo.GetUserSubscription(userID)
- if err != nil {
- return stripe.Subscription{}, stacktrace.Propagate(err, "")
- }
- if subscription.PaymentProvider != ente.Stripe {
- return stripe.Subscription{}, stacktrace.Propagate(ente.ErrCannotSwitchPaymentProvider, "")
- }
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, nil)
- if err != nil {
- return stripe.Subscription{}, stacktrace.Propagate(err, "")
- }
- return *stripeSubscription, nil
- }
- func (c *StripeController) getPlanAndAccount(stripeID string) (ente.BillingPlan, ente.StripeAccountCountry, error) {
- for stripeAccountCountry, billingPlansCountryWise := range c.BillingPlansPerAccount {
- for _, plans := range billingPlansCountryWise {
- for _, plan := range plans {
- if plan.StripeID == stripeID {
- return plan, stripeAccountCountry, nil
- }
- }
- }
- }
- return ente.BillingPlan{}, "", stacktrace.Propagate(ente.ErrNotFound, "")
- }
- func (c *StripeController) getEnteSubscriptionFromStripeSubscription(userID int64, stripeSubscription stripe.Subscription) (ente.Subscription, error) {
- productID := stripeSubscription.Items.Data[0].Price.ID
- plan, stripeAccountCountry, err := c.getPlanAndAccount(productID)
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- s := ente.Subscription{
- UserID: userID,
- PaymentProvider: ente.Stripe,
- ProductID: productID,
- Storage: plan.Storage,
- Attributes: ente.SubscriptionAttributes{CustomerID: stripeSubscription.Customer.ID, IsCancelled: false, StripeAccountCountry: stripeAccountCountry},
- OriginalTransactionID: stripeSubscription.ID,
- ExpiryTime: stripeSubscription.CurrentPeriodEnd * 1000 * 1000,
- }
- return s, nil
- }
- func (c *StripeController) UpdateBillingEmail(subscription ente.Subscription, newEmail string) error {
- params := &stripe.CustomerParams{Email: &newEmail}
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- _, err := client.Customers.Update(
- subscription.Attributes.CustomerID,
- params,
- )
- if err != nil {
- return stacktrace.Propagate(err, "failed to update stripe customer emailID")
- }
- return nil
- }
- func (c *StripeController) CancelSubAndDeleteCustomer(subscription ente.Subscription, logger *log.Entry) error {
- client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
- if !subscription.Attributes.IsCancelled {
- prorateRefund := true
- logger.Info("cancelling sub with prorated refund")
- _, err := client.Subscriptions.Update(subscription.OriginalTransactionID, nil)
- if err != nil {
- stripeError := err.(*stripe.Error)
- errorMsg := fmt.Sprintf("subscription updation failed during account deletion: %s, %s", stripeError.Msg, stripeError.Code)
- log.Error(errorMsg)
- c.DiscordController.Notify(errorMsg)
- if stripeError.HTTPStatusCode == http.StatusNotFound {
- log.Error("Ignoring error since an active subscription could not be found")
- return nil
- } else if stripeError.HTTPStatusCode == http.StatusBadRequest {
- log.Error("Bad request while trying to delete account")
- return nil
- }
- return stacktrace.Propagate(err, "")
- }
- _, err = client.Subscriptions.Cancel(subscription.OriginalTransactionID, &stripe.SubscriptionCancelParams{
- Prorate: &prorateRefund,
- })
- if err != nil {
- stripeError := err.(*stripe.Error)
- logger.Error(fmt.Sprintf("subscription cancel failed msg= %s for userID=%d"+stripeError.Msg, subscription.UserID))
- // ignore if subscription doesn't exist, already deleted
- if stripeError.HTTPStatusCode != 404 {
- return stacktrace.Propagate(err, "")
- }
- }
- err = c.BillingRepo.UpdateSubscriptionCancellationStatus(subscription.UserID, true)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- }
- logger.Info("deleting customer from stripe")
- _, err := client.Customers.Del(
- subscription.Attributes.CustomerID,
- &stripe.CustomerParams{},
- )
- if err != nil {
- stripeError := err.(*stripe.Error)
- switch stripeError.Type {
- case stripe.ErrorTypeInvalidRequest:
- if stripe.ErrorCodeResourceMissing == stripeError.Code {
- return nil
- }
- return stacktrace.Propagate(err, fmt.Sprintf("failed to delete customer %s", subscription.Attributes.CustomerID))
- default:
- return stacktrace.Propagate(err, fmt.Sprintf("failed to delete customer %s", subscription.Attributes.CustomerID))
- }
- }
- return nil
- }
|