Skip to content

feat(loans): move a mis-anchored schedule to the month it should start in - #46

Merged
parameshjava merged 1 commit into
mainfrom
fix/shift-emi-schedule-anchor
Aug 7, 2026
Merged

feat(loans): move a mis-anchored schedule to the month it should start in#46
parameshjava merged 1 commit into
mainfrom
fix/shift-emi-schedule-anchor

Conversation

@parameshjava

Copy link
Copy Markdown
Owner

Follow-up to the anchor detection already on main. That commit could tell you the schedule started a month late; this one lets you fix it. Includes migration 054_shift_emi_schedule.sql.

The case

A converted loan whose installments start 10 Sep 2026 when the cutover floor — greatest(start_date, emi_cutover_date) — puts #1 on 10 Aug 2026. Recompute EMI correctly reported "no changes": re-pricing only scales interest and never moves a due date. So the dialog diagnosed the problem and then offered nothing.

What the fix does

Moves dates and nothing else. Every unpaid installment shifts by the same whole number of months, so installment numbers, principal, interest and EMI are all untouched and the cadence is preserved exactly:

before after
first installment 10 Sep 2026 10 Aug 2026
last installment 10 Aug 2027 10 Jul 2027
amounts unchanged

Regenerating through fn_generate_emi_schedule (what the existing Recalculate button does) would have rebuilt every amount as well, which is far more than a wrong anchor calls for — and it refuses once anything is paid.

What it refuses

Any installment carrying money. A settled installment's due date is the date it was settled against, and moving the rows around it would interleave them with history. planScheduleShift checks it, and fn_shift_emi_schedule counts settled rows itself and raises before touching anything, so no caller can bypass it — including a direct RPC call.

Like migration 053, the write is all-or-nothing: a shortfall against the expected row count raises and rolls back, rather than leaving half the schedule on the old cadence.

The one lossy consequence

Shifting earlier can pull installments onto or before today. The confirmation says how many, in as many words:

1 installment will fall on or before today once moved, so it becomes immediately due and may attract a late fee.

Stated up front rather than discovered afterwards. In the reported loan it's zero — the whole schedule is still ahead.

UI

The amber warning in the Recompute EMI dialog gains the action, and the card's button becomes "Fix schedule dates" when drift is detected, so a drifted loan leads with the thing that's actually wrong rather than the re-pricing that isn't.

Testing

emi-schedule-shift.test.ts — 11 cases: the whole schedule moves by the drift with gaps preserved and every date still a 10th, installment numbers stay attached to their own rows, forward shift for a back-dated schedule, year-boundary roll, the becoming-due count, refusal on a full payment and on a part payment, waived rows moving with the rest, no-drift, and fingerprint sensitivity.

npm test 338 passing · npm run lint clean (one pre-existing warning) · npm run build passes.

Deploy note

Three migrations are now outstanding against main: 052, 053 and this 054. If 053 was applied before its signature changed, drop the old overload:

drop function if exists public.fn_reprice_emi_schedule(uuid, jsonb, numeric, numeric);

🤖 Generated with Claude Code

…t in

Detection landed last commit but left the admin with nowhere to go: the dialog
said the schedule starts a month late and then explained that re-pricing would
not fix it. This adds the fix.

It moves dates and nothing else. Every unpaid installment shifts by the same
whole number of months, so installment numbers, principal, interest and EMI are
all untouched and the cadence is preserved exactly — the reported loan's twelve
installments move from 10 Sep 2026 through 10 Aug 2027 back to 10 Aug 2026
through 10 Jul 2027. Regenerating via fn_generate_emi_schedule would have
rebuilt every amount too, which is far more than a wrong anchor calls for.

It is refused once any installment carries money: a settled installment's due
date is what it was settled against, and moving the rows around it would
interleave them with history. planScheduleShift checks it, and
fn_shift_emi_schedule (migration 054) counts settled rows itself and raises
before touching anything, so no caller can bypass it. Like 053 the write is
all-or-nothing — a shortfall against the expected row count aborts rather than
leaving half the schedule on the old cadence.

The confirmation names both ends of the move and, when the shift would pull
installments onto or before today, says how many become immediately due and may
attract a late fee. That is the one genuinely lossy consequence, so it is stated
rather than discovered afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aits-fcf-tracker Ready Ready Preview Aug 7, 2026 1:19pm

@parameshjava
parameshjava merged commit 9fbbf0e into main Aug 7, 2026
3 checks passed
@parameshjava
parameshjava deleted the fix/shift-emi-schedule-anchor branch August 7, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant