Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/app/organization/base-teller/base-teller.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ describe('BaseTellerComponent', () => {

it('uses the existing workflow routes and permission checks', () => {
expect(BASE_TELLER_WORKFLOWS).toMatchObject([
{
materialIcon: 'point_of_sale',
permission: 'READ_BASE_TELLER_CASH_ALLOCATION',
route: [
'/organization',
'base-teller',
'cash-allocations'
]
},
{
fontAwesomeIcon: 'piggy-bank',
permission: 'READ_TELLER',
Expand Down Expand Up @@ -117,13 +126,13 @@ describe('BaseTellerComponent', () => {
environment.productionMode = false;
const component = new BaseTellerComponent();

expect(component.workflows).toHaveLength(9);
expect(component.workflows).toHaveLength(10);
});

it('shows Bill and Service Payment when production mode is enabled', () => {
environment.productionMode = true;
const component = new BaseTellerComponent();

expect(component.workflows).toHaveLength(10);
expect(component.workflows).toHaveLength(11);
});
});
10 changes: 10 additions & 0 deletions src/app/organization/base-teller/base-teller.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export interface BaseTellerWorkflow {
}

export const BASE_TELLER_WORKFLOWS: readonly BaseTellerWorkflow[] = [
{
label: 'cashAllocation.title',
materialIcon: 'point_of_sale',
permission: 'READ_BASE_TELLER_CASH_ALLOCATION',
route: [
'/organization',
'base-teller',
'cash-allocations'
]
},
{
label: 'labels.heading.Savings Account Opening',
fontAwesomeIcon: 'piggy-bank',
Expand Down
46 changes: 46 additions & 0 deletions src/app/organization/base-teller/base-teller.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,50 @@ describe('BaseTellerService', () => {
req.flush([{ cashierId: 9, currentBalance: 40 }]);
expect((await resultPromise)[0].currentBalance).toBe(40);
});

it('uses the exact WEB-1221 context, preview, create, retrieve, and reprint endpoints', async () => {
const contextPromise = firstValueFrom(service.getCashAllocationContext(1, 'CRC'));
const contextRequest = httpMock.expectOne(
(request) => request.url === '/v2/base-teller/cash-allocations/context' && request.method === 'GET'
);
expect(contextRequest.request.params.get('officeId')).toBe('1');
expect(contextRequest.request.params.get('currencyCode')).toBe('CRC');
contextRequest.flush({ businessDate: '2026-09-26', currencies: [], cashiers: [] });
expect((await contextPromise).businessDate).toBe('2026-09-26');

const payload = {
idempotencyKey: 'stable-key',
operationType: 'SAFE_VAULT_OPENING' as const,
officeId: 1,
businessDate: '2026-09-26',
currencyCode: 'CRC',
amount: '100.00',
denominations: [{ denominationId: 'note-100', quantity: 1 }]
};
const previewPromise = firstValueFrom(service.previewCashAllocation(payload));
const previewRequest = httpMock.expectOne('/v2/base-teller/cash-allocations/preview');
expect(previewRequest.request.method).toBe('POST');
expect(previewRequest.request.body).toEqual(payload);
previewRequest.flush({ authoritativeTotal: '100.00' });
expect((await previewPromise).authoritativeTotal).toBe('100.00');

const createPromise = firstValueFrom(service.createCashAllocation(payload));
const createRequest = httpMock.expectOne('/v2/base-teller/cash-allocations');
expect(createRequest.request.method).toBe('POST');
expect(createRequest.request.body.idempotencyKey).toBe('stable-key');
createRequest.flush({ id: 77, reference: 'CA-77' });
expect((await createPromise).id).toBe(77);

const retrievePromise = firstValueFrom(service.getCashAllocation(77));
const retrieveRequest = httpMock.expectOne('/v2/base-teller/cash-allocations/77');
expect(retrieveRequest.request.method).toBe('GET');
retrieveRequest.flush({ id: 77, reference: 'CA-77' });
expect((await retrievePromise).reference).toBe('CA-77');

const reprintPromise = firstValueFrom(service.reprintCashAllocationReceipt(77));
const reprintRequest = httpMock.expectOne('/v2/base-teller/cash-allocations/77/receipt');
expect(reprintRequest.request.method).toBe('GET');
reprintRequest.flush({ id: 77, reference: 'CA-77' });
expect((await reprintPromise).reference).toBe('CA-77');
});
});
132 changes: 131 additions & 1 deletion src/app/organization/base-teller/base-teller.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,107 @@ export interface ServicePaymentReceipt {
denominations: ServicePaymentDenomination[];
}

export type CashAllocationAmount = number | string;
export type CashAllocationType = 'SAFE_VAULT_OPENING' | 'HEAD_CASHIER_ALLOCATION' | 'OPERATIONAL_TELLER_ALLOCATION';

export interface CashAllocationDenomination {
identifier: string;
type: string;
value: CashAllocationAmount;
quantity: number;
subtotal: CashAllocationAmount;
}

export interface CashAllocationCurrency {
code: string;
name: string;
displaySymbol: string;
decimalPlaces: number;
denominations: CashAllocationDenomination[];
}

export interface CashAllocationCashier {
cashierId: FineractId;
tellerId: FineractId;
tellerName: string;
staffId: FineractId;
cashierName: string;
headCashierSource: boolean;
currentBalance: CashAllocationAmount;
}

export interface CashAllocationContext {
businessDate: string;
officeId: FineractId;
officeName: string;
currencyCode?: string;
vaultBalance: CashAllocationAmount;
currencies: CashAllocationCurrency[];
cashiers: CashAllocationCashier[];
}

export interface CashAllocationRequest {
idempotencyKey: string;
operationType: CashAllocationType;
officeId: FineractId;
sourceCashierId?: FineractId;
destinationCashierId?: FineractId;
businessDate: string;
currencyCode: string;
amount: string;
denominations: Array<{
denominationId: string;
quantity: number;
}>;
note?: string;
}

export interface CashAllocationPreview {
operationType: CashAllocationType;
businessDate: string;
officeId: FineractId;
currencyCode: string;
source: string;
destination: string;
cashInflow: CashAllocationAmount;
cashOutflow: CashAllocationAmount;
checksInflow: CashAllocationAmount;
checksOutflow: CashAllocationAmount;
vouchersInflow: CashAllocationAmount;
vouchersOutflow: CashAllocationAmount;
authoritativeTotal: CashAllocationAmount;
denominations: CashAllocationDenomination[];
}

export interface CashAllocationReceipt {
id: FineractId;
reference: string;
operationType: CashAllocationType;
status: string;
businessDate: string;
officeId: FineractId;
officeName: string;
initiatedBy: FineractId;
initiatedByUsername: string;
sourceCashierId?: FineractId;
source: string;
destinationCashierId?: FineractId;
destination: string;
currencyCode: string;
totalAmount: CashAllocationAmount;
sourceBalanceBefore?: CashAllocationAmount;
sourceBalanceAfter?: CashAllocationAmount;
destinationBalanceBefore: CashAllocationAmount;
destinationBalanceAfter: CashAllocationAmount;
sourceCashierTransactionId?: FineractId;
destinationCashierTransactionId?: FineractId;
accountingTransactionId?: string;
note?: string;
createdOn: string;
completedOn?: string;
denominations: CashAllocationDenomination[];
}

/**
* Base Teller service.
*/
Expand All @@ -352,6 +453,7 @@ export class BaseTellerService {
private readonly servicePaymentsPath = '/v2/base-teller/service-payments';
private readonly cashierClosingsPath = '/v2/base-teller/closings';
private readonly cashOperationsPath = '/v2/base-teller/cash-operations';
private readonly cashAllocationsPath = '/v2/base-teller/cash-allocations';

getCashierClosingContext(
cashierId: FineractId,
Expand Down Expand Up @@ -429,7 +531,6 @@ export class BaseTellerService {
getCashManagementCurrencies(): Observable<{ selectedCurrencyOptions?: CurrencyOption[] }> {
return this.http.get<{ selectedCurrencyOptions?: CurrencyOption[] }>('/currencies');
}

/**
* Searches customers through the Base Teller savings-opening workflow API.
*/
Expand Down Expand Up @@ -610,4 +711,33 @@ export class BaseTellerService {
getServicePaymentReceipt(transactionId: FineractId): Observable<ServicePaymentReceipt> {
return this.http.get<ServicePaymentReceipt>(`${this.servicePaymentsPath}/${transactionId}/receipt`);
}

/** Retrieves deployment currencies, denominations, balances, and eligible active cashiers. */
getCashAllocationContext(officeId: FineractId, currencyCode?: string): Observable<CashAllocationContext> {
let params = new HttpParams().set('officeId', String(officeId));
if (currencyCode) {
params = params.set('currencyCode', currencyCode);
}
return this.http.get<CashAllocationContext>(`${this.cashAllocationsPath}/context`, { params });
}

/** Validates an allocation and returns the backend-authoritative transaction preview. */
previewCashAllocation(payload: CashAllocationRequest): Observable<CashAllocationPreview> {
return this.http.post<CashAllocationPreview>(`${this.cashAllocationsPath}/preview`, payload);
}

/** Finalizes an allocation using the request's backend idempotency key. */
createCashAllocation(payload: CashAllocationRequest): Observable<CashAllocationReceipt> {
return this.http.post<CashAllocationReceipt>(this.cashAllocationsPath, payload);
}

/** Retrieves the immutable completed allocation. */
getCashAllocation(allocationId: FineractId): Observable<CashAllocationReceipt> {
return this.http.get<CashAllocationReceipt>(`${this.cashAllocationsPath}/${allocationId}`);
}

/** Retrieves the immutable receipt through the separately permissioned reprint endpoint. */
reprintCashAllocationReceipt(allocationId: FineractId): Observable<CashAllocationReceipt> {
return this.http.get<CashAllocationReceipt>(`${this.cashAllocationsPath}/${allocationId}/receipt`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!--
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/.
-->

<h2 mat-dialog-title>{{ 'cashAllocation.dialog.confirmTitle' | translate }}</h2>
<mat-dialog-content>
<dl class="dialog-details">
<div>
<dt>{{ 'cashAllocation.labels.source' | translate }}</dt>
<dd>{{ preview.source }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.destination' | translate }}</dt>
<dd>{{ preview.destination }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.currency' | translate }}</dt>
<dd>{{ preview.currencyCode }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.cashInflow' | translate }}</dt>
<dd>{{ preview.cashInflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.cashOutflow' | translate }}</dt>
<dd>{{ preview.cashOutflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.checksInflow' | translate }}</dt>
<dd>{{ preview.checksInflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.checksOutflow' | translate }}</dt>
<dd>{{ preview.checksOutflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.vouchersInflow' | translate }}</dt>
<dd>{{ preview.vouchersInflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.vouchersOutflow' | translate }}</dt>
<dd>{{ preview.vouchersOutflow }}</dd>
</div>
<div>
<dt>{{ 'cashAllocation.labels.totalCash' | translate }}</dt>
<dd>{{ preview.authoritativeTotal }}</dd>
</div>
</dl>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" [mat-dialog-close]="false">{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" type="button" [mat-dialog-close]="true">
{{ 'cashAllocation.actions.confirmPost' | translate }}
</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* 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 { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogTitle
} from '@angular/material/dialog';

import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { CashAllocationPreview } from '../base-teller.service';

@Component({
selector: 'mifosx-cash-allocation-confirm-dialog',
templateUrl: './cash-allocation-confirm-dialog.component.html',
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CashAllocationConfirmDialogComponent {
readonly preview = inject<CashAllocationPreview>(MAT_DIALOG_DATA);
}
Loading
Loading