449 lines
14 KiB
TypeScript
449 lines
14 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 {
|
|
DailyScriptRate,
|
|
HoldingStatus,
|
|
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;
|
|
otherCharges?: 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;
|
|
glTransactionId?: string;
|
|
settledOnDate?: 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). */
|
|
/** Interest lines of all holdings that are due now or within the next few days. */
|
|
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
|
|
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
|
|
}
|
|
|
|
/** Posts one interest line (coupon / provision) with its JV voucher; the voucher number comes back as resourceIdentifier. */
|
|
postInterest(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string; transactionId?: string }> {
|
|
return this.http.post<{ resourceIdentifier?: string; transactionId?: string }>(
|
|
`/investment/purchases/${purchaseId}/interest-postings`,
|
|
body
|
|
);
|
|
}
|
|
|
|
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
|
|
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
|
|
return this.http.get<InvestmentInterestScheduleDto>(
|
|
`/investment/purchases/${purchaseId}/interest-schedule${params}`
|
|
);
|
|
}
|
|
|
|
// -------------------- Daily Script Master --------------------
|
|
getDailyRates(scriptId?: number): Observable<InvestmentDailyRateDto[]> {
|
|
const params = scriptId ? `?scriptId=${scriptId}` : '';
|
|
return this.http.get<InvestmentDailyRateDto[]>(`/investment/daily-rates${params}`);
|
|
}
|
|
|
|
/** Newest Daily Script Master rate of a script on or before a date; null when none has been entered. */
|
|
getLatestDailyRate(scriptId: number, onDate: string): Observable<InvestmentDailyRateDto | null> {
|
|
return this.http.get<InvestmentDailyRateDto | null>(
|
|
`/investment/daily-rates/latest?scriptId=${scriptId}&onDate=${encodeURIComponent(onDate)}`
|
|
);
|
|
}
|
|
|
|
createDailyRate(body: object): Observable<{ resourceId: number }> {
|
|
return this.http.post<{ resourceId: number }>('/investment/daily-rates', body);
|
|
}
|
|
|
|
deleteDailyRate(rateId: number): Observable<unknown> {
|
|
return this.http.delete(`/investment/daily-rates/${rateId}`);
|
|
}
|
|
|
|
/** Settles a deal: posts it to GL dated its settlement date. Under maker-checker the response only says it awaits approval. */
|
|
settlePurchase(purchaseId: number): Observable<{ transactionId?: string; rollbackTransaction?: boolean }> {
|
|
return this.http.post<{ transactionId?: string; rollbackTransaction?: boolean }>(
|
|
`/investment/purchases/${purchaseId}/settle`,
|
|
{}
|
|
);
|
|
}
|
|
|
|
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: p.status as HoldingStatus,
|
|
settlementDate: p.settlementDate,
|
|
glTransactionId: p.glTransactionId,
|
|
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;
|
|
/** Posted to GL: its JV voucher and GL transaction. */
|
|
posted: boolean;
|
|
voucherNo?: string;
|
|
glTransactionId?: string;
|
|
postedOnDate?: string;
|
|
/** The next line to post and its date has come. */
|
|
postable: boolean;
|
|
/** Unposted and due within the reminder window (or past). */
|
|
dueSoon: boolean;
|
|
}
|
|
|
|
/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
|
|
export interface InvestmentInterestDueDto {
|
|
purchaseId: number;
|
|
dealNo: string;
|
|
certNo: string;
|
|
scriptCode: string;
|
|
scriptDesc: string;
|
|
lineType: 'COUPON' | 'PROVISION';
|
|
intDate: string;
|
|
grossInterest: number;
|
|
amount: number;
|
|
postable: boolean;
|
|
overdue: 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;
|
|
bankGlAccountId: number;
|
|
bankGlCode: string;
|
|
bankGlName: string;
|
|
status: string;
|
|
rows: InvestmentInterestRowDto[];
|
|
}
|
|
|
|
/** A Daily Script Master row (see InvestmentDailyRateData in the backend). */
|
|
export interface InvestmentDailyRateDto {
|
|
id: number;
|
|
scriptId: number;
|
|
scriptCode: string;
|
|
scriptDesc: string;
|
|
bondType: string;
|
|
rateDate: string;
|
|
openRate: number | null;
|
|
highRate: number | null;
|
|
lowRate: number | null;
|
|
closeRate: number;
|
|
}
|
|
|
|
export function toDailyScriptRate(d: InvestmentDailyRateDto): DailyScriptRate {
|
|
return {
|
|
id: d.id,
|
|
rateDate: d.rateDate,
|
|
scriptCode: d.scriptCode,
|
|
scriptName: d.scriptDesc,
|
|
openRate: d.openRate,
|
|
closeRate: d.closeRate,
|
|
highRate: d.highRate,
|
|
lowRate: d.lowRate
|
|
};
|
|
}
|
|
|
|
/** 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.';
|
|
}
|