Skip to content

Commit 4a46d29

Browse files
authored
Merge branch 'dev' into WEB-1247-remove-orphaned-e2e-server-and-update-playwright-readme
2 parents 3b7e4c0 + 749a8b5 commit 4a46d29

11 files changed

Lines changed: 1868 additions & 0 deletions

‎src/app/home/activities.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ const activities: any[] = [
5252
path: '/organization/base-teller/savings-account-openings',
5353
permission: 'READ_TELLER'
5454
},
55+
{
56+
activity: 'labels.heading.Savings Account Deposit',
57+
path: '/organization/base-teller/savings-account-deposits',
58+
permission: 'DEPOSIT_SAVINGSACCOUNT'
59+
},
5560
{ activity: 'manage funds', path: '/organization/manage-funds' },
5661
{ activity: 'chart of accounts', path: '/accounting/chart-of-accounts' },
5762
{ activity: 'frequent postings', path: '/accounting/journal-entries/frequent-postings' },
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { provideHttpClient } from '@angular/common/http';
10+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
11+
import { TestBed } from '@angular/core/testing';
12+
import { firstValueFrom } from 'rxjs';
13+
import { describe, expect, it, beforeEach, afterEach } from '@jest/globals';
14+
15+
import { BaseTellerService } from './base-teller.service';
16+
17+
describe('BaseTellerService deposit workflow', () => {
18+
let service: BaseTellerService;
19+
let httpMock: HttpTestingController;
20+
21+
beforeEach(() => {
22+
TestBed.configureTestingModule({
23+
providers: [
24+
BaseTellerService,
25+
provideHttpClient(),
26+
provideHttpClientTesting()
27+
]
28+
});
29+
30+
service = TestBed.inject(BaseTellerService);
31+
httpMock = TestBed.inject(HttpTestingController);
32+
});
33+
34+
afterEach(() => {
35+
httpMock.verify();
36+
});
37+
38+
it('searches customers and savings accounts through the platform search endpoint', async () => {
39+
const resultPromise = firstValueFrom(service.searchDepositCustomersAndAccounts('amina'));
40+
41+
const req = httpMock.expectOne((request) => request.url === '/search' && request.method === 'GET');
42+
expect(req.request.params.get('query')).toBe('amina');
43+
expect(req.request.params.get('resource')).toBe('clients,savings');
44+
expect(req.request.params.get('exactMatch')).toBe('false');
45+
req.flush([{ entityId: 9 }]);
46+
47+
expect(await resultPromise).toEqual([{ entityId: 9 }]);
48+
});
49+
50+
it('loads customer savings accounts', async () => {
51+
const resultPromise = firstValueFrom(service.getDepositClientAccounts(22));
52+
53+
const req = httpMock.expectOne((request) => request.url === '/clients/22/accounts' && request.method === 'GET');
54+
req.flush({ savingsAccounts: [{ id: 7 }] });
55+
56+
expect(await resultPromise).toEqual({ savingsAccounts: [{ id: 7 }] });
57+
});
58+
59+
it('loads the savings transaction template payment types', async () => {
60+
const resultPromise = firstValueFrom(service.getSavingsDepositTemplate(7));
61+
62+
const req = httpMock.expectOne(
63+
(request) => request.url === '/savingsaccounts/7/transactions/template' && request.method === 'GET'
64+
);
65+
req.flush({ paymentTypeOptions: [{ id: 3, isCashPayment: true }] });
66+
67+
expect(await resultPromise).toEqual({ paymentTypeOptions: [{ id: 3, isCashPayment: true }] });
68+
});
69+
70+
it('posts a cash deposit payload to the savings transaction deposit command', async () => {
71+
const payload = {
72+
transactionDate: '16 September 2026',
73+
transactionAmount: 100,
74+
paymentTypeId: 3,
75+
dateFormat: 'dd MMMM yyyy',
76+
locale: 'en'
77+
};
78+
const resultPromise = firstValueFrom(service.depositToSavingsAccount(7, payload));
79+
80+
const req = httpMock.expectOne((request) => request.url === '/savingsaccounts/7/transactions');
81+
expect(req.request.method).toBe('POST');
82+
expect(req.request.params.get('command')).toBe('deposit');
83+
expect(req.request.body).toEqual(payload);
84+
req.flush({ resourceId: 55 });
85+
86+
expect(await resultPromise).toEqual({ resourceId: 55 });
87+
});
88+
89+
it('posts a check deposit payload using backend payment detail fields', async () => {
90+
const payload = {
91+
transactionDate: '16 September 2026',
92+
transactionAmount: 250,
93+
paymentTypeId: 4,
94+
accountNumber: 'CHK-ACCOUNT',
95+
checkNumber: 'CHK-009',
96+
routingCode: 'RT-1',
97+
receiptNumber: 'RC-1',
98+
bankNumber: 'BANK-1',
99+
dateFormat: 'dd MMMM yyyy',
100+
locale: 'en'
101+
};
102+
const resultPromise = firstValueFrom(service.depositToSavingsAccount(7, payload));
103+
104+
const req = httpMock.expectOne((request) => request.url === '/savingsaccounts/7/transactions');
105+
expect(req.request.params.get('command')).toBe('deposit');
106+
expect(req.request.body).toEqual(payload);
107+
req.flush({ resourceId: 56 });
108+
109+
expect(await resultPromise).toEqual({ resourceId: 56 });
110+
});
111+
112+
it('propagates backend errors from deposit submission', async () => {
113+
const payload = {
114+
transactionDate: '16 September 2026',
115+
transactionAmount: 100,
116+
paymentTypeId: 3,
117+
dateFormat: 'dd MMMM yyyy',
118+
locale: 'en'
119+
};
120+
const resultPromise = firstValueFrom(service.depositToSavingsAccount(7, payload)).catch((error) => error);
121+
122+
const req = httpMock.expectOne((request) => request.url === '/savingsaccounts/7/transactions');
123+
req.flush({ defaultUserMessage: 'paymentTypeId is required' }, { status: 400, statusText: 'Bad Request' });
124+
125+
expect((await resultPromise).status).toBe(400);
126+
});
127+
});

‎src/app/organization/base-teller/base-teller.service.ts‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,120 @@ import { HttpClient, HttpParams } from '@angular/common/http';
1313
/** rxjs Imports */
1414
import { Observable } from 'rxjs';
1515

16+
export type FineractId = string | number;
17+
18+
export interface DepositSearchResult {
19+
id?: FineractId;
20+
entityId?: FineractId;
21+
accountId?: FineractId;
22+
clientId?: FineractId;
23+
accountNo?: string;
24+
entityAccountNo?: string;
25+
displayName?: string;
26+
entityName?: string;
27+
name?: string;
28+
entityType?: string;
29+
subEntityType?: string;
30+
accountType?: string;
31+
}
32+
33+
export interface DepositClient {
34+
id?: FineractId;
35+
accountNo?: string;
36+
displayName?: string;
37+
fullname?: string;
38+
name?: string;
39+
externalId?: string;
40+
}
41+
42+
export interface DepositSavingsAccount {
43+
id?: FineractId;
44+
accountId?: FineractId;
45+
savingsId?: FineractId;
46+
accountNo?: string;
47+
accountNumber?: string;
48+
productName?: string;
49+
savingsProductName?: string;
50+
clientId?: FineractId;
51+
clientName?: string;
52+
clientDisplayName?: string;
53+
currency?: { code?: string };
54+
currencyCode?: string;
55+
status?:
56+
| {
57+
active?: boolean;
58+
value?: string;
59+
code?: string;
60+
}
61+
| string;
62+
summary?: {
63+
accountBalance?: number | null;
64+
availableBalance?: number | null;
65+
};
66+
accountBalance?: number | null;
67+
}
68+
69+
export interface DepositClientAccounts {
70+
savingsAccounts?: DepositSavingsAccount[];
71+
savingAccounts?: DepositSavingsAccount[];
72+
}
73+
74+
export interface DepositPaymentType {
75+
id: FineractId;
76+
name?: string;
77+
value?: string;
78+
description?: string;
79+
isCashPayment?: boolean;
80+
position?: number;
81+
codeName?: string;
82+
isSystemDefined?: boolean;
83+
}
84+
85+
export interface SavingsDepositTemplate {
86+
paymentTypeOptions?: DepositPaymentType[];
87+
}
88+
89+
export interface SavingsDepositCommandPayload {
90+
transactionDate: string;
91+
transactionAmount: number;
92+
paymentTypeId: FineractId;
93+
dateFormat: string;
94+
locale: string;
95+
accountNumber?: string;
96+
checkNumber?: string;
97+
routingCode?: string;
98+
receiptNumber?: string;
99+
bankNumber?: string;
100+
note?: string;
101+
}
102+
103+
export interface SavingsDepositCommandResult {
104+
resourceId?: FineractId;
105+
entityId?: FineractId;
106+
transactionId?: FineractId;
107+
savingsId?: FineractId;
108+
changes?: Record<string, unknown>;
109+
}
110+
111+
export interface SavingsDepositReceipt extends SavingsDepositCommandResult {
112+
id?: FineractId;
113+
accountNo?: string;
114+
accountNumber?: string;
115+
amount?: number | null;
116+
transactionAmount?: number | null;
117+
runningBalance?: number | null;
118+
cumulativeBalance?: number | null;
119+
paymentDetailData?: {
120+
receiptNumber?: string;
121+
paymentType?: {
122+
name?: string;
123+
};
124+
};
125+
transactionType?: {
126+
value?: string;
127+
};
128+
}
129+
16130
/**
17131
* Base Teller service.
18132
*/
@@ -63,4 +177,66 @@ export class BaseTellerService {
63177
getSavingsOpeningReceipt(receiptNumber: string | number): Observable<any> {
64178
return this.http.get(`${this.savingsAccountOpeningsPath}/${receiptNumber}`);
65179
}
180+
181+
/**
182+
* Searches clients and savings accounts using the authoritative platform search endpoint.
183+
*/
184+
searchDepositCustomersAndAccounts(
185+
searchTerm: string
186+
): Observable<DepositSearchResult[] | { pageItems?: DepositSearchResult[] }> {
187+
const params = new HttpParams()
188+
.set('exactMatch', 'false')
189+
.set('query', searchTerm)
190+
.set('resource', 'clients,savings');
191+
return this.http.get<DepositSearchResult[] | { pageItems?: DepositSearchResult[] }>('/search', { params });
192+
}
193+
194+
/**
195+
* Retrieves a client profile.
196+
*/
197+
getDepositClient(clientId: FineractId): Observable<DepositClient> {
198+
return this.http.get<DepositClient>(`/clients/${clientId}`);
199+
}
200+
201+
/**
202+
* Retrieves the customer's accounts so an existing active savings account can be selected.
203+
*/
204+
getDepositClientAccounts(clientId: FineractId): Observable<DepositClientAccounts> {
205+
return this.http.get<DepositClientAccounts>(`/clients/${clientId}/accounts`);
206+
}
207+
208+
/**
209+
* Retrieves the selected savings account with server-provided balances and status.
210+
*/
211+
getDepositSavingsAccount(savingsId: FineractId): Observable<DepositSavingsAccount> {
212+
const params = new HttpParams().set('associations', 'all');
213+
return this.http.get<DepositSavingsAccount>(`/savingsaccounts/${savingsId}`, { params });
214+
}
215+
216+
/**
217+
* Retrieves payment type options for the savings transaction command.
218+
*/
219+
getSavingsDepositTemplate(savingsId: FineractId): Observable<SavingsDepositTemplate> {
220+
return this.http.get<SavingsDepositTemplate>(`/savingsaccounts/${savingsId}/transactions/template`);
221+
}
222+
223+
/**
224+
* Deposits funds into an existing savings account.
225+
*/
226+
depositToSavingsAccount(
227+
savingsId: FineractId,
228+
payload: SavingsDepositCommandPayload
229+
): Observable<SavingsDepositCommandResult> {
230+
const params = new HttpParams().set('command', 'deposit');
231+
return this.http.post<SavingsDepositCommandResult>(`/savingsaccounts/${savingsId}/transactions`, payload, {
232+
params
233+
});
234+
}
235+
236+
/**
237+
* Retrieves the saved transaction as the authoritative receipt/details response.
238+
*/
239+
getSavingsDepositReceipt(savingsId: FineractId, transactionId: FineractId): Observable<SavingsDepositReceipt> {
240+
return this.http.get<SavingsDepositReceipt>(`/savingsaccounts/${savingsId}/transactions/${transactionId}`);
241+
}
66242
}

0 commit comments

Comments
 (0)