thread-master/apps/backend/internal/module/billing/stripe_provider.go

200 lines
6.4 KiB
Go
Raw Normal View History

2026-07-15 15:23:59 +00:00
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)