fix(cashier): post closure interest for real when the checkbox is ticked
Ticking "Close this account" on the Transaction Entry Action popup called
GET /clerk/closure-requests/preview/{id} -- a dry run that computed the
payout and immediately rolled it back, so the account's own balance stayed
unchanged until a manager later approved the closure.
Wired to the new POST /clerk/closure-requests/post-interest/{id} instead
(see the matching backend commit), which posts the interest for real and
returns the account's actual resulting balance. Renamed
ClerkClosureRequestService#preview to #postClosureInterest to match, and
reworded the popup copy and loading state so it no longer implies posting
happens later at Save ("posted to the account as of ..." instead of
"calculated to ... and posted the moment you press Save").
Also corrected a stale comment on the balance-check bypass for a pending
closure: it used to justify the bypass by saying interest was NOT YET
posted, which stopped being true the moment the checkbox itself started
posting it for real.
This commit is contained in:
@@ -48,11 +48,11 @@ export class ClerkClosureRequestService {
|
|||||||
return this.http.get(`/clerk/closure-requests/${id}`);
|
return this.http.get(`/clerk/closure-requests/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preview of what the member would receive on closure. The backend dry-runs the very same
|
/** Posts every rupee of interest the member has earned up to today straight onto the account —
|
||||||
* interest posting that closing the account will perform, then rolls it back — so the figure
|
* the same posting close() itself performs — and returns the resulting real balance. Not a dry
|
||||||
* matches the real payout to the paisa. Nothing is persisted. */
|
* run: ticking "Close this account" is meant to credit the interest immediately, not estimate it. */
|
||||||
preview(savingsAccountId: number): Observable<any> {
|
postClosureInterest(savingsAccountId: number): Observable<any> {
|
||||||
return this.http.get(`/clerk/closure-requests/preview/${savingsAccountId}`);
|
return this.http.post(`/clerk/closure-requests/post-interest/${savingsAccountId}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
approve(id: number): Observable<any> {
|
approve(id: number): Observable<any> {
|
||||||
|
|||||||
@@ -1804,7 +1804,7 @@
|
|||||||
|
|
||||||
@if (closeAccountChecked) {
|
@if (closeAccountChecked) {
|
||||||
@if (closurePreviewLoading) {
|
@if (closurePreviewLoading) {
|
||||||
<div class="te-aa-calc te-aa-calc--loading">Calculating interest to date…</div>
|
<div class="te-aa-calc te-aa-calc--loading">Posting interest to the account…</div>
|
||||||
} @else if (closurePreviewError) {
|
} @else if (closurePreviewError) {
|
||||||
<div class="te-aa-calc te-aa-calc--error">{{ closurePreviewError }}</div>
|
<div class="te-aa-calc te-aa-calc--error">{{ closurePreviewError }}</div>
|
||||||
} @else if (closurePreview) {
|
} @else if (closurePreview) {
|
||||||
@@ -1813,10 +1813,20 @@
|
|||||||
<span>Current Balance</span>
|
<span>Current Balance</span>
|
||||||
<span>₹{{ closurePreview.balance | number: '1.2-2' }}</span>
|
<span>₹{{ closurePreview.balance | number: '1.2-2' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="te-aa-calc__row">
|
@if (closurePreview.interest < 0) {
|
||||||
<span>Interest to date</span>
|
<!-- Negative only when this run corrects interest that was over-posted earlier: the
|
||||||
<span>+ ₹{{ closurePreview.interest | number: '1.2-2' }}</span>
|
delta is against the stored balance, so calling it "interest" would read as the
|
||||||
</div>
|
member being charged. The Payable figure below is the authoritative one either way. -->
|
||||||
|
<div class="te-aa-calc__row">
|
||||||
|
<span>Interest correction</span>
|
||||||
|
<span>− ₹{{ -closurePreview.interest | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="te-aa-calc__row">
|
||||||
|
<span>Interest to date</span>
|
||||||
|
<span>+ ₹{{ closurePreview.interest | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="te-aa-calc__row te-aa-calc__row--total">
|
<div class="te-aa-calc__row te-aa-calc__row--total">
|
||||||
<span>Payable on Closure</span>
|
<span>Payable on Closure</span>
|
||||||
<span>₹{{ closurePreview.total | number: '1.2-2' }}</span>
|
<span>₹{{ closurePreview.total | number: '1.2-2' }}</span>
|
||||||
@@ -1828,8 +1838,8 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="te-aa-calc__note">
|
<div class="te-aa-calc__note">
|
||||||
Interest calculated to {{ closurePreview.asOfDate }} using the same calculation the closure itself
|
Interest has been posted to the account as of {{ closurePreview.asOfDate }} — this is the account's real
|
||||||
performs. Cash is handed over rounded to whole rupees.
|
balance now, not an estimate. Cash is handed over rounded to whole rupees.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3746,9 +3746,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.showAccountActions = false;
|
this.showAccountActions = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ticking "Close this account" immediately previews what the member would receive — the balance
|
/** Ticking "Close this account" posts every rupee of interest the member has earned to date onto
|
||||||
* plus interest accrued to date — so the clerk sees the real figure before submitting. Nothing
|
* the account for real — the same posting close() itself performs — and shows the resulting real
|
||||||
* is posted; the binding amount is computed by Fineract's own close command at approval time. */
|
* 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 {
|
onCloseAccountToggle(): void {
|
||||||
if (!this.closeAccountChecked) {
|
if (!this.closeAccountChecked) {
|
||||||
this.closurePreview = null;
|
this.closurePreview = null;
|
||||||
@@ -3761,13 +3763,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.closurePreviewLoading = true;
|
this.closurePreviewLoading = true;
|
||||||
this.closurePreviewError = '';
|
this.closurePreviewError = '';
|
||||||
this.closureRequestService
|
this.closureRequestService
|
||||||
.preview(accountId)
|
.postClosureInterest(accountId)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: any) => {
|
catchError((err: any) => {
|
||||||
this.closurePreviewError =
|
this.closurePreviewError =
|
||||||
err?.error?.message ??
|
err?.error?.message ??
|
||||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
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);
|
return of(null);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -3864,13 +3866,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
||||||
const preview = this.closurePreview;
|
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(
|
this.showAlert(
|
||||||
'success',
|
'success',
|
||||||
'Closure Request Submitted',
|
'Closure Request Submitted',
|
||||||
[
|
[
|
||||||
`Account ${this.accountDetails?.accountNo} has been sent to the Bank Manager for closure approval.`,
|
`Account ${this.accountDetails?.accountNo} has been sent to the Bank Manager for closure approval.`,
|
||||||
preview
|
posted != null
|
||||||
? `Payout: ₹${preview.total.toLocaleString('en-IN', { minimumFractionDigits: 2 })} (balance ₹${preview.balance.toLocaleString('en-IN', { minimumFractionDigits: 2 })} + interest ₹${preview.interest.toLocaleString('en-IN', { minimumFractionDigits: 2 })}).`
|
? `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.`,
|
`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.'
|
'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
|
// Clerk raising a closure request: nothing moves yet, so the ordinary withdrawal gauntlet below
|
||||||
// does not apply and actively rejects a valid closure —
|
// does not apply and actively rejects a valid closure —
|
||||||
// · Balance: the payout is balance + interest accrued but NOT yet posted, so it is legitimately
|
// · Balance: the popup already posted the accrued interest onto the account before filling this
|
||||||
// MORE than today's withdrawable amount. close() posts that interest before withdrawing.
|
// 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.
|
// · 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.
|
// · 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.
|
// · Denomination: counted by the cashier at hand-over, not by the clerk here.
|
||||||
|
|||||||
Reference in New Issue
Block a user