summaryrefslogtreecommitdiff
path: root/internal/billing/operations.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/billing/operations.go84
1 files changed, 79 insertions, 5 deletions
diff --git a/internal/billing/operations.go b/internal/billing/operations.go
index a6dc653..461c59a 100644
--- a/internal/billing/operations.go
+++ b/internal/billing/operations.go
@@ -19,13 +19,13 @@ func (s *Service) CreatePortalSession(ctx context.Context, tenantID string) (Por
if !s.stripeEnabled || s.stripeClient == nil {
return PortalResult{}, ErrStripeDisabled
}
- var customerID string
- if err := s.db.QueryRow(ctx, `SELECT stripe_customer_id FROM stripe_customers WHERE tenant_id=$1`, tenantID).Scan(&customerID); err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
- return PortalResult{}, errors.New("no Stripe customer exists for this account")
- }
+ customerID, err := s.ensureStripeCustomer(ctx, tenantID)
+ if err != nil {
return PortalResult{}, err
}
+ if customerID == "" {
+ return PortalResult{}, errors.New("no Stripe customer exists for this account")
+ }
session, err := s.stripeClient.V1BillingPortalSessions.Create(ctx, &stripe.BillingPortalSessionCreateParams{
Customer: stripe.String(customerID), ReturnURL: stripe.String(s.stripePortalReturnURL),
})
@@ -353,6 +353,12 @@ func (s *Service) RunStripeOperations(ctx context.Context) {
cancel()
s.refreshOperationalMetrics(ctx)
for {
+ for i := 0; i < 4; i++ {
+ ok, _ := s.processAutoTopUpOnce(ctx)
+ if !ok {
+ break
+ }
+ }
for i := 0; i < 8; i++ {
ok, _ := s.processRefundOperation(ctx)
if !ok {
@@ -793,6 +799,74 @@ func (s *Service) Reconcile(ctx context.Context, limit int) (ReconciliationResul
return s.failReconciliation(ctx, result, fmt.Errorf("record clean reconciliation for order %s: %w", item.id, err))
}
}
+ if s.retrieveStripePaymentIntent == nil {
+ return s.failReconciliation(ctx, result, errors.New("Stripe PaymentIntent retrieval is unavailable"))
+ }
+ piRows, err := s.db.Query(ctx, `SELECT id::text,stripe_payment_intent_id,status,amount_minor,currency FROM topup_orders
+ WHERE trigger_type='auto' AND stripe_payment_intent_id IS NOT NULL ORDER BY created_at DESC LIMIT $1`, limit)
+ if err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ type paymentIntentOrder struct {
+ id, paymentIntent, status, currency string
+ amount int64
+ }
+ var paymentIntentOrders []paymentIntentOrder
+ for piRows.Next() {
+ var item paymentIntentOrder
+ if err := piRows.Scan(&item.id, &item.paymentIntent, &item.status, &item.amount, &item.currency); err != nil {
+ piRows.Close()
+ return s.failReconciliation(ctx, result, err)
+ }
+ paymentIntentOrders = append(paymentIntentOrders, item)
+ }
+ piRows.Close()
+ for _, item := range paymentIntentOrders {
+ intent, retrieveErr := s.retrieveStripePaymentIntent(ctx, item.paymentIntent, &stripe.PaymentIntentRetrieveParams{})
+ result.CheckedOrders++
+ if retrieveErr != nil {
+ message := truncateError(retrieveErr)
+ if err := s.updateOrderReconciliation(ctx, item.id, "mismatch", message); err != nil {
+ return s.failReconciliation(ctx, result, fmt.Errorf("record automatic top-up retrieval failure for order %s: %w", item.id, err))
+ }
+ result.Mismatches = append(result.Mismatches, map[string]any{"order_id": item.id, "type": "payment_intent_retrieve_failed", "error": message})
+ continue
+ }
+ if intent == nil || intent.Amount != item.amount || string(intent.Currency) != item.currency {
+ if err := s.updateOrderReconciliation(ctx, item.id, "mismatch", "amount or currency mismatch"); err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ result.Mismatches = append(result.Mismatches, map[string]any{"order_id": item.id, "type": "payment_intent_mismatch"})
+ continue
+ }
+ expectedPaid := item.status == "paid" || item.status == "partially_refunded" || item.status == "refunded" || item.status == "disputed"
+ stripePaid := intent.Status == stripe.PaymentIntentStatusSucceeded
+ if stripePaid && !expectedPaid {
+ if repairErr := s.creditAutoTopUpPaymentIntent(ctx, intent); repairErr != nil {
+ message := truncateError(repairErr)
+ result.Mismatches = append(result.Mismatches, map[string]any{"order_id": item.id, "type": "payment_intent_repair_failed", "error": message})
+ if err := s.updateOrderReconciliation(ctx, item.id, "mismatch", message); err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ continue
+ }
+ result.Repairs = append(result.Repairs, map[string]any{"order_id": item.id, "type": "credited_paid_payment_intent"})
+ if err := s.updateOrderReconciliation(ctx, item.id, "repaired", ""); err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ continue
+ }
+ if expectedPaid != stripePaid {
+ result.Mismatches = append(result.Mismatches, map[string]any{"order_id": item.id, "type": "payment_intent_state_mismatch", "local_status": item.status, "stripe_status": intent.Status})
+ if err := s.updateOrderReconciliation(ctx, item.id, "mismatch", "payment state mismatch"); err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ continue
+ }
+ if err := s.updateOrderReconciliation(ctx, item.id, "ok", ""); err != nil {
+ return s.failReconciliation(ctx, result, err)
+ }
+ }
result.MismatchCount = int64(len(result.Mismatches))
result.Status = "clean"
if result.MismatchCount > 0 {