Replace the dummy interest calculation tab with the real Interest Entry: pick Type / Script / Deal No and Retrieve the holding's schedule from the backend - every half-yearly coupon and every 31 Mar / 30 Sep closing provision up to maturity, with days (30/360), gross interest, amount already booked, net income and the Dr/Cr entry per line. Display only; nothing is posted to GL yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
345 lines
10 KiB
TypeScript
345 lines
10 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, HttpErrorResponse } from '@angular/common/http';
|
|
import { Observable, forkJoin } from 'rxjs';
|
|
import { map, tap } from 'rxjs/operators';
|
|
import {
|
|
InstrumentType,
|
|
InvestmentHolding,
|
|
PARTY_MASTER,
|
|
PartyMaster,
|
|
SCRIPT_MASTER,
|
|
ScriptMaster,
|
|
SlrType
|
|
} from './investment-data';
|
|
|
|
/** Investment party as returned by /investment/parties. */
|
|
export interface InvestmentPartyDto {
|
|
id: number;
|
|
partyCode: string;
|
|
bondType: InstrumentType;
|
|
partyName: string;
|
|
address1?: string;
|
|
address2?: string;
|
|
address3?: string;
|
|
city?: string;
|
|
pincode?: string;
|
|
phone?: string;
|
|
fax?: string;
|
|
bankType?: string;
|
|
glAccountId: number;
|
|
glCode: string;
|
|
glName: string;
|
|
rbiCode?: string;
|
|
limit?: number;
|
|
leiNo?: string;
|
|
cpBankName?: string;
|
|
cpBranchName?: string;
|
|
cpAccountNo?: string;
|
|
cpIfsc?: string;
|
|
active: boolean;
|
|
}
|
|
|
|
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
|
|
export interface InvestmentScriptDto {
|
|
id: number;
|
|
scriptCode: string;
|
|
bondType: InstrumentType;
|
|
slrType?: SlrType;
|
|
scriptDesc: string;
|
|
glAccountId: number;
|
|
glCode: string;
|
|
glName: string;
|
|
isin?: string;
|
|
faceValuePerUnit: number;
|
|
intRate?: number;
|
|
issueDate?: string;
|
|
maturityDate?: string;
|
|
companyName?: string;
|
|
address1?: string;
|
|
address2?: string;
|
|
address3?: string;
|
|
city?: string;
|
|
pincode?: string;
|
|
phone?: string;
|
|
fax?: string;
|
|
corpCode?: string;
|
|
active: boolean;
|
|
}
|
|
|
|
/** Purchase entry (holding) as returned by /investment/purchases (dates are DD-MM-YYYY). */
|
|
export interface InvestmentPurchaseDto {
|
|
id: number;
|
|
dealNo: string;
|
|
certNo: string;
|
|
bondType: InstrumentType;
|
|
scriptId: number;
|
|
scriptCode: string;
|
|
scriptDesc: string;
|
|
isin?: string;
|
|
partyId: number;
|
|
partyCode: string;
|
|
partyName: string;
|
|
dealDate: string;
|
|
dealTime?: string;
|
|
settlementType: string;
|
|
settlementDate: string;
|
|
faceValue: number;
|
|
purchaseRate: number;
|
|
units: number;
|
|
balanceUnits: number;
|
|
brokerage?: number;
|
|
stampDuty?: number;
|
|
bookValue: number;
|
|
premiumDiscount?: number;
|
|
intRate?: number;
|
|
issueDate?: string;
|
|
maturityDate?: string;
|
|
lastIntDate?: string;
|
|
accruedDays?: number;
|
|
accruedInterest?: number;
|
|
settlementAmount: number;
|
|
ytm?: number;
|
|
category: string;
|
|
subType?: string;
|
|
investmentGlAccountId: number;
|
|
investmentGlCode: string;
|
|
investmentGlName: string;
|
|
crGlAccountId: number;
|
|
crGlCode: string;
|
|
crGlName: string;
|
|
otherDetails?: string;
|
|
status: string;
|
|
}
|
|
|
|
export interface InvestmentGlAccount {
|
|
id: number;
|
|
glCode: string;
|
|
name: string;
|
|
}
|
|
|
|
/**
|
|
* Backend calls for the Investment masters (Bank / Party Master and Script Master). Results are also copied into the
|
|
* shared PARTY_MASTER / SCRIPT_MASTER lists so screens that still read those lists (Purchase Entry) offer the saved
|
|
* masters.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class InvestmentService {
|
|
private http = inject(HttpClient);
|
|
|
|
// -------------------- Bank / Party Master --------------------
|
|
getParties(): Observable<InvestmentPartyDto[]> {
|
|
return this.http.get<InvestmentPartyDto[]>('/investment/parties');
|
|
}
|
|
|
|
createParty(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
|
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/parties', body);
|
|
}
|
|
|
|
updateParty(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
|
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/parties/${id}`, body);
|
|
}
|
|
|
|
deleteParty(id: number): Observable<unknown> {
|
|
return this.http.delete(`/investment/parties/${id}`);
|
|
}
|
|
|
|
// -------------------- Script Master --------------------
|
|
getScripts(): Observable<InvestmentScriptDto[]> {
|
|
return this.http.get<InvestmentScriptDto[]>('/investment/scripts');
|
|
}
|
|
|
|
createScript(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
|
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/scripts', body);
|
|
}
|
|
|
|
updateScript(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
|
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/scripts/${id}`, body);
|
|
}
|
|
|
|
// -------------------- Purchase Entry --------------------
|
|
getPurchases(): Observable<InvestmentPurchaseDto[]> {
|
|
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
|
|
}
|
|
|
|
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
|
|
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
|
|
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
|
|
return this.http.get<InvestmentInterestScheduleDto>(
|
|
`/investment/purchases/${purchaseId}/interest-schedule${params}`
|
|
);
|
|
}
|
|
|
|
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
|
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
|
|
}
|
|
|
|
deletePurchase(id: number): Observable<unknown> {
|
|
return this.http.delete(`/investment/purchases/${id}`);
|
|
}
|
|
|
|
// -------------------- GL accounts --------------------
|
|
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
|
|
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
|
|
return this.http
|
|
.get<
|
|
Array<{ id: number; glCode: string; name: string; disabled: boolean }>
|
|
>('/glaccounts', { params: { type: '1', usage: '1', disabled: 'false' } })
|
|
.pipe(
|
|
map((list) =>
|
|
list
|
|
.map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
|
|
.sort((a, b) => a.glCode.localeCompare(b.glCode))
|
|
)
|
|
);
|
|
}
|
|
|
|
/** Loads both masters from the backend and refreshes the shared lists other screens read. */
|
|
syncSharedMasters(): Observable<{ parties: PartyMaster[]; scripts: ScriptMaster[] }> {
|
|
return forkJoin({ parties: this.getParties(), scripts: this.getScripts() }).pipe(
|
|
map(({ parties, scripts }) => ({ parties: parties.map(toPartyMaster), scripts: scripts.map(toScriptMaster) })),
|
|
tap(({ parties, scripts }) => {
|
|
PARTY_MASTER.splice(0, PARTY_MASTER.length, ...parties);
|
|
SCRIPT_MASTER.splice(0, SCRIPT_MASTER.length, ...scripts);
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
|
|
return {
|
|
id: p.id,
|
|
partyCode: p.partyCode,
|
|
partyName: p.partyName,
|
|
partyType: 'BANK',
|
|
bondType: p.bondType,
|
|
address1: p.address1,
|
|
address2: p.address2,
|
|
address3: p.address3,
|
|
city: p.city,
|
|
pincode: p.pincode,
|
|
phone: p.phone,
|
|
fax: p.fax,
|
|
bankType: p.bankType,
|
|
glAccountId: p.glAccountId,
|
|
glCode: p.glCode,
|
|
glName: p.glName,
|
|
rbiCode: p.rbiCode,
|
|
limit: p.limit ?? null,
|
|
leiNo: p.leiNo,
|
|
cpBankName: p.cpBankName,
|
|
cpBranchName: p.cpBranchName,
|
|
cpAccountNo: p.cpAccountNo,
|
|
cpIfsc: p.cpIfsc,
|
|
status: p.active ? 'ACTIVE' : 'INACTIVE'
|
|
};
|
|
}
|
|
|
|
export function toScriptMaster(s: InvestmentScriptDto): ScriptMaster {
|
|
return {
|
|
id: s.id,
|
|
scriptCode: s.scriptCode,
|
|
scriptName: s.scriptDesc,
|
|
instrumentType: s.bondType,
|
|
isin: s.isin ?? '',
|
|
faceValuePerUnit: s.faceValuePerUnit,
|
|
slrType: s.slrType,
|
|
glAccountId: s.glAccountId,
|
|
glCode: s.glCode,
|
|
glName: s.glName,
|
|
couponRate: s.intRate,
|
|
issueDate: s.issueDate,
|
|
maturityDate: s.maturityDate,
|
|
companyName: s.companyName,
|
|
address1: s.address1,
|
|
address2: s.address2,
|
|
address3: s.address3,
|
|
city: s.city,
|
|
pincode: s.pincode,
|
|
phone: s.phone,
|
|
fax: s.fax,
|
|
corpCode: s.corpCode,
|
|
active: s.active
|
|
};
|
|
}
|
|
|
|
/** A saved purchase as a row of the Holdings register. */
|
|
export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|
return {
|
|
id: p.id,
|
|
dealNo: p.dealNo,
|
|
certNo: p.certNo,
|
|
instrumentType: p.bondType,
|
|
scriptCode: p.scriptCode,
|
|
scriptName: p.scriptDesc,
|
|
party: p.partyName,
|
|
partyCode: p.partyCode,
|
|
subType: p.subType ?? '',
|
|
faceValue: p.faceValue,
|
|
purchaseRate: p.purchaseRate,
|
|
units: p.balanceUnits,
|
|
purchaseDate: p.settlementDate,
|
|
maturityDate: p.maturityDate || p.dealDate,
|
|
interestRate: p.intRate ?? 0,
|
|
interestFreq: p.bondType === 'MUTUAL_FUND' ? 'CUMULATIVE' : 'HALF_YEARLY',
|
|
glCode: p.investmentGlCode,
|
|
glName: p.investmentGlName,
|
|
crToAc: p.crGlCode,
|
|
brokerCode: '',
|
|
status: 'ACTIVE',
|
|
category: p.category,
|
|
bookValue: p.bookValue,
|
|
settlementAmount: p.settlementAmount
|
|
};
|
|
}
|
|
|
|
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
|
|
export interface InvestmentInterestRowDto {
|
|
type: 'COUPON' | 'PROVISION';
|
|
intDate: string;
|
|
fromDate: string;
|
|
toDate: string;
|
|
days: number;
|
|
grossInterest: number;
|
|
alreadyBooked: number;
|
|
amount: number;
|
|
due: boolean;
|
|
}
|
|
|
|
export interface InvestmentInterestScheduleDto {
|
|
purchaseId: number;
|
|
dealNo: string;
|
|
certNo: string;
|
|
scriptCode: string;
|
|
scriptDesc: string;
|
|
faceValue: number;
|
|
unitValue: number;
|
|
balanceUnits: number;
|
|
purchaseRate: number;
|
|
bookValue: number;
|
|
intRate: number;
|
|
dealDate: string;
|
|
lastIntDate: string;
|
|
maturityDate: string;
|
|
purchasedAccruedInterest: number;
|
|
couponPerHalfYear: number;
|
|
asOnDate: string;
|
|
interestGlCode: string;
|
|
interestGlName: string;
|
|
rows: InvestmentInterestRowDto[];
|
|
}
|
|
|
|
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
|
|
export function apiErrorMessage(err: unknown): string {
|
|
const e = err as HttpErrorResponse;
|
|
const body = e?.error;
|
|
return body?.errors?.[0]?.defaultUserMessage || body?.defaultUserMessage || e?.message || 'Request failed.';
|
|
}
|