package payments import ( "database/sql" "encoding/json" "fmt" "os" "time" stripelib "github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81/checkout/session" stripecustomer "github.com/stripe/stripe-go/v81/customer" "github.com/stripe/stripe-go/v81/subscription" ) // Config holds Stripe credentials loaded from environment variables. type Config struct { SecretKey string WebhookSecret string PriceIDs map[string]string // plan key → Stripe price ID } // ConfigFromEnv reads Stripe settings from environment variables. func ConfigFromEnv() Config { return Config{ SecretKey: os.Getenv("STRIPE_SECRET_KEY"), WebhookSecret: os.Getenv("STRIPE_WEBHOOK_SECRET"), PriceIDs: map[string]string{ "shared_starter": os.Getenv("STRIPE_PRICE_SHARED_STARTER"), "shared_pro": os.Getenv("STRIPE_PRICE_SHARED_PRO"), "shared_business": os.Getenv("STRIPE_PRICE_SHARED_BUSINESS"), "wp_starter": os.Getenv("STRIPE_PRICE_WP_STARTER"), "wp_pro": os.Getenv("STRIPE_PRICE_WP_PRO"), "wp_business": os.Getenv("STRIPE_PRICE_WP_BUSINESS"), "vps_1": os.Getenv("STRIPE_PRICE_VPS_1"), "vps_2": os.Getenv("STRIPE_PRICE_VPS_2"), "vps_3": os.Getenv("STRIPE_PRICE_VPS_3"), "vps_4": os.Getenv("STRIPE_PRICE_VPS_4"), }, } } // Ready reports whether the required Stripe credentials are set. func (c Config) Ready() bool { return c.SecretKey != "" && c.WebhookSecret != "" } // CreateCustomer creates a Stripe customer and returns the Stripe customer ID. func CreateCustomer(email, firstName, lastName string) (string, error) { params := &stripelib.CustomerParams{ Email: stripelib.String(email), Name: stripelib.String(firstName + " " + lastName), } c, err := stripecustomer.New(params) if err != nil { return "", fmt.Errorf("stripe create customer: %w", err) } return c.ID, nil } // CreateCheckoutSession creates a Stripe Checkout session and returns the hosted URL. func CreateCheckoutSession( cfg Config, customerID int64, stripeCustomerID, priceID, successURL, cancelURL string, ) (string, error) { params := &stripelib.CheckoutSessionParams{ Customer: stripelib.String(stripeCustomerID), Mode: stripelib.String(string(stripelib.CheckoutSessionModeSubscription)), LineItems: []*stripelib.CheckoutSessionLineItemParams{ { Price: stripelib.String(priceID), Quantity: stripelib.Int64(1), }, }, SuccessURL: stripelib.String(successURL), CancelURL: stripelib.String(cancelURL), Metadata: map[string]string{ "customer_id": fmt.Sprintf("%d", customerID), }, } s, err := session.New(params) if err != nil { return "", fmt.Errorf("stripe checkout session: %w", err) } return s.URL, nil } // CancelSubscription cancels a Stripe subscription by ID. func CancelSubscription(stripeSubID string) error { params := &stripelib.SubscriptionCancelParams{} _, err := subscription.Cancel(stripeSubID, params) if err != nil { return fmt.Errorf("stripe cancel subscription: %w", err) } return nil } // HandleCheckoutCompleted processes a checkout.session.completed webhook event. func HandleCheckoutCompleted(db *sql.DB, raw json.RawMessage) error { var cs stripelib.CheckoutSession if err := json.Unmarshal(raw, &cs); err != nil { return fmt.Errorf("unmarshal checkout session: %w", err) } if cs.Subscription == nil { return nil } customerIDStr, ok := cs.Metadata["customer_id"] if !ok { return fmt.Errorf("missing customer_id in metadata") } var customerID int64 fmt.Sscanf(customerIDStr, "%d", &customerID) now := time.Now().UTC().Format(time.RFC3339) _, err := db.Exec( `INSERT INTO subscriptions (customer_id, stripe_subscription_id, stripe_price_id, plan_name, status, current_period_end, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', '', ?, ?) ON CONFLICT(stripe_subscription_id) DO UPDATE SET status = 'active', updated_at = excluded.updated_at`, customerID, cs.Subscription.ID, "", "", now, now, ) return err } // HandleInvoicePaid processes an invoice.paid webhook event. func HandleInvoicePaid(db *sql.DB, raw json.RawMessage) error { var inv stripelib.Invoice if err := json.Unmarshal(raw, &inv); err != nil { return fmt.Errorf("unmarshal invoice: %w", err) } var customerID int64 if inv.Customer != nil { err := db.QueryRow( `SELECT id FROM customers WHERE stripe_customer_id = ?`, inv.Customer.ID, ).Scan(&customerID) if err != nil { return fmt.Errorf("find customer for stripe id %s: %w", inv.Customer.ID, err) } } pdfURL := "" if inv.InvoicePDF != "" { pdfURL = inv.InvoicePDF } periodStart := time.Unix(inv.PeriodStart, 0).UTC().Format(time.RFC3339) periodEnd := time.Unix(inv.PeriodEnd, 0).UTC().Format(time.RFC3339) now := time.Now().UTC().Format(time.RFC3339) _, err := db.Exec( `INSERT INTO invoices (customer_id, stripe_invoice_id, amount_cents, currency, status, invoice_pdf_url, period_start, period_end, created_at) VALUES (?, ?, ?, ?, 'paid', ?, ?, ?, ?) ON CONFLICT(stripe_invoice_id) DO UPDATE SET status = 'paid', invoice_pdf_url = excluded.invoice_pdf_url`, customerID, inv.ID, inv.AmountPaid, string(inv.Currency), pdfURL, periodStart, periodEnd, now, ) if err != nil { return fmt.Errorf("upsert invoice: %w", err) } if inv.Subscription != nil { _, _ = db.Exec( `UPDATE subscriptions SET current_period_end = ?, updated_at = ? WHERE stripe_subscription_id = ?`, periodEnd, now, inv.Subscription.ID, ) } return nil } // HandleInvoicePaymentFailed processes an invoice.payment_failed webhook event. func HandleInvoicePaymentFailed(db *sql.DB, raw json.RawMessage) error { var inv stripelib.Invoice if err := json.Unmarshal(raw, &inv); err != nil { return fmt.Errorf("unmarshal invoice: %w", err) } if inv.Subscription == nil { return nil } now := time.Now().UTC().Format(time.RFC3339) _, err := db.Exec( `UPDATE subscriptions SET status = 'past_due', updated_at = ? WHERE stripe_subscription_id = ?`, now, inv.Subscription.ID, ) return err } // HandleSubscriptionDeleted processes a customer.subscription.deleted webhook event. func HandleSubscriptionDeleted(db *sql.DB, raw json.RawMessage) error { var sub stripelib.Subscription if err := json.Unmarshal(raw, &sub); err != nil { return fmt.Errorf("unmarshal subscription: %w", err) } now := time.Now().UTC().Format(time.RFC3339) _, err := db.Exec( `UPDATE subscriptions SET status = 'cancelled', updated_at = ? WHERE stripe_subscription_id = ?`, now, sub.ID, ) return err }