feat(cashier+checker): add cashier deposit maker-checker flow and voucher system
- Add cashier deposit service and submit flow via maker-checker - New CashierDepositService: submit, approve, reject, list endpoints - Transaction-entry: cashier SD deposit goes to deposit-requests instead of direct credit - Checker inbox: new Deposit Requests tab with approve/reject review dialog - Cashier nav: My Deposit Requests tracking page - Withdrawal requests: show voucher_no from backend instead of JS-padded id - clerk-withdrawal.service: disburse denominationData accepts null for non-cash - Role-based checker enforcement: only manager/head-cashier can approve/reject - Maker cannot approve own request (self-approval blocked at backend) - Denomination total validation: breakdown must match transaction amount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
61cb09f352
commit
e5d6cd32dc
@@ -19,6 +19,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 { CashierDepositService } from '../cashier-deposit.service';
|
||||
import { SettingsService } from '../../settings/settings.service';
|
||||
|
||||
interface TxnRow {
|
||||
@@ -653,6 +654,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
private dialog = inject(MatDialog);
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private clerkWithdrawalService = inject(ClerkWithdrawalService);
|
||||
private cashierDepositService = inject(CashierDepositService);
|
||||
private router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
@@ -2277,15 +2279,25 @@ 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 balanceAfterTxn = isDisbursement
|
||||
? this.currentBalance // no further deduction — just handing cash
|
||||
: isDeposit
|
||||
? this.currentBalance + amount
|
||||
: this.currentBalance - amount;
|
||||
// Cashier savings deposit goes via maker-checker: balance credits only after manager approval
|
||||
const isDepositRequest = isDeposit && !this.isClerk && row.acTyp === 'SD';
|
||||
const balanceAfterTxn =
|
||||
isDisbursement || isDepositRequest
|
||||
? this.currentBalance // no immediate balance change
|
||||
: isDeposit
|
||||
? this.currentBalance + amount
|
||||
: this.currentBalance - amount;
|
||||
|
||||
this.confirmData = {
|
||||
type: isDisbursement ? 'DISBURSEMENT' : isDeposit ? 'DEPOSIT' : 'WITHDRAWAL',
|
||||
type: isDisbursement
|
||||
? 'DISBURSEMENT'
|
||||
: isDepositRequest
|
||||
? 'DEPOSIT REQUEST'
|
||||
: isDeposit
|
||||
? 'DEPOSIT'
|
||||
: 'WITHDRAWAL',
|
||||
isDisbursement,
|
||||
isDepositRequest,
|
||||
accountNo: this.accountDetails.glCode ?? this.accountDetails.accountNo,
|
||||
clientName: this.clientName,
|
||||
amount,
|
||||
@@ -2320,6 +2332,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cashier savings deposit → route through maker-checker (create PENDING request, no direct credit)
|
||||
// Use the dialog-confirmed amount — for cash that is the denomination total
|
||||
if (!this.isClerk && !this.reviewMode && isDeposit && row.acTyp === 'SD') {
|
||||
this.submitCashierDepositRequest(row, this.confirmData?.amount ?? amount);
|
||||
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) {
|
||||
@@ -2334,7 +2353,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// Do NOT post another savings transaction — just record disbursement
|
||||
if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) {
|
||||
this.saving = true;
|
||||
const denomJson = JSON.stringify(this.denominationCounts);
|
||||
// Send denominations only for cash payments — stale counts on a cheque
|
||||
// disbursement would fail the backend denomination-total check
|
||||
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.activeWithdrawalRequest;
|
||||
this.clerkWithdrawalService
|
||||
.disburse(req.id, denomJson, amount)
|
||||
@@ -2352,9 +2375,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
const disbVoucher = res.voucherNo || req.voucher_no || '—';
|
||||
this.showAlert('success', 'Cash Disbursed Successfully', [
|
||||
`₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} disbursed to customer.`,
|
||||
`Token No: ${req.tok_no} · Client: ${req.client_name}`
|
||||
`Token No: ${req.tok_no} · Client: ${req.client_name}`,
|
||||
`Voucher No: ${disbVoucher}`
|
||||
]);
|
||||
this.activeWithdrawalRequest = null;
|
||||
this.clearDenominations();
|
||||
@@ -2646,18 +2671,78 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
const ref = res.resourceId ?? res.requestId ?? res.id ?? 'N/A';
|
||||
const tok = row.tokNo?.trim() ?? '—';
|
||||
const voucherNo = res.voucherNo ?? '—';
|
||||
this.showAlert('success', 'Withdrawal Request Submitted', [
|
||||
`Withdrawal of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} sent for manager approval.`,
|
||||
'Balance will be deducted only after approval.',
|
||||
`Token No: ${tok} · Request No: ${ref}`
|
||||
`Token No: ${tok} · Voucher No: ${voucherNo}`
|
||||
]);
|
||||
this.onCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Build the deposit-request payload from the current row + loaded account. */
|
||||
private buildDepositPayload(row: TxnRow, amount: number): any {
|
||||
const today = new Date();
|
||||
const txnDateIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
const selectedPt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
// Send denominations only for cash payments — stale counts on a cheque
|
||||
// deposit would fail the backend denomination-total check
|
||||
const isCash = !!selectedPt?.isCashPayment;
|
||||
|
||||
return {
|
||||
accountNo: this.accountDetails.accountNo,
|
||||
accountType: 'SAVINGS',
|
||||
savingsAccountId: this.accountDetails.id,
|
||||
clientId: this.accountDetails.clientId ?? this.clientDetails?.id ?? null,
|
||||
clientName: this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? null,
|
||||
amount,
|
||||
txnDate: txnDateIso,
|
||||
paymentTypeId: row.paymentTypeId || null,
|
||||
paymentTypeName: selectedPt?.name ?? 'Cash',
|
||||
description: row.description || null,
|
||||
chequeNo: row.chequeNo || null,
|
||||
bankName: row.bankName || null,
|
||||
denominationData: isCash && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null
|
||||
};
|
||||
}
|
||||
|
||||
/** Cashier maker-checker: create a PENDING deposit request instead of crediting balance directly. */
|
||||
private submitCashierDepositRequest(row: TxnRow, amount: number): void {
|
||||
const payload = this.buildDepositPayload(row, amount);
|
||||
|
||||
this.saving = true;
|
||||
this.cashierDepositService
|
||||
.submit(payload)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
err?.error?.defaultUserMessage ??
|
||||
'Failed to submit deposit request. Please try again.';
|
||||
this.showAlert('error', 'Deposit Request Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
const voucherNo = res.voucherNo ?? '—';
|
||||
this.showAlert('success', 'Deposit Request Submitted', [
|
||||
`Deposit of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} sent for manager approval.`,
|
||||
'Balance will be credited only after approval.',
|
||||
`Voucher No: ${voucherNo}`
|
||||
]);
|
||||
this.clearDenominations();
|
||||
this.onCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.clerkUiState.clear();
|
||||
this.accountDetails = null;
|
||||
|
||||
Reference in New Issue
Block a user