summaryrefslogtreecommitdiff
path: root/internal/billing/profile_test.go
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-06 09:29:41 +1200
committerChia <Chia@93.nz>2026-08-06 09:32:46 +1200
commit41e322c53d7b4b796eb377d0df9c29ecd10ba431 (patch)
treec730526150e55e39b822d5197e4a20318ecaa449 /internal/billing/profile_test.go
parenteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (diff)
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
Diffstat (limited to 'internal/billing/profile_test.go')
-rw-r--r--internal/billing/profile_test.go135
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)
+ }
+}