This commit is contained in:
2026-08-16 11:37:42 +02:00
parent dc9ea628c1
commit 52f27e30fc
26 changed files with 1137 additions and 56 deletions
@@ -18,6 +18,7 @@ func ClientError(err error) (msg string, ok bool) {
errors.Is(err, ErrPlanNotFound),
errors.Is(err, ErrAmountRequired),
errors.Is(err, ErrStripeNotConfigured),
errors.Is(err, ErrStripeSelfServeUnavailable),
errors.Is(err, ErrStripePlanUnsupported),
errors.Is(err, ErrStripePriceMissing),
errors.Is(err, ErrStripeNoCustomer),
+24 -4
View File
@@ -99,12 +99,32 @@ func (s *StripeService) cfg(ctx context.Context) StripeConfig {
}
var (
ErrStripeNotConfigured = errors.New("stripe not configured")
ErrStripePlanUnsupported = errors.New("plan is not available for self-serve checkout")
ErrStripePriceMissing = errors.New("stripe price id not configured for plan/term")
ErrStripeBadSignature = errors.New("invalid stripe signature")
ErrStripeNotConfigured = errors.New("stripe not configured")
ErrStripeSelfServeUnavailable = errors.New("self-serve checkout is unavailable until Stripe is configured")
ErrStripePlanUnsupported = errors.New("plan is not available for self-serve checkout")
ErrStripePriceMissing = errors.New("stripe price id not configured for plan/term")
ErrStripeBadSignature = errors.New("invalid stripe signature")
)
// EnsureSelfServeCheckout allows live Stripe for any caller, and mock checkout
// only for platform admins (so new tenant users cannot buy while STRIPE_MOCK is on).
func (s *StripeService) EnsureSelfServeCheckout(ctx context.Context, platformAdmin bool) error {
_, cfg, err := s.bindCfg(ctx)
if err != nil {
return err
}
if !cfg.MockMode() {
return nil
}
if cfg.AllowMockPurchase() && platformAdmin {
return nil
}
if cfg.AllowMockPurchase() {
return ErrStripeSelfServeUnavailable
}
return ErrStripeNotConfigured
}
// CheckoutRequest is the body for POST /api/billing/checkout.
// Set Pack for a one-time AI credit top-up, or Plan (+ Term) for a subscription.
type CheckoutRequest struct {
+18
View File
@@ -93,6 +93,24 @@ func TestStripeConfigMockMode(t *testing.T) {
}
}
func TestEnsureSelfServeCheckout(t *testing.T) {
live := &StripeService{Cfg: StripeConfig{SecretKey: "sk_test_x"}}
if err := live.EnsureSelfServeCheckout(context.TODO(), false); err != nil {
t.Fatalf("live stripe should allow any admin: %v", err)
}
mock := &StripeService{Cfg: StripeConfig{ForceMock: true}}
if err := mock.EnsureSelfServeCheckout(context.TODO(), true); err != nil {
t.Fatalf("mock + platform admin should allow: %v", err)
}
if err := mock.EnsureSelfServeCheckout(context.TODO(), false); !errors.Is(err, ErrStripeSelfServeUnavailable) {
t.Fatalf("mock + non-platform admin: want ErrStripeSelfServeUnavailable, got %v", err)
}
empty := &StripeService{Cfg: StripeConfig{}}
if err := empty.EnsureSelfServeCheckout(context.TODO(), true); !errors.Is(err, ErrStripeNotConfigured) {
t.Fatalf("empty secret: want ErrStripeNotConfigured, got %v", err)
}
}
func TestCreateCheckoutSessionFailsClosedWithoutSecret(t *testing.T) {
s := &StripeService{Cfg: StripeConfig{WebOrigin: "http://localhost:5174"}}
_, err := s.CreateCheckoutSession(context.TODO(), uuid.Nil, "a@b.c", "Acme", CheckoutRequest{Plan: "starter", Term: "monthly"})