package handlers import ( "encoding/json" "log" "time" stripego "github.com/stripe/stripe-go/v84" "github.com/stripe/stripe-go/v84/subscription" ) const freeTierMonths = 3 // maybeScheduleFreeTierCancel fetches the subscription and, if it uses the free-tier // price, sets cancel_at to 3 months from now so it auto-cancels at end of term. func (a *App) maybeScheduleFreeTierCancel(subID string) { sub, err := a.stripe.GetSubscription(subID) if err != nil { log.Printf("webhook: could not fetch subscription %s: %v", subID, err) return } if sub.Items == nil || len(sub.Items.Data) == 0 { return } priceID := sub.Items.Data[0].Price.ID if !a.stripe.IsFreeTierPrice(priceID) { return } cancelAt := time.Now().AddDate(0, freeTierMonths, 0).Unix() if err := a.stripe.ScheduleSubscriptionCancelAt(subID, cancelAt); err != nil { log.Printf("webhook: failed to schedule free-tier cancel for sub %s: %v", subID, err) return } log.Printf("webhook: scheduled free-tier sub %s to cancel at %d (3 months)", subID, cancelAt) } type invoicePaidPayload struct { BillingReason stripego.InvoiceBillingReason `json:"billing_reason"` Subscription interface{} `json:"subscription"` Lines struct { Data []struct { Price struct { ID string `json:"id"` } `json:"price"` } `json:"data"` } `json:"lines"` } func (a *App) onInvoicePaid(event stripego.Event) { var raw invoicePaidPayload if err := json.Unmarshal(event.Data.Raw, &raw); err != nil { log.Printf("invoice unmarshal error: %v", err) return } if raw.BillingReason != stripego.InvoiceBillingReasonSubscriptionCycle { return } subID := "" switch v := raw.Subscription.(type) { case string: subID = v case map[string]interface{}: if id, ok := v["id"].(string); ok { subID = id } } if subID == "" || len(raw.Lines.Data) == 0 { return } priceID := raw.Lines.Data[0].Price.ID if priceID == "" || !a.stripe.IsYearTierPrice(priceID) { return } price100 := a.cfg.StripePriceIDMonth100 if price100 == "" { log.Printf("webhook: STRIPE_PRICE_ID_MONTH_100 not set, cannot migrate sub %s", subID) return } sub, err := a.stripe.GetSubscription(subID) if err != nil || sub.Items == nil || len(sub.Items.Data) == 0 { log.Printf("webhook: could not get subscription %s for migration: %v", subID, err) return } subItemID := sub.Items.Data[0].ID params := &stripego.SubscriptionParams{ Items: []*stripego.SubscriptionItemsParams{ {ID: stripego.String(subItemID), Price: stripego.String(price100)}, }, ProrationBehavior: stripego.String("none"), } if _, err := subscription.Update(subID, params); err != nil { log.Printf("webhook: failed to migrate sub %s to $100/mo: %v", subID, err) return } log.Printf("webhook: migrated sub %s from $20/year to $100/month", subID) }