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 TestAutoTopUpStripeContracts(t *testing.T) { service := &Service{ currency: "usd", stripeSuccessURL: "https://console.example.test/billing?topup=success", stripeCancelURL: "https://console.example.test/billing?topup=cancel", integrationIdentifier: "aigw_balance_abcdefgh", } setup := service.autoTopUpSetupSessionParams(AutoTopUpSetupInput{ TenantID: "tenant-123", CustomerEmail: " billing@example.test ", }, "") if setup.Mode == nil || *setup.Mode != string(stripe.CheckoutSessionModeSetup) || setup.Currency == nil || *setup.Currency != "usd" { t.Fatalf("unexpected setup contract %+v", setup) } if len(setup.PaymentMethodTypes) != 0 || setup.CustomerCreation == nil || *setup.CustomerCreation != string(stripe.CheckoutSessionCustomerCreationAlways) { t.Fatal("setup Checkout must create a customer and use Dashboard-managed payment methods") } if setup.CustomerEmail == nil || *setup.CustomerEmail != "billing@example.test" || setup.Metadata["aigw_action"] != autoTopUpAction { t.Fatal("setup Checkout customer or metadata contract is incomplete") } if setup.IdempotencyKey == nil || !strings.HasPrefix(*setup.IdempotencyKey, "aigw_autotopup_setup_") { t.Fatalf("setup idempotency key = %v", setup.IdempotencyKey) } payment := service.autoTopUpPaymentIntentParams( "tenant-123", "order-123", "cus_123", " billing@example.test ", "pm_123", "usd", 2000, ) if payment.Amount == nil || *payment.Amount != 2000 || payment.Currency == nil || *payment.Currency != "usd" || payment.Customer == nil || *payment.Customer != "cus_123" || payment.PaymentMethod == nil || *payment.PaymentMethod != "pm_123" { t.Fatalf("unexpected automatic top-up PaymentIntent %+v", payment) } if payment.Confirm == nil || !*payment.Confirm || payment.OffSession == nil || !*payment.OffSession || payment.ErrorOnRequiresAction == nil || !*payment.ErrorOnRequiresAction { t.Fatal("automatic top-up must be confirmed off-session and stop on required customer action") } if payment.ReceiptEmail == nil || *payment.ReceiptEmail != "billing@example.test" || payment.Metadata["aigw_topup_order_id"] != "order-123" { t.Fatal("automatic top-up receipt or reconciliation metadata is incomplete") } if payment.IdempotencyKey == nil || *payment.IdempotencyKey != "aigw_autotopup_order-123" { t.Fatalf("payment idempotency key = %v", payment.IdempotencyKey) } } 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) } }