Skip to content

fix(vsla): loan repayment claim-before-posting + persisted close-cycle share-out plan (audit A1-4/A4-4/A4-5) - #54

Open
munisp wants to merge 8 commits into
mainfrom
stage24/vsla-loan-fixes
Open

fix(vsla): loan repayment claim-before-posting + persisted close-cycle share-out plan (audit A1-4/A4-4/A4-5)#54
munisp wants to merge 8 commits into
mainfrom
stage24/vsla-loan-fixes

Conversation

@munisp

@munisp munisp commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Stage 24 audit fixes: VSLA loans + close-cycle

Fixes three findings from the stage-24 audits:

  • A1-4 / A4-5 — repayLoan CAS-after-posting: on an overpayment conflict the loser posted its ledger entry first and only then hit the repayment-sum guard, leaving an orphaned posting with no repayment row. Repayment is now claimed before posting: VslaLoanRepository.claimRepayment performs an atomic repaidKobo + amount <= totalDueKobo guard (pg UPDATE ... WHERE, in-memory equivalent), the ledger entry is posted, then the repayment row + status flip (recordRepayment) land; any failure rolls back the claim via rollbackRepaymentClaim. Same-idempotency-key replays adopt the prior entry without re-claiming; twin-key conflicts adopt the twin posting when the amount matches, else 409.
  • A4-4 — closeCycle resume recomputed from the reduced pool: the payout plan (memberId, shareKobo, residual) is now persisted to a new vsla.share_out_plans table (migration 050_vsla_close_cycle_plan.sql, guarded + re-apply-safe) before the OPEN→CLOSED CAS, so a crash/retry mid-payout pays the remaining members from the persisted plan instead of recomputing share-outs against the reduced pool balance. Concurrent closers adopt the persisted plan (unique cycle_id+member_id), and replays no longer recompute from ledger state.

Changes

  • database/repositories/vsla-carbon.repository.tsclaimRepayment/rollbackRepaymentClaim on VslaLoanRepository (interface + in-memory); new VslaShareOutPlanRepository port + in-memory impl.
  • database/repositories/vsla-carbon.pg-repository.ts — pg implementations (atomic claim guard, share-out plan CRUD with unique-conflict adopt).
  • modules/vsla-carbon/vsla-carbon.service.ts — claim-before-posting repayLoan; persisted-plan closeCycle with crash-resume + concurrent-closer adoption.
  • database/persistence.tokens.ts / database/database.module.tsVSLA_SHARE_OUT_PLAN_REPOSITORY token + provider/export wiring.
  • infra/postgres/050_vsla_close_cycle_plan.sqlvsla.share_out_plans table.
  • vsla-carbon.service.spec.ts — red specs from A1/A4 adopted as regression tests (7 new): no-orphan-posting on overpayment conflict, twin-key adoption, same-key crash-resume without double-claim, closeCycle crash mid-payout resumes from persisted plan, concurrent closers share one plan.

Checks (local)

  • Root npm run typecheck (all workspaces): clean.
  • apps/api vitest: 2464 passed / 0 failed (209 files); vsla-carbon specs 72/72 incl. new regression tests.
  • eslint on touched files: clean. lint:sql: 050 parses clean.

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