From 41e322c53d7b4b796eb377d0df9c29ecd10ba431 Mon Sep 17 00:00:00 2001 From: Chia Date: Thu, 6 Aug 2026 09:29:41 +1200 Subject: feat: complete commercial control plane, billing, auth, and model catalog - add PostgreSQL control-plane persistence with Redis-degraded hot reload - implement prepaid balance, usage ledger, Stripe top-up and reconciliation - add registration, email verification, password reset, invitations and RBAC - support TOTP, Passkey MFA, device sessions, quotas and rate limits - add tenant billing profiles, audit logs and operational readiness checks - build authenticated admin console, Quickstart, Playground and usage analytics - add public model catalog with pricing, filtering and cost estimation - support OpenAI Responses providers and provider health failover - validate real upstream usage reporting and balance settlement --- internal/billing/auto_topup_test.go | 155 ++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 internal/billing/auto_topup_test.go (limited to 'internal/billing/auto_topup_test.go') diff --git a/internal/billing/auto_topup_test.go b/internal/billing/auto_topup_test.go new file mode 100644 index 0000000..85edb3d --- /dev/null +++ b/internal/billing/auto_topup_test.go @@ -0,0 +1,155 @@ +package billing + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + "aigw/internal/controlplane" + + "github.com/stripe/stripe-go/v86" +) + +func TestAutoTopUpReturnURL(t *testing.T) { + success := autoTopUpReturnURL("https://console.example.test/admin/?topup=success", true) + if !strings.Contains(success, "autotopup=setup") || !strings.Contains(success, "session_id={CHECKOUT_SESSION_ID}") { + t.Fatalf("unexpected setup return URL %q", success) + } + cancel := autoTopUpReturnURL("https://console.example.test/admin/?topup=cancel&session_id=old", false) + if !strings.Contains(cancel, "autotopup=cancel") || strings.Contains(cancel, "session_id=") { + t.Fatalf("unexpected setup cancel URL %q", cancel) + } +} + +func TestAutomaticTopUpSetupAndCreditAreIdempotentPostgres(t *testing.T) { + databaseURL := os.Getenv("AIGW_TEST_DATABASE_URL") + if databaseURL == "" { + t.Skip("AIGW_TEST_DATABASE_URL is not set") + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := controlplane.MigrateDatabase(ctx, databaseURL); err != nil { + t.Fatal(err) + } + service, err := New(ctx, Options{ + DatabaseURL: databaseURL, Currency: "usd", MinTopUpMinor: 500, MaxTopUpMinor: 1_000_000, + StripeEnabled: true, StripeAPIKey: "rk_test_placeholder", StripeWebhookSecret: "whsec_integration_test", + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(service.Close) + + suffix := time.Now().UnixNano() + eventID := fmt.Sprintf("evt_auto_topup_%d", suffix) + var tenantID string + if err := service.db.QueryRow(ctx, `INSERT INTO tenants (slug,name) VALUES ($1,'Auto top-up integration') RETURNING id::text`, fmt.Sprintf("auto-topup-%d", suffix)).Scan(&tenantID); err != nil { + t.Fatal(err) + } + if _, err := service.db.Exec(ctx, `INSERT INTO tenant_wallets (tenant_id,currency) VALUES ($1,'usd')`, tenantID); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + cleanupCtx := context.Background() + if _, cleanupErr := service.db.Exec(cleanupCtx, `DELETE FROM stripe_webhook_events WHERE event_id=$1`, eventID); cleanupErr != nil { + t.Errorf("cleanup automatic top-up webhook: %v", cleanupErr) + } + for _, query := range []string{ + `DELETE FROM billing_ledger WHERE tenant_id=$1`, + `DELETE FROM topup_orders WHERE tenant_id=$1`, + `DELETE FROM tenant_auto_topup_settings WHERE tenant_id=$1`, + `DELETE FROM stripe_customers WHERE tenant_id=$1`, + `DELETE FROM tenant_wallets WHERE tenant_id=$1`, + `DELETE FROM tenants WHERE id=$1`, + } { + if _, cleanupErr := service.db.Exec(cleanupCtx, query, tenantID); cleanupErr != nil { + t.Errorf("cleanup automatic top-up integration data: %v", cleanupErr) + } + } + }) + + customerID := fmt.Sprintf("cus_auto_%d", suffix) + paymentMethodID := fmt.Sprintf("pm_auto_%d", suffix) + setupIntentID := fmt.Sprintf("seti_auto_%d", suffix) + setupSessionID := fmt.Sprintf("cs_auto_%d", suffix) + service.retrieveStripeSetupIntent = func(context.Context, string, *stripe.SetupIntentRetrieveParams) (*stripe.SetupIntent, error) { + return &stripe.SetupIntent{ + ID: setupIntentID, Status: stripe.SetupIntentStatusSucceeded, + Customer: &stripe.Customer{ID: customerID}, + PaymentMethod: &stripe.PaymentMethod{ID: paymentMethodID, Type: stripe.PaymentMethodTypeCard, + Card: &stripe.PaymentMethodCard{Brand: stripe.PaymentMethodCardBrandVisa, Last4: "4242", ExpMonth: 12, ExpYear: 2035}}, + }, nil + } + if _, err := service.db.Exec(ctx, `INSERT INTO tenant_auto_topup_settings (tenant_id,threshold_micros,topup_amount_minor,stripe_setup_session_id) VALUES ($1,5000000,2000,$2)`, tenantID, setupSessionID); err != nil { + t.Fatal(err) + } + raw, err := json.Marshal(map[string]any{ + "id": setupSessionID, "object": "checkout.session", "client_reference_id": tenantID, + "customer": customerID, "customer_email": "developer@example.test", "setup_intent": setupIntentID, + "metadata": map[string]string{"aigw_action": autoTopUpAction, "aigw_tenant_id": tenantID}, + }) + if err != nil { + t.Fatal(err) + } + event := stripe.Event{ID: eventID, Type: stripe.EventTypeCheckoutSessionCompleted, Data: &stripe.EventData{Raw: raw}} + if err := service.processStripeEvent(ctx, event); err != nil { + t.Fatal(err) + } + if err := service.processStripeEvent(ctx, event); err != nil { + t.Fatalf("replayed setup event: %v", err) + } + settings, err := service.UpdateAutoTopUp(ctx, UpdateAutoTopUpInput{ + TenantID: tenantID, Enabled: true, ThresholdMicros: 1_000_000, TopUpAmountMinor: 2000, + }) + if err != nil { + t.Fatal(err) + } + if !settings.Enabled || !settings.PaymentMethodConfigured || settings.PaymentMethodLast4 != "4242" { + t.Fatalf("unexpected settings: %+v", settings) + } + + var createdIntent *stripe.PaymentIntent + stripeCalls := 0 + service.createStripePaymentIntent = func(_ context.Context, params *stripe.PaymentIntentCreateParams) (*stripe.PaymentIntent, error) { + stripeCalls++ + createdIntent = &stripe.PaymentIntent{ + ID: fmt.Sprintf("pi_auto_%d", suffix), Status: stripe.PaymentIntentStatusSucceeded, + Amount: *params.Amount, AmountReceived: *params.Amount, Currency: stripe.Currency(*params.Currency), + Customer: &stripe.Customer{ID: *params.Customer}, PaymentMethod: &stripe.PaymentMethod{ID: *params.PaymentMethod}, + Metadata: params.Metadata, + } + return createdIntent, nil + } + processed, err := service.processAutoTopUpOnce(ctx) + if err != nil || !processed { + t.Fatalf("process automatic top-up: processed=%v err=%v", processed, err) + } + processed, err = service.processAutoTopUpOnce(ctx) + if err != nil || processed { + t.Fatalf("second automatic top-up: processed=%v err=%v", processed, err) + } + if stripeCalls != 1 { + t.Fatalf("Stripe calls = %d, want 1", stripeCalls) + } + if err := service.creditAutoTopUpPaymentIntent(ctx, createdIntent); err != nil { + t.Fatalf("replayed successful PaymentIntent: %v", err) + } + + var balance, ledgerCount, paidOrders int64 + if err := service.db.QueryRow(ctx, `SELECT balance_micros FROM tenant_wallets WHERE tenant_id=$1`, tenantID).Scan(&balance); err != nil { + t.Fatal(err) + } + if err := service.db.QueryRow(ctx, `SELECT count(*) FROM billing_ledger WHERE tenant_id=$1 AND source_type='stripe_payment_intent'`, tenantID).Scan(&ledgerCount); err != nil { + t.Fatal(err) + } + if err := service.db.QueryRow(ctx, `SELECT count(*) FROM topup_orders WHERE tenant_id=$1 AND trigger_type='auto' AND status='paid'`, tenantID).Scan(&paidOrders); err != nil { + t.Fatal(err) + } + if balance != 20_000_000 || ledgerCount != 1 || paidOrders != 1 { + t.Fatalf("balance=%d ledger=%d paid_orders=%d", balance, ledgerCount, paidOrders) + } +} -- cgit v1.2.3