implement core foundation
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go/v81"
|
||||
"github.com/stripe/stripe-go/v81/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)
|
||||
}
|
||||
|
||||
slog.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(sub.CurrentPeriodStart, 0).UTC(),
|
||||
CurrentPeriodEnd: time.Unix(sub.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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user