openova/clusters/_template
e3mrah 59367b2fe5
fix(billing): transactional voucher redemption — only decrement on order.placed success (Closes #2000) (#2011)
t38 walk caught the canonical TBD-V9 bug: customer redeems voucher
WALK-T38-2138 on a 50 OMR order, voucher credit is only 10 OMR, Stripe
is unconfigured in the Sovereign, Checkout returns 503 "payment processor
not configured" — but promo_codes.times_redeemed had already advanced
0→1, promo_redemptions row was inserted, and a credit_ledger grant was
written. Voucher shows "Exhausted 1/1" with no order to show for it; the
customer's one-per-customer promo is silently burned.

Root cause: store.RedeemPromoCode runs its own transaction (necessary
for the FOR UPDATE concurrency cap) and commits the three side effects
up front. The rest of the Checkout pipeline (GetCreditBalance, GetSettings,
CreditOnlyCheckout, Stripe customer + session creation) can fail without
undoing the redemption.

Fix (saga / compensating action):
- store.RollbackPromoCodeRedemption(customerID, code) — single tx that
  DELETEs promo_redemptions, decrements times_redeemed (GREATEST(..,0)
  underflow guarded), and DELETEs the credit_ledger redeem grant (filter
  reason='promo:<code>' AND order_id IS NULL so order spend ledger rows
  are not touched). Idempotent: 0-row DELETE on promo_redemptions
  short-circuits the rest, so re-running a failed checkout never
  double-decrements.
- handlers.Checkout tracks voucherRedeemed and calls
  RollbackPromoCodeRedemption on every downstream failure: settings load,
  Stripe-unconfigured 503 (the t38 walk path), CreateOrder failure,
  Stripe customer rejection, Stripe session rejection, plan-price
  unresolvable.
- Voucher only stays committed once (a) CreditOnlyCheckout commits the
  order+spend+sub transactionally and order.placed fires, or (b) the
  Stripe Checkout Session URL is handed back to the customer (canonical
  abandoned-cart: credit persists on ledger for the next attempt).

Tests:
- store_test.go: three new tests cover the rollback contract — happy
  path (all three side effects undone in one tx), idempotent no-op
  when no redemption row exists, empty-args no-op (no DB hit).
- checkout_test.go: TestCheckout_VoucherPartialCover_StripeUnconfigured_RollsBackRedemption
  is the t38 regression — full sqlmock walk asserting the rollback tx
  fires before the 503 response.

bp-catalyst-platform Chart.yaml + bootstrap-kit pin bumped 1.4.214 → 1.4.215.

Co-authored-by: Claude Code (hatiyildiz) <claude@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:21:34 +04:00
..
bootstrap-kit fix(billing): transactional voucher redemption — only decrement on order.placed success (Closes #2000) (#2011) 2026-05-20 03:21:34 +04:00
flux-system feat(day2-iac): Crossplane Compositions + per-Sovereign Flux cluster tree + catalyst-dns binary 2026-04-28 14:09:29 +02:00
infrastructure fix(bp-crossplane): align ProviderConfig secretRef with cloud-init seam (Refs #1947) (#1963) 2026-05-19 19:23:04 +04:00
sovereign-tls fix(sovereign-tls): cap Gateway annotations at 8 to satisfy gateway-api CRD (TBD-A36, Closes #1896, Refs #1897) (#1898) 2026-05-19 06:15:41 +04:00
kustomization.yaml fix(provisioner): cloud-init bootstrap-kit path matches per-FQDN cluster dir (resolves #218) (#256) 2026-04-30 17:11:44 +04:00