What is missing
Fineract accepts contractTermination and undoContractTermination on a loan. It is a distinct closure path for progressive, interest-recalculating loans — not the same thing as close, writeoff or foreclosure, all three of which this app already offers. Neither command appears anywhere in src/app.
Confirmed against a running apache/fineract, using the #268 discriminator:
| Probe |
Result |
POST /loans/{id}?command=totallyNotACommand |
error.msg.query.parameter.value.unsupported — unrecognised (control) |
POST /loans/{id}?command=contractTermination |
error.msg.loan.contract.termination.is.only.supported.for.progressive.loan.schedule.type — recognised |
POST /loans/{id}?command=undoContractTermination |
error.msg.loan.is.not.contract.terminated — recognised |
The platform's own error messages give the two conditions the UI needs: termination applies only to progressive-schedule loans, and the undo applies only to a loan already terminated. So both should be offered conditionally rather than unconditionally, the way #268 handled undo-last-disbursal.
What the UI offers today
The Actions menu on a real progressive Active loan — 17 entries, none of them contract termination:

Add Loan Charge · Add Collateral · Assign Loan Officer · Undo Disbursal · Waive Interest · Prepay Loan · Foreclosure · Close · Close as Rescheduled · Write Off · Charge Off · Merchant Issued Refund · Payout Refund · Goodwill Credit · Waive Interest Payment · Re-age Loan · Re-amortise Loan
For contrast, a cumulative loan gets the first 14 — the three progressive-only entries are correctly withheld, which is the existing pattern this work should follow:

Suggested scope
Two entries on the loan Actions menu, both gated on the loan's schedule type being progressive, and the undo additionally gated on the loan already being contract-terminated. The generated client already has the call shape — loansService.postLoansLoanId(loanId, request, 'contractTermination') — which is the same shape close and writeoff already use, so this is a new form, not a new pattern.
Request body follows PostLoansLoanIdRequest: a date plus dateFormat/locale, and an optional note.
Why it is worth doing
Without it, an institution ending a progressive contract early has to reach for close or foreclosure instead. Those are different events with different accounting consequences, so the loan's history ends up recording something that did not happen — and the distinction is exactly why the platform models termination separately.
Environment
main at 7c51d90b. Probes against apache/fineract running locally via deploy/docker-compose-e2e.yml; screenshots from loans seeded with the repository's own e2e/utils/seed-api.ts helpers.
What is missing
Fineract accepts
contractTerminationandundoContractTerminationon a loan. It is a distinct closure path for progressive, interest-recalculating loans — not the same thing asclose,writeofforforeclosure, all three of which this app already offers. Neither command appears anywhere insrc/app.Confirmed against a running
apache/fineract, using the #268 discriminator:POST /loans/{id}?command=totallyNotACommanderror.msg.query.parameter.value.unsupported— unrecognised (control)POST /loans/{id}?command=contractTerminationerror.msg.loan.contract.termination.is.only.supported.for.progressive.loan.schedule.type— recognisedPOST /loans/{id}?command=undoContractTerminationerror.msg.loan.is.not.contract.terminated— recognisedThe platform's own error messages give the two conditions the UI needs: termination applies only to progressive-schedule loans, and the undo applies only to a loan already terminated. So both should be offered conditionally rather than unconditionally, the way #268 handled undo-last-disbursal.
What the UI offers today
The Actions menu on a real progressive Active loan — 17 entries, none of them contract termination:
For contrast, a cumulative loan gets the first 14 — the three progressive-only entries are correctly withheld, which is the existing pattern this work should follow:
Suggested scope
Two entries on the loan Actions menu, both gated on the loan's schedule type being progressive, and the undo additionally gated on the loan already being contract-terminated. The generated client already has the call shape —
loansService.postLoansLoanId(loanId, request, 'contractTermination')— which is the same shapecloseandwriteoffalready use, so this is a new form, not a new pattern.Request body follows
PostLoansLoanIdRequest: a date plusdateFormat/locale, and an optional note.Why it is worth doing
Without it, an institution ending a progressive contract early has to reach for
closeorforeclosureinstead. Those are different events with different accounting consequences, so the loan's history ends up recording something that did not happen — and the distinction is exactly why the platform models termination separately.Environment
mainat7c51d90b. Probes againstapache/fineractrunning locally viadeploy/docker-compose-e2e.yml; screenshots from loans seeded with the repository's owne2e/utils/seed-api.tshelpers.