fix some issues wiht payments processed

This commit is contained in:
Blake Ridgway
2026-03-28 16:10:00 -05:00
parent 8827980043
commit a621b1deb9
3 changed files with 40 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"os" "os"
"time" "time"
@@ -45,6 +46,17 @@ func (c Config) Ready() bool {
return c.SecretKey != "" && c.WebhookSecret != "" return c.SecretKey != "" && c.WebhookSecret != ""
} }
// PlanName returns the human-readable plan key for a Stripe price ID,
// or an empty string if the price ID isn't in the configured map.
func (c Config) PlanName(priceID string) string {
for name, id := range c.PriceIDs {
if id == priceID {
return name
}
}
return ""
}
// CreateCustomer creates a Stripe customer and returns the Stripe customer ID. // CreateCustomer creates a Stripe customer and returns the Stripe customer ID.
func CreateCustomer(email, firstName, lastName string) (string, error) { func CreateCustomer(email, firstName, lastName string) (string, error) {
params := &stripelib.CustomerParams{ params := &stripelib.CustomerParams{
@@ -90,10 +102,13 @@ func CreateCheckoutSession(
return s.URL, nil return s.URL, nil
} }
// CancelSubscription cancels a Stripe subscription by ID. // CancelSubscription schedules a Stripe subscription to cancel at the end of
// the current billing period rather than immediately.
func CancelSubscription(stripeSubID string) error { func CancelSubscription(stripeSubID string) error {
params := &stripelib.SubscriptionCancelParams{} params := &stripelib.SubscriptionParams{
_, err := subscription.Cancel(stripeSubID, params) CancelAtPeriodEnd: stripelib.Bool(true),
}
_, err := subscription.Update(stripeSubID, params)
if err != nil { if err != nil {
return fmt.Errorf("stripe cancel subscription: %w", err) return fmt.Errorf("stripe cancel subscription: %w", err)
} }
@@ -101,7 +116,7 @@ func CancelSubscription(stripeSubID string) error {
} }
// HandleCheckoutCompleted processes a checkout.session.completed webhook event. // HandleCheckoutCompleted processes a checkout.session.completed webhook event.
func HandleCheckoutCompleted(db *sql.DB, raw json.RawMessage) error { func HandleCheckoutCompleted(db *sql.DB, cfg Config, raw json.RawMessage) error {
var cs stripelib.CheckoutSession var cs stripelib.CheckoutSession
if err := json.Unmarshal(raw, &cs); err != nil { if err := json.Unmarshal(raw, &cs); err != nil {
return fmt.Errorf("unmarshal checkout session: %w", err) return fmt.Errorf("unmarshal checkout session: %w", err)
@@ -119,15 +134,28 @@ func HandleCheckoutCompleted(db *sql.DB, raw json.RawMessage) error {
var customerID int64 var customerID int64
fmt.Sscanf(customerIDStr, "%d", &customerID) fmt.Sscanf(customerIDStr, "%d", &customerID)
// Fetch the full subscription to get the price ID and plan name.
priceID := ""
planName := ""
sub, err := subscription.Get(cs.Subscription.ID, nil)
if err != nil {
slog.Warn("checkout completed: fetch subscription from stripe", "err", err)
} else if len(sub.Items.Data) > 0 {
priceID = sub.Items.Data[0].Price.ID
planName = cfg.PlanName(priceID)
}
now := time.Now().UTC().Format(time.RFC3339) now := time.Now().UTC().Format(time.RFC3339)
_, err := db.Exec( _, err = db.Exec(
`INSERT INTO subscriptions `INSERT INTO subscriptions
(customer_id, stripe_subscription_id, stripe_price_id, plan_name, status, current_period_end, created_at, updated_at) (customer_id, stripe_subscription_id, stripe_price_id, plan_name, status, current_period_end, created_at, updated_at)
VALUES (?, ?, ?, ?, 'active', '', ?, ?) VALUES (?, ?, ?, ?, 'active', '', ?, ?)
ON CONFLICT(stripe_subscription_id) DO UPDATE SET ON CONFLICT(stripe_subscription_id) DO UPDATE SET
status = 'active', status = 'active',
stripe_price_id = excluded.stripe_price_id,
plan_name = excluded.plan_name,
updated_at = excluded.updated_at`, updated_at = excluded.updated_at`,
customerID, cs.Subscription.ID, "", "", now, now, customerID, cs.Subscription.ID, priceID, planName, now, now,
) )
return err return err
} }

View File

@@ -417,7 +417,7 @@ func (h *Handler) DashboardGET(w http.ResponseWriter, r *http.Request) {
flash = "Checkout was cancelled. No charge was made." flash = "Checkout was cancelled. No charge was made."
} }
if r.URL.Query().Get("cancelled") == "1" { if r.URL.Query().Get("cancelled") == "1" {
flash = "Your subscription has been cancelled." flash = "Your subscription has been cancelled and will not renew. You retain access until the end of the current billing period."
} }
if r.URL.Query().Get("error") == "cancel_failed" { if r.URL.Query().Get("error") == "cancel_failed" {
flash = "Could not cancel subscription. Please contact support." flash = "Could not cancel subscription. Please contact support."
@@ -597,7 +597,7 @@ func (h *Handler) WebhookPOST(w http.ResponseWriter, r *http.Request) {
switch event.Type { switch event.Type {
case "checkout.session.completed": case "checkout.session.completed":
if err := payments.HandleCheckoutCompleted(h.DB, event.Data.Raw); err != nil { if err := payments.HandleCheckoutCompleted(h.DB, h.Stripe, event.Data.Raw); err != nil {
slog.Error("webhook: checkout.session.completed", "err", err) slog.Error("webhook: checkout.session.completed", "err", err)
} }
case "invoice.paid": case "invoice.paid":
@@ -643,10 +643,10 @@ func (h *Handler) CancelPOST(w http.ResponseWriter, r *http.Request) {
now := time.Now().UTC().Format(time.RFC3339) now := time.Now().UTC().Format(time.RFC3339)
_, _ = h.DB.Exec( _, _ = h.DB.Exec(
`UPDATE subscriptions SET status = 'cancelled', updated_at = ? WHERE stripe_subscription_id = ?`, `UPDATE subscriptions SET status = 'cancelling', updated_at = ? WHERE stripe_subscription_id = ?`,
now, stripeSubID, now, stripeSubID,
) )
slog.Info("subscription cancelled", "customer_id", customerID, "stripe_sub_id", stripeSubID) slog.Info("subscription scheduled for cancellation", "customer_id", customerID, "stripe_sub_id", stripeSubID)
http.Redirect(w, r, "/dashboard?cancelled=1", http.StatusSeeOther) http.Redirect(w, r, "/dashboard?cancelled=1", http.StatusSeeOther)
} }

View File

@@ -48,9 +48,9 @@
</div> </div>
{{if .Subscription.CurrentPeriodEnd}} {{if .Subscription.CurrentPeriodEnd}}
<div class="status-row"> <div class="status-row">
<span class="status-row__label">next billing date</span> <span class="status-row__label">{{if eq .Subscription.Status "cancelling"}}access until{{else}}next billing date{{end}}</span>
<span class="status-row__dots"></span> <span class="status-row__dots"></span>
<span class="status-row__value">{{.Subscription.CurrentPeriodEnd}}</span> <span class="status-row__value">{{.Subscription.CurrentPeriodEnd | fmtDate}}</span>
</div> </div>
{{end}} {{end}}
</div> </div>