* refactor(sca): thread context.Context through reconciler entry points
* test(sca): add mid-loop cancel test for stale 3ds sweep
* refactor(api): rename shutdown ctx to shutdownCtx for clarity
* style(sca): fix typo in ReconcileUnshipped commend
* 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): 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>
so the ain was that older paid orders were getting buried in the #orders
channel in slack and i would just forget about them. Pin/unpin from
earlier helped, but if i missed pinning one or the channel got noisy, it
was still easy to lose track.
this adds a 24h cron that queries any paid order older than 24h with no
label yet and dumps them all into one #orders message. Each line shows
the order id, how old it is, total, where it is going, and a permalink
back to the orders slack thread so i can hop in and hit Buy Label
Ports the idea from terminal.shops cron/unshipped.ts but out stuch state
is different( we auto-promote paid->shipped on label-buy so
PRE_TRANSIT-stuck can not happen), so the query is "paid + no tracking
number + >24h" instead.
NO orders get a "Bring blocked" tag since i dont have the norwegian org
set up yet and can not actually ship those today. Also fires once 40s
after startup so a deploy does not black out the reminder for a full
day.
Adds Prometheus instrumentation across the checkout funnel, auth flow,
Stripe calls, rate limiters, and HTTP middleware (inflight gauge, request
and response sizes). Introduces a slog handler that counts log lines by
level so the dashboard can show error/warn rates; existing log.Printf
calls are routed through slog and counted as info.
Backs the new "terminalshop — Service Overview" Grafana dashboard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stripe checkout URLs are too long to produce a scannable QR code in a
terminal. Add an in-memory token→URL store and a /pay/{token} redirect
endpoint so the TUI shows a short URL (e.g. localhost:8080/pay/a3f1c2b4)
that fits in a QR. APP_URL config controls the base URL (defaults to
API_URL). Revert to returning sess.URL directly in CollectCard if this
approach needs to be abandoned.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a background goruotine that runs every 5 minutes to find pending
orders with no Stripe ID older than 10 mins and patches them if stripe
shows a succeeded PaymentIntent. Covers the edge case where the server
crashes after charging but before the DB transaction commits. Also tags
PaymentIntents with order_id metadata at createion to make them
searchable
the last implementation just accepted any input for the shipping
validation and then only validated the address on payment. this is now
changed to validation upon creation upon finishing the shipping address
form.
when a user completes the shipping form, a client-side check is first
made to just simply check if there are any empty fields, if this check
passes it gets check server-side by shippo. If the address is not found
it will return an address not found error, and the user will not be able
to continue to the payment form. The invalid address will also not be
saved to the database.
if the check pass, we simply save the address to the database with an
ID. this ID is then passe in when doing the payment, instead of
validating at the payment phase.
this implementation currently only uses Shippo, which i have come to
realize does NOT support shipping in countries outside of the US (i
should have read the friendly manual before using Shippo). This means
that a huge rewrite is due.
i will focus on adding address validation for Norway first since thats
where i live. i plan on using Bring's api for this validation. i will
still keep Shippo for US based address verification even though this
project is focused fon nordic shipping - just for a rainy day if some
poor soul decides to order from AMERICA