/** * 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 { return this.http.get('/denominationmaster/active'); } // ── Cash Exchange ── createCashExchange(data: any): Observable { return this.http.post('/cashexchange', data); } getCashExchanges(offset = 0, limit = 20): Observable { const params = new HttpParams().set('offset', offset).set('limit', limit); return this.http.get('/cashexchange', { params }); } getCashExchange(id: number): Observable { return this.http.get(`/cashexchange/${id}`); } reverseCashExchange(id: number, reason: string): Observable { return this.http.post(`/cashexchange/${id}/reverse`, { reason }); } // ── Tellers / Cashiers ── getTellers(officeId?: number): Observable { let params = new HttpParams(); if (officeId) params = params.set('officeId', String(officeId)); return this.http.get('/tellers', { params }); } getAllCashiers(): Observable { return this.http.get('/cashiers'); } getCashiersForTeller(tellerId: number): Observable { return this.http.get(`/tellers/${tellerId}/cashiers`); } // ── Cash Transfer ── createCashTransfer(data: any): Observable { return this.http.post('/cashtransfer', data); } getCashTransfers(offset = 0, limit = 20): Observable { const params = new HttpParams().set('offset', offset).set('limit', limit); return this.http.get('/cashtransfer', { params }); } getCashTransferById(id: number): Observable { return this.http.get(`/cashtransfer/${id}`); } reverseCashTransfer(id: number, reason: string): Observable { return this.http.post(`/cashtransfer/${id}/reverse`, { reason }); } // ── Cash Settlement ── createCashSettlement(data: any): Observable { return this.http.post('/cashsettlement', data); } getCashSettlements(offset = 0, limit = 20): Observable { const params = new HttpParams().set('offset', offset).set('limit', limit); return this.http.get('/cashsettlement', { params }); } getCashSettlementById(id: number): Observable { return this.http.get(`/cashsettlement/${id}`); } reverseCashSettlement(id: number, reason: string): Observable { return this.http.post(`/cashsettlement/${id}/reverse`, { reason }); } // ── Inter-cashier Transfer ── createInterCashierTransfer(data: any): Observable { return this.http.post('/cashintercashier', data); } getInterCashierTransfers(offset = 0, limit = 20): Observable { const params = new HttpParams().set('offset', offset).set('limit', limit); return this.http.get('/cashintercashier', { params }); } getInterCashierById(id: number): Observable { return this.http.get(`/cashintercashier/${id}`); } reverseInterCashierTransfer(id: number, reason: string): Observable { return this.http.post(`/cashintercashier/${id}/reverse`, { reason }); } // ── Reconciliation ── createReconciliation(data: any): Observable { return this.http.post('/cashreconciliation', data); } getReconciliations(): Observable { return this.http.get('/cashreconciliation'); } getReconciliationById(id: number): Observable { return this.http.get(`/cashreconciliation/${id}`); } getExpectedBalance(cashierId: number, asOfDate?: string): Observable { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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('/cashreports/excess', { params }); } cashierPositionReport(cashierId?: number, asOfDate?: string): Observable { 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 { let params = new HttpParams(); if (asOfDate) params = params.set('asOfDate', asOfDate); return this.http.get('/cashreports/cashier-position/me', { params }); } getCashierDayStatus(): Observable { return this.http.get('/cashreports/cashier-day-status'); } getGlEntries(date: string, cashierId?: number, tellerId?: number, offset = 0, limit = 20): Observable { 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 { const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date); return this.http.get('/cashsettlement/check', { params }); } checkReconciliation(cashierId: number, date: string): Observable { const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date); return this.http.get('/cashreconciliation/check', { params }); } getCashierMorningBalance(cashierId: number, date: string): Observable { const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date); return this.http.get('/cashreports/cashier-morning', { params }); } getExpectedBalanceForMe(asOfDate?: string): Observable { 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 { return this.http.get('/paymenttypes'); } /** One unapplied Share Sundry receipt, found by the voucher number the member quotes. */ getShareAdvanceByVoucher(voucherNo: string, glAccountNo: string, clientId?: number): Observable { // 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 { 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 { 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 { 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 { 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 { return this.http.get(`/shareadvancecollection/${collectionId}/gl-entries`); } /** Every additional-share tranche awaiting approval, across every share account. */ getShareAdditionRequests(): Observable { 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 { 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 { return this.http.get('/codes/name/GLEntryType/codevalues').pipe(catchError(() => of([]))); } }