WHAT THIS CHANGES
==================
Frontend counterpart to the backend FD in-place renewal, senior-citizen
rate, and Payment action work.
1. RENEWAL HISTORY TAB (new) -- fixed-deposit-account-view/renewal-history-tab/.
A vertical timeline, one card per term of the FD, oldest first. Backend
data arrives as a list of lowercase-keyed maps (Postgres unquoted SQL
aliases), which the component normalises. Includes term-rate-chart-dialog,
a popup showing one term's full rate-by-tenure chart, including the
senior-citizen column, with an "Applied" badge on the rate that was
actually used. Route added in fixed-deposits-routing.module.ts, tab
link added in fixed-deposit-account-view.component.html (shown only
when the account has renewal history).
2. PAYMENT DIALOG (new) -- fixed-deposit-account-view/custom-dialogs/payment-dialog/.
Card-based picker for the four supported closure types (Transfer to
Savings, Re-Invest Principal and Interest, Re-Invest Principal Only,
Partial Re-Invest), with Withdraw Deposit filtered out client-side.
Fetches its own template via a new fixedDepositsService method
(getFixedDepositsAccountPaymentTemplate, command=payment) rather than
reusing the Close template, since the Payment template is the only one
the backend includes PARTIAL_REINVEST in.
The Partial Re-Invest amount cap is date-dependent: the backend bounds
it against the account balance AFTER posting interest to the chosen
closed-on date, which lands on the maturity amount on or after
maturity and on the current balance before it. The dialog mirrors this
(reinvestCap / processingOnOrAfterMaturity) so the shown split actually
adds up and the client-side validator agrees with the server. This was
wrong in an earlier pass of this work (compared against a fixed figure
regardless of date) and was caught and fixed via a real reconciliation
check (77000 + 23000 did not equal the displayed total) before this
commit.
Added: a loading-failure state (previously the spinner ran forever on
a failed template fetch), an inline submit-error message (previously a
failed POST left the dialog open with no feedback), an empty-savings-
accounts hint, and accessibility attributes on the option cards
(role="radio", aria-checked, a visible check icon, focus-visible
outline) since selection was previously colour-only.
3. TRANSACTIONS TAB -- merges synthetic "FD Renewed" marker rows into the
real transaction list, since an in-place renewal posts no actual
ledger transaction and would otherwise look like the balance changed
for no reason.
4. INTEREST RATE CHART TAB -- added the senior-citizen rate column.
5. DEPOSIT CERTIFICATE DIALOG -- lists the full renewal history on FD
certificates. renewalCount is renewalHistory.length - 1 (the first
row is the original term, not a renewal); an earlier version of this
used the raw row count and overstated the renewal count by one on a
customer-facing document, caught and fixed before this commit.
6. CLIENTS GENERAL TAB -- fixedDepositAccountsForDisplay folds a renewed
FD's superseded terms out of the account list (each renewal used to
show as two unrelated accounts, one Closed and one Active). Memoised
on the source array's identity so it does not rebuild, and rebind the
table, on every change-detection tick. Only rewrites FD rows; Savings,
RD, Current, and Share account rows pass through this getter
untouched, and the RD/Savings sections of the template were left
bound directly to the original savingAccounts array.
KNOWN CAVEATS
=============
- This depends on the paired backend commit (same date, "feat(deposits):
FD in-place renewal, senior-citizen rate persistence, and Payment
action"). Deploying this frontend without that backend change means
the Payment dialog and Renewal History tab will call endpoints that
do not exist yet.
- Renewal-history rows rely on the backend returning lowercase column
keys (a Postgres-specific behaviour of unquoted SQL aliases via
JdbcTemplate). Confirmed correct against this project's Postgres
dev database; if this project ever runs on a different database
engine, that assumption would need re-checking.
- src/environments/.env.ts is intentionally excluded from this commit;
it is a local build-version stamp file that should never be
committed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
6.6 KiB
TypeScript
198 lines
6.6 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/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Injectable, inject } from '@angular/core';
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
|
|
/** rxjs Imports */
|
|
import { Observable } from 'rxjs';
|
|
|
|
/**
|
|
* Fixed Deposits Service.
|
|
*/
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class FixedDepositsService {
|
|
private http = inject(HttpClient);
|
|
|
|
/**
|
|
* @param accountId Account Id.
|
|
* @returns {Observable<any>} Fixed Deposits data.
|
|
*/
|
|
getFixedDepositsAccountData(accountId: string): Observable<any> {
|
|
const httpParams = new HttpParams().set('associations', 'all');
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}`, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId fixed deposits account Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
deleteFixedDepositsAccount(accountId: string): Observable<any> {
|
|
return this.http.delete(`/fixeddepositaccounts/${accountId}`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Account Id
|
|
* @param {string} command Command
|
|
* @param {any} data Data
|
|
* @returns {Observable<any>}
|
|
*/
|
|
executeFixedDepositsAccountCommand(accountId: string, command: string, data: any): Observable<any> {
|
|
const httpParams = new HttpParams().set('command', command);
|
|
return this.http.post(`/fixeddepositaccounts/${accountId}`, data, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @param {string} transactionId Transaction Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
getFixedDepositsAccountTransaction(accountId: string, transactionId: string): Observable<any> {
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}/transactions/${transactionId}`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @param {string} command Command
|
|
* @param {any} data Data
|
|
* @param {string} transactionId Transaction Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
executeFixedDepositsAccountTransactionsCommand(
|
|
accountId: string,
|
|
command: string,
|
|
data: any,
|
|
transactionId?: any
|
|
): Observable<any> {
|
|
const httpParams = new HttpParams().set('command', command);
|
|
return this.http.post(`/fixeddepositaccounts/${accountId}/transactions/${transactionId}`, data, {
|
|
params: httpParams
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
getFixedDepositsAccountClosureTemplate(accountId: string): Observable<any> {
|
|
const httpParams = new HttpParams().set('command', 'close');
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}/template`, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* Template for the Payment action. Same shape as the closure template, but its `onAccountClosureOptions` also
|
|
* include Partial Re-Invest, which the closure commands exclude because only Payment implements it.
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
getFixedDepositsAccountPaymentTemplate(accountId: string): Observable<any> {
|
|
const httpParams = new HttpParams().set('command', 'payment');
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}/template`, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
getFixedDepositsAccountTransactionTemplate(accountId: string): Observable<any> {
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}/transactions/template`);
|
|
}
|
|
|
|
/**
|
|
* @param clientId Client Id assosciated with fixed deposits account.
|
|
* @returns {Observable<any>} Fixed Deposits account template.
|
|
*/
|
|
getFixedDepositsAccountTemplate(clientId: string, productId?: string): Observable<any> {
|
|
let httpParams = new HttpParams().set('clientId', clientId);
|
|
httpParams = productId ? httpParams.set('productId', productId) : httpParams;
|
|
return this.http.get('/fixeddepositaccounts/template', { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* @param {string} accountId Fixed Deposits Account Id
|
|
* @returns {Observable<any>}
|
|
*/
|
|
getFixedDepositsAccountAndTemplate(accountId: any) {
|
|
const httpParams = new HttpParams().set('associations', 'charges,+linkedAccount').set('template', 'true');
|
|
return this.http.get(`/fixeddepositaccounts/${accountId}`, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* Calculate FD maturity using backend formula.
|
|
* When submittedOnDate + daysInYear are provided, uses exact calendar-based calculation
|
|
* matching the actual account creation engine.
|
|
*/
|
|
calculateFixedDepositInterest(params: {
|
|
principalAmount: number;
|
|
annualInterestRate: number;
|
|
tenureInMonths: number;
|
|
interestCompoundingPeriodInMonths: number;
|
|
interestPostingPeriodInMonths: number;
|
|
submittedOnDate?: string; // yyyy-MM-dd — enables exact calendar calculation
|
|
daysInYear?: number; // 360 or 365
|
|
}): Observable<any> {
|
|
let httpParams = new HttpParams();
|
|
Object.entries(params).forEach(
|
|
([
|
|
key,
|
|
val
|
|
]) => {
|
|
if (val !== undefined && val !== null) {
|
|
httpParams = httpParams.set(key, String(val));
|
|
}
|
|
}
|
|
);
|
|
return this.http.get(`/fixeddepositaccounts/calculate-fd-interest`, { params: httpParams });
|
|
}
|
|
|
|
/**
|
|
* @param fixedDepositAccount Fixed Deposit Account
|
|
* @returns {Observable<any>}
|
|
*/
|
|
createFixedDepositAccount(fixedDepositAccount: any): Observable<any> {
|
|
return this.http.post(`/fixeddepositaccounts`, fixedDepositAccount);
|
|
}
|
|
|
|
/**
|
|
* @param {any} accountId Account Id
|
|
* @param {any} fixedDepositAccount Fixed Deposit Account
|
|
* @returns {Observable<any>}
|
|
*/
|
|
updateFixedDepositAccount(accountId: any, fixedDepositAccount: any): Observable<any> {
|
|
return this.http.put(`/fixeddepositaccounts/${accountId}`, fixedDepositAccount);
|
|
}
|
|
|
|
/**
|
|
* @param clientId Client Id
|
|
* @param clientName Client Name
|
|
* @param fromAccountId Account Id
|
|
* @param locale Locale
|
|
* @param dateFormat Date Format
|
|
* @returns {Observable<any>} Standing Instructions
|
|
*/
|
|
getStandingInstructions(
|
|
clientId: string,
|
|
clientName: string,
|
|
fromAccountId: string,
|
|
locale: string,
|
|
dateFormat: string
|
|
): Observable<any> {
|
|
const httpParams = new HttpParams()
|
|
.set('clientId', clientId)
|
|
.set('clientName', clientName)
|
|
.set('fromAccountId', fromAccountId)
|
|
.set('fromAccountType', '2')
|
|
.set('locale', locale)
|
|
.set('dateFormat', dateFormat);
|
|
return this.http.get(`/standinginstructions`, { params: httpParams });
|
|
}
|
|
}
|