diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 9275e0c3e..0b62d5029 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -146,7 +146,7 @@ [class.ws-f__ctl--req]="!row.paymentTypeId" > - @for (pt of paymentTypes; track pt.id) { + @for (pt of paymentTypesForRow(row); track pt.id) { } @@ -231,8 +231,11 @@ Payment instrument
-
+ @if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) { + = โ‚น{{ (row.amount ?? 0) * rowSharePrices[row.acNo] | number: '1.2-2' }} @ โ‚น{{ + rowSharePrices[row.acNo] | number: '1.2-2' + }}/share + }
- @if (row.typ !== 'Withdrawal Slip') { + @if (row.acTyp === 'SHR' && row.typ === 'Enter Bank Transfer') { +
+ + +
+
+ + +
+ } @else if (row.typ !== 'Withdrawal Slip') {
+ @if (selectedRow?.acTyp === 'SHR') { +
+ No. of Shares Held{{ accountDetails.summary?.totalApprovedShares ?? 0 }} +
+
+ Market Price / Share{{ currencySymbol }}{{ accountDetails.currentMarketPrice | number: '1.2-2' }} +
+
+ Resolution No.{{ accountDetails.resolutionNumber || 'โ€”' }} +
+
+ Linked Savings A/C No.{{ accountDetails.savingsAccountNumber || 'โ€”' }} +
+ @if (accountDetails.linkedLoanAccountNo) { +
+ Linked Loan A/C No.{{ accountDetails.linkedLoanAccountNo }} +
+ } + }
@@ -724,7 +781,13 @@
1. - {{ accountDetails?.glCode ? 'GL Balance' : 'Current Balance' }} + {{ + selectedRow?.acTyp === 'SHR' + ? 'Share Balance' + : accountDetails?.glCode + ? 'GL Balance' + : 'Current Balance' + }} {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} @@ -1281,7 +1344,7 @@ [class.ws-f__ctl--req]="!row.paymentTypeId" > - @for (pt of paymentTypes; track pt.id) { + @for (pt of paymentTypesForRow(row); track pt.id) { } @@ -1360,7 +1423,7 @@
+ @if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) { + = โ‚น{{ (row.amount ?? 0) * rowSharePrices[row.acNo] | number: '1.2-2' }} @ โ‚น{{ + rowSharePrices[row.acNo] | number: '1.2-2' + }}/share + }
-
- - -
-
- - -
+ @if (row.acTyp === 'SHR' && row.typ === 'Enter Bank Transfer') { +
+ + +
+
+ + +
+ } @else { +
+ + +
+
+ + +
+ }
!pt.isCashPayment); + } + const isShareSettlementCredit = + row.typ === 'Enter Bank Transfer' && + row.dbCr === 'Cr' && + this.rows.some((r) => r.typ === 'Enter Bank Transfer' && r.dbCr === 'Dr' && r.acTyp === 'SHR'); + if (isShareSettlementCredit) { + return this.paymentTypes.filter((pt: any) => !pt.isCashPayment); + } + return this.paymentTypes; + } + // Per-row account id/name map: acNo โ†’ Fineract id/name (used for multi-row disbursement) rowAccountIds: Record = {}; rowAccountNames: Record = {}; + // Per-row Share account market price, keyed by acNo โ€” lets a Share row's "SHARE NO." field + // auto-fill the settlement amount on any other row added alongside it (calculator convenience + // only; a Share row still can never itself be saved from this screen โ€” see accountTypeConfig). + rowSharePrices: Record = {}; + // Per-row client id, keyed by acNo โ€” needed to submit a Share Withdrawal Request from a row + // that isn't necessarily the currently "selected" one. + rowClientIds: Record = {}; + // Per-row linked savings account number, keyed by the Share account's own acNo โ€” used to make + // sure a Share-to-linked-account Bank Transfer actually credits the right account. + rowLinkedSavingsAcNo: Record = {}; // Full account + client data accountDetails: any = null; @@ -1205,6 +1240,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { }; this.rows.push(mirror); this.selectedRow = mirror; + if (active.acTyp === 'SHR') { + this.syncShareLinkedAmounts(); + } return; } const row: TxnRow = { @@ -1499,6 +1537,182 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { }); } + /** One-step Share redemption from Bank Transfer mode: DR row is the Share account (its + * "SHARE NO." field), CR row must be that account's own linked savings account. Submits a + * governed Share Withdrawal Request (same maker-checker table/flow as the dedicated screen) โ€” + * approval both redeems the shares and settles the linked account automatically. */ + private submitShareRedemptionTransfer(shareRow: TxnRow, savingsRow: TxnRow): void { + // Belt-and-braces: the TXN TP. dropdown already hides cash payment types for the CR (savings) + // row (see paymentTypesForRow()), but re-check here too in case its payment type was set before + // that filter applied โ€” settlement is always a book-transfer to the linked savings account, and + // it's the CR row's payment type that actually governs how that credit posts, never the DR + // (Share) row's, which has no real payment-type concept. + const settlementPaymentType = this.paymentTypes.find((p: any) => p.id === savingsRow.paymentTypeId); + if (!savingsRow.paymentTypeId) { + this.showAlert('error', 'Settlement Payment Type Required', [ + 'Select a Payment Type on the CR (savings account) row โ€” it governs how the settlement credit posts.' + ]); + return; + } + if (settlementPaymentType?.isCashPayment) { + this.showAlert('error', 'Cash Not Allowed for Share Transactions', [ + 'Share redemption settlement is always a book-transfer to the linked savings account, never cash.', + 'Select a non-cash Payment Type on the CR row (e.g. Transfer or Share Redemption Transfer).' + ]); + return; + } + + const shareAccountId = this.rowAccountIds[shareRow.acNo]; + if (!shareAccountId) { + this.showAlert('error', 'Share Account Not Loaded', [`Please load account details for: ${shareRow.acNo}`]); + return; + } + if (!this.rowAccountIds[savingsRow.acNo]) { + this.showAlert('error', 'Savings Account Not Loaded', [`Please load account details for: ${savingsRow.acNo}`]); + return; + } + + const linkedAcNo = this.rowLinkedSavingsAcNo[shareRow.acNo]; + const enteredAcNo = savingsRow.acNo?.trim(); + const matchesLinked = + !!linkedAcNo && + (linkedAcNo === enteredAcNo || linkedAcNo.endsWith(enteredAcNo) || savingsRow.ecsAcNo === linkedAcNo); + if (linkedAcNo && !matchesLinked) { + this.showAlert('error', 'Wrong Credit Account', [ + `The CR account must be this Share account's own linked savings account: ${linkedAcNo}`, + `You entered: ${enteredAcNo}` + ]); + return; + } + if (!linkedAcNo) { + this.showAlert('error', 'No Linked Savings Account', [ + 'This Share account has no linked savings account configured โ€” settlement has nowhere to go.' + ]); + return; + } + + const shares = Number(shareRow.amount) || 0; + if (shares <= 0) { + this.showAlert('error', 'Share No. Required', ['Enter the number of shares to withdraw (greater than zero).']); + return; + } + if (!shareRow.resolutionNumber?.trim()) { + this.showAlert('error', 'Board Resolution Number Required', [ + 'A share withdrawal must be authorised by a Board Resolution.' + ]); + return; + } + if (!shareRow.meetingDate) { + this.showAlert('error', 'Date of Meeting Required', ['Enter the Board meeting date that passed the resolution.']); + return; + } + + const today = new Date(); + const requestedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + + // Full snapshot of the CR (settlement) row exactly as the clerk filled it in โ€” saved verbatim + // for audit purposes. Business logic still only uses settlementPaymentTypeId above; the rest of + // these fields are not re-read by the backend, they are the audit record of what was on screen. + const settlementRowSnapshot = { + branch: savingsRow.br, + accountType: savingsRow.acTyp, + accountNo: savingsRow.acNo, + ecsAccountNo: savingsRow.ecsAcNo || null, + dbCr: savingsRow.dbCr, + amount: savingsRow.amount ?? null, + chequeNo: savingsRow.chequeNo || null, + chequeDate: savingsRow.cheqDate || null, + tokenNo: savingsRow.tokNo || null, + description: savingsRow.description || null, + bankName: savingsRow.bankName || null, + routingCode: savingsRow.routingCode || null, + paymentTypeId: savingsRow.paymentTypeId, + paymentTypeName: settlementPaymentType?.name ?? null + }; + + const payload = { + shareAccountId, + accountNo: shareRow.acNo, + clientId: this.rowClientIds[shareRow.acNo] ?? null, + clientName: this.rowAccountNames[shareRow.acNo] ?? null, + requestedShares: shares, + requestedDate, + resolutionNumber: shareRow.resolutionNumber.trim(), + meetingDate: shareRow.meetingDate, + description: shareRow.description || null, + settlementPaymentTypeId: savingsRow.paymentTypeId, + settlementRowSnapshot + }; + + const sharePrice = this.rowSharePrices[shareRow.acNo] ?? 0; + const confirmLines = [ + `Share Account: ${shareRow.acNo} ยท Client: ${this.rowAccountNames[shareRow.acNo] ?? 'โ€”'}`, + `Shares to Withdraw: ${shares} @ โ‚น${sharePrice.toLocaleString('en-IN', { minimumFractionDigits: 2 })} = โ‚น${(shares * sharePrice).toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + `Board Resolution No.: ${payload.resolutionNumber}`, + `Date of Meeting: ${payload.meetingDate}`, + `Settlement Credit To: ${linkedAcNo}`, + `Settlement Payment Type: ${settlementPaymentType?.name ?? 'โ€”'}`, + ...(settlementRowSnapshot.ecsAccountNo ? [`ECS A/C No.: ${settlementRowSnapshot.ecsAccountNo}`] : []), + ...(settlementRowSnapshot.chequeNo ? [`Cheque No.: ${settlementRowSnapshot.chequeNo}`] : []), + ...(settlementRowSnapshot.chequeDate ? [`Cheque Date: ${settlementRowSnapshot.chequeDate}`] : []), + ...(settlementRowSnapshot.tokenNo ? [`Token No.: ${settlementRowSnapshot.tokenNo}`] : []), + ...(settlementRowSnapshot.description ? [`CR Row Remarks: ${settlementRowSnapshot.description}`] : []), + '', + 'Validation checks:', + ` โœ“ Share account and linked savings account both loaded`, + ` โœ“ CR account matches this Share account's linked savings account`, + ` โœ“ Settlement Payment Type is non-cash`, + ` โœ“ Board Resolution Number and Date of Meeting provided`, + '', + 'Pending Manager approval โ€” redemption and settlement both post together on approval.' + ]; + this.showConfirmAlert('info', 'Confirm Share Withdrawal Request', confirmLines, 'Submit for Approval', () => + this.doSubmitShareRedemptionTransfer(payload, shareRow, shares, linkedAcNo) + ); + } + + private doSubmitShareRedemptionTransfer( + payload: Record, + shareRow: TxnRow, + shares: number, + linkedAcNo: string + ): void { + this.saving = true; + this.clerkShareWithdrawalService + .submit(payload) + .pipe( + catchError((err: any) => { + const msg = + err?.error?.errors?.[0]?.defaultUserMessage ?? + err?.error?.developerMessage ?? + err?.error?.defaultUserMessage ?? + 'Share withdrawal submission failed. Please try again.'; + this.showAlert('error', 'Submission Failed', [msg]); + this.saving = false; + return of(null); + }) + ) + .subscribe((res: any) => { + this.saving = false; + if (res) { + this.showAlert('success', 'Share Withdrawal Submitted', [ + `${shares} share(s) from ${shareRow.acNo} โ†’ settlement to ${linkedAcNo} on approval.`, + 'Pending Manager approval. Track status in "My Share Requests".' + ]); + this.rows = []; + this.rowAccountIds = {}; + this.rowAccountNames = {}; + this.rowSharePrices = {}; + this.rowClientIds = {}; + this.rowLinkedSavingsAcNo = {}; + this.addRow(); + this.selectedRow = this.rows[0]; + this.accountDetails = null; + this.clientDetails = null; + } + }); + } + /** Cashier enters a token number โ†’ look up the APPROVED withdrawal request for auto-fill. */ onTokNoBlur(row: TxnRow): void { if (this.isClerk || !row.tokNo?.trim()) return; @@ -1535,6 +1749,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { }); } + /** When exactly one row on screen is a Share account with a known market price, auto-fills every + * OTHER row's amount as (shares entered on that row) ร— (its market price) โ€” a calculator + * convenience for filling the paired settlement entry. The Share row itself still can never be + * saved from this screen (see the SHR guard in confirmSubmit()) โ€” this only helps compute the + * correct amount to carry over into the separate manual Deposit Slip entry. */ + syncShareLinkedAmounts(): void { + const shareRows = this.rows.filter((r) => r.acTyp?.toUpperCase() === 'SHR' && this.rowSharePrices[r.acNo]); + if (shareRows.length !== 1) return; + const shareRow = shareRows[0]; + const price = this.rowSharePrices[shareRow.acNo] ?? 0; + const shares = Number(shareRow.amount) || 0; + const total = Math.round(shares * price * 100) / 100; + this.rows.forEach((r) => { + if (r !== shareRow) { + r.amount = total; + } + }); + } + onAcNoBlur(row: TxnRow, index: number): void { if (!row.acNo.trim()) return; this.selectedRow = row; @@ -1854,13 +2087,29 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.rowAccountIds[this.selectedRow.acNo] = acc.id; this.rowAccountNames[this.selectedRow.acNo] = acc.name ?? acc.displayName ?? acc.glAccountName ?? acc.accountNo ?? ''; + this.rowClientIds[this.selectedRow.acNo] = acc.clientId ?? null; } if (this.selectedRow) { - this.selectedRow.ecsAcNo = acc.glCode ?? acc.savingsAccountNumber ?? acc.accountNo ?? ''; + // Share accounts: ECS field is the share account's own number โ€” acc.savingsAccountNumber + // is a different, unrelated account (the linked settlement savings account) and must + // never be shown here. + this.selectedRow.ecsAcNo = + acTyp?.toUpperCase() === 'SHR' + ? (acc.accountNo ?? '') + : (acc.glCode ?? acc.savingsAccountNumber ?? acc.accountNo ?? ''); } if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp); this.computeSameDayNeftRtgs(acc.transactions); this.computeSameDayChequeClearing(acc.transactions); + if (acTyp?.toUpperCase() === 'SHR' && this.selectedRow?.acNo) { + // Keyed by the row's own typed acNo (e.g. "0011"), not the resolved full account number + // (e.g. "000000011") โ€” rowAccountIds/rowAccountNames above use the same key, and every + // lookup elsewhere (syncShareLinkedAmounts, submitShareRedemptionTransfer, the template's + // rowSharePrices[row.acNo]) reads via row.acNo, so this must match or every lookup misses. + this.rowSharePrices[this.selectedRow.acNo] = Number(acc.currentMarketPrice) || 0; + this.rowLinkedSavingsAcNo[this.selectedRow.acNo] = acc.savingsAccountNumber ?? ''; + this.syncShareLinkedAmounts(); + } return true; }; @@ -2110,6 +2359,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // โ”€โ”€ Balance getters โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ get currentBalance(): number { + if (this.selectedRow?.acTyp === 'SHR') { + const shares = Number(this.accountDetails?.summary?.totalApprovedShares ?? 0); + const unitPrice = Number(this.accountDetails?.currentMarketPrice ?? 0); + return shares * unitPrice; + } return this.accountDetails?.summary?.accountBalance ?? this.accountDetails?.organizationRunningBalance ?? 0; } @@ -2523,6 +2777,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { return; } + // Share redemption via linked-account transfer: DR=Share account, CR=its linked savings + // account. A Share account can never be debited by a plain ledger transfer (no such Fineract + // command exists) โ€” so this exact one-DR/one-CR shape is routed to submitShareRedemptionTransfer() + // instead, which submits a governed Share Withdrawal Request through the same maker-checker + // table as the dedicated screen; approval both redeems the shares and settles the linked account. + const shareDrRows = drRows.filter((r) => r.acTyp === 'SHR'); + if (shareDrRows.length > 0) { + if (drRows.length !== 1 || crRows.length !== 1) { + this.showAlert('error', 'Unsupported Share Transfer', [ + 'A Share redemption transfer supports exactly one DR row (the Share account) and one CR row (its linked savings account).' + ]); + return; + } + this.submitShareRedemptionTransfer(shareDrRows[0], crRows[0]); + return; + } + // Bank transfer: only savings (SD) or current (CA/CB) accounts allowed const allowedTypes = new Set([ 'SD', @@ -3025,6 +3296,24 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { const accountId = this.accountDetails.id; const cfg = this.accountTypeConfig(row.acTyp); + // Share accounts must never go through this generic Deposit/Withdrawal Slip flow โ€” both + // buildDepositPayload() and buildWithdrawalPayload() hardcode accountType: 'SAVINGS' and send + // savingsAccountId: this.accountDetails.id, which for a Share account is the SHARE account's + // own numeric id, not a real savings account id. Approving that request would call + // savingsAccountWithdrawal/Deposit against whatever savings account happens to share that same + // numeric id โ€” a completely unrelated account. Share purchase/redemption must go through the + // dedicated Buy Shares / Share Withdrawal Request screens, which carry the correct governance + // (resolution number, meeting date, minimum-tenure and linked-loan checks). + if (row.acTyp === 'SHR') { + this.showAlert('error', 'Not Supported for Share Accounts', [ + isDeposit + ? 'Share purchases must be made from the Buy Shares screen on the Share Account itself.' + : 'Share withdrawal (redeem shares) must be submitted from "New Share Withdrawal Request" in the Clerk Dashboard.', + 'This generic Deposit/Withdrawal Slip does not carry the Board Resolution Number or the governance checks required for shares.' + ]); + return; + } + // GL withdrawal โ†’ direct journal entry (no maker-checker) // GL deposit โ†’ falls through to maker-checker block below if (row.acTyp === 'GL' && !isDeposit) {