116 Commits

Author SHA1 Message Date
ebd3e20285 chores/random chores (#60)
* fix(tui): remove horrible code that was taking up space

* 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-29 20:14:05 +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
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
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
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
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
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
2f65cf0d90 feat(sca): cap per-order total ro prevent oversize charges (#26) 2026-06-01 22:40:20 +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
a8e45bc82b feat(sca): map stripe error codes to user-facing copy in tui (#15) 2026-05-28 10:28:47 +02:00
d9404ab31e style(tui): update refund page ui (#12) 2026-05-25 23:18:42 +02:00
a21485f91d fix(tui): way to much to write (#11) 2026-05-25 22:34:25 +02:00
bce94b6d10 feat(refund): add refund request flow (#10)
* feat(refund): add refund request flow

- tui modal composer with reason select + free-form message
  (pkg/tui/refund.go)
- POST /api/v1/orders/{id}/refund-request posts to orders slack thread
- 15 min server side cooldown via Order.LastRefundRequestAt (429 +
  retry-after)
- shared refundrequestreasons + can requestrefund hoisted to pkg/models
- slack message includes customer name, email, order total, quoted body
- tests over invalid-state, missing-message, and cooldown paths

* style(tui): run gofmt to fix formatting
2026-05-25 22:04:03 +02:00
dee6172875 refactor(tui): port to charm v2 (#9)
* refactor(tui): port to charm v2

bumped bubbletea, lipgloss, bubbles, wish, and huh to v2. Mostly
mechanical chur, new import paths, vewport getter/setter api, view()
returns tea.view now, huh.theme became an interface so the theme package
needed a small reshape. Render plumbing is gone (no more lipgloss.Render
field on the Model or MakeRenderer in main.go). All pages still work
over ssh. setting up for the refund composer next

* style(tui): gofmt main.go and menu.go
2026-05-25 19:09:16 +02:00
48d65c48fa feat(tui): render customer-facing event timeline in order detail veiew (#7)
Pulls events off the new order_events table and shows them as a timeline
  block in the order detail view: Paid, Label created, In transit,
  Delivered (etc) with timestamps. Filters out ops-only events like
  payment_critical, label_orphaned, and tracking_marked_manually so the
  customer never sees internal plumbing.

  DisplayLabel on OrderEvent decides what's customer-visible and what's
  redacted, so the same order_events rows can later power an admin view
  with no filter.
2026-05-24 20:14:14 +02:00
60f52f9851 add ci (#1)
* ci(checks): add pull request workflow and golangci-lint config

* refactor(lint): resolve golangci-lint findings

* style(tui): drop traling blank line for gofmt

* fix(cards): handle crypto/rand.Read error in storeRedirect
2026-05-23 21:43:30 +02:00
a4c446ba3c fix(tui): change the stripe abandoned payment polling duration from 2 to 10 mins 2026-05-23 13:15:15 +02:00
491f70e935 fix: add 2-minute timeout to browser payment poll so abandoned stripe checkout sessions stop polling 2026-05-23 12:50:35 +02:00
1752ca534f chore(tui): update some typage 2026-05-22 22:18:44 +02:00
b2bd6ed623 fix(cards): advance tui after duplicate card detection by pollin on updated_at instead of count 2026-05-22 22:09:55 +02:00
9d3ac9761d feat(tui): add tracking redirect endpoint, render the short uril in the tui order detail instead of the old broken stuff ive been trying. this change is made by claude code and has not been reviewed carefully, this might break 2026-05-20 15:26:38 +02:00
e49fa01ca9 feat(tui): use BEL terminator for tracking url osc 8 link - hopefully this works 2026-05-20 15:12:56 +02:00
d9e9895913 revert(tui): render raw tracking url, last change wouldnt render the url for some reason 2026-05-20 15:07:14 +02:00
f73e1d18da feat(tui): make tracking url clickable osc 8 hyperlink 2026-05-20 14:55:32 +02:00
1aaf82d7d6 feat(tui): poll orders every 10 seconds so that the order status refreshes live in the tui without logging out and in 2026-05-19 20:12:03 +02:00
9515603b32 feat(tui): split account orders into active and history tabls 2026-05-19 19:50:21 +02:00
e5ec247a6b feat(orders): customer-facking tracking labels and webhook response body
the big colored status on the order detail page used to just say
"Shipped" the moment i bought the label, which didnt really feel right
since the carrier hadnt actually picked anything up yet. now it walks
through "Label created" -> "In transit" -> "Delivered" as real scans
come in, and keeps the "Delivery Issue" / "Returned to sender" labels
for unhappy paths

also wired up the existing tracking_status_details column to the tui. i
skipped it before but the response actually includes a description of
the delivery, that is now subtext under the colored statu.

and the shippo webhook now returns a small json body instead of an empty
200. this is just for the operator that runs the curls commands to
     update the shipment, which makes debugging a lot less guesswork
2026-05-14 21:17:50 +02:00
700ad4926b feat(models): derive order state from status + tracking 2026-05-09 16:50:33 +02:00
c5f4e492c0 refactor(orders): embed tracking on orders, drop shipments table
realized per-shipment table is kinda bad, switched to single embedded tracking blob. since im only
shipping from my house  no split is needed. adds tracking_status, tracking_status_details,
tracking_status_updated_at columns. reverse-backfills any historical shipments rows back onto orders
(idempotent, table-guarded). UpdateTracking rewritten to write to otder columns direcly
2026-05-09 10:08:32 +02:00
add6e573b3 feat: extract shipments to dedicated table 2026-05-08 14:54:17 +02:00
9b45fb5138 updating 2026-05-08 13:43:03 +02:00
1849e324fc feat: add admin tracking updates 2026-05-08 10:47:57 +02:00
c90dadf9ad test was failing after some AI change. this should fix it 2026-05-06 13:53:38 +02:00
38cdda3d54 cleanup 2026-05-06 13:41:23 +02:00
c037a6e9f9 removing unused param 2026-05-05 22:05:29 +02:00
ffb476ec2d updating variable to camelcase 2026-05-05 22:04:24 +02:00
177b080117 some change 2026-05-05 21:04:02 +02:00
c96f922c70 refactor(tui): rebuild header with lipgloss/table
Replaces the manual string-concat + separator layout with a
lipgloss/table row, mirroring the reference TUI structure. Drops the
parallel bold-vs-gray styling for a single-weight accent/base scheme,
extracts a small tab() closure to remove the duplicated active/inactive
branches, and uses m.renderer.NewStyle() for the border so per-session
color profiles render correctly under SSH.

Cart label still tracks inCartFlow() (covers shipping/payment/review/
confirm), which the reference doesn't need since it has no multi-step
checkout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 09:51:21 +02:00
17df27ae20 some updates 2026-05-02 17:48:07 +02:00
52d58583e9 db telemetry 2026-05-02 16:29:07 +02:00
327bb9c867 cleanup 2026-05-02 11:28:28 +02:00
f47c10ed8a refactor: structured error handling + CreateBox/formatListItem in checkout
Replace ErrorMsg string with typed VisibleError struct. Add catch-all
error handler at top of Update() that auto-recovers cart on shop/cart
errors. ESC now dismisses error banners. Refactor shipping address and
payment card checkout lists to use CreateBox + formatListItem for
consistent bordered cards with radio indicators.

Written with Claude Code (claude.ai/code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 20:52:52 +02:00