summaryrefslogtreecommitdiff
path: root/internal/billing/auto_topup_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/billing/auto_topup_test.go')
-rw-r--r--internal/billing/auto_topup_test.go155
1 files changed, 155 insertions, 0 deletions
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)
+ }
+}