Commit Graph

218 Commits

Author SHA1 Message Date
7b08e794aa fix(tui): rework error and notice messages to be at the top of the tui, fixing the entire UI shifting when a message pops up 2026-06-19 18:17:14 +02:00
8288fa866c fix(tui): remove horrible code that was taking up space 2026-06-19 16:58:53 +02:00
9d5096b270 fix(privacy): forget saved card after privacy-mode checkout (#59) 2026-06-19 10:04:35 +02:00
85ed872c28 feat(privacy): add privacy mode (#58) 2026-06-19 09:31:44 +02:00
2c12ba586e feat(sca): charge checkout as customer-initiated transaction (#57)
Drop off_session from the initial PaymentIntent so the checkout charge is a Customer-Initiated Transaction (the customer is present in the TUI), not a Merchant-Initiated Transaction. MIT exemptions require a stored mandate we never capture; as a CIT, an SCA challenge surfaces as status=requires_action, the path the handler already had. The re-confirm in respondRequiresAction and the authentication_required error branch remain as defensive fallbacks. Source: stripe-samples accept-a-payment custom-payment-flow (no off_session on customer-present confirms).

Also clarify the MAX_ORDER_CENTS TRA-ceiling anchor comment as an approximate USD/EUR figure (config.go), and carry an unrelated 1-line spend-limit input width fix in pkg/tui/account.go.

Tests: add paymentIntentNewFn stub seam, TestConvertCartIsCustomerInitiated (asserts no off_session) and TestConvertCartRequiresAction (202 + redirect + audit row).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:48:00 +02:00
0c0b8e8059 fix(tui): accept spend limit input in dollars (#56)
Spend-limit input now takes dollars (e.g. 25.50) instead of raw cents.
Parsed to integer cents as dollars*100 + cents (no float multiply) so an
amount like 50.01 never loses a cent to binary rounding; the persisted
column and API still use cents. Current limit and prefill render via
formatCentsDollars. Tests updated to the dollar grammar.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:34:03 +02:00
7148ff1d4d feat(sca): add customer-set spend limit (#55) 2026-06-17 13:25:04 +02:00
22eeeeeac1 feat(sca): enforce user self-limit as lower-only cap in convert-cart (#54) 2026-06-17 10:24:55 +02:00
aa446cdfa5 feat(sca): add per-user spend cap override (#53)
* 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>
2026-06-16 14:56:38 +02:00
8df1058932 test(api): cover everyTick helper (#52)
* test(api): cover everyTick helper

* test(api): make everyTick cancel test deterministic
2026-06-16 13:34:55 +02:00
64b6f0d91a refactor(api): extract everyTick helper for reconciler goroutines (#51) 2026-06-15 10:17:02 +02:00
5dd196834f feat/sca pay redirects sweep (#50)
* feat(sca): sweep expired pay_redirects rows on a ticker

* fix(fix): revert the 10-second ticker to 10m after forgetting to revert it after testing
2026-06-15 00:39:31 +02:00
e85dc7fd4d fix(webhook): fail closed when stripe webhook secret is not set (#45) 2026-06-14 22:12:36 +02:00
ff562c7427 docs(docs): update docs to get rid of gopls warnings. update some code to newer standards (#49) 2026-06-14 21:42:02 +02:00
aa34043e9b feat(tuie): drive roast meter from product roast_level field (#48) 2026-06-11 16:28:04 +02:00
5dd22fb8be style(tui): update rest of tui to new ui pattern (#47) 2026-06-11 15:53:48 +02:00
52d533703b feat(tui): remake parts of the tui (#46)
* feat(tui): remake parts of the tui

* chore(tui): remove unused newViewport helper
2026-06-11 11:24:56 +02:00
c9f8b1e674 test(test): smoke test deploy script with dirty box tree (#44) 2026-06-09 19:39:09 +02:00
d592249efb fix: fail deploy loudly on stale pull or HEAD mismatch (#43) 2026-06-09 19:20:08 +02:00
cfc97eb1ff fix/temp disable sshCardAdd button (#42)
* fix: disable add-card-via-ssh, mark coming soo, skip in cursor nav

* style(tui): reword (coming soon) to (WIP)
2026-06-09 14:33:09 +02:00
c112797875 refactor(sca): thread context.Context through reconciler entry points (#41)
* 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
2026-06-09 00:31:56 +02:00
75225548ae feat(sca): emit abandoned_3ds_sweep_total counter from stale 3ds sweep (#40)
* feat(sca): emit abandoned_3ds_sweep_total counter from stale 3ds sweep

* test: assert sweep counter labels in remaining branch tests
2026-06-08 23:11:44 +02:00
53e934be3b chore(cap): raise per-order spend cap default from 200 to 500 (#39) 2026-06-07 23:47:43 +02:00
fc02f331d2 feat(audit): emit cart_rejected event when per-order cap blocks checkout (#38)
* feat(audit): emit cart_rejected event when per-order cap blocks checkout

* refactor(audit): drop dead persist call + harden cart_rejected test capture
2026-06-07 23:25:30 +02:00
bb960c31f8 feat(sca): sweep abandoned 3ds requires_action orders to failed (#37)
* 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
2026-06-07 18:16:33 +02:00
8b753e4f4b feat(sca): add requires_action orde status for 3ds in flight sate (#36)
* feat(sca): add requires_action orde status for 3ds in flight sate

* fix(sca): fix stuff the review commented on
2026-06-06 21:06:25 +02:00
90645958e5 chore(repo): change all instances of interface{} to any as of newer go standards (#35) 2026-06-06 13:59:09 +02:00
135f1361e4 fix(products): update pricing on products (#34)
* fix(products): update pricing on products

* fix(test): pin coffee price in cart-limit fixture

TestConvertCartLimit's quantities and expected totals were keyed to
the original 500-cent latte price. After the product price bump in
1ce63fb, coffee_id=4 became 2500 cents, so under_cap/at_cap subtests
overshot the $200 cap and over_cap's expected total magnitude was
wrong by 5x. Override coffee_id=4's price to 500 in test setup so the
cap math stays stable regardless of seed changes.
2026-06-05 23:49:03 +02:00
64deba7e03 feat(sca): wire retry-auth keybind on authfailed review screen (#33) 2026-06-05 22:52:54 +02:00
732353d9a4 fix(tui): fix the menu always selecting the first entry in the refund page (#32) 2026-06-04 00:09:07 +02:00
3420c8dce0 feat(sca): add retry-auth endpoint for failed 3ds challenges (#31)
* 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
2026-06-03 23:42:27 +02:00
f3b94c3271 chore(test): add gotestsum + tparse runner with ci wiring (#30)
* chore(test): add gotestsum + tparse runner with ci wiring

* chore(tests): change the tests around a little
2026-06-02 23:17:03 +02:00
67e19eca98 feat(sca): expire inactive saved cards (#29)
* 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>
2026-06-02 21:49:48 +02:00
728ac24c96 fix(sca): return over-limit checkout errors to cart (#28) 2026-06-02 10:28:56 +02:00
1096c85b10 chore(ops): add nightly backup verification scripts (#27) 2026-06-02 00:02:27 +02:00
2f65cf0d90 feat(sca): cap per-order total ro prevent oversize charges (#26) 2026-06-01 22:40:20 +02:00
c3e6eb2b84 fix(ci): pin deploy go-version to go.mod to match ci.yml (#25) 2026-05-30 23:29:19 +02:00
03009ea6df chore(ci): bump checkout and setup-go to node24-ready majors (#24) 2026-05-30 23:19:29 +02:00
c8b7545ca6 chore(ci): skip golangci-lint under act to avoid node path issue (#23) 2026-05-30 19:48:20 +02:00
3a3c7ee80b feat(sca): persist redirect tokens to sqlite with 10min ttl (#22) 2026-05-30 14:12:57 +02:00
7c8a2b562f feat(sca): handle 3ds checkout end to end with qr poll and race fix (#21)
* feat(sca): handle 3ds checkout end to end with qr poll and race fix

* fix(sca): drop unused pollOrderStatusCmd helper
2026-05-28 12:41:36 +02:00
1c5765d25b fix(sca): pass payment method on reconfirm so 3ds next_action surfaces (#20) 2026-05-28 12:20:35 +02:00
e6301e662f fix(sca): re-confirm payment intent on-session to surface 3ds next_action (#19) 2026-05-28 12:05:41 +02:00
8b2889a524 fix(sca): re-fetch payment intent when error response omits next_action (#18) 2026-05-28 11:53:54 +02:00
c2b2b6e834 fix: allow redirect next_action so 3ds challenge url surfaces (#17) 2026-05-28 11:37:55 +02:00
bba5a0dfa0 feat(sca): return 202 + 3ds redirect token when checkout requires auth (#16) 2026-05-28 11:20:16 +02:00
a8e45bc82b feat(sca): map stripe error codes to user-facing copy in tui (#15) 2026-05-28 10:28:47 +02:00
95b9fa91b1 feat(sca): add order status endpoint adn SCA error code mapping (#14)
* 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
2026-05-27 19:23:29 +02:00
0fe013c399 feat(cart): pass off_session=true on PaymentIntent for SCA exemption eligibility (#13) 2026-05-27 18:09:50 +02:00
d9404ab31e style(tui): update refund page ui (#12) 2026-05-25 23:18:42 +02:00