summaryrefslogtreecommitdiff
path: root/internal/billing/service_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/billing/service_test.go')
-rw-r--r--internal/billing/service_test.go174
1 files changed, 174 insertions, 0 deletions
diff --git a/internal/billing/service_test.go b/internal/billing/service_test.go
index 5e21b20..a75e4e0 100644
--- a/internal/billing/service_test.go
+++ b/internal/billing/service_test.go
@@ -84,6 +84,16 @@ func TestAuthorizeEnforcesAPIKeyMonthlySpendCapPostgres(t *testing.T) {
if err := service.Authorize(ctx, Authorization{RequestID: "req_key_budget_allowed", Principal: principal, Model: model, Body: []byte(`{"max_tokens":10}`)}); err != nil {
t.Fatalf("Authorize at exact cap: %v", err)
}
+ principal.MonthlySpendMicros = 0
+ principal.DailySpendMicros = 19
+ err = service.Authorize(ctx, Authorization{RequestID: "req_key_budget_daily_rejected", Principal: principal, Model: model, Body: []byte(`{"max_tokens":10}`)})
+ if !errors.Is(err, ErrDailyQuotaExceeded) {
+ t.Fatalf("Authorize daily error = %v, want ErrDailyQuotaExceeded", err)
+ }
+ principal.DailySpendMicros = 20
+ if err := service.Authorize(ctx, Authorization{RequestID: "req_key_budget_daily_allowed", Principal: principal, Model: model, Body: []byte(`{"max_tokens":10}`)}); err != nil {
+ t.Fatalf("Authorize at exact daily cap including pending reservation: %v", err)
+ }
}
func TestUsageCostUsesFixedPointAndRoundsOnce(t *testing.T) {
@@ -111,6 +121,35 @@ func TestReservationUsesExplicitOutputLimit(t *testing.T) {
}
}
+func TestEmbeddingReservationDoesNotReserveOutputTokens(t *testing.T) {
+ model := domain.Model{InputPriceMicrosPerMillion: 1_000_000, OutputPriceMicrosPerMillion: 50_000_000, MaxOutputTokens: 8192}
+ body := []byte(`{"model":"embedding","input":"hello","max_tokens":99999}`)
+ cost, err := reservationCost(model, body, 4096, domain.ProtocolOpenAIEmbeddings)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cost != int64(len(body)) {
+ t.Fatalf("embedding reservation = %d, want conservative input-only %d", cost, len(body))
+ }
+}
+
+func TestMeteredCostSupportsTokenImageAndSecondUnits(t *testing.T) {
+ cost, err := calculateMeteredCost([]meteredCharge{
+ {Unit: domain.MeteringUnitToken, Quantity: 500_000, PriceMicros: 2_000_000, PerQuantity: 1_000_000},
+ {Unit: domain.MeteringUnitImage, Quantity: 2, PriceMicros: 40_000, PerQuantity: 1},
+ {Unit: domain.MeteringUnitSecond, Quantity: 3, PriceMicros: 500, PerQuantity: 1},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cost != 1_081_500 {
+ t.Fatalf("metered cost = %d, want 1081500", cost)
+ }
+ if _, err := calculateMeteredCost([]meteredCharge{{Unit: "byte", Quantity: 1, PriceMicros: 1, PerQuantity: 1}}); err == nil {
+ t.Fatal("unsupported metering unit was accepted")
+ }
+}
+
func TestMinorToMicrosSupportsCurrencyExponents(t *testing.T) {
tests := []struct {
currency string
@@ -153,6 +192,57 @@ func TestIntegrationIdentifierSuffixUsesLetters(t *testing.T) {
}
}
+func TestStripeSDKVersionAndCheckoutContract(t *testing.T) {
+ if stripe.APIVersion != "2026-07-29.dahlia" {
+ t.Fatalf("Stripe API version = %q; review the integration before changing the pinned version", stripe.APIVersion)
+ }
+ service := &Service{
+ currency: "usd",
+ stripeSuccessURL: "https://console.example.test/billing?topup=success",
+ stripeCancelURL: "https://console.example.test/billing?topup=cancel",
+ integrationIdentifier: "aigw_balance_abcdefgh",
+ }
+ params := service.checkoutSessionParams("order-123", CheckoutInput{TenantID: "tenant-123", AmountMinor: 2500})
+ if params.Mode == nil || *params.Mode != string(stripe.CheckoutSessionModePayment) {
+ t.Fatalf("mode = %v", params.Mode)
+ }
+ if params.IntegrationIdentifier == nil || *params.IntegrationIdentifier != "aigw_balance_abcdefgh" {
+ t.Fatalf("integration identifier = %v", params.IntegrationIdentifier)
+ }
+ if len(params.PaymentMethodTypes) != 0 || len(params.ExcludedPaymentMethodTypes) != 0 {
+ t.Fatal("Checkout must use Dashboard-managed dynamic payment methods")
+ }
+ if params.AutomaticTax != nil || params.TaxIDCollection != nil {
+ t.Fatal("Stripe Tax must remain disabled unless registration is explicitly confirmed")
+ }
+ if params.InvoiceCreation == nil || params.InvoiceCreation.Enabled == nil || !*params.InvoiceCreation.Enabled {
+ t.Fatal("one-time prepaid top-up invoice creation is not enabled")
+ }
+ if len(params.LineItems) != 1 || params.LineItems[0].PriceData == nil || params.LineItems[0].PriceData.UnitAmount == nil || *params.LineItems[0].PriceData.UnitAmount != 2500 {
+ t.Fatalf("unexpected line item %+v", params.LineItems)
+ }
+ if params.IdempotencyKey == nil || *params.IdempotencyKey != "aigw_topup_order-123" {
+ t.Fatalf("idempotency key = %v", params.IdempotencyKey)
+ }
+}
+
+func TestCheckoutContractEnablesTaxOnlyWhenExplicitlyConfigured(t *testing.T) {
+ service := &Service{
+ currency: "usd",
+ stripeAutomaticTax: true,
+ stripeProductTaxCode: "txcd_10103000",
+ integrationIdentifier: "aigw_balance_abcdefgh",
+ }
+ params := service.checkoutSessionParams("order-tax", CheckoutInput{TenantID: "tenant-tax", AmountMinor: 1000})
+ if params.AutomaticTax == nil || params.AutomaticTax.Enabled == nil || !*params.AutomaticTax.Enabled ||
+ params.TaxIDCollection == nil || params.TaxIDCollection.Enabled == nil || !*params.TaxIDCollection.Enabled {
+ t.Fatal("explicit Stripe Tax configuration was not applied")
+ }
+ if params.LineItems[0].PriceData.ProductData.TaxCode == nil || *params.LineItems[0].PriceData.ProductData.TaxCode != "txcd_10103000" {
+ t.Fatal("canonical Stripe product tax code was not applied")
+ }
+}
+
func TestCheckoutReturnURLPreservesCallbackAndSessionPlaceholder(t *testing.T) {
success := checkoutReturnURL("https://console.example.test/admin/?topup=success", "order-123", true)
if !strings.Contains(success, "topup=success") || !strings.Contains(success, "order_id=order-123") ||
@@ -196,6 +286,62 @@ func TestWebhookRejectsInvalidSignatureBeforeProcessing(t *testing.T) {
}
}
+func TestIncompleteStripeCheckoutMarksOrderFailedPostgres(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",
+ StripeSuccessURL: "https://console.example.test/billing?topup=success",
+ StripeCancelURL: "https://console.example.test/billing?topup=cancel",
+ })
+ 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,'Incomplete Stripe Checkout') RETURNING id::text`, fmt.Sprintf("incomplete-checkout-%d", suffix)).Scan(&tenantID); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if _, cleanupErr := service.db.Exec(context.Background(), `DELETE FROM topup_orders WHERE tenant_id=$1`, tenantID); cleanupErr != nil {
+ t.Errorf("cleanup incomplete Checkout orders: %v", cleanupErr)
+ }
+ if _, cleanupErr := service.db.Exec(context.Background(), `DELETE FROM tenants WHERE id=$1`, tenantID); cleanupErr != nil {
+ t.Errorf("cleanup incomplete Checkout tenant: %v", cleanupErr)
+ }
+ })
+ if _, err := service.db.Exec(ctx, `INSERT INTO stripe_customers (tenant_id,stripe_customer_id,email) VALUES ($1,$2,'billing@example.test')`, tenantID, fmt.Sprintf("cus_incomplete_%d", suffix)); err != nil {
+ t.Fatal(err)
+ }
+ service.createStripeCheckout = func(context.Context, *stripe.CheckoutSessionCreateParams) (*stripe.CheckoutSession, error) {
+ return nil, nil
+ }
+
+ _, err = service.CreateCheckout(ctx, CheckoutInput{TenantID: tenantID, AmountMinor: 500})
+ if err == nil || !strings.Contains(err.Error(), "incomplete Checkout Session") {
+ t.Fatalf("CreateCheckout error = %v", err)
+ }
+ var status, reconciliationStatus, reconciliationError string
+ if err := service.db.QueryRow(ctx, `SELECT status,reconciliation_status,reconciliation_error
+ FROM topup_orders WHERE tenant_id=$1 ORDER BY created_at DESC LIMIT 1`, tenantID).
+ Scan(&status, &reconciliationStatus, &reconciliationError); err != nil {
+ t.Fatal(err)
+ }
+ if status != "failed" || reconciliationStatus != "unknown" || !strings.Contains(reconciliationError, "incomplete Checkout Session") {
+ t.Fatalf("order state status=%q reconciliation=%q error=%q", status, reconciliationStatus, reconciliationError)
+ }
+}
+
func TestStripeWebhookCreditsPaidOrderExactlyOncePostgres(t *testing.T) {
databaseURL := os.Getenv("AIGW_TEST_DATABASE_URL")
if databaseURL == "" {
@@ -434,4 +580,32 @@ func TestSettlementWorkerPersistsUsageAndReleasesReservationPostgres(t *testing.
if missingReservationStatus != "metering_failed" || missingJobStatus != "done" || balance != 990 || reserved != 30 || meteringStatus != "missing" {
t.Fatalf("missing usage reservation=%s job=%s balance=%d reserved=%d metering=%s", missingReservationStatus, missingJobStatus, balance, reserved, meteringStatus)
}
+ release, err := service.ReleaseUnmeteredReservation(ctx, tenantID, missingRequestID,
+ ReleaseReservationInput{Reason: "provider returned a non-meterable success"}, ResolutionActor{ID: "test-operator", Type: "integration"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if release.Status != "released" || release.ReservedMicros != 30 {
+ t.Fatalf("unexpected reservation release: %+v", release)
+ }
+ if err := service.db.QueryRow(ctx, `SELECT balance_micros,reserved_micros FROM tenant_wallets WHERE tenant_id=$1`, tenantID).Scan(&balance, &reserved); err != nil {
+ t.Fatal(err)
+ }
+ var releaseLedgerCount int
+ if err := service.db.QueryRow(ctx, `SELECT count(*) FROM billing_ledger WHERE source_type='unmetered_reservation' AND source_id=$1 AND kind='release' AND amount_micros=0`, missingRequestID).Scan(&releaseLedgerCount); err != nil {
+ t.Fatal(err)
+ }
+ if balance != 990 || reserved != 0 || releaseLedgerCount != 1 {
+ t.Fatalf("released wallet balance=%d reserved=%d ledger=%d", balance, reserved, releaseLedgerCount)
+ }
+ if err := service.db.QueryRow(ctx, `SELECT metering_status FROM usage_events WHERE request_id=$1`, missingRequestID).Scan(&meteringStatus); err != nil {
+ t.Fatal(err)
+ }
+ if meteringStatus != "released_unmetered" {
+ t.Fatalf("released metering status=%s", meteringStatus)
+ }
+ if _, err := service.ReleaseUnmeteredReservation(ctx, tenantID, missingRequestID,
+ ReleaseReservationInput{Reason: "idempotent retry"}, ResolutionActor{ID: "test-operator", Type: "integration"}); err != nil {
+ t.Fatalf("idempotent release retry: %v", err)
+ }
}