From eadb2ffe85c43cf6fc741c9823cd28eedb4a844c Mon Sep 17 00:00:00 2001 From: Chia Date: Wed, 5 Aug 2026 22:01:29 +1200 Subject: feat: harden prepaid billing and commercial operations --- cmd/reconcile-billing/main.go | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 cmd/reconcile-billing/main.go (limited to 'cmd/reconcile-billing/main.go') diff --git a/cmd/reconcile-billing/main.go b/cmd/reconcile-billing/main.go new file mode 100644 index 0000000..79329ad --- /dev/null +++ b/cmd/reconcile-billing/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "time" + + "aigw/internal/billing" +) + +func main() { + databaseURLEnv := flag.String("database-url-env", "AIGW_DATABASE_URL", "environment variable containing the PostgreSQL URL") + stripeKeyEnv := flag.String("stripe-key-env", "AIGW_STRIPE_API_KEY", "environment variable containing the Stripe restricted key") + currency := flag.String("currency", "usd", "wallet currency") + limit := flag.Int("limit", 200, "maximum recent top-up orders to reconcile") + resolveMissing := flag.Bool("resolve-confirmed-missing", false, "close uncredited pending orders whose Stripe Sessions are confirmed missing") + resolutionReason := flag.String("resolution-reason", "maintenance reconciliation confirmed the uncredited Checkout Session is absent from the configured Stripe account", "auditable reason for resolving missing orders") + flag.Parse() + databaseURL, stripeKey := os.Getenv(*databaseURLEnv), os.Getenv(*stripeKeyEnv) + if databaseURL == "" || stripeKey == "" { + fmt.Fprintln(os.Stderr, "database and Stripe key environment variables are required") + os.Exit(1) + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + service, err := billing.New(ctx, billing.Options{DatabaseURL: databaseURL, Currency: *currency, StripeEnabled: true, StripeAPIKey: stripeKey}) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer service.Close() + result, err := service.Reconcile(ctx, *limit) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if *resolveMissing { + for _, mismatch := range result.Mismatches { + if mismatch["type"] != "stripe_session_missing" { + continue + } + orderID, _ := mismatch["order_id"].(string) + order, getErr := service.GetTopUpOrder(ctx, "", orderID) + if getErr != nil { + fmt.Fprintln(os.Stderr, getErr) + os.Exit(1) + } + var resolveErr error + if order.Status == "paid" { + _, resolveErr = service.ReverseMissingTopUpCredit(ctx, order.TenantID, order.ID, + billing.ResolveMissingTopUpInput{Reason: *resolutionReason}, billing.ResolutionActor{ID: "reconcile-billing", Type: "maintenance"}) + } else { + _, resolveErr = service.ResolveMissingTopUp(ctx, order.TenantID, order.ID, + billing.ResolveMissingTopUpInput{Reason: *resolutionReason}, billing.ResolutionActor{ID: "reconcile-billing", Type: "maintenance"}) + } + if resolveErr != nil { + fmt.Fprintln(os.Stderr, resolveErr) + os.Exit(1) + } + } + result, err = service.Reconcile(ctx, *limit) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(result); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if result.MismatchCount > 0 { + os.Exit(2) + } +} -- cgit v1.2.3