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:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user