Compare commits

...
Author SHA1 Message Date
anshulandClaude Opus 5.5 ca01e9b65b feat(investment): Interest Entry screen with coupon and provision schedule
Replace the dummy interest calculation tab with the real Interest Entry:
pick Type / Script / Deal No and Retrieve the holding's schedule from
the backend - every half-yearly coupon and every 31 Mar / 30 Sep closing
provision up to maturity, with days (30/360), gross interest, amount
already booked, net income and the Dr/Cr entry per line. Display only;
nothing is posted to GL yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:36:01 +05:30
anshulandClaude Opus 5.5 a4e0a22205 feat(investment): wire Bank/Party, Script Master and Purchase Entry to the backend
- Bank / Party Master and Script Master follow the bank's existing CBS
  screens (address, bank type, GL A/c, RBI code, limit, LEI, counterparty
  bank details; SLR type, auto script code, ISIN, interest rate, issue /
  maturity date, auto period, company details) and save to
  /investment/parties and /investment/scripts. GL dropdowns list enabled
  ASSET detail accounts from the chart of accounts.
- Purchase Entry simplified to the mandatory fields; script details,
  settlement date, last interest date, units, book value, premium/discount,
  accrued interest (30/360 up to the deal date), settlement amount and YTM
  are filled automatically; optional fields sit under "More Details";
  an Accounting Preview shows the GL entry. Save / Retrieve / Drop use
  /investment/purchases and the Holdings register lists saved purchases.
- New InvestmentService for the investment API calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:38:10 +05:30
6 changed files with 2093 additions and 583 deletions
@@ -176,8 +176,9 @@
<!-- ===================== 01 PURCHASE ENTRY ===================== -->
@if (activeTab === 'purchase') {
<div class="pg-card">
<!-- 1. Deal details — the only fields the dealer has to fill -->
<div class="pos-section-label">
Investment - Govt.Security/Bond Entry ({{ instrumentLabel(purchaseForm.bondType) }})
Purchase Entry — {{ instrumentLabel(purchaseForm.bondType) }} (fields marked * are mandatory)
</div>
<div class="inv-form-grid">
<label class="inv-form-field">
@@ -188,17 +189,6 @@
}
</select>
</label>
<label class="inv-form-field"
><span>Deal No</span><input type="text" [(ngModel)]="purchaseForm.dealNo" readonly
/></label>
<label class="inv-form-field"
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.dealDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Deal Time (24 hr, HH:mm:ss)</span
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
/></label>
<label class="inv-form-field"><span>* ISIN</span><input type="text" [(ngModel)]="purchaseForm.isin" /></label>
<label class="inv-form-field">
<span>* Script Code</span>
<select [(ngModel)]="purchaseForm.scriptCode" (ngModelChange)="onScriptChange()">
@@ -208,167 +198,46 @@
}
</select>
</label>
<label class="inv-form-field"
><span>* Cert No</span><input type="text" [(ngModel)]="purchaseForm.certNo"
/></label>
<label class="inv-form-field">
<span>Settlement Type</span>
<span>* Party Code</span>
<select [(ngModel)]="purchaseForm.partyCode">
<option value="">-- Select --</option>
@for (p of activePartyOptions; track p.partyCode) {
<option [value]="p.partyCode">{{ p.partyCode }} — {{ p.partyName }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>* Deal Date</span
><mifosx-date-ddmmyyyy
[(ngModel)]="purchaseForm.dealDate"
(ngModelChange)="onDealDateChange()"
></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>* Settlement Type</span>
<select [(ngModel)]="purchaseForm.settlementType">
<option value="T+0">T+0</option>
<option value="T+1">T+1</option>
<option value="T+2">T+2</option>
</select>
</label>
<label class="inv-form-field"
><span>Settlement Dt</span
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.settlementDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>* Party Code</span>
<select [(ngModel)]="purchaseForm.partyCode">
<option value="">-- Select --</option>
@for (p of partyOptions; track p.partyCode) {
<option [value]="p.partyName">{{ p.partyName }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Sub Type</span>
<select [(ngModel)]="purchaseForm.subType">
@for (s of subTypeOptions; track s.subTypeCode + s.subTypeName) {
<option [value]="s.subTypeName">{{ s.subTypeName }}</option>
}
</select>
</label>
@if (isMutualFund) {
<label class="inv-form-field">
<span>* Product Name</span>
<select [(ngModel)]="purchaseForm.productName">
<option value="">-- Select --</option>
@for (p of mfProductOptions; track p.productCode) {
<option [value]="p.productName">{{ p.productName }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Holding Days</span><input type="number" [(ngModel)]="purchaseForm.holdingDays" min="0"
><span>* Consider Amt (₹)</span><input type="number" [(ngModel)]="purchaseForm.considerAmt" min="0"
/></label>
<label class="inv-form-field"
><span>* Consider Amt</span><input type="number" [(ngModel)]="purchaseForm.considerAmt" min="0"
/></label>
<label class="inv-form-field"
><span>Trade Amt</span><input type="number" [(ngModel)]="purchaseForm.tradeAmt" min="0"
/></label>
<label class="inv-form-field"
><span>NAV</span><input type="number" [(ngModel)]="purchaseForm.nav" min="0" step="0.0001"
/></label>
<label class="inv-form-field"
><span>Stamp Duty</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
/></label>
<label class="inv-form-field"
><span>StampDuty A/c</span><input type="text" [(ngModel)]="purchaseForm.stampDutyAc"
><span>* NAV</span><input type="number" [(ngModel)]="purchaseForm.nav" min="0" step="0.0001"
/></label>
} @else {
<label class="inv-form-field"
><span>Face Value</span><input type="number" [(ngModel)]="purchaseForm.faceValue" min="0"
><span>* Face Value (₹)</span><input type="number" [(ngModel)]="purchaseForm.faceValue" min="0"
/></label>
<label class="inv-form-field"
><span>Purchase Rate (per 100)</span
><span>* Purchase Rate (per ₹100)</span
><input type="number" [(ngModel)]="purchaseForm.purchaseRate" min="0" step="0.0001"
/></label>
<label class="inv-form-field"
><span>Unit Val</span><input type="text" [value]="purchaseUnitVal" readonly
/></label>
<label class="inv-form-field"
><span>Book Value</span><input type="text" [value]="purchaseBookValue | number: '1.2-2'" readonly
/></label>
<label class="inv-form-field"
><span>Premium Amt.</span><input type="number" [(ngModel)]="purchaseForm.premiumAmt"
/></label>
}
<label class="inv-form-field"
><span>* No Unit</span><input type="text" [value]="purchaseUnits" readonly
/></label>
<label class="inv-form-field"
><span>* CR. To A/c</span><input type="text" [(ngModel)]="purchaseForm.crToAc"
/></label>
<label class="inv-form-field"
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="purchaseForm.tbillGlAc"
/></label>
<label class="inv-form-field"
><span>Brokerage</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
/></label>
<label class="inv-form-field">
<span>Broker Nm.</span>
<select [(ngModel)]="purchaseForm.brokerName">
<option value="">-- Select --</option>
@for (b of brokerOptions; track b.brokerCode) {
<option [value]="b.brokerCode">{{ b.brokerCode }} — {{ b.brokerName }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Trade No.</span><input type="text" [(ngModel)]="purchaseForm.tradeNo"
/></label>
<label class="inv-form-field"
><span>Order No.</span><input type="text" [(ngModel)]="purchaseForm.orderNo"
/></label>
<label class="inv-form-field">
<span>Recomm. Authority</span>
<select [(ngModel)]="purchaseForm.recommAuthority">
<option value="">-- Select --</option>
@for (a of authorityOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Sanction Authority</span>
<select [(ngModel)]="purchaseForm.sanctionAuthority">
<option value="">-- Select --</option>
@for (a of authorityOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>* Issue Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Period(Year)</span><input type="number" [(ngModel)]="purchaseForm.periodYear" min="0"
/></label>
<label class="inv-form-field"
><span>Period(Month)</span><input type="number" [(ngModel)]="purchaseForm.periodMonth" min="0" max="11"
/></label>
<label class="inv-form-field"
><span>Period(Days)</span><input type="number" [(ngModel)]="purchaseForm.periodDays" min="0"
/></label>
<label class="inv-form-field"
><span>Desc Inv</span><input type="text" [(ngModel)]="purchaseForm.descInv"
/></label>
<label class="inv-form-field"
><span>Folio No</span><input type="text" [(ngModel)]="purchaseForm.folioNo"
/></label>
<label class="inv-form-field"
><span>Call Option</span><input type="text" [(ngModel)]="purchaseForm.callOption"
/></label>
<label class="inv-form-field"
><span>Meet Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.meetDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Reso No</span><input type="text" [(ngModel)]="purchaseForm.resoNo"
/></label>
<label class="inv-form-field"
><span>From Dist</span><input type="text" [(ngModel)]="purchaseForm.fromDist"
/></label>
<label class="inv-form-field"
><span>To Dist</span><input type="text" [(ngModel)]="purchaseForm.toDist"
/></label>
<label class="inv-form-field"
><span>Redum/Mat Dt</span
><input type="text" [(ngModel)]="purchaseForm.redumMatDt" placeholder="DD-MM-YYYY (blank = use period)"
/></label>
<label class="inv-form-field">
<span>* Category</span>
<select [(ngModel)]="purchaseForm.category">
@@ -380,32 +249,211 @@
</label>
@if (!isMutualFund) {
<label class="inv-form-field"
><span>Int Rate</span><input type="number" [(ngModel)]="purchaseForm.intRate" min="0" step="0.01"
><span>* Last Interest Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
></label>
}
<label class="inv-form-field">
<span>* CR. To A/c (Paid From)</span>
<select [(ngModel)]="purchaseForm.crToAc">
@for (g of settlementBankOptions; track g.glCode) {
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
}
</select>
</label>
</div>
<!-- 2. Calculated — filled by the system, read-only -->
<div class="pos-section-label">Calculated by System</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Deal No</span><input type="text" [value]="purchaseForm.dealNo" readonly
/></label>
<label class="inv-form-field"
><span>Cert No</span><input type="text" [value]="purchaseForm.certNo" readonly
/></label>
<label class="inv-form-field"
><span>Settlement Date</span><input type="text" [value]="purchaseSettlementDate" readonly
/></label>
<label class="inv-form-field"
><span>ISIN</span><input type="text" [value]="purchaseForm.isin || '—'" readonly
/></label>
<label class="inv-form-field"
><span>Sub Type</span><input type="text" [value]="purchaseForm.subType || '—'" readonly
/></label>
@if (!isMutualFund) {
<label class="inv-form-field"
><span>Int Rate %</span><input type="text" [value]="purchaseForm.intRate ?? '—'" readonly
/></label>
<label class="inv-form-field"
><span>YTM (computed)</span><input type="text" [value]="purchaseYtm" readonly
><span>Issue Date</span><input type="text" [value]="purchaseForm.issueDate || '—'" readonly
/></label>
<label class="inv-form-field"
><span>Maturity Date</span><input type="text" [value]="purchaseMaturityDate || '—'" readonly
/></label>
<label class="inv-form-field"
><span>Period (Y / M / D)</span
><input
type="text"
[value]="purchaseForm.periodYear + ' / ' + purchaseForm.periodMonth + ' / ' + purchaseForm.periodDays"
readonly
/></label>
}
<label class="inv-form-field"
><span>No. of Units</span><input type="text" [value]="purchaseUnits | number: '1.0-4'" readonly
/></label>
<label class="inv-form-field"
><span>Book Value</span><input type="text" [value]="'₹' + (purchaseBookValue | number: '1.2-2')" readonly
/></label>
@if (!isMutualFund) {
<label class="inv-form-field"
><span>Premium (+) / Discount (−)</span
><input type="text" [value]="'₹' + (purchasePremiumDiscount | number: '1.0-0')" readonly
/></label>
<label class="inv-form-field"
><span>Accrued Interest ({{ purchaseAccruedDays }} days)</span
><input type="text" [value]="'₹' + (purchaseAccruedInterest | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"><span>YTM %</span><input type="text" [value]="purchaseYtm" readonly /></label>
}
<label class="inv-form-field"
><span>Total Settlement Amount</span
><input type="text" [value]="'₹' + (purchaseSettlementAmount | number: '1.2-2')" readonly
/></label>
</div>
<!-- 3. Accounting preview -->
<div class="pos-section-label">Accounting Preview (posted after checker approval)</div>
@if (purchaseAccountingPreview.length) {
<table class="pg-table">
<thead>
<tr>
<th>Dr / Cr</th>
<th>GL Code</th>
<th>GL Name</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>
@for (l of purchaseAccountingPreview; track l.side + l.glCode) {
<tr>
<td>{{ l.side }}</td>
<td>{{ l.glCode }}</td>
<td>{{ l.glName || '—' }}</td>
<td class="num">₹{{ l.amount | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
} @else {
<p class="pg-sub">Select a script, enter the amount and CR. To A/c to see the entry.</p>
}
<!-- 4. More details — optional, collapsed by default -->
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--ghost" (click)="showMoreDetails = !showMoreDetails">
{{ showMoreDetails ? '▾ Hide More Details' : '▸ More Details (optional)' }}
</button>
</div>
@if (showMoreDetails) {
<div class="pos-section-label">More Details (optional)</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Deal Time (HH:mm:ss)</span
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
/></label>
<label class="inv-form-field">
<span>Int Type</span>
<select [(ngModel)]="purchaseForm.intType">
@for (i of intTypeOptions; track i.value) {
<option [value]="i.value">{{ i.label }}</option>
<span>Sub Type</span>
<select [(ngModel)]="purchaseForm.subType">
@for (s of subTypeOptions; track s.subTypeCode + s.subTypeName) {
<option [value]="s.subTypeName">{{ s.subTypeName }}</option>
}
</select>
</label>
@if (isMutualFund) {
<label class="inv-form-field"
><span>Trade Amt</span><input type="number" [(ngModel)]="purchaseForm.tradeAmt" min="0"
/></label>
<label class="inv-form-field"
><span>Stamp Duty</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
/></label>
<label class="inv-form-field"
><span>Stamp Duty A/c</span><input type="text" [(ngModel)]="purchaseForm.stampDutyAc"
/></label>
<label class="inv-form-field"
><span>Holding Days</span><input type="number" [(ngModel)]="purchaseForm.holdingDays" min="0"
/></label>
} @else {
<label class="inv-form-field">
<span>Int Type</span>
<select [(ngModel)]="purchaseForm.intType">
@for (i of intTypeOptions; track i.value) {
<option [value]="i.value">{{ i.label }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Bond Rating</span>
<select [(ngModel)]="purchaseForm.bondRating">
@for (r of bondRatingOptions; track r) {
<option [value]="r">{{ r }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>SGL with Bank</span>
<select [(ngModel)]="purchaseForm.sglWithBank">
<option value="">-- Select --</option>
@for (s of sglBankOptions; track s.sglCode) {
<option [value]="s.bankName">{{ s.bankName }} — {{ s.branch }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>TBill GL A/c</span><input type="text" [(ngModel)]="purchaseForm.tbillGlAc"
/></label>
}
<label class="inv-form-field"
><span>Brokerage (₹)</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
/></label>
<label class="inv-form-field">
<span>Broker</span>
<select [(ngModel)]="purchaseForm.brokerName">
<option value="">-- Select --</option>
@for (b of brokerOptions; track b.brokerCode) {
<option [value]="b.brokerCode">{{ b.brokerCode }} — {{ b.brokerName }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Int Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.intDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Last Int.dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Accrued Interest (computed)</span
><input type="text" [value]="purchaseAccruedInterest | number: '1.2-2'" readonly
><span>Trade No.</span><input type="text" [(ngModel)]="purchaseForm.tradeNo"
/></label>
<label class="inv-form-field"
><span>Settlement Amt of Pur. (computed)</span
><input type="text" [value]="purchaseSettlementAmount | number: '1.4-4'" readonly
><span>Order No.</span><input type="text" [(ngModel)]="purchaseForm.orderNo"
/></label>
<label class="inv-form-field">
<span>Recomm. Authority</span>
<select [(ngModel)]="purchaseForm.recommAuthority">
<option value="">-- Select --</option>
@for (a of authorityOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Sanction Authority</span>
<select [(ngModel)]="purchaseForm.sanctionAuthority">
<option value="">-- Select --</option>
@for (a of authorityOptions; track a) {
<option [value]="a">{{ a }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Meet Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.meetDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Reso No</span><input type="text" [(ngModel)]="purchaseForm.resoNo"
/></label>
<label class="inv-form-field">
<span>Deal Type</span>
@@ -423,6 +471,80 @@
}
</select>
</label>
<label class="inv-form-field">
<span>Payment Mode</span>
<select [(ngModel)]="purchaseForm.paymentMode">
<option value="">-- Select --</option>
@for (p of paymentModeOptions; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Delivery Mode</span>
<select [(ngModel)]="purchaseForm.deliveryMode">
<option value="">-- Select --</option>
@for (d of deliveryModeOptions; track d) {
<option [value]="d">{{ d }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>LIEN</span>
<select [(ngModel)]="purchaseForm.lien">
<option value="NO">NO</option>
<option value="YES">YES</option>
</select>
</label>
@if (purchaseForm.lien === 'YES') {
<label class="inv-form-field"
><span>LIEN Amount</span><input type="number" [(ngModel)]="purchaseForm.lienAmount"
/></label>
<label class="inv-form-field">
<span>LIEN Reason</span>
<select [(ngModel)]="purchaseForm.lienReason">
<option value="">-- Select --</option>
@for (l of lienReasonOptions; track l.reasonCode) {
<option [value]="l.reasonDescription">{{ l.reasonDescription }}</option>
}
</select>
</label>
}
<label class="inv-form-field"
><span>Folio No</span><input type="text" [(ngModel)]="purchaseForm.folioNo"
/></label>
<label class="inv-form-field"
><span>Call Option</span><input type="text" [(ngModel)]="purchaseForm.callOption"
/></label>
<label class="inv-form-field"
><span>Register Address</span><input type="text" [(ngModel)]="purchaseForm.registerAdd"
/></label>
<label class="inv-form-field"
><span>Register City</span><input type="text" [(ngModel)]="purchaseForm.regiCity"
/></label>
<label class="inv-form-field"
><span>Register Pin</span><input type="text" [(ngModel)]="purchaseForm.regiPin"
/></label>
<label class="inv-form-field"
><span>Issue Date (override)</span
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Redum / Mat Date (override)</span
><input type="text" [(ngModel)]="purchaseForm.redumMatDt" placeholder="DD-MM-YYYY"
/></label>
<label class="inv-form-field"
><span>Int Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.intDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Desc Inv</span><input type="text" [(ngModel)]="purchaseForm.descInv"
/></label>
<label class="inv-form-field"
><span>From Dist</span><input type="text" [(ngModel)]="purchaseForm.fromDist"
/></label>
<label class="inv-form-field"
><span>To Dist</span><input type="text" [(ngModel)]="purchaseForm.toDist"
/></label>
<label class="inv-form-field">
<span>Mark CBLO</span>
<select [(ngModel)]="purchaseForm.markCblo">
@@ -433,14 +555,6 @@
<label class="inv-form-field"
><span>Amortize Amt.</span><input type="number" [(ngModel)]="purchaseForm.amortizeAmt"
/></label>
<label class="inv-form-field">
<span>Bond Rating</span>
<select [(ngModel)]="purchaseForm.bondRating">
@for (r of bondRatingOptions; track r) {
<option [value]="r">{{ r }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Corp. Bond</span>
<select [(ngModel)]="purchaseForm.corpBond">
@@ -448,76 +562,19 @@
<option value="YES">YES</option>
</select>
</label>
<label class="inv-form-field">
<span>SGL with Bank</span>
<select [(ngModel)]="purchaseForm.sglWithBank">
<option value="">-- Select --</option>
@for (s of sglBankOptions; track s.sglCode) {
<option [value]="s.bankName">{{ s.bankName }} — {{ s.branch }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Register Add</span><input type="text" [(ngModel)]="purchaseForm.registerAdd"
><span>Remarks</span><input type="text" [(ngModel)]="purchaseForm.remarks"
/></label>
<label class="inv-form-field"
><span>Regi City</span><input type="text" [(ngModel)]="purchaseForm.regiCity"
><span>Description</span><input type="text" [(ngModel)]="purchaseForm.description"
/></label>
<label class="inv-form-field"
><span>Regi Pin</span><input type="text" [(ngModel)]="purchaseForm.regiPin"
/></label>
}
<label class="inv-form-field">
<span>LIEN</span>
<select [(ngModel)]="purchaseForm.lien">
<option value="NO">NO</option>
<option value="YES">YES</option>
</select>
</label>
@if (purchaseForm.lien === 'YES') {
<label class="inv-form-field"
><span>LIEN Amount</span><input type="number" [(ngModel)]="purchaseForm.lienAmount"
/></label>
<label class="inv-form-field">
<span>LIEN Reason</span>
<select [(ngModel)]="purchaseForm.lienReason">
<option value="">-- Select --</option>
@for (l of lienReasonOptions; track l.reasonCode) {
<option [value]="l.reasonDescription">{{ l.reasonDescription }}</option>
}
</select>
</label>
}
<label class="inv-form-field">
<span>Payment Mode</span>
<select [(ngModel)]="purchaseForm.paymentMode">
<option value="">--Select--</option>
@for (p of paymentModeOptions; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Delivery Mode</span>
<select [(ngModel)]="purchaseForm.deliveryMode">
<option value="">--Select--</option>
@for (d of deliveryModeOptions; track d) {
<option [value]="d">{{ d }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Remarks</span><input type="text" [(ngModel)]="purchaseForm.remarks"
/></label>
<label class="inv-form-field"
><span>Description</span><input type="text" [(ngModel)]="purchaseForm.description"
/></label>
<label class="inv-form-field"
><span>Maturity / Redemption Value (computed)</span
><input type="text" [value]="'₹' + (purchaseMaturityAmount | number: '1.2-2')" readonly
/></label>
</div>
</div>
}
<div class="inv-form-actions">
<label class="inv-form-field"
><span>Retrieve Cert No</span><input type="text" placeholder="e.g. 30" [(ngModel)]="retrieveCertNo"
/></label>
<button type="button" class="pg-btn pg-btn--primary" (click)="savePurchase()">Save</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="retrievePurchase()">Retrieve</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropPurchase()">Drop</button>
@@ -569,20 +626,14 @@
</div>
}
<!-- ===================== 03 INTEREST ENTRY / CALCULATION ===================== -->
<!-- ===================== 03 INTEREST ENTRY ===================== -->
@if (activeTab === 'interest') {
<div class="pg-card">
<div class="pos-section-label">Investment - Govt./Bond Interest calculation</div>
<div class="pos-section-label">Interest Entry — coupon and 31 Mar / 30 Sep provision schedule</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcFromDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcToDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>Type</span>
<select [(ngModel)]="intCalcType" (ngModelChange)="onIntCalcTypeChange()">
<select [(ngModel)]="intType" (ngModelChange)="onIntFilterChange()">
<option value="">-- All Types --</option>
@for (o of bondTypeOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
@@ -591,67 +642,149 @@
</label>
<label class="inv-form-field">
<span>Script</span>
<select [(ngModel)]="intCalcScript">
<select [(ngModel)]="intScript" (ngModelChange)="onIntFilterChange()">
<option value="">-- All Scripts --</option>
@for (s of scriptOptions; track s.scriptCode) {
@for (s of intScriptOptions; track s.scriptCode) {
<option [value]="s.scriptCode">{{ s.scriptCode }} — {{ s.scriptName }}</option>
}
</select>
</label>
<label class="inv-form-field"><span>Deal No</span><input type="text" [(ngModel)]="intCalcDealNo" /></label>
<label class="inv-form-field"
><span>ALL</span><input type="checkbox" [(ngModel)]="intCalcAll" style="height: 18px; width: 18px"
/></label>
<label class="inv-form-field">
<span>Post To (Interest Income GL)</span>
<select [(ngModel)]="intCalcGlCode">
@for (g of interestIncomeGlOptions; track g.glCode) {
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
<span>* Deal No</span>
<select [(ngModel)]="intPurchaseId" (ngModelChange)="intSchedule = null">
<option [ngValue]="null">-- Select --</option>
@for (h of intDealOptions; track h.id) {
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} (Cert {{ h.certNo }})</option>
}
</select>
</label>
<label class="inv-form-field"
><span>As On Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intAsOnDate"></mifosx-date-ddmmyyyy
></label>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="processInterest()">Process</button>
<button
type="button"
class="pg-btn pg-btn--primary"
[disabled]="intLoading"
(click)="retrieveInterestSchedule()"
>
{{ intLoading ? 'Retrieving…' : 'Retrieve' }}
</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
</div>
@if (intCalcExcludedNpi.length > 0) {
@if (intSchedule; as sc) {
<div class="pos-section-label">Purchase Details</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Deal No / Cert No</span><input type="text" [value]="sc.dealNo + ' / ' + sc.certNo" readonly
/></label>
<label class="inv-form-field"
><span>Script</span><input type="text" [value]="sc.scriptCode + ' — ' + sc.scriptDesc" readonly
/></label>
<label class="inv-form-field"
><span>Face Value Held</span><input type="text" [value]="'₹' + (sc.faceValue | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Interest Rate</span><input type="text" [value]="sc.intRate + '%'" readonly
/></label>
<label class="inv-form-field"
><span>Coupon per Half-Year</span
><input type="text" [value]="'₹' + (sc.couponPerHalfYear | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Deal Date</span><input type="text" [value]="sc.dealDate" readonly
/></label>
<label class="inv-form-field"
><span>Last Interest Date</span><input type="text" [value]="sc.lastIntDate" readonly
/></label>
<label class="inv-form-field"
><span>Maturity Date</span><input type="text" [value]="sc.maturityDate" readonly
/></label>
<label class="inv-form-field"
><span>Accrued Interest Paid at Purchase</span
><input type="text" [value]="'₹' + (sc.purchasedAccruedInterest | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Book Value</span><input type="text" [value]="'₹' + (sc.bookValue | number: '1.2-2')" readonly
/></label>
</div>
<div class="pos-section-label">
Interest Schedule ({{ sc.rows.length }} lines) — due up to {{ sc.asOnDate }}: ₹{{
intDueTotal | number: '1.2-2'
}}
</div>
<p class="pg-sub" style="padding: 0 16px">
Days are 30/360. A provision books the interest earned since the last coupon; the coupon then clears what is
already booked. Display only — nothing is posted to GL yet.
</p>
<table class="pg-table">
<thead>
<tr>
<th>Type</th>
<th>Int Date</th>
<th>From</th>
<th>To</th>
<th class="num">Days</th>
<th class="num">Int %</th>
<th class="num">Gross Interest</th>
<th class="num">Already Booked</th>
<th class="num col-in">Amount</th>
<th>Accounting</th>
</tr>
</thead>
<tbody>
@for (r of sc.rows; track r.intDate) {
<tr [style.opacity]="r.due ? 1 : 0.6">
<td>
<span class="pg-chip">{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}</span>
@if (r.due) {
<span class="pg-chip">Due</span>
}
</td>
<td>{{ r.intDate }}</td>
<td>{{ r.fromDate }}</td>
<td>{{ r.toDate }}</td>
<td class="num">{{ r.days }}</td>
<td class="num">{{ sc.intRate }}</td>
<td class="num">₹{{ r.grossInterest | number: '1.2-2' }}</td>
<td class="num">₹{{ r.alreadyBooked | number: '1.2-2' }}</td>
<td class="num col-in">₹{{ r.amount | number: '1.2-2' }}</td>
<td style="white-space: nowrap; font-size: 12px">
@if (r.type === 'COUPON') {
<div>Dr Bank ₹{{ r.grossInterest | number: '1.0-0' }}</div>
@if (r.alreadyBooked > 0) {
<div>
Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.0-0' }}
</div>
}
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div>
} @else {
<div>Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.0-0' }}</div>
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div>
}
</td>
</tr>
} @empty {
<tr>
<td colspan="10" class="pg-empty">
No interest lines — check the rate, last interest date and maturity date.
</td>
</tr>
}
</tbody>
</table>
<p class="pg-sub" style="padding: 8px 16px 12px">
Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }}. Posting to GL will be enabled after
approval.
</p>
} @else {
<p class="pg-sub" style="padding: 0 16px 12px">
{{ intCalcExcludedNpi.length }} holding(s) excluded — classified Non-Performing, interest is not posted on
accrual basis (collectible on cash basis only, via Interest Receive in the Investment Interest module).
Select a Deal No and press Retrieve to see its interest schedule.
</p>
}
<div class="pos-section-label">Holdings matching the filter ({{ intCalcTargets.length }})</div>
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Script</th>
<th>Type</th>
<th class="num">Rate</th>
<th>Frequency</th>
<th class="num col-in">Interest to Post</th>
</tr>
</thead>
<tbody>
@for (h of intCalcTargets; track h.certNo) {
<tr>
<td>
<span class="pg-chip">{{ h.certNo }}</span>
</td>
<td>{{ h.scriptName }}</td>
<td>{{ instrumentLabel(h.instrumentType) }}</td>
<td class="num">{{ h.interestRate }}%</td>
<td>{{ h.interestFreq }}</td>
<td class="num col-in">₹{{ interestAccrued(h) | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="pg-empty">No holdings match the selected filters.</td>
</tr>
}
</tbody>
</table>
</div>
}
@@ -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,
@@ -31,16 +26,20 @@ import {
BOND_RATING_OPTIONS,
INT_TYPE_OPTIONS,
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,
InterestFreq,
INSTRUMENT_LABELS,
accruedInterest,
accruedInterestBetween,
canAccrueInterest,
daysToMaturity,
maturityAmount,
addPeriod,
@@ -49,6 +48,13 @@ import {
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
import {
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
InvestmentService,
apiErrorMessage,
toHolding
} from '../investment.service';
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
type BondTab =
@@ -131,6 +137,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 +195,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<string, number>();
/** 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,23 +281,23 @@ export class BondInvestmentComponent {
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
sellForm: BondSellForm = this.blankSellForm();
readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS;
// Redemption Entry
redeemCertNo = '';
redeemDate = this.asOnDate;
redeemRemarks = '';
// Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) —
// posts interest accrued strictly within [intCalcFromDate, intCalcToDate], same period-based
// basis as the Investment Interest module's Govt./Bond Interest Calculation.
intCalcFromDate = addPeriod(this.asOnDate, 0, -3);
intCalcToDate = this.asOnDate;
intCalcType: BondType | '' = '';
intCalcScript = '';
intCalcDealNo = '';
intCalcAll = false;
intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
// 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every
// half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only:
// nothing is posted to GL until the posting rules are approved.
intType: BondType | '' = '';
intScript = '';
intPurchaseId: number | null = null;
intAsOnDate = this.asOnDate;
intSchedule: InvestmentInterestScheduleDto | null = null;
intLoading = false;
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
// Write-off
writeoffCertNo = '';
@@ -305,20 +362,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 +400,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 +537,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 +606,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<string, string | number> {
const f = this.purchaseForm;
const fields: Record<string, string | number | null> = {
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<string, string | number> = {};
for (const [
k,
v
] of Object.entries(fields)) {
if (v !== null && v !== undefined && v !== '') {
out[k] = v;
}
}
return out;
}
cancelPurchase(): void {
@@ -576,9 +805,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 +817,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 +830,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 {
@@ -658,78 +893,53 @@ export class BondInvestmentComponent {
}
// -------------------- Interest calculation --------------------
/** Holdings the Interest Calculation "Process" run would cover, given the filters. A
* Non-Performing holding is always excluded — same accrual-basis rule as the Investment
* Interest module (it's still collectible on cash basis via Interest Receive there). */
get intCalcTargets(): InvestmentHolding[] {
const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
if (this.intCalcAll) {
return base;
/** Saved holdings for the Deal No dropdown, narrowed by Type and Script. */
get intDealOptions(): InvestmentHolding[] {
return this.holdings.filter(
(h) =>
h.id != null &&
h.status === 'ACTIVE' &&
(!this.intType || h.instrumentType === this.intType) &&
(!this.intScript || h.scriptCode === this.intScript)
);
}
get intScriptOptions(): typeof SCRIPT_MASTER {
return this.scriptOptions.filter((s) => !this.intType || s.instrumentType === this.intType);
}
onIntFilterChange(): void {
if (this.intScript && !this.intScriptOptions.some((s) => s.scriptCode === this.intScript)) {
this.intScript = '';
}
return base.filter((h) => {
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false;
if (this.intCalcDealNo && h.certNo !== this.intCalcDealNo.trim()) return false;
return true;
});
}
get intCalcExcludedNpi(): InvestmentHolding[] {
return this.holdings.filter((h) => h.status === 'ACTIVE' && !canAccrueInterest(h));
}
/** Re-suggests the Interest Income GL when the Type filter changes — still overridable. */
onIntCalcTypeChange(): void {
if (this.intCalcType) {
this.intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE[this.intCalcType];
if (this.intPurchaseId != null && !this.intDealOptions.some((h) => h.id === this.intPurchaseId)) {
this.intPurchaseId = null;
}
this.intSchedule = null;
}
glName(glCode: string): string {
return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
}
/** Interest accrued strictly within [intCalcFromDate, intCalcToDate] — a period-based
* Interest Calculation & Post run, same basis as the Investment Interest module. */
interestAccrued(h: InvestmentHolding): number {
return accruedInterestBetween(h, this.intCalcFromDate || h.purchaseDate, this.intCalcToDate || this.asOnDate);
}
processInterest(): void {
const targets = this.intCalcTargets.filter((h) => this.interestAccrued(h) > 0);
if (targets.length === 0) {
this.toast('No holdings have interest due to be posted for this period.');
retrieveInterestSchedule(): void {
if (this.intPurchaseId == null) {
this.toast('Select a Deal No first.');
return;
}
const toDate = this.intCalcToDate || this.asOnDate;
const fromDate = this.intCalcFromDate || toDate;
const glCode = this.intCalcGlCode;
const glName = this.glName(glCode);
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
const amt = this.interestAccrued(h);
h.interestPosted = (h.interestPosted ?? 0) + amt;
h.interestPostedTillDate = toDate;
return {
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'INTEREST',
voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`,
txnDate: toDate,
amount: amt,
particulars: `Interest calculated (accrual) for period ${fromDate} to ${toDate} - ${h.scriptName}`,
glCode,
glName
};
this.intLoading = true;
this.investmentService.getInterestSchedule(this.intPurchaseId, this.intAsOnDate || undefined).subscribe({
next: (schedule) => {
this.intSchedule = schedule;
this.intLoading = false;
},
error: (err) => {
this.intSchedule = null;
this.intLoading = false;
this.toast(apiErrorMessage(err));
}
});
this.txns = [
...newTxns,
...this.txns
];
this.intCalcFromDate = toDate;
const total = newTxns.reduce((s, t) => s + t.amount, 0);
this.toast(
`Interest processed for ${targets.length} holding(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.`
);
}
/** Total of the lines falling due up to the as-on date. */
get intDueTotal(): number {
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
}
// -------------------- Sell / Redeem Entry --------------------
+426 -24
View File
@@ -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<InstrumentType, string> = {
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 {
@@ -106,38 +106,112 @@
</button>
</div>
<!-- PARTY MASTER -->
<!-- PARTY MASTER (layout follows the bank's existing CBS Bank / Party Master) -->
@if (activeTab === 'party') {
<div class="pg-card">
<div class="pos-section-label">Add Bank / Party</div>
<div class="pos-section-label">Bank / Party Master — Entry</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Party Name *</span><input type="text" [(ngModel)]="newParty.partyName"
/></label>
<label class="inv-form-field">
<span>Party Type *</span>
<select [(ngModel)]="newParty.partyType">
<span>* Bond Type</span>
<select [(ngModel)]="partyForm.bondType">
<option [ngValue]="undefined">-- Select --</option>
<option value="BANK">BANK</option>
<option value="CORPORATE">CORPORATE</option>
<option value="GOVT">GOVT</option>
<option value="AMC">AMC</option>
<option value="EXCHANGE">EXCHANGE</option>
<option value="COOP_SOCIETY">COOP SOCIETY</option>
@for (t of instrumentTypes; track t) {
<option [value]="t">{{ t }}</option>
}
</select>
</label>
<label class="inv-form-field"><span>PAN No</span><input type="text" [(ngModel)]="newParty.panNo" /></label>
<label class="inv-form-field"
><span>* Party Code</span><input type="text" [value]="partyForm.partyCode || nextPartyCode" readonly
/></label>
<label class="inv-form-field"
><span>* Party Name</span><input type="text" [(ngModel)]="partyForm.partyName"
/></label>
<label class="inv-form-field"
><span>Address Line 1 (Building / Flat No.)</span
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="partyForm.address1"
/></label>
<label class="inv-form-field"
><span>Address Line 2 (Street / Area)</span
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="partyForm.address2"
/></label>
<label class="inv-form-field"
><span>Address Line 3 (Locality / Landmark)</span
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="partyForm.address3"
/></label>
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="partyForm.city" /></label>
<label class="inv-form-field"
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="partyForm.pincode"
/></label>
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="partyForm.phone" /></label>
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="partyForm.fax" /></label>
<label class="inv-form-field">
<span>Bank Type</span>
<select [(ngModel)]="partyForm.bankType">
<option [ngValue]="undefined">-- Select --</option>
@for (b of bankTypeOptions; track b) {
<option [value]="b">{{ b }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>* GL A/c No.</span>
<select [(ngModel)]="partyForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
</select>
</label>
<label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
<label class="inv-form-field"
><span>Limit (₹)</span><input type="number" min="0" [(ngModel)]="partyForm.limit"
/></label>
<label class="inv-form-field"><span>LEI No</span><input type="text" [(ngModel)]="partyForm.leiNo" /></label>
<label class="inv-form-field">
<span>Status</span>
<select [(ngModel)]="partyForm.status">
<option [ngValue]="undefined">ACTIVE</option>
<option value="INACTIVE">INACTIVE</option>
</select>
</label>
</div>
<div class="pos-section-label">Counterparty's Bank Details</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Name of Bank</span><input type="text" [(ngModel)]="partyForm.cpBankName"
/></label>
<label class="inv-form-field"
><span>Name of Branch</span><input type="text" [(ngModel)]="partyForm.cpBranchName"
/></label>
<label class="inv-form-field"
><span>Account No.</span><input type="text" [(ngModel)]="partyForm.cpAccountNo"
/></label>
<label class="inv-form-field"
><span>IFSC</span><input type="text" maxlength="11" [(ngModel)]="partyForm.cpIfsc"
/></label>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="addParty()">Save</button>
<label class="inv-form-field"
><span>Search Party Code</span><input type="text" placeholder="e.g. PTY-001" [(ngModel)]="partySearchCode"
/></label>
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveParty()">Save</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchParty()">Search</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropParty()">Drop</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Exit</button>
</div>
<table class="pg-table">
<thead>
<tr>
<th>Party Code</th>
<th>Bond Type</th>
<th>Party Name</th>
<th>Type</th>
<th>PAN No</th>
<th>City</th>
<th>Bank Type</th>
<th>GL A/c</th>
<th class="num">Limit</th>
<th>Counterparty Bank / IFSC</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@@ -146,9 +220,14 @@
<td>
<span class="pg-chip">{{ p.partyCode }}</span>
</td>
<td>{{ p.bondType || '—' }}</td>
<td>{{ p.partyName }}</td>
<td>{{ p.partyType }}</td>
<td>{{ p.panNo }}</td>
<td>{{ p.city || '—' }}</td>
<td>{{ p.bankType || '—' }}</td>
<td>{{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }}</td>
<td class="num">{{ p.limit != null ? '₹' + (p.limit | number: '1.0-0') : '—' }}</td>
<td>{{ p.cpBankName ? p.cpBankName + (p.cpIfsc ? ' / ' + p.cpIfsc : '') : '—' }}</td>
<td>{{ p.status ?? 'ACTIVE' }}</td>
</tr>
}
</tbody>
@@ -156,42 +235,118 @@
</div>
}
<!-- SCRIPT MASTER -->
<!-- SCRIPT MASTER (layout follows the bank's existing CBS Script Master) -->
@if (activeTab === 'script') {
<div class="pg-card">
<div class="pos-section-label">Add Script</div>
<div class="pos-section-label">Script Master — Entry</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Script Code</span><input type="text" [(ngModel)]="newScript.scriptCode"
/></label>
<label class="inv-form-field"
><span>Script Name *</span><input type="text" [(ngModel)]="newScript.scriptName"
/></label>
<label class="inv-form-field">
<span>Instrument Type *</span>
<select [(ngModel)]="newScript.instrumentType">
<span>* Bond Type</span>
<select [(ngModel)]="scriptForm.instrumentType" [disabled]="!!scriptForm.scriptCode">
<option [ngValue]="undefined">-- Select --</option>
@for (t of instrumentTypes; track t) {
<option [value]="t">{{ t }}</option>
}
</select>
</label>
<label class="inv-form-field"><span>ISIN</span><input type="text" [(ngModel)]="newScript.isin" /></label>
<label class="inv-form-field">
<span>SLR</span>
<select [(ngModel)]="scriptForm.slrType">
<option [ngValue]="undefined">-- Select --</option>
@for (o of slrTypeOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>Face Value / Unit</span><input type="number" [(ngModel)]="newScript.faceValuePerUnit"
><span>* Script Code</span><input type="text" [value]="scriptForm.scriptCode || nextScriptCode" readonly
/></label>
<label class="inv-form-field"
><span>* Script Desc</span
><input type="text" placeholder="e.g. 7.50%GOI2034" [(ngModel)]="scriptForm.scriptName"
/></label>
<label class="inv-form-field">
<span>* GL A/c No.</span>
<select [(ngModel)]="scriptForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
/></label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %</span
><input type="number" step="0.01" min="0" [(ngModel)]="scriptForm.couponRate"
/></label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Issue Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Redum / Mat Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.maturityDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Period (Year)</span><input type="text" [value]="scriptPeriod.years" readonly
/></label>
<label class="inv-form-field"
><span>Period (Month)</span><input type="text" [value]="scriptPeriod.months" readonly
/></label>
<label class="inv-form-field"
><span>Period (Days)</span><input type="text" [value]="scriptPeriod.days" readonly
/></label>
</div>
<div class="pos-section-label">Company Details (Bond / Mutual Fund)</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Company Name</span><input type="text" [(ngModel)]="scriptForm.companyName"
/></label>
<label class="inv-form-field"
><span>Address Line 1 (Building / Flat No.)</span
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="scriptForm.address1"
/></label>
<label class="inv-form-field"
><span>Address Line 2 (Street / Area)</span
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="scriptForm.address2"
/></label>
<label class="inv-form-field"
><span>Address Line 3 (Locality / Landmark)</span
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="scriptForm.address3"
/></label>
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="scriptForm.city" /></label>
<label class="inv-form-field"
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="scriptForm.pincode"
/></label>
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="scriptForm.phone" /></label>
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="scriptForm.fax" /></label>
<label class="inv-form-field"
><span>Corp. Code</span><input type="text" [(ngModel)]="scriptForm.corpCode"
/></label>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="addScript()">Save</button>
<label class="inv-form-field"
><span>Search Script Code</span><input type="text" placeholder="e.g. G00001" [(ngModel)]="scriptSearchCode"
/></label>
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveScript()">Save</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchScript()">Search</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Exit</button>
</div>
<table class="pg-table">
<thead>
<tr>
<th>Script Code</th>
<th>Script Name</th>
<th>Instrument</th>
<th>Bond Type</th>
<th>SLR</th>
<th>Script Desc</th>
<th>GL A/c</th>
<th>ISIN</th>
<th class="num">Face Value/Unit</th>
<th class="num">Int Rate %</th>
<th>Issue Date</th>
<th>Maturity</th>
</tr>
</thead>
<tbody>
@@ -200,10 +355,14 @@
<td>
<span class="pg-chip">{{ s.scriptCode }}</span>
</td>
<td>{{ s.scriptName }}</td>
<td>{{ s.instrumentType }}</td>
<td>{{ s.slrType || '—' }}</td>
<td>{{ s.scriptName }}</td>
<td>{{ s.glCode || '—' }}</td>
<td>{{ s.isin || '—' }}</td>
<td class="num">{{ s.faceValuePerUnit }}</td>
<td class="num">{{ s.couponRate ?? '—' }}</td>
<td>{{ s.issueDate || '—' }}</td>
<td>{{ s.maturityDate || '—' }}</td>
</tr>
}
</tbody>
@@ -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<typeof setTimeout> | undefined;
newParty: Partial<PartyMaster> = {};
newScript: Partial<ScriptMaster> = {};
// 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<PartyMaster> = {};
partySearchCode = '';
scriptForm: Partial<ScriptMaster> = {};
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<SubTypeMaster> = {};
newBroker: Partial<BrokerMaster> = {};
newControlParam: Partial<ControlParameter> = {};
@@ -116,43 +130,216 @@ export class InvestmentMastersComponent {
newSglBank: Partial<SglBankMaster> = {};
newDailyRate: Partial<DailyScriptRate> = {};
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<InstrumentType, string> = {
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(
+344
View File
@@ -0,0 +1,344 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import {
InstrumentType,
InvestmentHolding,
PARTY_MASTER,
PartyMaster,
SCRIPT_MASTER,
ScriptMaster,
SlrType
} from './investment-data';
/** Investment party as returned by /investment/parties. */
export interface InvestmentPartyDto {
id: number;
partyCode: string;
bondType: InstrumentType;
partyName: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
bankType?: string;
glAccountId: number;
glCode: string;
glName: string;
rbiCode?: string;
limit?: number;
leiNo?: string;
cpBankName?: string;
cpBranchName?: string;
cpAccountNo?: string;
cpIfsc?: string;
active: boolean;
}
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
export interface InvestmentScriptDto {
id: number;
scriptCode: string;
bondType: InstrumentType;
slrType?: SlrType;
scriptDesc: string;
glAccountId: number;
glCode: string;
glName: string;
isin?: string;
faceValuePerUnit: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
companyName?: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
corpCode?: string;
active: boolean;
}
/** Purchase entry (holding) as returned by /investment/purchases (dates are DD-MM-YYYY). */
export interface InvestmentPurchaseDto {
id: number;
dealNo: string;
certNo: string;
bondType: InstrumentType;
scriptId: number;
scriptCode: string;
scriptDesc: string;
isin?: string;
partyId: number;
partyCode: string;
partyName: string;
dealDate: string;
dealTime?: string;
settlementType: string;
settlementDate: string;
faceValue: number;
purchaseRate: number;
units: number;
balanceUnits: number;
brokerage?: number;
stampDuty?: number;
bookValue: number;
premiumDiscount?: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
lastIntDate?: string;
accruedDays?: number;
accruedInterest?: number;
settlementAmount: number;
ytm?: number;
category: string;
subType?: string;
investmentGlAccountId: number;
investmentGlCode: string;
investmentGlName: string;
crGlAccountId: number;
crGlCode: string;
crGlName: string;
otherDetails?: string;
status: string;
}
export interface InvestmentGlAccount {
id: number;
glCode: string;
name: string;
}
/**
* Backend calls for the Investment masters (Bank / Party Master and Script Master). Results are also copied into the
* shared PARTY_MASTER / SCRIPT_MASTER lists so screens that still read those lists (Purchase Entry) offer the saved
* masters.
*/
@Injectable({ providedIn: 'root' })
export class InvestmentService {
private http = inject(HttpClient);
// -------------------- Bank / Party Master --------------------
getParties(): Observable<InvestmentPartyDto[]> {
return this.http.get<InvestmentPartyDto[]>('/investment/parties');
}
createParty(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/parties', body);
}
updateParty(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/parties/${id}`, body);
}
deleteParty(id: number): Observable<unknown> {
return this.http.delete(`/investment/parties/${id}`);
}
// -------------------- Script Master --------------------
getScripts(): Observable<InvestmentScriptDto[]> {
return this.http.get<InvestmentScriptDto[]>('/investment/scripts');
}
createScript(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/scripts', body);
}
updateScript(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/scripts/${id}`, body);
}
// -------------------- Purchase Entry --------------------
getPurchases(): Observable<InvestmentPurchaseDto[]> {
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
}
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
return this.http.get<InvestmentInterestScheduleDto>(
`/investment/purchases/${purchaseId}/interest-schedule${params}`
);
}
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
}
deletePurchase(id: number): Observable<unknown> {
return this.http.delete(`/investment/purchases/${id}`);
}
// -------------------- GL accounts --------------------
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
return this.http
.get<
Array<{ id: number; glCode: string; name: string; disabled: boolean }>
>('/glaccounts', { params: { type: '1', usage: '1', disabled: 'false' } })
.pipe(
map((list) =>
list
.map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
.sort((a, b) => a.glCode.localeCompare(b.glCode))
)
);
}
/** Loads both masters from the backend and refreshes the shared lists other screens read. */
syncSharedMasters(): Observable<{ parties: PartyMaster[]; scripts: ScriptMaster[] }> {
return forkJoin({ parties: this.getParties(), scripts: this.getScripts() }).pipe(
map(({ parties, scripts }) => ({ parties: parties.map(toPartyMaster), scripts: scripts.map(toScriptMaster) })),
tap(({ parties, scripts }) => {
PARTY_MASTER.splice(0, PARTY_MASTER.length, ...parties);
SCRIPT_MASTER.splice(0, SCRIPT_MASTER.length, ...scripts);
})
);
}
}
export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
return {
id: p.id,
partyCode: p.partyCode,
partyName: p.partyName,
partyType: 'BANK',
bondType: p.bondType,
address1: p.address1,
address2: p.address2,
address3: p.address3,
city: p.city,
pincode: p.pincode,
phone: p.phone,
fax: p.fax,
bankType: p.bankType,
glAccountId: p.glAccountId,
glCode: p.glCode,
glName: p.glName,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,
cpBankName: p.cpBankName,
cpBranchName: p.cpBranchName,
cpAccountNo: p.cpAccountNo,
cpIfsc: p.cpIfsc,
status: p.active ? 'ACTIVE' : 'INACTIVE'
};
}
export function toScriptMaster(s: InvestmentScriptDto): ScriptMaster {
return {
id: s.id,
scriptCode: s.scriptCode,
scriptName: s.scriptDesc,
instrumentType: s.bondType,
isin: s.isin ?? '',
faceValuePerUnit: s.faceValuePerUnit,
slrType: s.slrType,
glAccountId: s.glAccountId,
glCode: s.glCode,
glName: s.glName,
couponRate: s.intRate,
issueDate: s.issueDate,
maturityDate: s.maturityDate,
companyName: s.companyName,
address1: s.address1,
address2: s.address2,
address3: s.address3,
city: s.city,
pincode: s.pincode,
phone: s.phone,
fax: s.fax,
corpCode: s.corpCode,
active: s.active
};
}
/** A saved purchase as a row of the Holdings register. */
export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
return {
id: p.id,
dealNo: p.dealNo,
certNo: p.certNo,
instrumentType: p.bondType,
scriptCode: p.scriptCode,
scriptName: p.scriptDesc,
party: p.partyName,
partyCode: p.partyCode,
subType: p.subType ?? '',
faceValue: p.faceValue,
purchaseRate: p.purchaseRate,
units: p.balanceUnits,
purchaseDate: p.settlementDate,
maturityDate: p.maturityDate || p.dealDate,
interestRate: p.intRate ?? 0,
interestFreq: p.bondType === 'MUTUAL_FUND' ? 'CUMULATIVE' : 'HALF_YEARLY',
glCode: p.investmentGlCode,
glName: p.investmentGlName,
crToAc: p.crGlCode,
brokerCode: '',
status: 'ACTIVE',
category: p.category,
bookValue: p.bookValue,
settlementAmount: p.settlementAmount
};
}
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
export interface InvestmentInterestRowDto {
type: 'COUPON' | 'PROVISION';
intDate: string;
fromDate: string;
toDate: string;
days: number;
grossInterest: number;
alreadyBooked: number;
amount: number;
due: boolean;
}
export interface InvestmentInterestScheduleDto {
purchaseId: number;
dealNo: string;
certNo: string;
scriptCode: string;
scriptDesc: string;
faceValue: number;
unitValue: number;
balanceUnits: number;
purchaseRate: number;
bookValue: number;
intRate: number;
dealDate: string;
lastIntDate: string;
maturityDate: string;
purchasedAccruedInterest: number;
couponPerHalfYear: number;
asOnDate: string;
interestGlCode: string;
interestGlName: string;
rows: InvestmentInterestRowDto[];
}
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
export function apiErrorMessage(err: unknown): string {
const e = err as HttpErrorResponse;
const body = e?.error;
return body?.errors?.[0]?.defaultUserMessage || body?.defaultUserMessage || e?.message || 'Request failed.';
}