- 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>
458 lines
20 KiB
TypeScript
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([])));
|
|
}
|
|
}
|