fix(notifications): P2 extreme-audit P1 fixes before deploy

Independent audit verdict was NOT SAFE; all findings fixed:
- dispatcher Idempotency-Key is now per-attempt (${id}:${attempts}),
  not row id alone, so a transient-failure response cached by Resend
  cannot permanently suppress every retry and silently lose an email.
- enqueueDues is fully try/catch wrapped so a notifications-table or
  render failure can never throw into / wedge the Stripe money path.
- dues_lapsed dedupe falls back to the subscription id when periodEnd
  is null (canceled/deleted subs), so future lapses no longer collapse
  to one key and get suppressed. New test; suite 499.
- documented the dispatcher's once/min concurrency assumption.
This commit is contained in:
Moheeb Zara
2026-05-17 05:06:02 -07:00
parent 7eb83676e7
commit f1bae44afd
5 changed files with 125 additions and 44 deletions
+25
View File
@@ -47,6 +47,31 @@ describe('duesDedupeKey', () => {
expect(a).toBe('dues_lapsed:m1:2026-06-01')
expect(a).not.toBe(b)
})
it('lapse falls back to subscription id when periodEnd is null', () => {
// canceled/deleted subs carry no item period; without the fallback every
// lapse would collapse to dues_lapsed:m1 and suppress future notices.
const k = duesDedupeKey('dues_lapsed', {
memberId: 'm1',
periodEnd: null,
subscriptionId: 'sub_A',
})
expect(k).toBe('dues_lapsed:m1:sub_A')
// A comeback subscription has a new id, so a later lapse stays distinct.
const k2 = duesDedupeKey('dues_lapsed', {
memberId: 'm1',
periodEnd: null,
subscriptionId: 'sub_B',
})
expect(k2).not.toBe(k)
// periodEnd still wins when present (stable per billing period).
expect(
duesDedupeKey('dues_lapsed', {
memberId: 'm1',
periodEnd: '2026-06-01',
subscriptionId: 'sub_A',
}),
).toBe('dues_lapsed:m1:2026-06-01')
})
})
describe('formatMoney', () => {