diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html
index de42824e6..bfcc033c6 100644
--- a/src/app/investment/bond-investment/bond-investment.component.html
+++ b/src/app/investment/bond-investment/bond-investment.component.html
@@ -176,8 +176,9 @@
@if (activeTab === 'purchase') {
+
- Investment - Govt.Security/Bond Entry ({{ instrumentLabel(purchaseForm.bondType) }})
+ Purchase Entry — {{ instrumentLabel(purchaseForm.bondType) }} (fields marked * are mandatory)
-
-
-
-
-
+
+
-
-
-
-
@if (isMutualFund) {
-
-
-
-
-
} @else {
-
-
-
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@if (!isMutualFund) {
+ }
+
+
+
+
+
Calculated by System
+
+
+
+
+
+
+ @if (!isMutualFund) {
+
+
+
+ }
+
+
+ @if (!isMutualFund) {
+
+
+
+ }
+
+
+
+
+
Accounting Preview (posted after checker approval)
+ @if (purchaseAccountingPreview.length) {
+
+
+
+ | Dr / Cr |
+ GL Code |
+ GL Name |
+ Amount |
+
+
+
+ @for (l of purchaseAccountingPreview; track l.side + l.glCode) {
+
+ | {{ l.side }} |
+ {{ l.glCode }} |
+ {{ l.glName || '—' }} |
+ ₹{{ l.amount | number: '1.2-2' }} |
+
+ }
+
+
+ } @else {
+
Select a script, enter the amount and CR. To A/c to see the entry.
+ }
+
+
+
+
+
+ @if (showMoreDetails) {
+
More Details (optional)
+
+
+ @if (isMutualFund) {
+
+
+
+
+ } @else {
+
+
+
+
+ }
+
+
-
-
+
+
+
+
+
+
+
+ @if (purchaseForm.lien === 'YES') {
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+ Retrieve Cert No
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts
index e947df3f3..414a45801 100644
--- a/src/app/investment/bond-investment/bond-investment.component.ts
+++ b/src/app/investment/bond-investment/bond-investment.component.ts
@@ -6,16 +6,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
- GOVT_SECURITY_HOLDINGS,
- NON_SLR_HOLDINGS,
- BOND_HOLDINGS,
- MUTUAL_FUND_HOLDINGS,
- ALL_TXNS,
SCRIPT_MASTER,
PARTY_MASTER,
SUBTYPE_MASTER,
@@ -33,6 +28,14 @@ import {
AUTHORITY_OPTIONS,
INTEREST_INCOME_GL_MASTER,
INTEREST_INCOME_GL_BY_TYPE,
+ INVESTMENT_GL_BY_TYPE,
+ INVESTMENT_GL_LOOKUP,
+ INTEREST_RECEIVABLE_GL,
+ SETTLEMENT_BANK_GL_OPTIONS,
+ periodBetween,
+ parseDDMMYYYY,
+ days360,
+ lastHalfYearlyCouponDate,
InvestmentHolding,
InvestmentTxn,
InstrumentType,
@@ -49,6 +52,7 @@ import {
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
+import { InvestmentPurchaseDto, InvestmentService, apiErrorMessage, toHolding } from '../investment.service';
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
type BondTab =
@@ -131,6 +135,13 @@ interface BondPurchaseForm {
stampDutyAc: string;
}
+interface AccountingPreviewLine {
+ side: 'Dr' | 'Cr';
+ glCode: string;
+ glName: string;
+ amount: number;
+}
+
interface BondSellForm {
/** Legacy screen has SELL / REDEEM / MUTUAL FUND SELL radio at the top. */
mode: 'SELL' | 'REDEEM' | 'MUTUAL_FUND_SELL';
@@ -182,23 +193,67 @@ interface BondSellForm {
DateDdmmyyyyPickerComponent
]
})
-export class BondInvestmentComponent {
+export class BondInvestmentComponent implements OnInit {
+ private readonly investmentService = inject(InvestmentService);
+
+ /** Script and party dropdowns offer the masters saved in the backend. */
+ ngOnInit(): void {
+ this.investmentService.syncSharedMasters().subscribe({
+ error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
+ });
+ this.investmentService.getAssetGlAccounts().subscribe({
+ next: (gls) =>
+ (this.glIdByCode = new Map(
+ gls.map((g) => [
+ g.glCode,
+ g.id
+ ])
+ )),
+ error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
+ });
+ this.loadPurchases();
+ }
+
+ /** GL id for a GL code — the purchase API takes ids, the form works with codes. */
+ private glIdByCode = new Map();
+
+ /** The Holdings register and transaction log are the purchases saved in the backend. */
+ private loadPurchases(): void {
+ this.investmentService.getPurchases().subscribe({
+ next: (purchases) => {
+ this.holdings = purchases.map(toHolding);
+ this.txns = purchases.map((d) => this.purchaseTxn(d));
+ this.purchaseForm = this.blankPurchaseForm();
+ },
+ error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
+ });
+ }
+
+ private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
+ return {
+ certNo: d.certNo,
+ instrumentType: d.bondType,
+ txnType: 'PURCHASE',
+ voucherNo: `INV/PUR/${d.dealNo}`,
+ txnDate: d.dealDate,
+ amount: d.settlementAmount,
+ particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
+ glCode: d.investmentGlCode,
+ glName: d.investmentGlName
+ };
+ }
+
activeTab: BondTab = 'holdings';
+ /** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */
+ showMoreDetails = false;
+ /** Cert No typed in the Retrieve box (Cert No itself is system-generated and read-only). */
+ retrieveCertNo = '';
bondType: BondType = 'GOVT_SECURITY';
asOnDate = todayDDMMYYYY();
- holdings: InvestmentHolding[] = [
- ...GOVT_SECURITY_HOLDINGS,
- ...NON_SLR_HOLDINGS,
- ...BOND_HOLDINGS,
- ...MUTUAL_FUND_HOLDINGS
- ];
- txns: InvestmentTxn[] = ALL_TXNS.filter((t) => ([
- 'GOVT_SECURITY',
- 'NON_SLR',
- 'BOND',
- 'MUTUAL_FUND'
- ] as InstrumentType[]).includes(t.instrumentType));
+ /** Loaded from the backend (saved purchases) in ngOnInit. */
+ holdings: InvestmentHolding[] = [];
+ txns: InvestmentTxn[] = [];
// Master-driven dropdown sources
readonly scriptOptions = SCRIPT_MASTER;
@@ -224,6 +279,7 @@ export class BondInvestmentComponent {
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
sellForm: BondSellForm = this.blankSellForm();
+ readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS;
// Redemption Entry
redeemCertNo = '';
@@ -305,20 +361,37 @@ export class BondInvestmentComponent {
return INSTRUMENT_LABELS[type];
}
+ /** Preview of the deal number the backend will assign: FY prefix (e.g. 202627) + running number. */
private nextDealNo(): string {
- return `2026${String(27000039 + this.holdings.length).padStart(9, '0')}`;
+ const d = parseDDMMYYYY(this.asOnDate);
+ const fyStart = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
+ const prefix = `${fyStart}${String((fyStart + 1) % 100).padStart(2, '0')}`;
+ const nums = (this.holdings ?? [])
+ .map((h) => h.dealNo)
+ .filter((n): n is string => !!n && n.startsWith(prefix))
+ .map((n) => parseInt(n.slice(prefix.length), 10));
+ return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(6, '0')}`;
+ }
+
+ /** Certificate No is system-generated, as in the existing CBS (greyed there). */
+ /** Preview of the certificate number the backend will assign (running number). */
+ private nextCertNo(): string {
+ const nums = (this.holdings ?? []).map((h) => parseInt(h.certNo, 10)).filter((n) => !isNaN(n));
+ return String((nums.length ? Math.max(...nums) : 0) + 1);
}
private blankPurchaseForm(): BondPurchaseForm {
const type = this.bondType ?? 'GOVT_SECURITY';
+ this.showMoreDetails = false;
+ this.retrieveCertNo = '';
return {
bondType: type,
dealNo: this.nextDealNo(),
dealDate: this.asOnDate,
- dealTime: '',
+ dealTime: new Date().toTimeString().slice(0, 8),
isin: '',
scriptCode: '',
- certNo: '',
+ certNo: this.nextCertNo(),
settlementType: 'T+0',
settlementDt: this.asOnDate,
faceValue: null,
@@ -326,7 +399,7 @@ export class BondInvestmentComponent {
partyCode: '',
subType: type === 'GOVT_SECURITY' ? 'SLR' : type === 'NON_SLR' ? 'NON SLR' : 'CORPORATE BOND',
tbillGlAc: '',
- crToAc: '310070',
+ crToAc: SETTLEMENT_BANK_GL_OPTIONS[0]?.glCode ?? '',
premiumAmt: null,
brokerage: null,
brokerName: '',
@@ -463,13 +536,41 @@ export class BondInvestmentComponent {
}
/** Accrued Interest at purchase — interest earned by the seller since the last coupon date. */
+ /** Interest owed to the seller from the last coupon date up to the deal date, on a 30/360 basis —
+ * interest is counted to the event date, not the date the money moves (Jitendra Sir). */
get purchaseAccruedInterest(): number {
const f = this.purchaseForm;
if (!f.intRate || !f.lastIntDt || !f.dealDate) {
return 0;
}
- const yrs = Math.max(0, yearsBetween(f.lastIntDt, f.dealDate));
- return Math.round(this.purchaseBaseAmount * (f.intRate / 100) * yrs);
+ return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * days360(f.lastIntDt, f.dealDate)) / 360);
+ }
+
+ get purchaseAccruedDays(): number {
+ const f = this.purchaseForm;
+ return f.lastIntDt && f.dealDate ? days360(f.lastIntDt, f.dealDate) : 0;
+ }
+
+ /** Premium (+) or discount (−) paid over face value. */
+ get purchasePremiumDiscount(): number {
+ const f = this.purchaseForm;
+ if (this.isMutualFund || !f.faceValue || !f.purchaseRate) {
+ return 0;
+ }
+ return Math.round((f.faceValue * (f.purchaseRate - 100)) / 100);
+ }
+
+ /** Re-suggests the last coupon date when the deal date changes. */
+ onDealDateChange(): void {
+ this.suggestLastIntDate();
+ }
+
+ private suggestLastIntDate(): void {
+ const f = this.purchaseForm;
+ const s = this.purchaseScript;
+ if (!this.isMutualFund && s?.maturityDate && f.dealDate) {
+ f.lastIntDt = lastHalfYearlyCouponDate(s.maturityDate, f.dealDate) || f.lastIntDt;
+ }
}
/** Settlement Amt of Purchase — book value plus accrued interest payable to the seller. */
@@ -504,69 +605,196 @@ export class BondInvestmentComponent {
});
}
- /** Fills ISIN + interest rate when a script is picked, mirroring the legacy form's lookup. */
+ get purchaseScript(): (typeof SCRIPT_MASTER)[number] | undefined {
+ return this.scriptOptions.find((x) => x.scriptCode === this.purchaseForm.scriptCode);
+ }
+
+ /** Picking a script pulls its master details into the deal, as the existing CBS does. */
onScriptChange(): void {
- const s = this.scriptOptions.find((x) => x.scriptCode === this.purchaseForm.scriptCode);
- if (s) {
- this.purchaseForm.isin = s.isin;
+ const s = this.purchaseScript;
+ if (!s) {
+ return;
}
+ const f = this.purchaseForm;
+ f.isin = s.isin;
+ if (s.couponRate != null) {
+ f.intRate = s.couponRate;
+ }
+ if (s.issueDate) {
+ f.issueDate = s.issueDate;
+ }
+ if (s.maturityDate) {
+ f.redumMatDt = s.maturityDate;
+ const p = periodBetween(s.issueDate, s.maturityDate);
+ f.periodYear = p.years;
+ f.periodMonth = p.months;
+ f.periodDays = p.days;
+ }
+ if (s.slrType) {
+ f.subType = s.slrType === 'NON_SLR' ? 'NON SLR' : 'SLR';
+ }
+ if (this.isMutualFund) {
+ f.productName = s.scriptName;
+ }
+ this.suggestLastIntDate();
+ }
+
+ /** Settlement Date = Deal Date + T+n days (greyed / auto in the existing CBS). */
+ get purchaseSettlementDate(): string {
+ const f = this.purchaseForm;
+ if (!f.dealDate) {
+ return '';
+ }
+ const n = parseInt(f.settlementType.replace('T+', ''), 10) || 0;
+ return addPeriod(f.dealDate, 0, 0, n);
+ }
+
+ /** Parties offered for a new deal — inactive parties are hidden. */
+ get activePartyOptions(): typeof PARTY_MASTER {
+ return this.partyOptions.filter((p) => p.status !== 'INACTIVE');
+ }
+
+ get purchaseInvestmentGlCode(): string {
+ return this.purchaseScript?.glCode || INVESTMENT_GL_BY_TYPE[this.purchaseForm.bondType];
+ }
+
+ glNameOf(glCode: string): string {
+ return INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ?? '';
+ }
+
+ /** The journal entry this purchase will post once the checker approves it. */
+ get purchaseAccountingPreview(): AccountingPreviewLine[] {
+ const book = this.purchaseBookValue;
+ const accrued = this.purchaseAccruedInterest;
+ const total = this.purchaseSettlementAmount;
+ const crGl = this.purchaseForm.crToAc;
+ if (!book || !crGl) {
+ return [];
+ }
+ const invGl = this.purchaseInvestmentGlCode;
+ const lines: AccountingPreviewLine[] = [
+ { side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }];
+ if (accrued > 0) {
+ lines.push({
+ side: 'Dr',
+ glCode: INTEREST_RECEIVABLE_GL.glCode,
+ glName: INTEREST_RECEIVABLE_GL.glName,
+ amount: accrued
+ });
+ }
+ lines.push({ side: 'Cr', glCode: crGl, glName: this.glNameOf(crGl), amount: total });
+ return lines;
}
savePurchase(): void {
const f = this.purchaseForm;
- if (!f.certNo || !this.purchaseBaseAmount || !f.partyCode) {
- this.toast('Please fill Cert No, Face Value / Consider Amt and Party Code.');
+ const missing: string[] = [];
+ if (!f.scriptCode) missing.push('Script Code');
+ if (!f.partyCode) missing.push('Party Code');
+ if (!f.dealDate) missing.push('Deal Date');
+ if (this.isMutualFund) {
+ if (!f.considerAmt) missing.push('Consider Amt');
+ if (!f.nav) missing.push('NAV');
+ } else {
+ if (!f.faceValue) missing.push('Face Value');
+ if (!f.purchaseRate) missing.push('Purchase Rate');
+ if (!f.lastIntDt) missing.push('Last Interest Date');
+ }
+ if (!f.category) missing.push('Category');
+ if (!f.crToAc) missing.push('CR. To A/c');
+ if (missing.length) {
+ this.toast(`Please fill: ${missing.join(', ')}.`);
return;
}
- const holding: InvestmentHolding = {
- certNo: f.certNo,
- instrumentType: f.bondType,
- scriptCode: f.scriptCode || f.isin || f.productName,
- scriptName: this.isMutualFund
- ? f.productName
- : this.scriptOptions.find((s) => s.scriptCode === f.scriptCode)?.scriptName || f.scriptCode || f.isin,
- party: f.partyCode,
- subType: f.subType,
- faceValue: this.purchaseBaseAmount,
- purchaseRate: this.isMutualFund ? f.nav || 0 : f.purchaseRate || 100,
- units: this.purchaseUnits,
- purchaseDate: f.issueDate,
- maturityDate: this.purchaseMaturityDate || f.issueDate,
- interestRate: f.intRate || 0,
- interestFreq: this.isMutualFund ? 'CUMULATIVE' : f.intType,
- glCode:
- '31' +
- (f.bondType === 'GOVT_SECURITY'
- ? '0020'
- : f.bondType === 'NON_SLR'
- ? '0030'
- : f.bondType === 'BOND'
- ? '0040'
- : '0050'),
- glName: `INVESTMENT IN ${INSTRUMENT_LABELS[f.bondType].toUpperCase()}`,
- crToAc: f.crToAc,
- brokerCode: f.brokerName,
- status: 'ACTIVE'
+ const script = this.purchaseScript;
+ const party = this.partyOptions.find((p) => p.partyCode === f.partyCode);
+ const crGlAccountId = this.glIdByCode.get(f.crToAc);
+ if (!script?.id || !party?.id) {
+ this.toast('Script and Party must be saved in Investment Masters first.');
+ return;
+ }
+ if (!crGlAccountId) {
+ this.toast(`GL ${f.crToAc} was not found in the chart of accounts.`);
+ return;
+ }
+ const body = {
+ bondType: f.bondType,
+ scriptId: script.id,
+ partyId: party.id,
+ dealDate: f.dealDate,
+ dealTime: f.dealTime || '',
+ settlementType: f.settlementType,
+ ...(this.isMutualFund
+ ? { considerAmt: f.considerAmt, nav: f.nav, stampDuty: f.stampDuty ?? null }
+ : { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
+ brokerage: f.brokerage ?? null,
+ category: f.category,
+ crGlAccountId,
+ subType: f.subType || '',
+ otherDetails: this.purchaseOtherDetails(),
+ dateFormat: 'dd-MM-yyyy',
+ locale: 'en'
};
- this.holdings = [
- holding,
- ...this.holdings
- ];
- this.txns = [
- {
- certNo: holding.certNo,
- instrumentType: f.bondType,
- txnType: 'PURCHASE',
- voucherNo: `INV/PUR/${f.dealNo}`,
- txnDate: f.dealDate,
- amount: this.purchaseSettlementAmount,
- particulars: `Purchase Deal ${f.dealNo} - ${holding.scriptName} - ${holding.party}${f.category ? ' [' + f.category + ']' : ''}`
+ this.investmentService.createPurchase(body).subscribe({
+ next: (res) => {
+ this.toast(`Deal ${res.resourceIdentifier} saved. GL posting will follow once accounting is approved.`);
+ this.activeTab = 'holdings';
+ this.loadPurchases();
},
- ...this.txns
- ];
- this.toast(`${INSTRUMENT_LABELS[f.bondType]} Certificate ${holding.certNo} saved successfully.`);
- this.purchaseForm = this.blankPurchaseForm();
- this.activeTab = 'holdings';
+ error: (err) => this.toast(apiErrorMessage(err))
+ });
+ }
+
+ /** The optional "More Details" fields, kept with the deal as they were entered. */
+ private purchaseOtherDetails(): Record {
+ const f = this.purchaseForm;
+ const fields: Record = {
+ tradeNo: f.tradeNo,
+ orderNo: f.orderNo,
+ brokerCode: f.brokerName,
+ recommAuthority: f.recommAuthority,
+ sanctionAuthority: f.sanctionAuthority,
+ meetDate: f.meetDt,
+ resoNo: f.resoNo,
+ dealType: f.dealType,
+ market: f.market,
+ markCblo: f.markCblo,
+ paymentMode: f.paymentMode,
+ deliveryMode: f.deliveryMode,
+ lien: f.lien,
+ lienAmount: f.lienAmount,
+ lienReason: f.lienReason,
+ bondRating: f.bondRating,
+ corpBond: f.corpBond,
+ sglWithBank: f.sglWithBank,
+ tbillGlAc: f.tbillGlAc,
+ intType: f.intType,
+ intDate: f.intDate,
+ folioNo: f.folioNo,
+ callOption: f.callOption,
+ descInv: f.descInv,
+ fromDist: f.fromDist,
+ toDist: f.toDist,
+ amortizeAmt: f.amortizeAmt,
+ registerAddress: f.registerAdd,
+ registerCity: f.regiCity,
+ registerPin: f.regiPin,
+ remarks: f.remarks,
+ description: f.description,
+ tradeAmt: f.tradeAmt,
+ holdingDays: f.holdingDays,
+ stampDutyAc: f.stampDutyAc
+ };
+ const out: Record = {};
+ for (const [
+ k,
+ v
+ ] of Object.entries(fields)) {
+ if (v !== null && v !== undefined && v !== '') {
+ out[k] = v;
+ }
+ }
+ return out;
}
cancelPurchase(): void {
@@ -576,9 +804,10 @@ export class BondInvestmentComponent {
/** Legacy "Retrieve" button — loads an existing deal back into the form by Cert No. */
retrievePurchase(): void {
- const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo.trim());
+ const certNo = this.retrieveCertNo.trim();
+ const h = this.holdings.find((x) => x.certNo === certNo);
if (!h) {
- this.toast(`Cert No ${this.purchaseForm.certNo} not found.`);
+ this.toast(`Cert No ${certNo || '(blank)'} not found.`);
return;
}
this.purchaseForm = {
@@ -587,7 +816,8 @@ export class BondInvestmentComponent {
certNo: h.certNo,
scriptCode: h.scriptCode,
isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '',
- partyCode: h.party,
+ dealNo: h.dealNo ?? '',
+ partyCode: h.partyCode ?? '',
subType: h.subType,
faceValue: h.faceValue,
considerAmt: h.faceValue,
@@ -599,22 +829,26 @@ export class BondInvestmentComponent {
intType: h.interestFreq,
brokerName: h.brokerCode,
crToAc: h.crToAc,
- productName: h.scriptName
+ productName: h.scriptName,
+ category: h.category ?? ''
};
this.bondType = h.instrumentType as BondType;
this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
}
dropPurchase(): void {
- const certNo = this.purchaseForm.certNo;
- if (!this.holdings.some((h) => h.certNo === certNo)) {
- this.toast('Nothing to drop — this deal is not yet saved.');
+ const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo);
+ if (!h?.id) {
+ this.toast('Retrieve a saved deal first, then Drop.');
return;
}
- this.holdings = this.holdings.filter((h) => h.certNo !== certNo);
- this.txns = this.txns.filter((t) => t.certNo !== certNo);
- this.purchaseForm = this.blankPurchaseForm();
- this.toast(`Cert No ${certNo} dropped.`);
+ this.investmentService.deletePurchase(h.id).subscribe({
+ next: () => {
+ this.toast(`Deal ${h.dealNo} (Cert No ${h.certNo}) dropped.`);
+ this.loadPurchases();
+ },
+ error: (err) => this.toast(apiErrorMessage(err))
+ });
}
exitPurchase(): void {
diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts
index 0ea88ce1a..d570dd23e 100644
--- a/src/app/investment/investment-data.ts
+++ b/src/app/investment/investment-data.ts
@@ -123,6 +123,16 @@ export interface InvestmentHolding {
* never re-counts days up to this date, so re-running a Calculation & Post for an overlapping
* or repeated period always yields 0 instead of double-posting the same interest. */
interestPostedTillDate?: string;
+ /** RBI investment category chosen at purchase — HTM / AFS / HFT. */
+ category?: string;
+ /** SGL / CSGL account the security is held in (custody, not an accounting GL). */
+ sglAccountNo?: string;
+ /** Backend id / deal number / party code once the purchase is saved (absent on sample rows). */
+ id?: number;
+ dealNo?: string;
+ partyCode?: string;
+ bookValue?: number;
+ settlementAmount?: number;
}
export interface InvestmentTxn {
@@ -248,6 +258,51 @@ export function accruedInterestBetween(h: InvestmentHolding, fromStr: string, to
return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25));
}
+/** Days between two DD-MM-YYYY dates on a 30/360 basis (every month 30 days), as the bank's existing
+ * CBS counts them — e.g. 10-08 → 30-09 = 50 days, 10-08 → 07-10 = 57 days. */
+export function days360(fromStr: string, toStr: string): number {
+ const a = parseDDMMYYYY(fromStr);
+ const b = parseDDMMYYYY(toStr);
+ if (isNaN(a.getTime()) || isNaN(b.getTime()) || b <= a) {
+ return 0;
+ }
+ const d1 = Math.min(a.getDate(), 30);
+ const d2 = Math.min(b.getDate(), 30);
+ return (b.getFullYear() - a.getFullYear()) * 360 + (b.getMonth() - a.getMonth()) * 30 + (d2 - d1);
+}
+
+/** Most recent half-yearly coupon date on or before `onStr`, derived from the maturity date (coupons
+ * fall on the maturity day/month and six months either side of it). */
+export function lastHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
+ const mat = parseDDMMYYYY(maturityStr);
+ const on = parseDDMMYYYY(onStr);
+ if (isNaN(mat.getTime()) || isNaN(on.getTime())) {
+ return '';
+ }
+ const day = mat.getDate();
+ const couponOn = (year: number, month: number): Date => {
+ const lastDay = new Date(year, month + 1, 0).getDate();
+ return new Date(year, month, Math.min(day, lastDay));
+ };
+ const months = [
+ mat.getMonth() % 6,
+ (mat.getMonth() % 6) + 6
+ ];
+ let best: Date | undefined;
+ for (const year of [
+ on.getFullYear() - 1,
+ on.getFullYear()
+ ]) {
+ for (const m of months) {
+ const c = couponOn(year, m);
+ if (c <= on && (!best || c > best)) {
+ best = c;
+ }
+ }
+ }
+ return best ? formatDDMMYYYY(best) : '';
+}
+
export function formatDDMMYYYY(d: Date): string {
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
}
@@ -1404,99 +1459,319 @@ export const CASH_BANK_GL_MASTER: InvestmentGlOption[] = [
{ glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' }
];
+/** Investment GLs a script / party can be mapped to. PROPOSED codes awaiting approval — the
+ * backend will take these from the chart of accounts, never hard-coded. */
+export const INVESTMENT_GL_OPTIONS: InvestmentGlOption[] = [
+ { glCode: '100701', glName: 'INVESTMENT IN CENTRAL GOVT SECURITIES' },
+ { glCode: '100702', glName: 'INVESTMENT IN STATE GOVT SECURITIES' },
+ { glCode: '100703', glName: 'INVESTMENT IN BONDS / DEBENTURES' },
+ { glCode: '100704', glName: 'FIXED DEPOSITS WITH OTHER BANKS' },
+ { glCode: '100705', glName: 'INVESTMENT IN MUTUAL FUNDS' },
+ { glCode: '100706', glName: 'INVESTMENT IN SHARES' }
+];
+
+/** Fallback investment GL when a script has none mapped. */
+export const INVESTMENT_GL_BY_TYPE: Record = {
+ GOVT_SECURITY: '100701',
+ NON_SLR: '100703',
+ BOND: '100703',
+ FD: '100704',
+ MUTUAL_FUND: '100705',
+ SHARE: '100706'
+};
+
+/** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */
+export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
+ glCode: '100603',
+ glName: 'INTEREST RECEIVABLE ON INVESTMENTS (PROVISION)'
+};
+
+/** Bank accounts a purchase can be paid from — these GLs already exist in the chart of accounts. */
+export const SETTLEMENT_BANK_GL_OPTIONS: InvestmentGlOption[] = [
+ { glCode: '100201', glName: 'IDBI BANK' },
+ { glCode: '100202', glName: 'YES BANK' }
+];
+
+/** Every GL the Investment screens can show by name. */
+export const INVESTMENT_GL_LOOKUP: InvestmentGlOption[] = [
+ ...INVESTMENT_GL_OPTIONS,
+ INTEREST_RECEIVABLE_GL,
+ ...SETTLEMENT_BANK_GL_OPTIONS
+];
+
+/** SLR classification on the Script Master, as in the bank's existing CBS ("GOI" = Central Govt). */
+export type SlrType = 'GOI' | 'STATE_GOVT' | 'TREASURY' | 'NON_SLR';
+
+export const SLR_TYPE_OPTIONS: Array<{ value: SlrType; label: string }> = [
+ { value: 'GOI', label: 'GOI' },
+ { value: 'STATE_GOVT', label: 'STATE GOVT' },
+ { value: 'TREASURY', label: 'TREASURY' },
+ { value: 'NON_SLR', label: 'NON SLR' }
+];
+
export interface ScriptMaster {
+ /** Backend id once the script is saved (absent on the sample rows). */
+ id?: number;
scriptCode: string;
scriptName: string;
instrumentType: InstrumentType;
isin: string;
faceValuePerUnit: number;
+ // Fields below follow the bank's existing CBS Script Master; a purchase picks them up instead
+ // of the dealer re-typing them on every deal.
+ slrType?: SlrType;
+ /** Investment GL this script is booked to. */
+ glAccountId?: number;
+ glCode?: string;
+ glName?: string;
+ active?: boolean;
+ /** Interest (coupon) rate % per annum. */
+ couponRate?: number;
+ issueDate?: string;
+ maturityDate?: string;
+ // Issuer details — used for Bond / Mutual Fund scripts.
+ companyName?: string;
+ address1?: string;
+ address2?: string;
+ address3?: string;
+ city?: string;
+ pincode?: string;
+ phone?: string;
+ fax?: string;
+ corpCode?: string;
+}
+
+/** Period between two DD-MM-YYYY dates as years / months / days, as shown on the Script Master. */
+export function periodBetween(fromStr?: string, toStr?: string): { years: number; months: number; days: number } {
+ const none = { years: 0, months: 0, days: 0 };
+ if (!fromStr || !toStr) {
+ return none;
+ }
+ const from = parseDDMMYYYY(fromStr);
+ const to = parseDDMMYYYY(toStr);
+ if (isNaN(from.getTime()) || isNaN(to.getTime()) || to < from) {
+ return none;
+ }
+ let years = to.getFullYear() - from.getFullYear();
+ let months = to.getMonth() - from.getMonth();
+ let days = to.getDate() - from.getDate();
+ if (days < 0) {
+ months -= 1;
+ days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
+ }
+ if (months < 0) {
+ years -= 1;
+ months += 12;
+ }
+ return { years, months, days };
}
export const SCRIPT_MASTER: ScriptMaster[] = [
- { scriptCode: 'FD-CUM', scriptName: 'Cumulative Fixed Deposit', instrumentType: 'FD', isin: '', faceValuePerUnit: 1 },
+ {
+ scriptCode: 'FD-CUM',
+ scriptName: 'Cumulative Fixed Deposit',
+ instrumentType: 'FD',
+ isin: '',
+ faceValuePerUnit: 1,
+ slrType: 'NON_SLR',
+ glCode: '100704'
+ },
{
scriptCode: 'FD-MIP',
scriptName: 'Monthly Interest Payout FD',
instrumentType: 'FD',
isin: '',
- faceValuePerUnit: 1
+ faceValuePerUnit: 1,
+ slrType: 'NON_SLR',
+ glCode: '100704'
},
{
scriptCode: 'IN0020230012',
scriptName: '7.26% GOI 2033',
instrumentType: 'GOVT_SECURITY',
isin: 'IN0020230012',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'GOI',
+ glCode: '100701',
+ couponRate: 7.26,
+ issueDate: '15-01-2023',
+ maturityDate: '15-01-2033'
},
{
scriptCode: 'IN0020220089',
scriptName: '7.10% GOI 2029',
instrumentType: 'GOVT_SECURITY',
isin: 'IN0020220089',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'GOI',
+ glCode: '100701',
+ couponRate: 7.1,
+ issueDate: '10-03-2022',
+ maturityDate: '10-03-2029'
+ },
+ {
+ scriptCode: 'IN0020210056',
+ scriptName: '6.79% GOI 2027',
+ instrumentType: 'GOVT_SECURITY',
+ isin: 'IN0020210056',
+ faceValuePerUnit: 100,
+ slrType: 'GOI',
+ glCode: '100701',
+ couponRate: 6.79,
+ issueDate: '05-06-2021',
+ maturityDate: '05-06-2027'
+ },
+ {
+ scriptCode: 'IN0020240034',
+ scriptName: '7.18% GOI 2037',
+ instrumentType: 'GOVT_SECURITY',
+ isin: 'IN0020240034',
+ faceValuePerUnit: 100,
+ slrType: 'GOI',
+ glCode: '100701',
+ couponRate: 7.18,
+ issueDate: '22-04-2024',
+ maturityDate: '22-04-2037'
+ },
+ {
+ scriptCode: 'MH-SDL-2031',
+ scriptName: '8.05% Maharashtra SDL 2031',
+ instrumentType: 'GOVT_SECURITY',
+ isin: '',
+ faceValuePerUnit: 100,
+ slrType: 'STATE_GOVT',
+ glCode: '100702',
+ couponRate: 8.05,
+ issueDate: '15-02-2021',
+ maturityDate: '15-02-2031'
+ },
+ {
+ scriptCode: 'GJ-SDL-2026',
+ scriptName: '7.85% Gujarat SDL 2026',
+ instrumentType: 'GOVT_SECURITY',
+ isin: '',
+ faceValuePerUnit: 100,
+ slrType: 'STATE_GOVT',
+ glCode: '100702',
+ couponRate: 7.85,
+ issueDate: '18-09-2016',
+ maturityDate: '18-09-2026'
},
{
scriptCode: 'TBILL-364D',
scriptName: '364 Days Treasury Bill',
instrumentType: 'GOVT_SECURITY',
isin: '',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'TREASURY',
+ glCode: '100701',
+ couponRate: 0,
+ issueDate: '02-07-2026',
+ maturityDate: '02-07-2027'
+ },
+ {
+ scriptCode: 'TBILL-91D',
+ scriptName: '91 Days Treasury Bill',
+ instrumentType: 'GOVT_SECURITY',
+ isin: '',
+ faceValuePerUnit: 100,
+ slrType: 'TREASURY',
+ glCode: '100701',
+ couponRate: 0,
+ issueDate: '15-05-2026',
+ maturityDate: '13-08-2026'
},
{
scriptCode: 'REC-7.5-2031',
scriptName: 'REC Ltd Bond 7.50% 2031',
instrumentType: 'NON_SLR',
isin: 'INE020B08JN0',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'NON_SLR',
+ glCode: '100703',
+ couponRate: 7.5,
+ issueDate: '11-08-2021',
+ maturityDate: '11-08-2031',
+ companyName: 'REC Limited',
+ city: 'New Delhi'
},
{
scriptCode: 'NABARD-7.35-2030',
scriptName: 'NABARD Bond 7.35% 2030',
instrumentType: 'NON_SLR',
isin: 'INE261F09EU1',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'NON_SLR',
+ glCode: '100703',
+ couponRate: 7.35,
+ issueDate: '20-09-2020',
+ maturityDate: '20-09-2030',
+ companyName: 'NABARD',
+ city: 'Mumbai'
},
{
scriptCode: 'LTF-8.1-2028',
scriptName: 'L&T Finance NCD 8.10% 2028',
instrumentType: 'BOND',
isin: 'INE027E07BR5',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'NON_SLR',
+ glCode: '100703',
+ couponRate: 8.1,
+ companyName: 'L&T Finance Ltd',
+ city: 'Mumbai'
},
{
scriptCode: 'BAF-7.95-2027',
scriptName: 'Bajaj Finance NCD 7.95% 2027',
instrumentType: 'BOND',
isin: 'INE296A07SP0',
- faceValuePerUnit: 100
+ faceValuePerUnit: 100,
+ slrType: 'NON_SLR',
+ glCode: '100703',
+ couponRate: 7.95,
+ companyName: 'Bajaj Finance Ltd',
+ city: 'Pune'
},
{
scriptCode: 'SBI-OVN',
scriptName: 'SBI Overnight Fund',
instrumentType: 'MUTUAL_FUND',
isin: 'INF200KA1Q40',
- faceValuePerUnit: 1000
+ faceValuePerUnit: 1000,
+ slrType: 'NON_SLR',
+ glCode: '100705',
+ companyName: 'SBI Mutual Fund',
+ city: 'Mumbai'
},
{
scriptCode: 'HDFC-LIQ',
scriptName: 'HDFC Liquid Fund',
instrumentType: 'MUTUAL_FUND',
isin: 'INF179KA1WR9',
- faceValuePerUnit: 1000
+ faceValuePerUnit: 1000,
+ slrType: 'NON_SLR',
+ glCode: '100705',
+ companyName: 'HDFC Mutual Fund',
+ city: 'Mumbai'
},
{
scriptCode: 'SBIN',
scriptName: 'State Bank of India',
instrumentType: 'SHARE',
isin: 'INE062A01020',
- faceValuePerUnit: 1
+ faceValuePerUnit: 1,
+ slrType: 'NON_SLR',
+ glCode: '100706'
},
{
scriptCode: 'HDFCBANK',
scriptName: 'HDFC Bank Ltd',
instrumentType: 'SHARE',
isin: 'INE040A01034',
- faceValuePerUnit: 1
+ faceValuePerUnit: 1,
+ slrType: 'NON_SLR',
+ glCode: '100706'
}
];
@@ -1561,23 +1836,150 @@ export const BROKER_MASTER: BrokerMaster[] = [
];
export interface PartyMaster {
+ /** Backend id once the party is saved (absent on the sample rows). */
+ id?: number;
partyCode: string;
partyName: string;
partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY';
- panNo: string;
+ panNo?: string;
+ // Fields below follow the bank's existing CBS Bank / Party Master.
+ bondType?: InstrumentType;
+ address1?: string;
+ address2?: string;
+ address3?: string;
+ city?: string;
+ pincode?: string;
+ phone?: string;
+ fax?: string;
+ bankType?: string;
+ /** GL this party is mapped to. */
+ glAccountId?: number;
+ glCode?: string;
+ glName?: string;
+ rbiCode?: string;
+ /** Exposure limit for this party (₹). */
+ limit?: number | null;
+ leiNo?: string;
+ // Counterparty's bank details
+ cpBankName?: string;
+ cpBranchName?: string;
+ cpAccountNo?: string;
+ cpIfsc?: string;
+ /** Absent means ACTIVE. An INACTIVE party is not offered for new deals. */
+ status?: 'ACTIVE' | 'INACTIVE';
}
+/** Sample Bank Type options until the bank confirms its list. */
+export const BANK_TYPE_OPTIONS = [
+ 'SCHEDULED COMMERCIAL BANK',
+ 'STATE CO-OPERATIVE BANK',
+ 'DISTRICT CENTRAL CO-OPERATIVE BANK',
+ 'URBAN CO-OPERATIVE BANK',
+ 'RBI / GOVERNMENT',
+ 'OTHER'
+];
+
export const PARTY_MASTER: PartyMaster[] = [
- { partyCode: 'PTY-001', partyName: 'Jila Sahakari Kendriya Bank', partyType: 'BANK', panNo: 'AABCJ1234K' },
- { partyCode: 'PTY-002', partyName: 'State Cooperative Bank Ltd', partyType: 'BANK', panNo: 'AABCS5678L' },
- { partyCode: 'PTY-003', partyName: 'RBI - SGL', partyType: 'GOVT', panNo: 'AAACR4849R' },
- { partyCode: 'PTY-004', partyName: 'REC Limited', partyType: 'CORPORATE', panNo: 'AAACR0641G' },
- { partyCode: 'PTY-005', partyName: 'NABARD', partyType: 'CORPORATE', panNo: 'AAACN0272B' },
- { partyCode: 'PTY-006', partyName: 'L&T Finance Ltd', partyType: 'CORPORATE', panNo: 'AAACL0140P' },
- { partyCode: 'PTY-007', partyName: 'SBI Mutual Fund', partyType: 'AMC', panNo: 'AAATS4325L' },
- { partyCode: 'PTY-008', partyName: 'HDFC Mutual Fund', partyType: 'AMC', panNo: 'AAATH1234M' },
- { partyCode: 'PTY-009', partyName: 'NSE/BSE', partyType: 'EXCHANGE', panNo: 'AAACN0028G' },
- { partyCode: 'PTY-010', partyName: 'District Cooperative Bank', partyType: 'COOP_SOCIETY', panNo: 'AABCD9988K' }
+ {
+ partyCode: 'PTY-001',
+ partyName: 'Jila Sahakari Kendriya Bank',
+ partyType: 'BANK',
+ bondType: 'FD',
+ glCode: '100704',
+ city: 'Vidisha',
+ bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-002',
+ partyName: 'State Cooperative Bank Ltd',
+ partyType: 'BANK',
+ bondType: 'FD',
+ glCode: '100704',
+ city: 'Bhopal',
+ bankType: 'STATE CO-OPERATIVE BANK',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-003',
+ partyName: 'RBI - SGL',
+ partyType: 'GOVT',
+ bondType: 'GOVT_SECURITY',
+ glCode: '100701',
+ city: 'Mumbai',
+ bankType: 'RBI / GOVERNMENT',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-004',
+ partyName: 'REC Limited',
+ partyType: 'CORPORATE',
+ bondType: 'NON_SLR',
+ glCode: '100703',
+ city: 'New Delhi',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-005',
+ partyName: 'NABARD',
+ partyType: 'CORPORATE',
+ bondType: 'NON_SLR',
+ glCode: '100703',
+ city: 'Mumbai',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-006',
+ partyName: 'L&T Finance Ltd',
+ partyType: 'CORPORATE',
+ bondType: 'BOND',
+ glCode: '100703',
+ city: 'Mumbai',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-007',
+ partyName: 'SBI Mutual Fund',
+ partyType: 'AMC',
+ bondType: 'MUTUAL_FUND',
+ glCode: '100705',
+ city: 'Mumbai',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-008',
+ partyName: 'HDFC Mutual Fund',
+ partyType: 'AMC',
+ bondType: 'MUTUAL_FUND',
+ glCode: '100705',
+ city: 'Mumbai',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-009',
+ partyName: 'NSE/BSE',
+ partyType: 'EXCHANGE',
+ bondType: 'SHARE',
+ glCode: '100706',
+ city: 'Mumbai',
+ bankType: 'OTHER',
+ status: 'ACTIVE'
+ },
+ {
+ partyCode: 'PTY-010',
+ partyName: 'District Cooperative Bank',
+ partyType: 'COOP_SOCIETY',
+ bondType: 'SHARE',
+ glCode: '100706',
+ city: 'Vidisha',
+ bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
+ status: 'ACTIVE'
+ }
];
export interface SubTypeMaster {
diff --git a/src/app/investment/investment-masters/investment-masters.component.html b/src/app/investment/investment-masters/investment-masters.component.html
index ebc0dbf2a..9670bd9fa 100644
--- a/src/app/investment/investment-masters/investment-masters.component.html
+++ b/src/app/investment/investment-masters/investment-masters.component.html
@@ -106,38 +106,112 @@
-
+
@if (activeTab === 'party') {
-
Add Bank / Party
+
Bank / Party Master — Entry
- Party Name *
- Party Type *
-
- PAN No
+ * Party Code
+ * Party Name
+ Address Line 1 (Building / Flat No.)
+ Address Line 2 (Street / Area)
+ Address Line 3 (Locality / Landmark)
+ City
+ Pincode
+ Phone
+ Fax
+
+ Bank Type
+
+
+ @for (b of bankTypeOptions; track b) {
+
+ }
+
+
+
+ * GL A/c No.
+
+
+ @for (g of investmentGlOptions; track g.id) {
+
+ }
+
+
+ RBI Code
+ Limit (₹)
+ LEI No
+
+ Status
+
+
+
+
+
+
+
Counterparty's Bank Details
+
+ Name of Bank
+ Name of Branch
+ Account No.
+ IFSC
-
+ Search Party Code
+
+
+
+
+
| Party Code |
+ Bond Type |
Party Name |
- Type |
- PAN No |
+ City |
+ Bank Type |
+ GL A/c |
+ Limit |
+ Counterparty Bank / IFSC |
+ Status |
@@ -146,9 +220,14 @@
{{ p.partyCode }}
|
+ {{ p.bondType || '—' }} |
{{ p.partyName }} |
- {{ p.partyType }} |
- {{ p.panNo }} |
+ {{ p.city || '—' }} |
+ {{ p.bankType || '—' }} |
+ {{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }} |
+ {{ p.limit != null ? '₹' + (p.limit | number: '1.0-0') : '—' }} |
+ {{ p.cpBankName ? p.cpBankName + (p.cpIfsc ? ' / ' + p.cpIfsc : '') : '—' }} |
+ {{ p.status ?? 'ACTIVE' }} |
}
@@ -156,42 +235,118 @@
}
-
+
@if (activeTab === 'script') {
-
Add Script
+
Script Master — Entry
- Script Code
- Script Name *
- Instrument Type *
-
+ * Bond Type
+
@for (t of instrumentTypes; track t) {
}
- ISIN
+
+ SLR
+
+
+ @for (o of slrTypeOptions; track o.value) {
+
+ }
+
+
Face Value / Unit* Script Code
+ * Script Desc
+
+ * GL A/c No.
+
+
+ @for (g of investmentGlOptions; track g.id) {
+
+ }
+
+
+ {{ scriptIsDebtSecurity ? '* ' : '' }}ISIN
+ {{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %
+ {{ scriptIsDebtSecurity ? '* ' : '' }}Issue Date
+ Redum / Mat Date
+ Period (Year)
+ Period (Month)
+ Period (Days)
+
+
Company Details (Bond / Mutual Fund)
+
+ Company Name
+ Address Line 1 (Building / Flat No.)
+ Address Line 2 (Street / Area)
+ Address Line 3 (Locality / Landmark)
+ City
+ Pincode
+ Phone
+ Fax
+ Corp. Code
-
+ Search Script Code
+
+
+
+
| Script Code |
- Script Name |
- Instrument |
+ Bond Type |
+ SLR |
+ Script Desc |
+ GL A/c |
ISIN |
- Face Value/Unit |
+ Int Rate % |
+ Issue Date |
+ Maturity |
@@ -200,10 +355,14 @@
{{ s.scriptCode }}
|
- {{ s.scriptName }} |
{{ s.instrumentType }} |
+ {{ s.slrType || '—' }} |
+ {{ s.scriptName }} |
+ {{ s.glCode || '—' }} |
{{ s.isin || '—' }} |
- {{ s.faceValuePerUnit }} |
+ {{ s.couponRate ?? '—' }} |
+ {{ s.issueDate || '—' }} |
+ {{ s.maturityDate || '—' }} |
}
diff --git a/src/app/investment/investment-masters/investment-masters.component.ts b/src/app/investment/investment-masters/investment-masters.component.ts
index 31d8d1116..967892e37 100644
--- a/src/app/investment/investment-masters/investment-masters.component.ts
+++ b/src/app/investment/investment-masters/investment-masters.component.ts
@@ -6,14 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
- SCRIPT_MASTER,
BROKER_MASTER,
- PARTY_MASTER,
SUBTYPE_MASTER,
CONTROL_PARAMETERS,
MUTUAL_FUND_PRODUCTS,
@@ -31,9 +29,14 @@ import {
ScriptMarketDetail,
DailyScriptRate,
LienReason,
- InstrumentType
+ InstrumentType,
+ BANK_TYPE_OPTIONS,
+ SLR_TYPE_OPTIONS,
+ INVESTMENT_GL_LOOKUP,
+ periodBetween
} from '../investment-data';
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
+import { InvestmentGlAccount, InvestmentService, apiErrorMessage } from '../investment.service';
type MasterTab =
| 'party'
@@ -65,12 +68,12 @@ type MasterTab =
DateDdmmyyyyPickerComponent
]
})
-export class InvestmentMastersComponent {
+export class InvestmentMastersComponent implements OnInit {
activeTab: MasterTab = 'party';
- scripts: ScriptMaster[] = [...SCRIPT_MASTER];
+ scripts: ScriptMaster[] = [];
brokers: BrokerMaster[] = [...BROKER_MASTER];
- parties: PartyMaster[] = [...PARTY_MASTER];
+ parties: PartyMaster[] = [];
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
@@ -106,8 +109,19 @@ export class InvestmentMastersComponent {
toastVisible = false;
private toastTimer: ReturnType | undefined;
- newParty: Partial = {};
- newScript: Partial = {};
+ // Bank / Party Master and Script Master follow the bank's existing CBS screens (one entry form
+ // with Save / Search / Cancel / Drop / Exit) and are stored in the backend through
+ // InvestmentService; the other master tabs are still sample data.
+ private readonly investmentService = inject(InvestmentService);
+ partyForm: Partial = {};
+ partySearchCode = '';
+ scriptForm: Partial = {};
+ scriptSearchCode = '';
+ saving = false;
+ readonly bankTypeOptions = BANK_TYPE_OPTIONS;
+ readonly slrTypeOptions = SLR_TYPE_OPTIONS;
+ /** Enabled ASSET detail GLs from the chart of accounts. */
+ investmentGlOptions: InvestmentGlAccount[] = [];
newSubType: Partial = {};
newBroker: Partial = {};
newControlParam: Partial = {};
@@ -116,43 +130,216 @@ export class InvestmentMastersComponent {
newSglBank: Partial = {};
newDailyRate: Partial = {};
+ ngOnInit(): void {
+ this.investmentService.getAssetGlAccounts().subscribe({
+ next: (gls) => (this.investmentGlOptions = gls),
+ error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
+ });
+ this.reloadMasters();
+ }
+
setTab(tab: MasterTab): void {
this.activeTab = tab;
}
- addParty(): void {
- if (!this.newParty.partyName || !this.newParty.partyType) {
- this.toast('Enter Party Name and Type.');
- return;
- }
- this.parties = [
- {
- partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`,
- panNo: '',
- ...this.newParty
- } as PartyMaster,
- ...this.parties
- ];
- this.newParty = {};
- this.toast('Party/Bank master added.');
+ /** Re-reads both masters from the backend (also refreshes the lists the Purchase screen uses). */
+ private reloadMasters(): void {
+ this.investmentService.syncSharedMasters().subscribe({
+ next: ({ parties, scripts }) => {
+ this.parties = parties;
+ this.scripts = scripts;
+ },
+ error: (err) => this.toast(`Could not load masters: ${apiErrorMessage(err)}`)
+ });
}
- addScript(): void {
- if (!this.newScript.scriptName || !this.newScript.instrumentType) {
- this.toast('Enter Script Name and Instrument Type.');
+ glName(glCode?: string): string {
+ return (
+ this.investmentGlOptions.find((g) => g.glCode === glCode)?.name ??
+ INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ??
+ ''
+ );
+ }
+
+ // -------------------- Bank / Party Master --------------------
+ /** Preview of the code the backend will assign; the saved code always comes from the backend. */
+ get nextPartyCode(): string {
+ const nums = this.parties.map((p) => parseInt(p.partyCode.replace(/\D/g, ''), 10)).filter((n) => !isNaN(n));
+ return `PTY-${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(3, '0')}`;
+ }
+
+ saveParty(): void {
+ const f = this.partyForm;
+ if (!f.bondType || !f.partyName || !f.glAccountId) {
+ this.toast('Enter Bond Type, Party Name and GL A/c No.');
return;
}
- this.scripts = [
- {
- scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`,
- isin: '',
- faceValuePerUnit: 100,
- ...this.newScript
- } as ScriptMaster,
- ...this.scripts
- ];
- this.newScript = {};
- this.toast('Script master added.');
+ const body = {
+ bondType: f.bondType,
+ partyName: f.partyName,
+ address1: f.address1 ?? '',
+ address2: f.address2 ?? '',
+ address3: f.address3 ?? '',
+ city: f.city ?? '',
+ pincode: f.pincode ?? '',
+ phone: f.phone ?? '',
+ fax: f.fax ?? '',
+ bankType: f.bankType ?? '',
+ glAccountId: Number(f.glAccountId),
+ rbiCode: f.rbiCode ?? '',
+ limit: f.limit ?? null,
+ leiNo: f.leiNo ?? '',
+ cpBankName: f.cpBankName ?? '',
+ cpBranchName: f.cpBranchName ?? '',
+ cpAccountNo: f.cpAccountNo ?? '',
+ cpIfsc: f.cpIfsc ?? '',
+ active: f.status !== 'INACTIVE',
+ locale: 'en'
+ };
+ const request = f.id ? this.investmentService.updateParty(f.id, body) : this.investmentService.createParty(body);
+ this.saving = true;
+ request.subscribe({
+ next: (res) => {
+ this.saving = false;
+ this.toast(`Party ${res.resourceIdentifier ?? f.partyCode} ${f.id ? 'updated' : 'saved'}.`);
+ this.partyForm = {};
+ this.reloadMasters();
+ },
+ error: (err) => {
+ this.saving = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
+ searchParty(): void {
+ const p = this.parties.find((x) => x.partyCode.toUpperCase() === this.partySearchCode.trim().toUpperCase());
+ if (!p) {
+ this.toast(`Party Code ${this.partySearchCode} not found.`);
+ return;
+ }
+ this.partyForm = { ...p };
+ this.toast(`Party ${p.partyCode} loaded.`);
+ }
+
+ cancelParty(): void {
+ this.partyForm = {};
+ this.partySearchCode = '';
+ }
+
+ dropParty(): void {
+ const f = this.partyForm;
+ if (!f.id) {
+ this.toast('Search a saved party first, then Drop.');
+ return;
+ }
+ this.investmentService.deleteParty(f.id).subscribe({
+ next: () => {
+ this.toast(`Party ${f.partyCode} dropped.`);
+ this.partyForm = {};
+ this.reloadMasters();
+ },
+ error: (err) => this.toast(apiErrorMessage(err))
+ });
+ }
+
+ // -------------------- Script Master --------------------
+ /** Coupon / maturity terms only apply to debt securities. */
+ get scriptIsDebtSecurity(): boolean {
+ return [
+ 'GOVT_SECURITY',
+ 'NON_SLR',
+ 'BOND'
+ ].includes(this.scriptForm.instrumentType ?? '');
+ }
+
+ /** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
+ get nextScriptCode(): string {
+ const prefixByType: Record = {
+ GOVT_SECURITY: 'G',
+ NON_SLR: 'N',
+ BOND: 'B',
+ MUTUAL_FUND: 'M',
+ FD: 'F',
+ SHARE: 'S'
+ };
+ const type = this.scriptForm.instrumentType;
+ if (!type) {
+ return '';
+ }
+ const prefix = prefixByType[type];
+ const pattern = new RegExp(`^${prefix}(\\d{5})$`);
+ const nums = this.scripts
+ .map((s) => pattern.exec(s.scriptCode)?.[1])
+ .filter((n): n is string => !!n)
+ .map((n) => parseInt(n, 10));
+ return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(5, '0')}`;
+ }
+
+ get scriptPeriod(): { years: number; months: number; days: number } {
+ return periodBetween(this.scriptForm.issueDate, this.scriptForm.maturityDate);
+ }
+
+ saveScript(): void {
+ const f = this.scriptForm;
+ if (!f.instrumentType || !f.scriptName || !f.glAccountId) {
+ this.toast('Enter Bond Type, Script Desc and GL A/c No.');
+ return;
+ }
+ if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate)) {
+ this.toast('Enter ISIN, Int Rate and Issue Date for this security.');
+ return;
+ }
+ const body = {
+ bondType: f.instrumentType,
+ slrType: f.slrType ?? '',
+ scriptDesc: f.scriptName,
+ glAccountId: Number(f.glAccountId),
+ isin: f.isin ?? '',
+ intRate: f.couponRate ?? null,
+ issueDate: f.issueDate ?? '',
+ maturityDate: f.maturityDate ?? '',
+ companyName: f.companyName ?? '',
+ address1: f.address1 ?? '',
+ address2: f.address2 ?? '',
+ address3: f.address3 ?? '',
+ city: f.city ?? '',
+ pincode: f.pincode ?? '',
+ phone: f.phone ?? '',
+ fax: f.fax ?? '',
+ corpCode: f.corpCode ?? '',
+ dateFormat: 'dd-MM-yyyy',
+ locale: 'en'
+ };
+ const request = f.id ? this.investmentService.updateScript(f.id, body) : this.investmentService.createScript(body);
+ this.saving = true;
+ request.subscribe({
+ next: (res) => {
+ this.saving = false;
+ this.toast(`Script ${res.resourceIdentifier ?? f.scriptCode} ${f.id ? 'updated' : 'saved'}.`);
+ this.scriptForm = {};
+ this.reloadMasters();
+ },
+ error: (err) => {
+ this.saving = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
+ searchScript(): void {
+ const s = this.scripts.find((x) => x.scriptCode.toUpperCase() === this.scriptSearchCode.trim().toUpperCase());
+ if (!s) {
+ this.toast(`Script Code ${this.scriptSearchCode} not found.`);
+ return;
+ }
+ this.scriptForm = { ...s };
+ this.toast(`Script ${s.scriptCode} loaded.`);
+ }
+
+ cancelScript(): void {
+ this.scriptForm = {};
+ this.scriptSearchCode = '';
}
addSubType(): void {
@@ -279,46 +466,121 @@ export class InvestmentMastersComponent {
return one(
[
'Party Code',
+ 'Bond Type',
'Party Name',
- 'Type',
- 'PAN No'
+ 'Address',
+ 'City',
+ 'Pincode',
+ 'Phone',
+ 'Fax',
+ 'Bank Type',
+ 'GL A/c No',
+ 'RBI Code',
+ 'Limit',
+ 'LEI No',
+ 'Counterparty Bank',
+ 'Branch',
+ 'Account No',
+ 'IFSC',
+ 'Status'
],
[
12,
+ 14,
30,
- 16,
- 14
+ 36,
+ 14,
+ 10,
+ 14,
+ 14,
+ 24,
+ 12,
+ 12,
+ 14,
+ 22,
+ 24,
+ 20,
+ 18,
+ 14,
+ 10
],
this.parties.map((p) => [
p.partyCode,
+ p.bondType ?? '',
p.partyName,
- p.partyType,
- p.panNo
+ [
+ p.address1,
+ p.address2,
+ p.address3
+ ]
+ .filter((x) => !!x)
+ .join(', '),
+ p.city ?? '',
+ p.pincode ?? '',
+ p.phone ?? '',
+ p.fax ?? '',
+ p.bankType ?? '',
+ p.glCode ?? '',
+ p.rbiCode ?? '',
+ p.limit ?? null,
+ p.leiNo ?? '',
+ p.cpBankName ?? '',
+ p.cpBranchName ?? '',
+ p.cpAccountNo ?? '',
+ p.cpIfsc ?? '',
+ p.status ?? 'ACTIVE'
])
);
case 'script':
return one(
[
'Script Code',
- 'Script Name',
- 'Instrument',
+ 'Bond Type',
+ 'SLR',
+ 'Script Desc',
+ 'GL A/c No',
'ISIN',
- 'Face Value/Unit'
+ 'Int Rate %',
+ 'Issue Date',
+ 'Maturity Date',
+ 'Period (Y/M/D)',
+ 'Face Value/Unit',
+ 'Company Name',
+ 'Corp Code'
],
[
+ 14,
16,
+ 12,
30,
+ 12,
16,
- 16,
- 16
+ 10,
+ 14,
+ 14,
+ 14,
+ 14,
+ 24,
+ 12
],
- this.scripts.map((s) => [
- s.scriptCode,
- s.scriptName,
- s.instrumentType,
- s.isin,
- s.faceValuePerUnit
- ])
+ this.scripts.map((s) => {
+ const p = periodBetween(s.issueDate, s.maturityDate);
+ return [
+ s.scriptCode,
+ s.instrumentType,
+ s.slrType ?? '',
+ s.scriptName,
+ s.glCode ?? '',
+ s.isin,
+ s.couponRate ?? null,
+ s.issueDate ?? '',
+ s.maturityDate ?? '',
+ s.issueDate && s.maturityDate ? `${p.years}/${p.months}/${p.days}` : '',
+ s.faceValuePerUnit,
+ s.companyName ?? '',
+ s.corpCode ?? ''
+ ];
+ })
);
case 'subtype':
return one(
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
new file mode 100644
index 000000000..52a92ac78
--- /dev/null
+++ b/src/app/investment/investment.service.ts
@@ -0,0 +1,300 @@
+/**
+ * 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');
+ }
+
+ 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
+ };
+}
+
+/** 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.';
+}