diff options
Diffstat (limited to 'internal/billing/profile_test.go')
| -rw-r--r-- | internal/billing/profile_test.go | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/internal/billing/profile_test.go b/internal/billing/profile_test.go new file mode 100644 index 0000000..e263bb8 --- /dev/null +++ b/internal/billing/profile_test.go @@ -0,0 +1,135 @@ +package billing + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + "time" + + "aigw/internal/controlplane" + + "github.com/stripe/stripe-go/v86" +) + +func TestNormalizeBillingProfile(t *testing.T) { + input := UpdateBillingProfileInput{TenantID: " tenant ", LegalName: " Example Limited ", BillingEmail: "BILLING@EXAMPLE.TEST", + AddressLine1: " 1 Queen Street ", City: " Auckland ", PostalCode: "1010", Country: "nz"} + result, err := normalizeBillingProfile(input) + if err != nil { + t.Fatal(err) + } + if result.TenantID != "tenant" || result.LegalName != "Example Limited" || result.BillingEmail != "billing@example.test" || result.Country != "NZ" { + t.Fatalf("unexpected normalized profile: %+v", result) + } + for _, invalid := range []UpdateBillingProfileInput{ + {TenantID: "tenant", LegalName: "Example", BillingEmail: "not-an-email", AddressLine1: "1 Street", City: "Auckland", PostalCode: "1010", Country: "NZ"}, + {TenantID: "tenant", LegalName: "Example", BillingEmail: "billing@example.test", AddressLine1: "1 Street", City: "Auckland", PostalCode: "1010", Country: "New Zealand"}, + {TenantID: "tenant", BillingEmail: "billing@example.test", AddressLine1: "1 Street", City: "Auckland", PostalCode: "1010", Country: "NZ"}, + } { + if _, err := normalizeBillingProfile(invalid); !errors.Is(err, ErrInvalidBillingProfile) { + t.Fatalf("error = %v, want invalid billing profile", err) + } + } +} + +func TestBillingProfileStripeSynchronizationPostgres(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", StripeEnabled: true, StripeAPIKey: "rk_test_placeholder"}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(service.Close) + + suffix := time.Now().UnixNano() + var tenantID string + if err := service.db.QueryRow(ctx, `INSERT INTO tenants (slug,name) VALUES ($1,'Billing profile integration') RETURNING id::text`, fmt.Sprintf("billing-profile-%d", suffix)).Scan(&tenantID); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + cleanupCtx := context.Background() + for _, query := range []string{ + `DELETE FROM tenant_billing_profiles WHERE tenant_id=$1`, + `DELETE FROM stripe_customers WHERE tenant_id=$1`, + `DELETE FROM tenants WHERE id=$1`, + } { + if _, cleanupErr := service.db.Exec(cleanupCtx, query, tenantID); cleanupErr != nil { + t.Errorf("cleanup billing profile: %v", cleanupErr) + } + } + }) + + customerID := fmt.Sprintf("cus_profile_%d", suffix) + createCalls, updateCalls := 0, 0 + service.createStripeCustomer = func(_ context.Context, params *stripe.CustomerCreateParams) (*stripe.Customer, error) { + createCalls++ + if params.IdempotencyKey == nil || *params.IdempotencyKey != "aigw_customer_"+tenantID || params.Address == nil || *params.Address.Country != "NZ" || *params.Name != "Example Limited" || params.Metadata["aigw_tenant_id"] != tenantID { + t.Fatalf("unexpected Stripe create params: %+v", params) + } + return &stripe.Customer{ID: customerID}, nil + } + service.updateStripeCustomer = func(_ context.Context, id string, params *stripe.CustomerUpdateParams) (*stripe.Customer, error) { + updateCalls++ + if id != customerID || params.Address == nil || *params.Address.PostalCode != "1010" { + t.Fatalf("unexpected Stripe update: id=%s params=%+v", id, params) + } + return &stripe.Customer{ID: customerID}, nil + } + + input := UpdateBillingProfileInput{TenantID: tenantID, LegalName: "Example Limited", BillingEmail: "billing@example.test", + AddressLine1: "1 Queen Street", City: "Auckland", Region: "Auckland", PostalCode: "1010", Country: "nz"} + profile, err := service.UpdateBillingProfile(ctx, input) + if err != nil { + t.Fatal(err) + } + if createCalls != 1 || updateCalls != 0 || !profile.Configured || !profile.StripeCustomerConfigured || profile.StripeSyncStatus != "synced" || profile.StripeSyncedAt == nil { + t.Fatalf("unexpected first synchronization: create=%d update=%d profile=%+v", createCalls, updateCalls, profile) + } + + input.LegalName = "Example API Limited" + profile, err = service.UpdateBillingProfile(ctx, input) + if err != nil { + t.Fatal(err) + } + if createCalls != 1 || updateCalls != 1 || profile.LegalName != "Example API Limited" || profile.StripeSyncStatus != "synced" { + t.Fatalf("unexpected update synchronization: create=%d update=%d profile=%+v", createCalls, updateCalls, profile) + } + + service.updateStripeCustomer = func(context.Context, string, *stripe.CustomerUpdateParams) (*stripe.Customer, error) { + return nil, errors.New("temporary Stripe outage") + } + input.City = "Wellington" + if _, err := service.UpdateBillingProfile(ctx, input); !errors.Is(err, ErrBillingProfileSync) { + t.Fatalf("error = %v, want Stripe sync error", err) + } + profile, err = service.GetBillingProfile(ctx, tenantID) + if err != nil { + t.Fatal(err) + } + if profile.City != "Wellington" || profile.StripeSyncStatus != "failed" || profile.StripeSyncError == "" { + t.Fatalf("failed synchronization did not preserve local profile: %+v", profile) + } + + service.updateStripeCustomer = func(context.Context, string, *stripe.CustomerUpdateParams) (*stripe.Customer, error) { + return &stripe.Customer{ID: customerID}, nil + } + if _, err := service.ensureStripeCustomer(ctx, tenantID); err != nil { + t.Fatal(err) + } + profile, err = service.GetBillingProfile(ctx, tenantID) + if err != nil { + t.Fatal(err) + } + if profile.StripeSyncStatus != "synced" || profile.StripeSyncError != "" { + t.Fatalf("profile did not recover after retry: %+v", profile) + } +} |
