package httpapi import ( "errors" "io" "net/http" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" ) func (s *Server) stripeSvc() *billing.StripeService { if s.Stripe != nil { return s.Stripe } s.Stripe = &billing.StripeService{ Pool: s.Pool, Billing: s.Billing, Cfg: billing.StripeConfig{ SecretKey: s.Config.StripeSecretKey, WebhookSecret: s.Config.StripeWebhookSecret, WebOrigin: s.Config.WebOrigin, PublicAPIURL: s.Config.PublicAPIURL, PriceIDs: s.Config.StripePriceIDs, ForceMock: s.Config.StripeMock, }, } return s.Stripe } func (s *Server) handleStripeStatus(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) st, err := s.stripeSvc().Status(r.Context(), cid) if err != nil { Error(w, http.StatusInternalServerError, "failed to load stripe status") return } JSON(w, http.StatusOK, st) } func (s *Server) handleStripeCheckout(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) uid, _ := UserIDFromContext(r.Context()) if !s.allowCompanyOwnerOrPlatform(w, r) { return } platformAdmin, err := s.checkPlatformAdmin(r.Context(), uid) if err != nil { LogAndError(w, http.StatusInternalServerError, "failed to verify platform admin", err) return } if err := s.stripeSvc().EnsureSelfServeCheckout(r.Context(), platformAdmin); err != nil { ClientOrLog(w, http.StatusServiceUnavailable, "checkout unavailable", err, billing.ClientError) return } var body billing.CheckoutRequest if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } email, name := "", "" _ = s.Pool.QueryRow(r.Context(), `SELECT email, COALESCE(name, '') FROM users WHERE id = $1`, uid).Scan(&email, &name) var companyName string _ = s.Pool.QueryRow(r.Context(), `SELECT name FROM companies WHERE id = $1`, cid).Scan(&companyName) res, err := s.stripeSvc().CreateCheckoutSession(r.Context(), cid, email, companyName, body) if err != nil { ClientOrLog(w, http.StatusBadRequest, "checkout failed", err, billing.ClientError) return } JSON(w, http.StatusOK, res) } func (s *Server) handleStripePortal(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) if !s.allowCompanyOwnerOrPlatform(w, r) { return } res, err := s.stripeSvc().CreatePortalSession(r.Context(), cid) if err != nil { ClientOrLog(w, http.StatusBadRequest, "portal session failed", err, billing.ClientError) return } JSON(w, http.StatusOK, res) } // handleStripeWebhook is public (no session/CSRF). Signature verified when STRIPE_WEBHOOK_SECRET is set. func (s *Server) handleStripeWebhook(w http.ResponseWriter, r *http.Request) { const maxBody = 1 << 20 // 1 MiB body, err := io.ReadAll(io.LimitReader(r.Body, maxBody+1)) if err != nil { Error(w, http.StatusBadRequest, "failed to read body") return } if len(body) > maxBody { Error(w, http.StatusRequestEntityTooLarge, "body too large") return } sig := r.Header.Get("Stripe-Signature") if err := s.stripeSvc().HandleWebhook(r.Context(), body, sig); err != nil { switch { case errors.Is(err, billing.ErrStripeBadSignature): Error(w, http.StatusBadRequest, "invalid signature") case errors.Is(err, billing.ErrStripeNotConfigured): Error(w, http.StatusServiceUnavailable, "stripe webhooks not configured") default: // Avoid leaking internal apply/DB details to an unauthenticated caller. LogAndError(w, http.StatusBadRequest, "webhook processing failed", err) } return } JSON(w, http.StatusOK, map[string]string{"status": "ok"}) }