/** * 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 { 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). */ /** Interest lines of all holdings that are due now or within the next few days. */ getDueInterest(): Observable { return this.http.get('/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 { const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : ''; return this.http.get( `/investment/purchases/${purchaseId}/interest-schedule${params}` ); } // -------------------- Daily Script Master -------------------- getDailyRates(scriptId?: number): Observable { const params = scriptId ? `?scriptId=${scriptId}` : ''; return this.http.get(`/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 { return this.http.get( `/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 { 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 { 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: 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.'; }