package billing import ( "context" "encoding/json" "errors" "strconv" "time" "github.com/stripe/stripe-go/v82" "github.com/stripe/stripe-go/v82/client" "github.com/stripe/stripe-go/v82/webhook" ) type StripeProvider struct{ api *client.API } func NewStripeProvider(secretKey string) *StripeProvider { return &StripeProvider{api: client.New(secretKey, nil)} } func (p *StripeProvider) CreateCheckout(ctx context.Context, input CheckoutParams) (*ProviderCheckout, error) { uidText := stripe.String(formatUID(input.UID)) params := &stripe.CheckoutSessionParams{ Params: stripe.Params{Context: ctx}, Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)), SuccessURL: stripe.String(input.SuccessURL), CancelURL: stripe.String(input.CancelURL), ClientReferenceID: uidText, Metadata: map[string]string{"uid": formatUID(input.UID), "attempt_id": input.AttemptID, "plan_id": input.PlanID}, SubscriptionData: &stripe.CheckoutSessionSubscriptionDataParams{Metadata: map[string]string{"uid": formatUID(input.UID), "attempt_id": input.AttemptID}}, LineItems: []*stripe.CheckoutSessionLineItemParams{{Price: stripe.String(input.PriceID), Quantity: stripe.Int64(1)}}, } if input.CustomerID != "" { params.Customer = stripe.String(input.CustomerID) } params.SetIdempotencyKey(input.IdempotencyKey) session, err := p.api.CheckoutSessions.New(params) if err != nil { return nil, err } return checkoutFromStripe(session), nil } func (p *StripeProvider) GetCheckout(ctx context.Context, id string) (*ProviderCheckout, error) { params := &stripe.CheckoutSessionParams{Params: stripe.Params{Context: ctx}} params.AddExpand("line_items.data.price") session, err := p.api.CheckoutSessions.Get(id, params) if err != nil { return nil, err } return checkoutFromStripe(session), nil } func checkoutFromStripe(s *stripe.CheckoutSession) *ProviderCheckout { co := &ProviderCheckout{SessionID: s.ID, URL: s.URL, ExpiresAt: s.ExpiresAt * int64(time.Second), Status: string(s.Status), PaymentStatus: string(s.PaymentStatus)} if s.Customer != nil { co.CustomerID = s.Customer.ID } if s.Subscription != nil { co.SubscriptionID = s.Subscription.ID } if s.LineItems != nil { for _, item := range s.LineItems.Data { if item != nil && item.Price != nil { co.PriceID = item.Price.ID break } } } return co } func (p *StripeProvider) GetSubscription(ctx context.Context, id string) (*ProviderSubscription, error) { params := &stripe.SubscriptionParams{Params: stripe.Params{Context: ctx}} params.AddExpand("items.data.price") s, err := p.api.Subscriptions.Get(id, params) if err != nil { return nil, err } out := &ProviderSubscription{ID: s.ID, Status: string(s.Status), CancelAtPeriodEnd: s.CancelAtPeriodEnd} if s.Customer != nil { out.CustomerID = s.Customer.ID } if s.Items != nil { for _, item := range s.Items.Data { if item == nil { continue } if item.Price != nil && out.PriceID == "" { out.PriceID = item.Price.ID } if item.CurrentPeriodStart > out.CurrentPeriodStart { out.CurrentPeriodStart = item.CurrentPeriodStart } if item.CurrentPeriodEnd > out.CurrentPeriodEnd { out.CurrentPeriodEnd = item.CurrentPeriodEnd } } } return out, nil } func (p *StripeProvider) CreatePortal(ctx context.Context, customerID, returnURL string) (string, error) { s, err := p.api.BillingPortalSessions.New(&stripe.BillingPortalSessionParams{Params: stripe.Params{Context: ctx}, Customer: stripe.String(customerID), ReturnURL: stripe.String(returnURL)}) if err != nil { return "", err } return s.URL, nil } func (p *StripeProvider) VerifyEvent(payload []byte, signature, secret string) (*Event, error) { e, err := webhook.ConstructEventWithOptions(payload, signature, secret, webhook.ConstructEventOptions{Tolerance: 5 * time.Minute}) if err != nil { return nil, errors.Join(ErrInvalidSignature, err) } out := &Event{ID: e.ID, Type: string(e.Type), Created: e.Created} var obj stripeObject if err := json.Unmarshal(e.Data.Raw, &obj); err != nil { return nil, err } out.ObjectID = obj.ID out.CustomerID = objectID(obj.Customer) out.Status = obj.Status out.PeriodStart, out.PeriodEnd = obj.PeriodStart, obj.PeriodEnd out.CancelAtPeriodEnd = obj.CancelAtPeriodEnd out.SubscriptionID = objectID(obj.Subscription) if out.SubscriptionID == "" { out.SubscriptionID = objectID(obj.Parent.SubscriptionDetails.Subscription) } if len(obj.Items.Data) > 0 { out.PriceID = objectID(obj.Items.Data[0].Price) out.PeriodStart, out.PeriodEnd = obj.Items.Data[0].CurrentPeriodStart, obj.Items.Data[0].CurrentPeriodEnd } if len(obj.Lines.Data) > 0 { line := obj.Lines.Data[0] out.PriceID = objectID(line.Price) if out.PriceID == "" { out.PriceID = objectID(line.Pricing.PriceDetails.Price) } if line.Period.Start != 0 { out.PeriodStart, out.PeriodEnd = line.Period.Start, line.Period.End } } return out, nil } type stripeObject struct { ID string `json:"id"` Customer json.RawMessage `json:"customer"` Subscription json.RawMessage `json:"subscription"` Status string `json:"status"` PeriodStart int64 `json:"period_start"` PeriodEnd int64 `json:"period_end"` CancelAtPeriodEnd bool `json:"cancel_at_period_end"` Parent struct { SubscriptionDetails struct { Subscription json.RawMessage `json:"subscription"` } `json:"subscription_details"` } `json:"parent"` Items struct { Data []struct { Price json.RawMessage `json:"price"` CurrentPeriodStart int64 `json:"current_period_start"` CurrentPeriodEnd int64 `json:"current_period_end"` } `json:"data"` } `json:"items"` Lines struct { Data []struct { Price json.RawMessage `json:"price"` Period struct { Start int64 `json:"start"` End int64 `json:"end"` } `json:"period"` Pricing struct { PriceDetails struct { Price json.RawMessage `json:"price"` } `json:"price_details"` } `json:"pricing"` } `json:"data"` } `json:"lines"` } func objectID(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var id string if json.Unmarshal(raw, &id) == nil { return id } var obj struct { ID string `json:"id"` } _ = json.Unmarshal(raw, &obj) return obj.ID } func formatUID(uid int64) string { return strconv.FormatInt(uid, 10) } var _ Provider = (*StripeProvider)(nil)