/** * 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 { return this.http.get('/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 { return this.http.delete(`/investment/parties/${id}`); } // -------------------- Script Master -------------------- getScripts(): Observable { return this.http.get('/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 { return this.http.get('/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 { const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : ''; return this.http.get( `/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 { 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 { 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.'; }