Files
accounted/app/api
Mattsson 1a725c121a fix(whatsapp): four #1991/#1992 hardening residuals in the receipt channel (#2349)
* fix(whatsapp): four #1991/#1992 hardening residuals in the receipt channel

Sends now say HOW they failed (failure: http_rejected | transport_error),
and the company question falls back to numbered text only on an HTTP
rejection. A timeout means Meta may already have delivered the interactive
question, so that case rolls the question back instead of putting a second
copy on the phone; the next receipt re-asks.

Both drains (the company answer and the single-live-company path) share
one helper that stamps rows older than Meta's ~30-day media retention as
company_choice_expired instead of re-opening them into the MAX_ATTEMPTS
error path, and tell the sender once (M20) how many receipts could not be
recovered. STAGED_MEDIA_MAX_AGE_MS moved to conversation.ts so the sweep
and the drains read one definition.

POST /link/default-company checks LIVE membership with the same
companies!inner(archived_at) filter intake uses, so a default pointing at
an archived company is refused (403) instead of saved and then silently
ignored; a failed membership read is a 500, not a 403.

consumeLinkCode is tri-state: a failed lookup or claim returns
'transient_error' and the webhook answers with a neutral retry (M21)
instead of M2 "the code is wrong" to a user holding a valid code. In
degraded mode (quota RPC down too) M21 sits behind the same fail-closed
throttle as M2.

Refs #2062

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xry8E1FuYbbedbwvZAxLv

* fix(whatsapp): drain failures are reported and swept, M21 shares the M2 throttle

Review pass on #2349 (CodeRabbit, four findings, one commit):

- drainParkedRows reads the error of both UPDATEs, logs each, and returns
  failed: true. The answer stays applied (pin set, options claimed) and the
  single-company path still clears the dead question: both turn the rows
  that are still parked into orphans, and a new sweep pass re-opens parked
  rows whose conversation has no open company question (older than two
  minutes, so a row parked just before its question is armed is left
  alone) through the same drain and processes them.
- badCodeThrottled counts M21 alongside M2, so a code-shaped flood during a
  lookup outage cannot earn one M21 per message in degraded mode.
- The M20 expiry notice logs a failed send. It stays best-effort like M17,
  M18 and M19: the extension has no durable outbound retry, and the rows
  the notice describes are already terminal.

Refs #2062

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xry8E1FuYbbedbwvZAxLv

* fix(whatsapp): type the orphan scan rows and the cron summary fixture

reopenedOrphans joined SweepSummary in the previous commit; the cron route
test's fixture and the PostgREST embed cast in the orphan pass had not
followed (check:types caught both).

Refs #2062

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xry8E1FuYbbedbwvZAxLv

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 20:14:06 +02:00
..
2026-07-25 22:56:17 +02:00
2026-07-23 16:16:55 +02:00
2026-07-05 03:05:09 +02:00
2026-07-08 09:54:46 +02:00