117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
package billing
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/flowy-live/llink/internal/utils/flog"
|
|
|
|
"github.com/stripe/stripe-go/v85"
|
|
"github.com/stripe/stripe-go/v85/webhook"
|
|
)
|
|
|
|
func (s *serviceImpl) HandleWebhook(ctx context.Context, payload []byte, signature string) error {
|
|
event, err := webhook.ConstructEvent(payload, signature, s.cfg.WebhookSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("verify stripe signature: %w", err)
|
|
}
|
|
|
|
flog.Info("stripe webhook", "type", event.Type, "id", event.ID)
|
|
|
|
switch event.Type {
|
|
case "checkout.session.completed":
|
|
// subscription.created fires right after with full detail; we handle
|
|
// the subscription there.
|
|
return nil
|
|
case "customer.subscription.created", "customer.subscription.updated":
|
|
return s.handleSubscriptionUpsert(ctx, event)
|
|
case "customer.subscription.deleted":
|
|
return s.handleSubscriptionDeleted(ctx, event)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *serviceImpl) handleSubscriptionUpsert(ctx context.Context, event stripe.Event) error {
|
|
var sub stripe.Subscription
|
|
if err := json.Unmarshal(event.Data.Raw, &sub); err != nil {
|
|
return fmt.Errorf("decode subscription: %w", err)
|
|
}
|
|
|
|
networkID := sub.Metadata["network_id"]
|
|
if networkID == "" {
|
|
return fmt.Errorf("subscription %s missing network_id metadata", sub.ID)
|
|
}
|
|
|
|
local, err := subscriptionFromStripe(&sub, networkID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.repo.upsertSubscription(ctx, local); err != nil {
|
|
return fmt.Errorf("upsert subscription: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceImpl) handleSubscriptionDeleted(ctx context.Context, event stripe.Event) error {
|
|
var sub stripe.Subscription
|
|
if err := json.Unmarshal(event.Data.Raw, &sub); err != nil {
|
|
return fmt.Errorf("decode subscription: %w", err)
|
|
}
|
|
|
|
networkID := sub.Metadata["network_id"]
|
|
if networkID == "" {
|
|
return fmt.Errorf("subscription %s missing network_id metadata", sub.ID)
|
|
}
|
|
|
|
if err := s.repo.deleteSubscriptionByID(ctx, sub.ID); err != nil {
|
|
return fmt.Errorf("delete subscription: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func subscriptionFromStripe(sub *stripe.Subscription, networkID string) (*Subscription, error) {
|
|
if len(sub.Items.Data) == 0 {
|
|
return nil, fmt.Errorf("subscription %s has no items", sub.ID)
|
|
}
|
|
item := sub.Items.Data[0]
|
|
cadence, err := cadenceFromInterval(item.Price)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("subscription %s: %w", sub.ID, err)
|
|
}
|
|
|
|
var customerID string
|
|
if sub.Customer != nil {
|
|
customerID = sub.Customer.ID
|
|
}
|
|
|
|
return &Subscription{
|
|
ID: sub.ID,
|
|
NetworkID: networkID,
|
|
StripeCustomerID: customerID,
|
|
Status: string(sub.Status),
|
|
PriceID: item.Price.ID,
|
|
Cadence: cadence,
|
|
Quantity: int(item.Quantity),
|
|
CancelAtPeriodEnd: sub.CancelAtPeriodEnd,
|
|
CurrentPeriodStart: time.Unix(item.CurrentPeriodStart, 0).UTC(),
|
|
CurrentPeriodEnd: time.Unix(item.CurrentPeriodEnd, 0).UTC(),
|
|
}, nil
|
|
}
|
|
|
|
func cadenceFromInterval(price *stripe.Price) (Cadence, error) {
|
|
if price == nil || price.Recurring == nil {
|
|
return "", fmt.Errorf("price is not recurring")
|
|
}
|
|
switch price.Recurring.Interval {
|
|
case stripe.PriceRecurringIntervalMonth:
|
|
return CadenceMonthly, nil
|
|
case stripe.PriceRecurringIntervalYear:
|
|
return CadenceAnnual, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported price interval %q", price.Recurring.Interval)
|
|
}
|
|
}
|