Goal
Make invoice expiry real on the MVP demo backend: past-due PENDING invoices must transition to EXPIRED, and POST /api/invoices/:id/verify must reject settlement with INVOICE_EXPIRED instead of accepting Horizon-matched payments on stale invoices.
Context
The frontend already surfaces expiry (frontend/app/pay/[id]/page.tsx countdown + EXPIRED UI; dashboard expired filter). Backend storage implements the transition but the MVP server never calls it:
backend/src/storage/memory-storage.ts — markExpiredInvoices() transitions PENDING → EXPIRED when expiresAt < now
backend/src/services/invoice-memory.service.ts — exposes markExpiredInvoices()
backend/src/utils/verify-errors.ts — defines INVOICE_EXPIRED + message
backend/src/server-mvp.ts — verify only checks status !== 'PENDING'; never reads expiresAt and never calls markExpiredInvoices()
backend/tests/memory-storage.test.ts — unit coverage for expiry in isolation
The Postgres path runs a periodic sweep in payment-monitor.service.ts; the MVP demo path does not.
Acceptance criteria
Out of scope
- Pay-page UI changes (already handles
EXPIRED)
- Automatic payment detection / Horizon streaming
- Proof PDF / quittance output changes
- Shared TypeScript API contract types
- Postgres
payment-monitor.service.ts refactor
Conflict rule
- Do not edit:
frontend/app/pay/[id]/page.tsx, frontend/lib/export.ts, frontend/lib/mock-api.ts
Complexity
Complexity: Medium — 150 points (Stellar Wave)
Goal
Make invoice expiry real on the MVP demo backend: past-due PENDING invoices must transition to
EXPIRED, andPOST /api/invoices/:id/verifymust reject settlement withINVOICE_EXPIREDinstead of accepting Horizon-matched payments on stale invoices.Context
The frontend already surfaces expiry (
frontend/app/pay/[id]/page.tsxcountdown +EXPIREDUI; dashboard expired filter). Backend storage implements the transition but the MVP server never calls it:backend/src/storage/memory-storage.ts—markExpiredInvoices()transitionsPENDING→EXPIREDwhenexpiresAt < nowbackend/src/services/invoice-memory.service.ts— exposesmarkExpiredInvoices()backend/src/utils/verify-errors.ts— definesINVOICE_EXPIRED+ messagebackend/src/server-mvp.ts— verify only checksstatus !== 'PENDING'; never readsexpiresAtand never callsmarkExpiredInvoices()backend/tests/memory-storage.test.ts— unit coverage for expiry in isolationThe Postgres path runs a periodic sweep in
payment-monitor.service.ts; the MVP demo path does not.Acceptance criteria
server-mvp.tsrunsinvoiceService.markExpiredInvoices()on a sensible interval after startup (mirror ~60s cadence inpayment-monitor.service.ts)POST /api/invoices/:id/verifycalls expiry check before Horizon lookup; past-due invoices return HTTP 400 withcode: INVOICE_EXPIREDGET /api/invoices/:idand list/stats reflectEXPIREDafter sweep (no stalePENDINGpastexpiresAt)EXPIREDafter sweep; verify on expired →INVOICE_EXPIRED; valid pending verify still succeedsdocs/VERIFY.mderror table for expiry rejection on MVP pathOut of scope
EXPIRED)payment-monitor.service.tsrefactorConflict rule
frontend/app/pay/[id]/page.tsx,frontend/lib/export.ts,frontend/lib/mock-api.tsComplexity
Complexity: Medium — 150 points (Stellar Wave)