fix(cron): prevent duplicate repayment_due notifications per loan#38
fix(cron): prevent duplicate repayment_due notifications per loan#38MarvyNwaokobia wants to merge 3 commits into
Conversation
Use a Redis-based dedup guard (24h TTL) so each borrower receives at most one repayment_due notification per loan per cycle window. Also replace the hardcoded 30-day heuristic with the actual due date derived from term_ledgers and LEDGER_CLOSE_SECONDS. Closes LabsCrypt#10
ogazboiz
left a comment
There was a problem hiding this comment.
this is solid, the dedup is done right: a real redis lock (cacheService.setNotExists(loan_due_notified:${loanId}, "1", 24h) in src/cron/loanCheckCron.ts:57-66), so the first caller per loan in the 24h window sends and the rest continue, no in-memory set that resets. the query correctly targets contract_events (not the loan_events view) with valid columns, and mock ordering/completeness are fine. one thing blocks it:
- prettier fails on src/tests/loanCheckCron.test.ts (lines ~31, 66, 101, 135-137), and CI's lint step fails the whole job on that. run: npx prettier --write src/tests/loanCheckCron.test.ts
minor, not blocking: if setNotExists marks the key but createNotification then throws, the catch aborts and the key stays set for 24h, suppressing that loan's notification even though nothing was sent. consider deleting the key on send failure (or notify-then-mark) so a transient error doesn't swallow the notification.
heads up: your CI run is gated (first PR, action_required), i'll approve it so you get the signal. fix the prettier line and this is ready.
if you want to keep contributing, join us on Telegram: https://t.me/+DOylgFv1jyJlNzM0
Delete the per-loan dedup cache key when createNotification throws so a transient error doesn't suppress the notification for the full 24h window. Also format test file to pass CI's prettier check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ogazboiz
left a comment
There was a problem hiding this comment.
the prettier item is fixed and the dedup logic is solid (real redis lock loan_due_notified:, query targets contract_events not the loan_events view). you also picked up my non-blocking note, there's now a test for releasing the dedup key when the notification send fails so it can retry. merging once ci is green. nice.
if you want to keep contributing, join us on Telegram: https://t.me/+DOylgFv1jyJlNzM0
ps: CI is red right now on the migrate step, but that is a pre-existing main-branch issue (a commonjs migration file under our ESM package), nothing to do with your change. once that is sorted this lands.
node-pg-migrate loads every migration file before running any, so CI only
ever surfaced the first failure (a CommonJS file under an ESM package) and
masked a chain of further runtime errors behind it. migrate:up has never
completed on a clean DB. Fixes, in execution order:
- 1776/1778/1784: convert CommonJS (exports./module.exports) to ESM so the
files load under "type": "module". 1784 also called db.query(), which
node-pg-migrate never provides to migrations — routed through pgm.sql().
- 1772: event_types jsonb default "[]::jsonb" rendered as invalid JSON;
use pgm.func("'[]'::jsonb").
- 1778: trigger referenced update_updated_at_column(), created by no
migration — define it before the trigger.
- 1781: payload / next_retry_at are already created by 1772 — guard the
re-adds with ifNotExists.
- 1784: drop FK to loan_events(loan_id); loan_events is an append-only event
table (and later a view) with no unique loan_id to reference.
- 1787: email_enabled/sms_enabled/phone already added by 1773 — guard with
ifNotExists.
- 1788: replace pgm.renameIndex (not a method in node-pg-migrate v8) with
raw ALTER INDEX ... RENAME TO.
- 1789: loan_events existence check used pg_tables (excludes views) and tried
to CREATE TABLE over the 1788 backward-compat view; use to_regclass.
Verified locally against a fresh Postgres: migrate:up applies all 27
migrations (exit 0) and is a no-op on re-run. Unblocks the migrate step for
all branches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
loanCheckCronsends at most onerepayment_duenotification per loan per 24-hour window, stopping the spam of 24+ notifications/day30 daysheuristic with the actual due date computed fromterm_ledgers * LEDGER_CLOSE_SECONDS, matching howloanControllercalculatesnext_payment_deadlineChanges
term_ledgersfor due date calculation; addedcacheService.setNotExistsper-loan dedup before creating notificationsTest plan
npm test -- --testPathPattern=loanCheckCron)Closes #10