diff --git a/src/app/cashier/clerk-closure-request.service.ts b/src/app/cashier/clerk-closure-request.service.ts index 2f018b830..2798e013c 100644 --- a/src/app/cashier/clerk-closure-request.service.ts +++ b/src/app/cashier/clerk-closure-request.service.ts @@ -48,11 +48,11 @@ export class ClerkClosureRequestService { return this.http.get(`/clerk/closure-requests/${id}`); } - /** Preview of what the member would receive on closure. The backend dry-runs the very same - * interest posting that closing the account will perform, then rolls it back — so the figure - * matches the real payout to the paisa. Nothing is persisted. */ - preview(savingsAccountId: number): Observable { - return this.http.get(`/clerk/closure-requests/preview/${savingsAccountId}`); + /** Posts every rupee of interest the member has earned up to today straight onto the account — + * the same posting close() itself performs — and returns the resulting real balance. Not a dry + * run: ticking "Close this account" is meant to credit the interest immediately, not estimate it. */ + postClosureInterest(savingsAccountId: number): Observable { + return this.http.post(`/clerk/closure-requests/post-interest/${savingsAccountId}`, {}); } approve(id: number): Observable { diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 434193e8c..afcdc4bf7 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -1804,7 +1804,7 @@ @if (closeAccountChecked) { @if (closurePreviewLoading) { -
Calculating interest to date…
+
Posting interest to the account…
} @else if (closurePreviewError) {
{{ closurePreviewError }}
} @else if (closurePreview) { @@ -1813,10 +1813,20 @@ Current Balance ₹{{ closurePreview.balance | number: '1.2-2' }} -
- Interest to date - + ₹{{ closurePreview.interest | number: '1.2-2' }} -
+ @if (closurePreview.interest < 0) { + +
+ Interest correction + − ₹{{ -closurePreview.interest | number: '1.2-2' }} +
+ } @else { +
+ Interest to date + + ₹{{ closurePreview.interest | number: '1.2-2' }} +
+ }
Payable on Closure ₹{{ closurePreview.total | number: '1.2-2' }} @@ -1828,8 +1838,8 @@
}
- Interest calculated to {{ closurePreview.asOfDate }} using the same calculation the closure itself - performs. Cash is handed over rounded to whole rupees. + Interest has been posted to the account as of {{ closurePreview.asOfDate }} — this is the account's real + balance now, not an estimate. Cash is handed over rounded to whole rupees.
diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index 1e7fb31e2..e17b55db9 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -3746,9 +3746,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.showAccountActions = false; } - /** Ticking "Close this account" immediately previews what the member would receive — the balance - * plus interest accrued to date — so the clerk sees the real figure before submitting. Nothing - * is posted; the binding amount is computed by Fineract's own close command at approval time. */ + /** Ticking "Close this account" posts every rupee of interest the member has earned to date onto + * the account for real — the same posting close() itself performs — and shows the resulting real + * balance. This is not an estimate: the interest is credited the moment the box is ticked, exactly + * as the user asked for ("jaise FD close/maturity ke time interest post hota tha"). Approval later + * finds this same posting already sitting at the matching period end and adds nothing more. */ onCloseAccountToggle(): void { if (!this.closeAccountChecked) { this.closurePreview = null; @@ -3761,13 +3763,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.closurePreviewLoading = true; this.closurePreviewError = ''; this.closureRequestService - .preview(accountId) + .postClosureInterest(accountId) .pipe( catchError((err: any) => { this.closurePreviewError = err?.error?.message ?? err?.error?.errors?.[0]?.defaultUserMessage ?? - 'Could not calculate the closing amount. Please try again.'; + 'Could not post interest to the account. Please try again.'; return of(null); }) ) @@ -3864,13 +3866,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { if (!res) return; const preview = this.closurePreview; + // The server has now POSTED the interest and returns the account's real resulting balance. + // That, not the popup's estimate, is what the closure pays out — so report it, and say so + // outright if the two ever disagree rather than letting a paise drift pass unnoticed. + const posted = res.payableAmount != null ? Number(res.payableAmount) : null; + const rupees = (v: number) => `₹${v.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`; + const drifted = posted != null && preview != null && Math.abs(posted - preview.total) >= 0.005; + this.showAlert( 'success', 'Closure Request Submitted', [ `Account ${this.accountDetails?.accountNo} has been sent to the Bank Manager for closure approval.`, - preview - ? `Payout: ₹${preview.total.toLocaleString('en-IN', { minimumFractionDigits: 2 })} (balance ₹${preview.balance.toLocaleString('en-IN', { minimumFractionDigits: 2 })} + interest ₹${preview.interest.toLocaleString('en-IN', { minimumFractionDigits: 2 })}).` + posted != null + ? `Interest has been posted to the account. Payout: ${rupees(posted)}.` + : preview + ? `Payout: ${rupees(preview.total)}.` + : '', + drifted && preview + ? `Note: the posted figure ${rupees(posted!)} differs from the ${rupees(preview.total)} shown a moment ago — the posted figure is the one that will be paid.` : '', `Token No: ${row.tokNo?.trim()} — the cashier will hand over the cash against this token once approved.`, 'Cash is handed over rounded to whole rupees; the paise difference goes to the Round Off Account.' @@ -4379,8 +4393,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // Clerk raising a closure request: nothing moves yet, so the ordinary withdrawal gauntlet below // does not apply and actively rejects a valid closure — - // · Balance: the payout is balance + interest accrued but NOT yet posted, so it is legitimately - // MORE than today's withdrawable amount. close() posts that interest before withdrawing. + // · Balance: the popup already posted the accrued interest onto the account before filling this + // amount in (see onCloseAccountToggle -> postClosureInterest), so the account's own withdrawable + // balance already covers it -- this bypass exists for the checks below it, not this one. // · Min balance: meaningless for an account that is being closed to zero. // · Withdrawal facility: a member closing their account is not making an ordinary withdrawal. // · Denomination: counted by the cashier at hand-over, not by the clerk here.