Robustness follow-up to the POS×loyalty integration. Points are earned after a POS order commits,
in a fire-and-forget call that must never fail the sale — but the loyalty backend is scale-to-zero on Fly, so a
cold start can time out and the order is left at loyalty_earn_status = 'pending'/'failed' with the points
simply missing. Nothing read those columns. Now a sweep does.
/api/cafe/cron/loyalty-earn-retry finds orders
stuck at pending/failed (with a member, aged 2 min–7 days) and re-calls /pos/earn.
That endpoint is idempotent on posOrderId (points-ledger unique index + spin chances keyed on the
order), so an order that actually earned just returns its prior result — no double-award.
Fired every minute by the cron-trigger; it no-ops (no backend call) when nothing is stuck, so it only wakes the
scale-to-zero backend when there's real work. No migration.
| Check | Result |
|---|---|
| Flip a real order earned → failed (ledger = 1 row) | seeded |
| POST the sweep route | 200 · {pending:1, earned:1, failed:0} |
| Order status after | earned |
| Points ledger before / after | 1 / 1 — no duplicate points |
| Second POST (nothing stuck) | {pending:0, earned:0} — no backend call |
?dry=1 | reports backlog, writes nothing |
| POST without the cron secret | 401 |
| DAO query (nix_db_staging) | picks up the stuck order + its line items |
| Staging smoke | 5/5 |
nix-cafe 00f652d (nix-cafe-staging Worker). The cron-trigger worker redeploy (so it fires on schedule) + prod CAFE_CRON_SECRET ride along at the scheduled prod promote.