Files
cbsnexora/src/app/cashier/cashier.service.ts
T
keshavandClaude Opus 5.5 b6994c0b76 feat(rbi-reserve,savings): Form 1/Form 9 returns UI, branch-wise interest posting reports and summary
- Reports > Interest Posting Reports: per branch and product, generate/regenerate
  at any time, approve; branch column, HO-on-behalf marker, posted txn per account
- Reports > Interest Posting Summary (branch-wise) for the head office, with Excel export
- Day Close warns and disables Close Day while the branch's interest report is unapproved
- Savings product accounting: Interest On Savings (expense), Savings Interest Payable
  and Paid (liability rows, required)
- RBI Form 1 statutory and Form 9 return screens, reserve config updates
- Interest posting report GETs exempted from the response cache

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 13:20:01 +05:30

458 lines
20 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, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
/** HTTP service for the Cashier menu features. */
@Injectable({ providedIn: 'root' })
export class CashierService {
private http = inject(HttpClient);
// ── Active denominations ──
getActiveDenominations(): Observable<any[]> {
return this.http.get<any[]>('/denominationmaster/active');
}
// ── Cash Exchange ──
createCashExchange(data: any): Observable<any> {
return this.http.post('/cashexchange', data);
}
getCashExchanges(offset = 0, limit = 20): Observable<any> {
const params = new HttpParams().set('offset', offset).set('limit', limit);
return this.http.get<any>('/cashexchange', { params });
}
getCashExchange(id: number): Observable<any> {
return this.http.get(`/cashexchange/${id}`);
}
reverseCashExchange(id: number, reason: string): Observable<any> {
return this.http.post(`/cashexchange/${id}/reverse`, { reason });
}
// ── Tellers / Cashiers ──
getTellers(officeId?: number): Observable<any[]> {
let params = new HttpParams();
if (officeId) params = params.set('officeId', String(officeId));
return this.http.get<any[]>('/tellers', { params });
}
getAllCashiers(): Observable<any[]> {
return this.http.get<any[]>('/cashiers');
}
getCashiersForTeller(tellerId: number): Observable<any> {
return this.http.get(`/tellers/${tellerId}/cashiers`);
}
// ── Cash Transfer ──
createCashTransfer(data: any): Observable<any> {
return this.http.post('/cashtransfer', data);
}
getCashTransfers(offset = 0, limit = 20): Observable<any> {
const params = new HttpParams().set('offset', offset).set('limit', limit);
return this.http.get<any>('/cashtransfer', { params });
}
getCashTransferById(id: number): Observable<any> {
return this.http.get(`/cashtransfer/${id}`);
}
reverseCashTransfer(id: number, reason: string): Observable<any> {
return this.http.post(`/cashtransfer/${id}/reverse`, { reason });
}
// ── Cash Settlement ──
createCashSettlement(data: any): Observable<any> {
return this.http.post('/cashsettlement', data);
}
getCashSettlements(offset = 0, limit = 20): Observable<any> {
const params = new HttpParams().set('offset', offset).set('limit', limit);
return this.http.get<any>('/cashsettlement', { params });
}
getCashSettlementById(id: number): Observable<any> {
return this.http.get(`/cashsettlement/${id}`);
}
reverseCashSettlement(id: number, reason: string): Observable<any> {
return this.http.post(`/cashsettlement/${id}/reverse`, { reason });
}
// ── Inter-cashier Transfer ──
createInterCashierTransfer(data: any): Observable<any> {
return this.http.post('/cashintercashier', data);
}
getInterCashierTransfers(offset = 0, limit = 20): Observable<any> {
const params = new HttpParams().set('offset', offset).set('limit', limit);
return this.http.get<any>('/cashintercashier', { params });
}
getInterCashierById(id: number): Observable<any> {
return this.http.get(`/cashintercashier/${id}`);
}
reverseInterCashierTransfer(id: number, reason: string): Observable<any> {
return this.http.post(`/cashintercashier/${id}/reverse`, { reason });
}
// ── Reconciliation ──
createReconciliation(data: any): Observable<any> {
return this.http.post('/cashreconciliation', data);
}
getReconciliations(): Observable<any[]> {
return this.http.get<any[]>('/cashreconciliation');
}
getReconciliationById(id: number): Observable<any> {
return this.http.get(`/cashreconciliation/${id}`);
}
getExpectedBalance(cashierId: number, asOfDate?: string): Observable<any> {
let params = new HttpParams().set('cashierId', String(cashierId));
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreconciliation/expected', { params });
}
// ── Reports ──
closingCashReport(date: string): Observable<any> {
const params = new HttpParams().set('date', date);
return this.http.get('/cashreports/closing', { params });
}
/**
* Every voucher raised on one date — deposits, withdrawals, disbursements, collections and
* bank transfers in a single list, with the day's credit and debit totals.
*/
dayBook(date: string, side?: 'CR' | 'DR', status?: string): Observable<any> {
let params = new HttpParams().set('date', date);
if (side) params = params.set('side', side);
if (status) params = params.set('status', status);
return this.http.get('/cashreports/day-book', { params });
}
/**
* The vouchers raised over a period, one row each, with what was debited and what credited.
*
* Who appears is decided by the server from the caller's own roles — a cashier sees only their
* own work whatever they ask for — so `username` is a filter for those allowed to use it, not a
* way to choose whose report this is.
*/
voucherReport(
from: string,
to: string,
username?: string,
voucherType?: string,
cashOnly?: boolean,
cashiersOnly?: boolean,
systemOnly?: boolean
): Observable<any> {
let params = new HttpParams().set('from', from).set('to', to);
if (username) {
params = params.set('username', username);
}
if (voucherType) {
params = params.set('voucherType', voucherType);
}
// Only sent when asked for: the server treats a missing flag as the full register, so the
// cashier screens opt in to cash and the back-office ones keep seeing everything.
if (cashOnly) {
params = params.set('cashOnly', 'true');
}
// Opt-in the same way: the counter registers ask for the drawer-holders only, the back-office
// ones keep seeing whoever raised a voucher.
if (cashiersOnly) {
params = params.set('cashiersOnly', 'true');
}
// The postings nobody filed a request for: interest a nightly run credited, a charge taken from
// a balance, an escheat, a journal keyed to the ledger. Nowhere else shows them — My Vouchers is
// the reader's own work and a scheduled job is nobody's.
if (systemOnly) {
params = params.set('systemOnly', 'true');
}
return this.http.get('/vouchers/report', { params });
}
/**
* The signed-in user's own day: every voucher they raised and every one they verified, receipts
* and payments in one list. Always scoped to the caller by the server — there is no username
* parameter, because "my work" for somebody else is the register's job, not this screen's.
*/
myVoucherWork(from: string, to: string): Observable<any> {
const params = new HttpParams().set('from', from).set('to', to);
return this.http.get('/vouchers/my-work', { params });
}
/** One voucher and every debit/credit line it is made of — the full slip behind a summarised
* row (a "+2 more" badge in a register only ever shows the first side of each). */
voucherDetail(voucherNo: string): Observable<any> {
const params = new HttpParams().set('voucherNo', voucherNo);
return this.http.get('/vouchers/detail', { params });
}
/**
* Account register for one date: what opened and closed that day, each account's balance as it
* stood then, and what it holds now.
*/
accountRegister(date: string, section?: string): Observable<any> {
let params = new HttpParams().set('date', date);
if (section && section !== 'ALL') params = params.set('section', section);
return this.http.get('/accountregister', { params });
}
// ── Branch day register ──
/** The branch's day register, plus which day is currently open. Defaults to the caller's office. */
branchDayRegister(officeId?: number, limit?: number): Observable<any> {
let params = new HttpParams();
if (officeId) params = params.set('officeId', String(officeId));
if (limit) params = params.set('limit', String(limit));
return this.http.get('/branchday', { params });
}
/**
* The savings products whose interest posting report is due for the date and not yet approved —
* the backend refuses to close any branch's day while one is pending.
*/
interestDayCloseCheck(
date?: string,
officeId?: number
): Observable<{ businessDate: string; canClose: boolean; pendingProducts: string[] }> {
let params = new HttpParams();
if (date) params = params.set('date', date);
if (officeId) params = params.set('officeId', String(officeId));
return this.http.get<{ businessDate: string; canClose: boolean; pendingProducts: string[] }>(
'/savingsinterestpostingreports/day-close-check',
{ params }
);
}
/** Days opened and never closed — today's own open day excluded. */
branchDayMissed(officeId?: number): Observable<any> {
let params = new HttpParams();
if (officeId) params = params.set('officeId', String(officeId));
return this.http.get('/branchday/missed', { params });
}
/**
* What the day should close at, and the arithmetic behind it, so the closing figure does not
* have to be worked out by hand.
*/
branchDayClosePreview(businessDate?: string, officeId?: number): Observable<any> {
let params = new HttpParams();
if (businessDate) params = params.set('businessDate', businessDate);
if (officeId) params = params.set('officeId', String(officeId));
return this.http.get('/branchday/close-preview', { params });
}
/** openingDifferenceReason is required when the opening figure differs from yesterday's closing. */
openBranchDay(
businessDate: string,
openingCash?: number,
officeId?: number,
denominations?: { denominationId: number; denominationValue: number; count: number }[],
openingDifferenceReason?: string
): Observable<any> {
return this.http.post('/branchday/open', {
businessDate,
openingCash: openingCash != null ? String(openingCash) : null,
officeId: officeId ?? null,
denominations: denominations?.length ? denominations : null,
openingDifferenceReason: openingDifferenceReason ?? null,
locale: 'en'
});
}
/** forceCloseReason is required when closing a date earlier than today. */
closeBranchDay(
businessDate: string,
closingCash: number,
remarks?: string,
forceCloseReason?: string,
officeId?: number,
denominations?: { denominationId: number; denominationValue: number; count: number }[]
): Observable<any> {
return this.http.post('/branchday/close', {
businessDate,
closingCash: String(closingCash),
remarks: remarks ?? null,
forceCloseReason: forceCloseReason ?? null,
officeId: officeId ?? null,
denominations: denominations?.length ? denominations : null,
locale: 'en'
});
}
/** The opening/closing denomination breakdown recorded for one business day, if any was captured. */
branchDayDenominations(officeId?: number, businessDate?: string): Observable<any> {
let params = new HttpParams();
if (officeId) params = params.set('officeId', String(officeId));
if (businessDate) params = params.set('businessDate', businessDate);
return this.http.get('/branchday/denominations', { params });
}
/** What the office's cash-at-vault GL account currently holds — zero means the count itself will
* define its opening balance; non-zero means the count must match it exactly. */
cashGlStatus(officeId?: number): Observable<any> {
let params = new HttpParams();
if (officeId) params = params.set('officeId', String(officeId));
return this.http.get('/branchday/cash-gl-status', { params });
}
/** One-step new-institution path: defines (or reconciles against) the cash GL's opening balance
* from a counted denomination breakdown, then opens the business day with that same count. */
migrateCashDenomination(
officeId: number,
businessDate: string,
denominations: { denominationId: number; denominationValue: number; count: number }[],
openingDifferenceReason?: string
): Observable<any> {
return this.http.post('/branchday/migrate-cash-denomination', {
officeId,
businessDate,
denominations,
openingDifferenceReason: openingDifferenceReason ?? null,
locale: 'en'
});
}
/** Denomination-wise stock a vault (teller) currently holds — "how much of what does the bank have". */
vaultPosition(tellerId?: number, asOfDate?: string): Observable<any> {
let params = new HttpParams();
if (tellerId) params = params.set('tellerId', String(tellerId));
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreports/vault-position', { params });
}
excessTransactionReport(threshold: number, fromDate?: string, toDate?: string): Observable<any[]> {
let params = new HttpParams().set('threshold', String(threshold));
if (fromDate) params = params.set('fromDate', fromDate);
if (toDate) params = params.set('toDate', toDate);
return this.http.get<any[]>('/cashreports/excess', { params });
}
cashierPositionReport(cashierId?: number, asOfDate?: string): Observable<any> {
let params = new HttpParams();
if (cashierId) params = params.set('cashierId', String(cashierId));
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreports/cashier-position', { params });
}
getCashierPositionForMe(asOfDate?: string): Observable<any> {
let params = new HttpParams();
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreports/cashier-position/me', { params });
}
getCashierDayStatus(): Observable<any> {
return this.http.get('/cashreports/cashier-day-status');
}
getGlEntries(date: string, cashierId?: number, tellerId?: number, offset = 0, limit = 20): Observable<any> {
let params = new HttpParams().set('date', date).set('offset', String(offset)).set('limit', String(limit));
if (cashierId) params = params.set('cashierId', String(cashierId));
if (tellerId) params = params.set('tellerId', String(tellerId));
return this.http.get('/cashreports/gl-entries', { params });
}
checkSettlement(cashierId: number, date: string): Observable<any> {
const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date);
return this.http.get('/cashsettlement/check', { params });
}
checkReconciliation(cashierId: number, date: string): Observable<any> {
const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date);
return this.http.get('/cashreconciliation/check', { params });
}
getCashierMorningBalance(cashierId: number, date: string): Observable<any> {
const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date);
return this.http.get('/cashreports/cashier-morning', { params });
}
getExpectedBalanceForMe(asOfDate?: string): Observable<any> {
let params = new HttpParams();
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreconciliation/expected/me', { params });
}
// ── Current cashier identity ──
// Returns cashierId + cashierName for the logged-in user, or null if not a cashier.
getCurrentCashierInfo(): Observable<{ cashierId: number; cashierName: string } | null> {
return this.getCashierPositionForMe().pipe(
map((pos: any) => (pos?.cashierId ? { cashierId: pos.cashierId, cashierName: pos.cashierName ?? '' } : null)),
catchError(() => of(null))
);
}
// ── Payment Types ──
getPaymentTypes(): Observable<any[]> {
return this.http.get<any[]>('/paymenttypes');
}
/** One unapplied Share Sundry receipt, found by the voucher number the member quotes. */
getShareAdvanceByVoucher(voucherNo: string, glAccountNo: string, clientId?: number): Observable<any> {
// The voucher travels as a query parameter, not a path segment: numbers are formatted
// CR/001/2108/0001, and Spring Security's strict firewall rejects the encoded slashes
// (%2F) outright with a 400 before the request ever reaches the resource.
// clientId lets the backend adopt a cashier deposit made straight to a GL account, which
// carries no member of its own. glAccountNo is required — which GL account to search in is
// always chosen by hand, never resolved from a share product's accounting setup, so a broken
// or missing product mapping can never make a genuine voucher unreachable.
let params = new HttpParams().set('voucherNo', voucherNo).set('glAccountNo', glAccountNo);
if (clientId) {
params = params.set('clientId', String(clientId));
}
return this.http.get('/shareadvancecollection/by-voucher', { params });
}
/** GL accounts selectable when searching for a voucher, for the picker on the Apply Collection screen. */
getShareAdvanceGlAccounts(): Observable<any> {
return this.http.get('/shareadvancecollection/glaccounts');
}
/** Share value + any outstanding entry fee for one share account, shown before a voucher search. */
getShareAdvanceRequirement(shareAccountId: number): Observable<any> {
return this.http.get('/shareadvancecollection/requirement', { params: { shareAccountId: String(shareAccountId) } });
}
/**
* Everything an Apply screen needs, in one call: `own` is this member's own unapplied receipts,
* ready to tick; `rows` is every other Share Collection receipt still available — unclaimed GL
* deposits and receipts linked to another member — which attach by voucher instead.
*
* One request rather than two: both lists come from the same table under the same filters, and
* splitting them across endpoints only meant the same rows were fetched twice and, where they
* overlapped, drawn twice on the same screen.
*/
getShareCollectionReceipts(clientId?: number): Observable<any> {
const params = clientId ? new HttpParams().set('clientId', String(clientId)) : undefined;
return this.http.get('/shareadvancecollection/unattributed', { params });
}
/** Moves a receipt out of Sundry into share capital for an activated account. */
applyShareAdvance(collectionId: number, data: any): Observable<any> {
return this.http.post(`/shareadvancecollection/${collectionId}/apply`, data);
}
/** The receipt's own real Dr/Cr GL legs — never the GL account currently picked in a search box,
* which is unrelated screen state that can name a different account than this receipt sits in. */
getShareAdvanceGlEntries(collectionId: number): Observable<any> {
return this.http.get(`/shareadvancecollection/${collectionId}/gl-entries`);
}
/** Every additional-share tranche awaiting approval, across every share account. */
getShareAdditionRequests(): Observable<any> {
return this.http.get('/shareadditionrequests');
}
/**
* Full detail for one pending tranche — the list row plus route-specific proof of funding: the
* voucher(s) behind a cash tranche, or the savings account already debited for a savings tranche.
*/
getShareAdditionRequest(transactionId: number): Observable<any> {
return this.http.get(`/shareadditionrequests/${transactionId}`);
}
/**
* Entry Type options for a GL credit (General / Share Collection / DD Issue / …) — the
* "GLEntryType" code under Admin > System > Manage Codes, so an admin can add another type later
* without a deployment. Empty on failure rather than erroring, since a row missing this dropdown's
* options should still be usable for every other field.
*/
getEntryTypeOptions(): Observable<any[]> {
return this.http.get<any[]>('/codes/name/GLEntryType/codevalues').pipe(catchError(() => of([])));
}
}