A cash deposit posted to the wrong account is moved to the right one in four steps: unpass the voucher, a manager approves, the cashier enters the correct account, a manager approves again. Nothing changes on any account until the final approval. - Voucher Correction page with four-step strip and popups: today's vouchers to unpass (admin or clerk), the unpassed vouchers waiting for their correct account (cashier) with the wrong and correct account side by side and the balance check, and the caller's own requests. A cashier whose customer has used the money closes the request and deposits separately. - Reached from the cashier bar, the clerk bar and the Cashier menu; the manager reviews both steps under Approvals > Voucher Corrections, laid out like the other approval tabs. Routes for Cash Transfers approvals are added to the same tab bar. - A Correction column on My Vouchers, the day book and the voucher register (and in the voucher detail popup) says whether a voucher is unpassed, revoked in favour of another, a correction, or closed, with the reason. - The deposit taken off the wrong account no longer shows on its passbook or transaction list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import { Injectable, inject } from '@angular/core';
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { Observable } from 'rxjs';
|
|
|
|
/** One row of a voucher correction, as the server keeps it (snake_case columns). */
|
|
export interface VoucherCorrection {
|
|
id: number;
|
|
original_voucher_no: string;
|
|
original_txn_id: number;
|
|
wrong_savings_account_id: number;
|
|
wrong_account_no: string;
|
|
wrong_client_name: string | null;
|
|
amount: number;
|
|
txn_date: string | null;
|
|
status: 'UNPASS_PENDING' | 'UNPASSED' | 'UNPASS_REJECTED' | 'CORRECTION_PENDING' | 'COMPLETED' | 'CLOSED';
|
|
reason: string | null;
|
|
rejection_reason: string | null;
|
|
unpass_by_name: string | null;
|
|
unpass_at: string | null;
|
|
unpass_approved_by_name: string | null;
|
|
correct_account_no: string | null;
|
|
correct_client_name: string | null;
|
|
correction_by_name: string | null;
|
|
correction_at: string | null;
|
|
correction_approved_by_name: string | null;
|
|
new_voucher_no: string | null;
|
|
}
|
|
|
|
/** A cash deposit voucher of today that can still be unpassed. */
|
|
export interface EligibleVoucher {
|
|
voucherNo: string;
|
|
amount: number;
|
|
createdAt: string;
|
|
cashier: string;
|
|
txnId: number;
|
|
savingsAccountId: number;
|
|
accountNo: string;
|
|
clientName: string | null;
|
|
paymentType: string;
|
|
}
|
|
|
|
/** What the server knows about a savings account typed in as the correct one. */
|
|
export interface AccountLookup {
|
|
id: number;
|
|
accountNo: string;
|
|
clientId: number | null;
|
|
clientName: string | null;
|
|
balance: number;
|
|
currencyCode: string;
|
|
canReceive: boolean;
|
|
}
|
|
|
|
/** HTTP service for the voucher correction workflow: unpass, approve, re-enter, approve. */
|
|
@Injectable({ providedIn: 'root' })
|
|
export class VoucherCorrectionService {
|
|
private http = inject(HttpClient);
|
|
private readonly base = '/vouchers/corrections';
|
|
|
|
eligible(search?: string): Observable<EligibleVoucher[]> {
|
|
let params = new HttpParams();
|
|
if (search) params = params.set('search', search);
|
|
return this.http.get<EligibleVoucher[]>(`${this.base}/eligible`, { params });
|
|
}
|
|
|
|
unpass(voucherNo: string, reason: string): Observable<any> {
|
|
return this.http.post(`${this.base}/unpass`, { voucherNo, reason });
|
|
}
|
|
|
|
list(status?: string, mine = false): Observable<VoucherCorrection[]> {
|
|
let params = new HttpParams().set('mine', String(mine));
|
|
if (status) params = params.set('status', status);
|
|
return this.http.get<VoucherCorrection[]>(this.base, { params });
|
|
}
|
|
|
|
get(id: number): Observable<VoucherCorrection> {
|
|
return this.http.get<VoucherCorrection>(`${this.base}/${id}`);
|
|
}
|
|
|
|
accountLookup(accountNo: string): Observable<AccountLookup> {
|
|
return this.http.get<AccountLookup>(`${this.base}/account-lookup`, { params: { accountNo } });
|
|
}
|
|
|
|
submitCorrection(id: number, correctAccountNo: string): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/correction`, { correctAccountNo });
|
|
}
|
|
|
|
/** The cashier gives up the correction, e.g. when the wrong account no longer holds the money. */
|
|
close(id: number, reason: string): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/close`, { reason });
|
|
}
|
|
|
|
approveUnpass(id: number): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/unpass/approve`, {});
|
|
}
|
|
|
|
rejectUnpass(id: number, reason: string): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/unpass/reject`, { reason });
|
|
}
|
|
|
|
approveCorrection(id: number): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/correction/approve`, {});
|
|
}
|
|
|
|
rejectCorrection(id: number, reason: string): Observable<any> {
|
|
return this.http.post(`${this.base}/${id}/correction/reject`, { reason });
|
|
}
|
|
|
|
/** Ids of the transactions of this account that a completed correction removed. */
|
|
voidedTransactions(savingsAccountId: number): Observable<number[]> {
|
|
return this.http.get<number[]>(`${this.base}/voided-transactions`, { params: { savingsAccountId } });
|
|
}
|
|
}
|