* 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
* 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
* 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
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.
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.
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
Shippo's transaction validation rejects labels where the from-address
has no email — fails at the poll step with
"Attribute address_from.email must not be empty."
Add Email to Address, plumb it into addrPayload, and read
SHIP_FROM_EMAIL alongside the other SHIP_FROM_* env vars.
Shippo's /v1/orders endpoint rejects >2 decimal places on parcel
weight and >4 on line_items[].weight. Converting ounces to kg
(oz * 0.0283495) routinely produces 6-decimal values, so the first
real label purchase 400'd with "Ensure that there are no more than
N decimal places".
Round at the payload boundary in createShippoOrder rather than in
the caller — keeps the precision of the source (Coffee.Ounces) and
only loses it when the wire format demands it.
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.