ente/server/pkg/controller/stripe.go
2024-03-27 16:20:58 +05:30

740 lines
30 KiB
Go

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 and renewal failures resulting in
// subscriptions going past due.
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
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)
}
fullStripeSub, err := c.getStripeSubscriptionWithPaymentMethod(currentSubscription)
if err != nil {
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
}
isSEPA := isSEPASubscription(fullStripeSub)
if stripeSubscription.Status == stripe.SubscriptionStatusPastDue && !isSEPA {
// Unfortunately, customer.subscription.updated is only fired for SEPA
// payments in case of updation failures (not for purchase or renewal
// failures). So for consistency (and to avoid duplicate mails), we
// trigger on-hold emails for SEPA within handlePaymentIntentFailed.
err = c.sendAccountOnHoldEmail(userID)
if err != nil {
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
}
}
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, "")
}
err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, true)
if err != nil {
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
}
err = c.sendAccountOnHoldEmail(userID)
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")
}
stripeSubscription, err := c.getStripeSubscriptionWithPaymentMethod(subscription)
if err != nil {
return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "")
}
isSEPA := isSEPASubscription(stripeSubscription)
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")
client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
newStripeSubscription, err := client.Subscriptions.Update(subscription.OriginalTransactionID, &params)
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) getStripeSubscriptionWithPaymentMethod(subscription ente.Subscription) (stripe.Subscription, error) {
client := c.StripeClients[subscription.Attributes.StripeAccountCountry]
params := &stripe.SubscriptionParams{}
params.AddExpand("default_payment_method")
stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, params)
if err != nil {
return stripe.Subscription{}, stacktrace.Propagate(err, "")
}
return *stripeSubscription, nil
}
func (c *StripeController) sendAccountOnHoldEmail(userID int64) error {
user, err := c.UserRepo.Get(userID)
if err != nil {
return stacktrace.Propagate(err, "")
}
err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io",
ente.AccountOnHoldEmailSubject, ente.OnHoldTemplate, map[string]interface{}{
"PaymentProvider": "Stripe",
}, nil)
return err
}
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
}
func isSEPASubscription(stripeSubscription stripe.Subscription) bool {
isSEPA := false
if stripeSubscription.DefaultPaymentMethod != nil {
isSEPA = stripeSubscription.DefaultPaymentMethod.Type == stripe.PaymentMethodTypeSepaDebit
} else {
log.Info("No default payment method found")
}
return isSEPA
}