* feat(sca): add per-user spend cap override
* feat(sca): add admin endpoint to set per-user order cap
- PATCH /api/v1/users/{id}/order-cap (RequireAdmin / X-Admin-Key) sets or
clears User.MaxOrderCents; null clears (revert to global), >=0 sets,
negative rejected 400 INVALID_CAP at the write boundary
- harden ConvertCart: a negative override falls back to the global cap with
a slog.Warn (mirrors loadMaxOrderCents) so a bad DB write cannot silently
void the fraud control
- emit user_order_cap_set audit event (set/cleared)
- tests: admin handler table + RequireAdmin gate router test + negative
read-path fallback case
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(api): drop orphan comment referencing nonexistent request struct
The paragraph above SetUserOrderCap documented a setUserOrderCapRequest
struct that does not exist; the handler decodes into a raw map instead.
The raw-map rationale already lives inline at the decode site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(audit): emit cart_rejected event when per-order cap blocks checkout
* refactor(audit): drop dead persist call + harden cart_rejected test capture
* feat(sca): sweep abandoned 3ds requires_action orders to failed
* fix(sca): address pr review on abandoned 3ds sweep
- Drop stripe.Key write in ReconcileStale3DSOrders; wire once in api/main.go after cfg.Load so concurrent reconcilers don't
race the SDK global.
- Add empty-PI short-circuit before paymentintent.Get to bury a future regression in a slog.Warn instead of a Stripe 4xx.
- Swap fmt.Printf for slog.Warn with k/v attrs in loadAbandoned3DSThresholdMinutes so Loki sees structured fields instead of
literal %q/%d.
- Comment the updated_at predicate (RetryAuth bumps it via GORM auto-stamp, intentional grace-window extension) + the
conditional-UPDATE/map-form choice.
- Tests: processing/requires_capture skip, unknown-status skip, stripe-error skip, empty-PI skip, full env-parse table for
loadAbandoned3DSThresholdMinutes.
* fix(stripe): wire key once at init drop per-call writes
* refactor(stripe): drop dead stripeKey fields and pre-call sdk params
* feat(sca): add retry-auth endpoint for failed 3ds challenges
* fix(sca): retry-auth unexpected-state branch out of card-decline
just fixes up in some of the things the review of the last commit
noticed
* feat(sca): expire inactive saved cards
* fix(sca): address card ttl review feedback
- Correct sql typos breaking GET /cards and /view/init (used_id, id_default)
- Stop swallowing backfillCardStorageExpiration error in Migrate
- Wrap expireStoredCard local writes in a transaction and run stripe
detach + audit only after commit, preventing double-fire on retry
- Drop redundant ttl refresh in ConvertCart; webhook is authoritative
- Rename validation_expired_at metric label to validation_card_expired
- Fix CardStorageExpiresAt doc comment and test failure message typos
- Add handler-level tests for expireStoredCards (user scoping + cart deref)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(sca): move card expiration to periodic reconciler
Read paths (GetCards, GetViewInit) now filter expired cards from
query results instead of detaching them inline, so request handlers
no longer block on Stripe API calls. Physical deletion runs in a
new ReconcileExpiredCards goroutine on an hourly ticker, mirroring
the existing ReconcileOrders / ReconcileUnshipped jobs. Per-card
action sites (SetCard, SetDefaultCard, ConvertCart) still expire
synchronously since they operate on one row the user is acting on.
ViewHandler's stripeKey dependency is dropped now that GetViewInit
no longer touches Stripe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sca): address card ttl review feedback
- guard zero order.CardID before refreshing card storage ttl in
payment_intent.succeeded webhook to avoid noisy warn logs on payment
paths that did not use a saved card
- delete unused Card.RefreshStorageTTL model method; the SQL-only
refreshCardStorageTTL helper is the single canonical path
- clarify duplicate-card webhook path: storage_expires_at reset is a
card-add refresh, not a payment-success refresh, so last_used_at
intentionally stays untouched
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(sca): cover card ttl read paths and expired-card sync sites
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(sca): add order status endpoint adn SCA error code mapping
adds server-side infra for the upcoming SCA / 3DS challenge flow im
working on. No behavior change to existing checkout paths.
* fix(sca): suppress unused lint on SCA scaffolding helpers
Adds POST /api/v1/orders/{id}/label (admin-only) that buys a shipping
label through Shippo and writes carrier + tracking + label fields back
onto the order, transitioning paid -> shipped.
Audit events now emit at Info/Warn/Error so the log-level counter has variety
instead of being pinned to info. Panic recovery uses structured slog.Error
with method/path/stack attrs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
adds a /pay/{token} redirect endpoint that generates a short URL
pointing to a stripe checkout sessions in setup mode. The TUI shows a QR
code and URL on the payment page so users can add a card via the browser
instead of entering raw card data over SSH.
the checkout.session.completed webhook saves the card to the database.
the TUI polls GET /api/v1/cards every second and auto-proceeds to
confirmation once the new card is detected.
also adds a dev.sh startup script that runs all four required processes.
(SSH server, API server, Stripe CLI listener, ngrok) in a single
terminal