From 6507e682033ed1391a0fd3a80b40be9b46be2ae7 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Fri, 16 Oct 2026 12:31:03 +0530 Subject: [PATCH] feat(cashier): FD is credit-only on the transaction entry, and a Bank Transfer can fund an FD - A Fixed Deposit row is credit-only on the clerk and cashier screens (isFdCreditOnly): its DR button is disabled, and Save refuses any FD row left on DR (for example by a Withdrawal Slip TYPE). Money leaves an FD only through its maturity or premature closure; the clerk's "Close this account" flow is the one exception. - Bank Transfer / Fund Transfer accepts an FD on the CR side, so an FD can be funded from the customer's own Savings or Current account. The server enforces the funding rules (first and only funding, exactly the principal, the source account's owner, freeze status and available balance). Requires the backend change "FD funding rules and no FD withdrawals from clerk/cashier requests". Co-Authored-By: Claude Opus 5.5 --- .../transaction-entry.component.html | 8 ++-- .../transaction-entry.component.ts | 38 +++++++++++++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index ffc38955f..c429ad732 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -184,7 +184,7 @@ class="ws-f__ctl" [(ngModel)]="row.acTyp" (click)="$event.stopPropagation()" - (change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')" + (change)="isCreditOnly(row) && (row.dbCr = 'Cr')" > @for (a of acTypOptionsFor(row); track a) { @@ -267,7 +267,7 @@ type="button" class="ws-seg__btn ws-seg__btn--dr" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" - [disabled]="isGlCreditOnly(row)" + [disabled]="isCreditOnly(row)" (click)="row.dbCr = 'Dr'" > DR @@ -1644,7 +1644,7 @@ class="ws-f__ctl" [(ngModel)]="row.acTyp" (click)="$event.stopPropagation()" - (change)="isGlCreditOnly(row) && (row.dbCr = 'Cr')" + (change)="isCreditOnly(row) && (row.dbCr = 'Cr')" > @for (a of acTypOptionsFor(row); track a) { @@ -1715,7 +1715,7 @@ type="button" class="ws-seg__btn ws-seg__btn--dr" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" - [disabled]="isGlCreditOnly(row)" + [disabled]="isCreditOnly(row)" (click)="$event.stopPropagation(); row.dbCr = 'Dr'" > DR diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index 413652c93..f0be400ff 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -2344,6 +2344,20 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { ); } + /** + * A Fixed Deposit row is credit-only on both the clerk and the cashier screens: an FD is funded once, with + * exactly its principal, and money leaves it only through its maturity or premature closure. The one + * exception is the clerk's own "Close this account" flow, which raises a closure request. + */ + isFdCreditOnly(row: TxnRow): boolean { + return row.acTyp === 'FD' && !this.pendingClosure; + } + + /** DR is not offered for this row (see isGlCreditOnly and isFdCreditOnly). */ + isCreditOnly(row: TxnRow): boolean { + return this.isGlCreditOnly(row) || this.isFdCreditOnly(row); + } + /** Called when the TYPE dropdown changes — sets sensible defaults for TXN TP and DR/CR */ /** * A cashier row that was filled from an approved token (withdrawal / account-closure) is cash @@ -5272,6 +5286,20 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { } onSave(): void { + // Money never leaves a Fixed Deposit from this screen, clerk or cashier: no withdrawal slip, payment, + // collection or transfer debits an FD. It is paid out only through its maturity or premature closure + // (the clerk's "Close this account" flow). The DR button is already disabled for FD rows (isFdCreditOnly); + // this also catches a row set to DR by its TYPE, such as a Withdrawal Slip. + const fdDebitRows = this.rows.filter((r) => this.isFdCreditOnly(r) && r.dbCr === 'Dr'); + if (fdDebitRows.length > 0) { + this.showAlert('error', 'Fixed Deposit Cannot Be Debited', [ + `FD account(s) ${fdDebitRows.map((r) => r.acNo).join(', ')} cannot be debited or withdrawn from.`, + 'Money leaves a Fixed Deposit only through its maturity or premature closure.', + 'An FD can only be credited — once, with exactly its principal.' + ]); + return; + } + // Demand drafts commit here like everything else on the worksheet. Checked first because a DD // row shares its shape with the ordinary entries below — without this it would fall through and // post as a plain transaction, with the draft never raised and the charges silently dropped. @@ -5349,7 +5377,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { return; } - // Bank transfer: only savings (SD) or current (CA/CB) accounts allowed + // Bank transfer: savings (SD) or current (CA/CB) accounts on either side, plus a Fixed Deposit (FD) on + // the CR side only -- funding it from the customer's own account. The server checks the funding rules + // (first and only funding, exactly the principal, the source account's owner and available balance). const allowedTypes = new Set([ 'SD', 'CA', @@ -5357,13 +5387,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { ]); const invalidAccounts = [ ...drRows, - ...crRows + ...crRows.filter((r) => r.acTyp !== 'FD') ].filter((r) => !allowedTypes.has(r.acTyp)); if (invalidAccounts.length > 0) { this.showAlert('error', 'Invalid Account Type', [ `Account type(s) not allowed in Bank Transfer: ${invalidAccounts.map((r) => r.acTyp + ' (' + r.acNo + ')').join(', ')}`, - 'Only Savings (SD) and Current (CA) accounts are permitted.', - 'GL, FD and SHR accounts are not allowed — use Disbursement/Collection instead.', + 'Debit side: only Savings (SD) and Current (CA) accounts. Credit side: SD, CA or a Fixed Deposit (FD).', + 'GL and SHR accounts are not allowed — use Disbursement/Collection instead.', 'RD installments are paid through a cashier Receipt or Auto Debit.' ]); return;