Two fixes to POST /api/v1/record-payment:
- The Fava duplicate check caught every exception and proceeded to
write, so a transient Fava blip produced double entries. It now
fails closed: transport errors return 503 and the client retries.
While here: the check queried {base_url}/api/journal, but base_url
already ends in /api — the doubled path 404'd, meaning the
duplicate check has silently never run.
- The endpoint now goes through the same processed_payments claim
gate as the background invoice listener, so the webhook+poller pair
can't both record the same payment_hash: a 'done' claim replays as
"already recorded", an in-flight claim returns 409.
Addresses CODE-REVIEW-2026-06 finding #10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Fava-side duplicate checks (add_entry_idempotent, journal-link
scan) are read-then-write races: on restart with a persisted invoice
queue, or webhook + poller firing together, both callers pass the
"not present" check and both insert.
New processed_payments table (m005) keyed on payment_hash; exactly
one claimant wins the INSERT ... ON CONFLICT DO NOTHING. Lifecycle:
'processing' while the write is in flight, 'done' after; failed
recordings release the claim so redelivery retries, and 'processing'
rows from a crashed process are cleared at listener startup.
Also wraps the invoice-listener loop body in try/except so one poison
payment can't kill payment recording for the process lifetime.
Addresses CODE-REVIEW-2026-06 findings #4 and #9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>