summaryrefslogtreecommitdiff
path: root/internal/billing/operations_test.go
blob: 4d903836d767982118137087408025ba9b5076f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package billing

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"

	"aigw/internal/controlplane"

	"github.com/stripe/stripe-go/v86"
)

func TestOperationalStatusReadiness(t *testing.T) {
	now := time.Now().UTC()
	recent := now.Add(-time.Minute)
	clean := OperationalStatus{
		StripeEnabled: true, ReconciliationStatus: "clean", ReconciliationCompletedAt: &recent,
	}
	if !clean.Ready(now) {
		t.Fatal("clean operational state should be ready")
	}
	oldWebhook := now.Add(-6 * time.Minute)
	stuckWebhook := clean
	stuckWebhook.UnprocessedWebhooks = 1
	stuckWebhook.OldestUnprocessedWebhook = &oldWebhook
	if stuckWebhook.Ready(now) {
		t.Fatal("stuck webhook should fail readiness")
	}
	freshRefund := now.Add(-time.Minute)
	processingRefund := clean
	processingRefund.RefundBacklog = 1
	processingRefund.OldestRefund = &freshRefund
	if !processingRefund.Ready(now) {
		t.Fatal("fresh refund operation should remain ready during its processing window")
	}
	unmetered := clean
	unmetered.UnmeteredSuccesses = 1
	if unmetered.Ready(now) {
		t.Fatal("unmetered success must fail readiness")
	}
}

func TestCustomerPortalSessionContractPostgres(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",
		StripePortalReturnURL: "https://console.example.test/billing",
	})
	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,'Portal contract') RETURNING id::text`, fmt.Sprintf("portal-%d", suffix)).Scan(&tenantID); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		if _, cleanupErr := service.db.Exec(context.Background(), `DELETE FROM tenants WHERE id=$1`, tenantID); cleanupErr != nil {
			t.Errorf("cleanup portal contract tenant: %v", cleanupErr)
		}
	})
	customerID := fmt.Sprintf("cus_portal_%d", suffix)
	if _, err := service.db.Exec(ctx, `INSERT INTO stripe_customers (tenant_id,stripe_customer_id,email) VALUES ($1,$2,'billing@example.test')`, tenantID, customerID); err != nil {
		t.Fatal(err)
	}
	service.createStripePortalSession = func(_ context.Context, params *stripe.BillingPortalSessionCreateParams) (*stripe.BillingPortalSession, error) {
		if params.Customer == nil || *params.Customer != customerID || params.ReturnURL == nil || *params.ReturnURL != service.stripePortalReturnURL {
			t.Fatalf("unexpected Portal params %+v", params)
		}
		return &stripe.BillingPortalSession{URL: "https://billing.stripe.test/session"}, nil
	}
	result, err := service.CreatePortalSession(ctx, tenantID)
	if err != nil || result.URL != "https://billing.stripe.test/session" {
		t.Fatalf("CreatePortalSession result=%+v err=%v", result, err)
	}
	service.createStripePortalSession = func(context.Context, *stripe.BillingPortalSessionCreateParams) (*stripe.BillingPortalSession, error) {
		return nil, nil
	}
	if _, err := service.CreatePortalSession(ctx, tenantID); err == nil {
		t.Fatal("incomplete Stripe Portal response was accepted")
	}
}