feat(shares): route dividend cash claims through cashier token disbursement
Cash claims now require a token at claim submission; a cashier redeems it at Transaction Entry to record physical hand-over, mirroring the existing clerk-withdrawal-request flow -- no double-disbursement, and the manager never hands over cash directly at approval time. Also: dividend-claim tokens fall back through the same TOK. NO. lookup used for withdrawal requests, silenced against the global error interceptor since the caller already handles every outcome; the account summary card now shows "Dividend Claim" instead of a misleading "A/c Savings" label when resolving one; and cash amounts are floored to whole rupees client-side to match cash's whole-rupee denominations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { catchError, debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { forkJoin, of, Subject } from 'rxjs';
|
||||
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -20,6 +20,7 @@ import { CashierService } from '../cashier.service';
|
||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||
import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service';
|
||||
import { ClerkWithdrawalService } from '../clerk-withdrawal.service';
|
||||
import { DividendClaimRequestService } from '../../clerk/dividend-claim-request.service';
|
||||
import { ClerkDisbursementService } from '../clerk-disbursement.service';
|
||||
import { ClerkShareWithdrawalService } from '../clerk-share-withdrawal.service';
|
||||
import { CashierDepositService } from '../cashier-deposit.service';
|
||||
@@ -749,6 +750,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Active approved withdrawal (cashier disburse flow)
|
||||
activeWithdrawalRequest: any = null;
|
||||
activeDividendClaimRequest: any = null;
|
||||
loadingWithdrawal = false;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
@@ -757,6 +759,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
private dialog = inject(MatDialog);
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private clerkWithdrawalService = inject(ClerkWithdrawalService);
|
||||
private dividendClaimRequestService = inject(DividendClaimRequestService);
|
||||
private clerkDisbursementService = inject(ClerkDisbursementService);
|
||||
private clerkShareWithdrawalService = inject(ClerkShareWithdrawalService);
|
||||
private cashierDepositService = inject(CashierDepositService);
|
||||
@@ -1731,10 +1734,28 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const token = row.tokNo.trim();
|
||||
this.loadingWithdrawal = true;
|
||||
this.activeWithdrawalRequest = null;
|
||||
this.activeDividendClaimRequest = null;
|
||||
this.clerkWithdrawalService
|
||||
.getByToken(token)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
// Not a withdrawal-request token at all (as opposed to PENDING/WITHDRAWN/REJECTED,
|
||||
// which are legitimate matches with their own specific error) — try the dividend
|
||||
// cash-claim token pool before giving up, since both share the same TOK. NO. field.
|
||||
if (err?.status === 404) {
|
||||
return this.dividendClaimRequestService.getByToken(token).pipe(
|
||||
catchError((divErr: any) => {
|
||||
this.loadingWithdrawal = false;
|
||||
const msg: string =
|
||||
divErr?.error?.message ??
|
||||
divErr?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
`Token '${token}' could not be verified. Please try again.`;
|
||||
this.showAlert('error', `Token '${token}'`, [msg]);
|
||||
return of(null);
|
||||
}),
|
||||
map((divReq: any) => (divReq ? { __dividendClaim: true, ...divReq } : null))
|
||||
);
|
||||
}
|
||||
this.loadingWithdrawal = false;
|
||||
const msg: string =
|
||||
err?.error?.message ??
|
||||
@@ -1747,6 +1768,24 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
.subscribe((req: any) => {
|
||||
this.loadingWithdrawal = false;
|
||||
if (!req) return;
|
||||
|
||||
if (req.__dividendClaim) {
|
||||
this.activeDividendClaimRequest = req;
|
||||
// No real savings/current account backs a dividend cash claim (it's a pure GL
|
||||
// liability settled at claim-approval time) — stub just enough of accountDetails
|
||||
// and kycDetails for onSave()'s generic guards to pass without stale/unrelated data.
|
||||
this.accountDetails = { id: null, accountNo: 'DIVIDEND-CLAIM-' + req.id, clientName: req.client_name };
|
||||
this.kycDetails = null;
|
||||
this.clientDetails = null;
|
||||
this.accountError = '';
|
||||
row.acNo = req.client_name ?? 'Dividend Claim #' + req.id;
|
||||
row.acTyp = 'SD';
|
||||
row.amount = Number(req.amount) || null;
|
||||
row.dbCr = 'Dr';
|
||||
this.selectedRow = row;
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeWithdrawalRequest = req;
|
||||
row.acNo = req.account_no ?? '';
|
||||
row.acTyp = 'SD';
|
||||
@@ -3012,7 +3051,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// Cashier direct withdrawal BLOCKED — must use approved maker-checker token
|
||||
if (!this.isClerk && !this.reviewMode && !isDeposit && !this.activeWithdrawalRequest) {
|
||||
if (
|
||||
!this.isClerk &&
|
||||
!this.reviewMode &&
|
||||
!isDeposit &&
|
||||
!this.activeWithdrawalRequest &&
|
||||
!this.activeDividendClaimRequest
|
||||
) {
|
||||
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||
'Withdrawals cannot be processed directly by the cashier.',
|
||||
'All withdrawals must go through the Clerk → Manager approval process.',
|
||||
@@ -3021,9 +3066,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Approved withdrawal: amount/denomination must exactly match
|
||||
if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) {
|
||||
const approvedAmt = Number(this.activeWithdrawalRequest.amount);
|
||||
// Approved withdrawal / dividend cash claim: amount/denomination must exactly match
|
||||
if (!this.isClerk && (this.activeWithdrawalRequest || this.activeDividendClaimRequest) && !isDeposit) {
|
||||
const rawApprovedAmt = Number(this.activeWithdrawalRequest?.amount ?? this.activeDividendClaimRequest?.amount);
|
||||
// A dividend cash claim's liability is stored with paise precision (a proportional profit
|
||||
// share), but cash can only be handed over in whole-rupee notes/coins -- compare against
|
||||
// the floored amount so an exact denomination match is actually achievable. Ordinary
|
||||
// withdrawal-request amounts are already whole rupees by convention, so this only changes
|
||||
// behavior for dividend claims.
|
||||
const approvedAmt = this.activeDividendClaimRequest ? Math.floor(rawApprovedAmt) : rawApprovedAmt;
|
||||
if (Math.abs(amount - approvedAmt) > 0.005) {
|
||||
if (isCash) {
|
||||
this.showAlert('error', 'Denomination Mismatch — Approved Withdrawal', [
|
||||
@@ -3116,6 +3167,30 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const isCash = !!selectedPtEarly?.isCashPayment;
|
||||
const isGlType = row.acTyp === 'GL';
|
||||
|
||||
// Dividend cash claim token disbursement: the GL entry (Dr Dividend Payable / Cr Cash at
|
||||
// Main Vault) already posted at claim-approval time — there is no real savings/current
|
||||
// account behind this row, so skip balance/min-balance/KYC/dormant/facility checks entirely
|
||||
// (none apply to a GL-only liability payout) and go straight to the confirm dialog.
|
||||
if (this.activeDividendClaimRequest) {
|
||||
if (!row.paymentTypeId) {
|
||||
this.showAlert('error', 'Payment Type Required', ['Please select Payment Type (Cash / Cheque / NEFT etc.)']);
|
||||
return;
|
||||
}
|
||||
if (isWithdrawal && this.denominationTotal > 0 && this.hasWithdrawalDenomOverdraft) {
|
||||
const lines = this.denominationOverdrafts.map(
|
||||
(o) => `${o.label}: Need ${o.requested}, Available ${o.available} (Short by ${o.shortfall})`
|
||||
);
|
||||
this.showAlert('error', 'Insufficient Denomination Stock', [
|
||||
'Your cash drawer does not have enough notes/coins:',
|
||||
...lines,
|
||||
'Adjust denomination or request vault replenishment.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly);
|
||||
return;
|
||||
}
|
||||
|
||||
// GL transactions post as journal entries — skip account-level facility/balance checks
|
||||
if (isGlType) {
|
||||
if (!this.cashVaultGlId) {
|
||||
@@ -3261,7 +3336,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void {
|
||||
// Token-based cashier disbursement: balance was already deducted at manager approval
|
||||
const isDisbursement = !isDeposit && !this.isClerk && !!this.activeWithdrawalRequest;
|
||||
const isDisbursement =
|
||||
!isDeposit && !this.isClerk && (!!this.activeWithdrawalRequest || !!this.activeDividendClaimRequest);
|
||||
// Cashier deposit (savings/current/GL) goes via maker-checker: balance credits only after manager approval
|
||||
const isDepositRequest =
|
||||
isDeposit &&
|
||||
@@ -3292,7 +3368,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
paymentType: selectedPt?.name ?? 'Cash',
|
||||
chequeNo: row.chequeNo || null,
|
||||
symbol: this.currencySymbol,
|
||||
tokNo: this.activeWithdrawalRequest?.tok_no ?? null,
|
||||
tokNo: this.activeWithdrawalRequest?.tok_no ?? this.activeDividendClaimRequest?.tok_no ?? null,
|
||||
isDdCashGl: this.isDdCashGlRow(row),
|
||||
ddFaceValue: row.amount ?? 0,
|
||||
ddCharges: row.ddCharges ?? 0,
|
||||
@@ -3354,9 +3430,52 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dividend cash claim token disbursement → GL entry already posted at claim-approval time.
|
||||
// Do NOT post any new transaction/journal entry — just record the physical hand-over via
|
||||
// DividendClaimRequestService, mirroring the activeWithdrawalRequest branch just below.
|
||||
if (!this.isClerk && this.activeDividendClaimRequest && !isDeposit) {
|
||||
this.saving = true;
|
||||
const selectedPtDisb = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
const denomJson =
|
||||
selectedPtDisb?.isCashPayment && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null;
|
||||
const req = this.activeDividendClaimRequest;
|
||||
this.dividendClaimRequestService
|
||||
.disburse(req.id, denomJson, amount)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.message ??
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
'Disbursement recording failed. Please try again.';
|
||||
this.showAlert('error', 'Disbursement Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
this.showAlert('success', 'Cash Disbursed Successfully', [
|
||||
`₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} disbursed to customer.`,
|
||||
`Token No: ${req.tok_no} · Client: ${req.client_name}`
|
||||
]);
|
||||
this.activeDividendClaimRequest = null;
|
||||
this.clearDenominations();
|
||||
this.loadOpeningPosition();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard block: cashier direct withdrawal never reaches here — caught in onSave()
|
||||
// Extra safety net in case confirmSubmit() is ever called directly
|
||||
if (!this.isClerk && !this.reviewMode && !isDeposit && !this.activeWithdrawalRequest) {
|
||||
if (
|
||||
!this.isClerk &&
|
||||
!this.reviewMode &&
|
||||
!isDeposit &&
|
||||
!this.activeWithdrawalRequest &&
|
||||
!this.activeDividendClaimRequest
|
||||
) {
|
||||
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||
'Withdrawals must go through the maker-checker approval process.',
|
||||
'Enter the approved token number to disburse.'
|
||||
|
||||
Reference in New Issue
Block a user