Compare commits

...
8 Commits
Author SHA1 Message Date
anshul 0404197c1b feat(investment): interest voucher posting UI, home pending alert and day-end reminder 2026-10-14 15:11:43 +05:30
anshul 8a6becff86 Merge origin/dev-latest into feature/investment 2026-10-14 10:49:37 +05:30
anshulandClaude Opus 5.5 c803e20628 feat(investment): settle purchases from Holdings; broken-period interest to settlement date
- Purchases are saved as Pending Settlement. Holdings show the status and
  a Settle button (from the settlement date), and the GL transaction id
  once settled.
- The transaction log shows "Not posted (pending settlement)" until a
  deal is settled, and dates the purchase on its settlement date.
- Broken-period interest is counted up to the settlement date.
- Accounting Preview title and the Holdings Settlement Date column
  reflect settlement-date posting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:28:19 +05:30
anshulandClaude Opus 5.5 6a04a92113 feat(investment): Daily Script Master, market rate on purchase, broken-period interest as expense
- 09 Daily Script Master now saves to and loads from the backend (sample
  rows removed), with Drop to correct a wrong rate.
- Purchase Entry takes the rate (NAV for a mutual fund) from the newest
  Daily Script Master rate on or before the deal date, shows which rate
  was used, warns when the rate entered is outside the day's low-high,
  and clears the rate when the script has none.
- Broken-period interest paid to the seller is debited to expense GL
  500606 instead of Interest Receivable (RBI UCB Investment Directions,
  para 152, confirmed by Jitendra Sir); Interest Entry labels it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:15 +05:30
anshulandClaude Opus 5.5 88f34f58e3 feat(investment): deal time, interest cycle and charges on Purchase Entry
As per Jitendra Sir's review:
- Deal Time is now a mandatory field next to Deal Date.
- New Interest Cycle section: the two coupon days, last and next coupon,
  next coupon amount and days to it.
- Stamp duty, brokerage and other charges sit on the main form. They are
  expenses on their own GLs (500601/500602/500603), so they are added to
  the settlement amount but no longer to the book value, and each shows
  as its own debit line in the accounting preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:54:33 +05:30
anshul 88c4242692 Merge origin/dev-latest into feature/investment 2026-10-12 10:35:53 +05:30
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
10 changed files with 2728 additions and 614 deletions
@@ -386,6 +386,23 @@
</div>
</div>
}
<!-- Investment interest vouchers that are due but not posted. A reminder only: unlike the interest
above it never stops the close (Jitendra Sir); the voucher can still be posted afterwards. -->
@if (preview?.investmentInterestPending?.length) {
<div class="bd-warn">
<mat-icon>notifications_active</mat-icon>
<div>
<strong>Reminder: investment interest vouchers are pending.</strong>
@for (t of preview.investmentInterestPending; track t.purchaseId) {
<div>
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.intDate }} · ₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }}
</div>
}
Post them under Investment → Govt Security / Bond → 03 Interest Entry. You can still close the day.
</div>
</div>
}
@if (preview?.savingsInterestPending?.length) {
<div class="bd-warn">
<mat-icon>warning</mat-icon>
+17
View File
@@ -47,6 +47,23 @@
</div>
</div>
<!-- Pending investment interest vouchers (coupon / provision due now or within two days) -->
@if (pendingInvestmentInterest.length) {
<a class="investment-interest-alert" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }">
<span class="iia-icon">🔔</span>
<span class="iia-text">
<strong>{{ pendingInvestmentInterest.length }} investment interest voucher(s) pending</strong>
@for (t of pendingInvestmentInterest; track t.purchaseId) {
<span class="iia-item">
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</span>
}
</span>
<span class="iia-action">Open Interest Entry →</span>
</a>
}
<!-- Quick Action Cards -->
<div class="quick-actions">
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
+36
View File
@@ -424,3 +424,39 @@ $accent: #0ea5e9;
grid-template-columns: 1fr;
}
}
.investment-interest-alert {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 12px 0;
padding: 12px 16px;
border: 1px solid #f5c26b;
border-left: 4px solid #e08e0b;
border-radius: 8px;
background: #fff8e6;
color: #5c3d00;
text-decoration: none;
.iia-icon {
font-size: 20px;
line-height: 1.2;
}
.iia-text {
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
}
.iia-item {
font-size: 13px;
}
.iia-action {
align-self: center;
font-weight: 600;
white-space: nowrap;
}
}
+14
View File
@@ -26,6 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service'
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
import { SettingsService } from 'app/settings/settings.service';
import { DscService } from 'app/users/dsc/dsc.service';
import { InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service';
/** Custom Components */
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
@@ -109,6 +110,7 @@ export class HomeComponent implements OnInit, AfterViewInit {
this.tenant = this.tenantIdentifier();
this.setFilteredActivities();
this.loadDscStatus();
this.loadPendingInvestmentInterest();
if (!this.authenticationService.hasDialogBeenShown()) {
this.dialog.open(WarningDialogComponent, {
disableClose: false,
@@ -118,6 +120,18 @@ export class HomeComponent implements OnInit, AfterViewInit {
}
}
/** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
private readonly investmentService = inject(InvestmentService);
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
private loadPendingInvestmentInterest(): void {
this.investmentService.getDueInterest().subscribe({
next: (due) => (this.pendingInvestmentInterest = due ?? []),
error: () => (this.pendingInvestmentInterest = [])
});
}
/**
* Surface a small "DSC verified" badge in the welcome banner when the
* signed-in user has at least one ACTIVE DSC certificate. Silent on
@@ -137,11 +137,12 @@
<th class="num">Units</th>
<th class="num">Face Value</th>
<th class="num">Rate</th>
<th>Purchase Date</th>
<th>Settlement Date</th>
<th>Maturity Date</th>
<th class="num col-in">Accrued Interest</th>
<th class="num col-net">Maturity Value</th>
<th>Status</th>
<th>Settlement</th>
</tr>
</thead>
<tbody>
@@ -160,12 +161,23 @@
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td>
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
<td>
<span [class]="statusClass(h)">{{ h.status }}</span>
<span [class]="statusClass(h)">{{
h.status === 'PENDING_SETTLEMENT' ? 'PENDING SETTLEMENT' : h.status
}}</span>
</td>
<td>
@if (h.status === 'PENDING_SETTLEMENT') {
<button type="button" class="pg-btn pg-btn--primary" (click)="settleDeal(h)">
Settle ({{ h.settlementDate }})
</button>
} @else if (h.glTransactionId) {
<span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span>
}
</td>
</tr>
} @empty {
<tr>
<td colspan="11" class="pg-empty">No {{ instrumentLabel(bondType) }} holdings yet.</td>
<td colspan="12" class="pg-empty">No {{ instrumentLabel(bondType) }} holdings yet.</td>
</tr>
}
</tbody>
@@ -176,8 +188,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 +201,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 +210,50 @@
}
</select>
</label>
<label class="inv-form-field">
<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>* Cert No</span><input type="text" [(ngModel)]="purchaseForm.certNo"
><span>* Deal Date</span
><mifosx-date-ddmmyyyy
[(ngModel)]="purchaseForm.dealDate"
(ngModelChange)="onDealDateChange()"
></mifosx-date-ddmmyyyy
></label>
<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>Settlement Type</span>
<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 +265,258 @@
</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>
<label class="inv-form-field"
><span>Stamp Duty (₹)</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
/></label>
<label class="inv-form-field"
><span>Brokerage (₹)</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
/></label>
<label class="inv-form-field"
><span>Other Charges (₹)</span><input type="number" [(ngModel)]="purchaseForm.otherCharges" min="0"
/></label>
</div>
@if (purchaseMarketRate) {
<p class="pg-sub" style="padding: 0 16px 8px">
Market rate (Daily Script Master) as on {{ purchaseMarketRate.rateDate }}: Close
{{ purchaseMarketRate.closeRate | number: '1.2-4' }}
@if (purchaseMarketRate.lowRate && purchaseMarketRate.highRate) {
(Low {{ purchaseMarketRate.lowRate | number: '1.2-4' }} – High
{{ purchaseMarketRate.highRate | number: '1.2-4' }})
}
@if (purchaseMarketRate.rateDate !== purchaseForm.dealDate) {
— no rate entered for the deal date, the latest earlier rate is used.
}
</p>
@if (purchaseRateOutOfRange) {
<p class="pg-sub" style="padding: 0 16px 8px; color: #b42318; font-weight: 600">
Warning: the rate entered is outside the day's market range.
</p>
}
} @else if (purchaseMarketRate === null) {
<p class="pg-sub" style="padding: 0 16px 8px; color: #b42318">
No rate in the Daily Script Master for this script — enter the day's rate there first.
</p>
}
<!-- 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>Charges (expense)</span
><input type="text" [value]="'₹' + (purchaseTotalCharges | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Total Settlement Amount</span
><input type="text" [value]="'₹' + (purchaseSettlementAmount | number: '1.2-2')" readonly
/></label>
</div>
@if (!isMutualFund && purchaseCouponDates) {
<div class="pos-section-label">Interest Cycle</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Interest Dates (half-yearly)</span><input type="text" [value]="purchaseCouponDates" readonly
/></label>
<label class="inv-form-field"
><span>Last Coupon</span><input type="text" [value]="purchaseForm.lastIntDt || '—'" readonly
/></label>
<label class="inv-form-field"
><span>Next Coupon</span><input type="text" [value]="purchaseNextCouponDate || '—'" readonly
/></label>
<label class="inv-form-field"
><span>Next Coupon Amount</span
><input type="text" [value]="'₹' + (purchaseNextCouponAmount | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Days to Next Coupon</span><input type="text" [value]="purchaseDaysToNextCoupon" readonly
/></label>
</div>
}
<!-- 3. Accounting preview -->
<div class="pos-section-label">Accounting Preview (posted on Settle, dated the settlement date)</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>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 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>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 +534,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 +618,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 +625,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 +689,47 @@
</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>
@if (intDueTasks.length) {
<div class="pos-section-label">🔔 Interest entries due ({{ intDueTasks.length }})</div>
<table class="pg-table">
<thead>
<tr>
<th>Deal No</th>
<th>Script</th>
<th>Type</th>
<th>Int Date</th>
<th class="num">Amount</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@for (t of intDueTasks; track t.purchaseId) {
<tr>
<td>
<span class="pg-chip">{{ t.dealNo }}</span>
</td>
<td>{{ t.scriptCode }} — {{ t.scriptDesc }}</td>
<td>{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }}</td>
<td>{{ t.intDate }}</td>
<td class="num">₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }}</td>
<td>{{ t.overdue ? 'Overdue — post now' : t.postable ? 'Due today' : 'Due on ' + t.intDate }}</td>
<td>
<button type="button" class="pg-btn pg-btn--ghost" (click)="openDueTask(t)">Open</button>
</td>
</tr>
}
</tbody>
</table>
}
<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 +738,182 @@
</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>Broken Period Interest Paid (expense 500606)</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 ({{ intVisibleRows.length }} of {{ 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. Post each line with Generate Voucher on or after its date, in date order — the JV voucher is
passed as it is raised.
</p>
<div class="inv-form-grid">
<label class="inv-form-field">
<span>Coupon received into (Bank)</span>
<select [(ngModel)]="intBankGlCode">
@for (g of settlementBankOptions; track g.glCode) {
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>Schedule</span>
<button type="button" class="pg-btn pg-btn--ghost" (click)="intShowFullSchedule = !intShowFullSchedule">
{{ intShowFullSchedule ? 'Show up to next year' : 'Show full schedule to maturity' }}
</button>
</label>
</div>
<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>
<th>Voucher</th>
</tr>
</thead>
<tbody>
@for (r of intVisibleRows; track r.type + 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>
<td style="white-space: nowrap">
@if (r.posted) {
<span class="pg-chip" [title]="'GL ' + r.glTransactionId">Posted · {{ r.voucherNo }}</span>
} @else if (r.postable) {
<button
type="button"
class="pg-btn pg-btn--primary"
[disabled]="intPosting"
(click)="generateVoucher(r)"
>
Generate Voucher
</button>
} @else if (r.dueSoon) {
<span class="pg-chip">🔔 Due {{ r.intDate }}</span>
}
</td>
</tr>
} @empty {
<tr>
<td colspan="11" 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 }} · Interest receivable GL:
{{ interestReceivableGl.glCode }}.
</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>
}
@@ -1100,7 +1362,7 @@
<td>{{ t.certNo }}</td>
<td>{{ t.txnType }}</td>
<td>{{ t.particulars }}</td>
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : t.glName || '—' }}</td>
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
@@ -6,16 +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 { ActivatedRoute } from '@angular/router';
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 +27,26 @@ 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,
BROKEN_PERIOD_INTEREST_GL,
STAMP_DUTY_GL,
BROKERAGE_GL,
OTHER_CHARGES_GL,
SETTLEMENT_BANK_GL_OPTIONS,
periodBetween,
parseDDMMYYYY,
days360,
lastHalfYearlyCouponDate,
nextHalfYearlyCouponDate,
couponDatesLabel,
InvestmentHolding,
InvestmentTxn,
InstrumentType,
InterestFreq,
INSTRUMENT_LABELS,
accruedInterest,
accruedInterestBetween,
canAccrueInterest,
daysToMaturity,
maturityAmount,
addPeriod,
@@ -49,6 +55,16 @@ import {
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
import {
InvestmentDailyRateDto,
InvestmentInterestDueDto,
InvestmentInterestRowDto,
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
InvestmentService,
apiErrorMessage,
toHolding
} from '../investment.service';
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
type BondTab =
@@ -129,6 +145,14 @@ interface BondPurchaseForm {
holdingDays: number | null;
stampDuty: number | null;
stampDutyAc: string;
otherCharges: number | null;
}
interface AccountingPreviewLine {
side: 'Dr' | 'Cr';
glCode: string;
glName: string;
amount: number;
}
interface BondSellForm {
@@ -182,23 +206,75 @@ interface BondSellForm {
DateDdmmyyyyPickerComponent
]
})
export class BondInvestmentComponent {
export class BondInvestmentComponent implements OnInit {
private readonly investmentService = inject(InvestmentService);
private readonly route = inject(ActivatedRoute);
/** Script and party dropdowns offer the masters saved in the backend. */
ngOnInit(): void {
// The home-screen interest alert links here with ?tab=interest.
if (this.route.snapshot.queryParamMap.get('tab') === 'interest') {
this.activeTab = 'interest';
}
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();
this.loadDueInterest();
}
/** 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)}`)
});
}
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
const posted = !!d.glTransactionId;
return {
certNo: d.certNo,
instrumentType: d.bondType,
txnType: 'PURCHASE',
voucherNo: `INV/PUR/${d.dealNo}`,
txnDate: d.settlementDate,
amount: d.settlementAmount,
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
glCode: posted ? d.investmentGlCode : '',
glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
};
}
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 +300,30 @@ 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;
/** Interest lines of all holdings due now or within two days — what Jitendra Sir asked to be reminded of. */
intDueTasks: InvestmentInterestDueDto[] = [];
/** By default the schedule shows only lines up to the end of next year (and anything already posted). */
intShowFullSchedule = false;
/** Bank a coupon is received into; defaults to the purchase's paying bank. */
intBankGlCode = '';
intPosting = false;
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
// Write-off
writeoffCertNo = '';
@@ -305,20 +388,38 @@ 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 = '';
this.purchaseMarketRate = undefined;
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 +427,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: '',
@@ -374,7 +475,8 @@ export class BondInvestmentComponent {
nav: null,
holdingDays: null,
stampDuty: null,
stampDutyAc: ''
stampDutyAc: '',
otherCharges: null
};
}
@@ -444,14 +546,41 @@ export class BondInvestmentComponent {
return this.isMutualFund ? this.purchaseForm.nav || 0 : 100;
}
/** Book Value — what the investment is carried at, i.e. amount actually paid. */
/** Book Value — the price paid for the security. Charges are expenses and stay out of it (Jitendra Sir). */
get purchaseBookValue(): number {
const f = this.purchaseForm;
if (this.isMutualFund) {
return this.purchaseBaseAmount + (f.stampDuty || 0) + (f.brokerage || 0);
return this.purchaseBaseAmount;
}
const rate = f.purchaseRate || 100;
return Math.round((this.purchaseBaseAmount * rate) / 100) + (f.brokerage || 0);
return Math.round((this.purchaseBaseAmount * rate) / 100);
}
/** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */
get purchaseTotalCharges(): number {
const f = this.purchaseForm;
return (f.stampDuty || 0) + (f.brokerage || 0) + (f.otherCharges || 0);
}
// Interest cycle of the security being bought: its two coupon days and the next coupon after the deal.
get purchaseCouponDates(): string {
return this.purchaseMaturityDate ? couponDatesLabel(this.purchaseMaturityDate) : '';
}
get purchaseNextCouponDate(): string {
const f = this.purchaseForm;
return this.purchaseMaturityDate && f.dealDate
? nextHalfYearlyCouponDate(this.purchaseMaturityDate, f.dealDate)
: '';
}
get purchaseNextCouponAmount(): number {
const f = this.purchaseForm;
return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 200) : 0;
}
get purchaseDaysToNextCoupon(): number {
return this.purchaseNextCouponDate ? days360(this.purchaseForm.dealDate, this.purchaseNextCouponDate) : 0;
}
get purchaseYtm(): number {
@@ -462,19 +591,47 @@ export class BondInvestmentComponent {
return approxYtm(f.intRate, f.purchaseRate || 100, yearsBetween(f.issueDate, this.purchaseMaturityDate));
}
/** Accrued Interest at purchase — interest earned by the seller since the last coupon date. */
/** Broken-period interest owed to the seller from the last coupon date up to the settlement date, 30/360. */
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) * this.purchaseAccruedDays) / 360);
}
/** Settlement Amt of Purchase — book value plus accrued interest payable to the seller. */
get purchaseAccruedDays(): number {
const f = this.purchaseForm;
// Up to the settlement date, when the bond leaves the seller (RBI para 83; Jitendra Sir).
return f.lastIntDt && this.purchaseSettlementDate ? days360(f.lastIntDt, this.purchaseSettlementDate) : 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();
this.loadMarketRate();
}
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, accrued interest payable to the seller and the charges. */
get purchaseSettlementAmount(): number {
return this.purchaseBookValue + this.purchaseAccruedInterest;
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
}
get purchaseMaturityAmount(): number {
@@ -504,69 +661,264 @@ 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();
this.loadMarketRate();
}
/**
* Market rate of the script for the deal date from the Daily Script Master (newest rate on or before it). It fills
* the purchase rate (NAV for a mutual fund); the dealer may still change it. undefined = not looked up yet,
* null = no rate entered for the script.
*/
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
private loadMarketRate(): void {
const s = this.purchaseScript;
const f = this.purchaseForm;
if (!s?.id || !f.dealDate) {
this.purchaseMarketRate = undefined;
return;
}
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
next: (rate) => {
this.purchaseMarketRate = rate;
// No market rate: clear the field rather than leave another script's rate in it.
if (this.isMutualFund) {
f.nav = rate ? rate.closeRate : null;
} else {
f.purchaseRate = rate ? rate.closeRate : null;
}
},
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
});
}
/** True when the rate entered is outside the day's low–high in the Daily Script Master. */
get purchaseRateOutOfRange(): boolean {
const m = this.purchaseMarketRate;
const f = this.purchaseForm;
const rate = this.isMutualFund ? f.nav : f.purchaseRate;
if (!m || rate == null) {
return false;
}
return (m.lowRate != null && rate < m.lowRate) || (m.highRate != null && rate > m.highRate);
}
/** 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 }];
// Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir).
if (accrued > 0) {
lines.push({
side: 'Dr',
glCode: BROKEN_PERIOD_INTEREST_GL.glCode,
glName: BROKEN_PERIOD_INTEREST_GL.glName,
amount: accrued
});
}
const f = this.purchaseForm;
for (const [
gl,
amount
] of [
[
STAMP_DUTY_GL,
f.stampDuty
],
[
BROKERAGE_GL,
f.brokerage
],
[
OTHER_CHARGES_GL,
f.otherCharges
]
] as const) {
if (amount && amount > 0) {
lines.push({ side: 'Dr', glCode: gl.glCode, glName: gl.glName, amount });
}
}
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 (!f.dealTime) missing.push('Deal Time');
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 }
: { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
stampDuty: f.stampDuty ?? null,
brokerage: f.brokerage ?? null,
otherCharges: f.otherCharges ?? 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 — pending settlement. Settle it from Holdings on or after the settlement date.`
);
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 +928,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 +940,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 +953,44 @@ 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.');
/** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */
settleDeal(h: InvestmentHolding): void {
if (!h.id) {
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.settlePurchase(h.id).subscribe({
next: (res) => {
this.toast(
res.rollbackTransaction
? `Settlement of deal ${h.dealNo} sent for approval (Checker Inbox).`
: `Deal ${h.dealNo} settled and posted to GL (transaction ${res.transactionId}).`
);
this.loadPurchases();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
dropPurchase(): void {
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.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 +1034,111 @@ 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.intBankGlCode = schedule.bankGlCode;
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')}.`
);
}
loadDueInterest(): void {
this.investmentService.getDueInterest().subscribe({
next: (tasks) => (this.intDueTasks = tasks),
error: () => (this.intDueTasks = [])
});
}
/** Opens a due task's holding in the schedule below. */
openDueTask(t: InvestmentInterestDueDto): void {
this.intType = '';
this.intScript = '';
this.intPurchaseId = t.purchaseId;
this.retrieveInterestSchedule();
}
/** Lines shown: up to 31 December of next year, plus anything already posted — unless the full schedule is asked for. */
get intVisibleRows(): InvestmentInterestRowDto[] {
const rows = this.intSchedule?.rows ?? [];
if (this.intShowFullSchedule) {
return rows;
}
const upTo = new Date(new Date().getFullYear() + 1, 11, 31);
return rows.filter((r) => r.posted || parseDDMMYYYY(r.intDate) <= upTo);
}
/** Posts one interest line with its JV voucher (passed as it is raised), then refreshes the schedule and tasks. */
generateVoucher(r: InvestmentInterestRowDto): void {
const sc = this.intSchedule;
if (!sc || this.intPosting) {
return;
}
const bankGlAccountId =
r.type === 'COUPON' ? (this.glIdByCode.get(this.intBankGlCode) ?? sc.bankGlAccountId) : undefined;
const body = {
lineType: r.type,
intDate: r.intDate,
...(bankGlAccountId ? { bankGlAccountId } : {}),
dateFormat: 'dd-MM-yyyy',
locale: 'en'
};
this.intPosting = true;
this.investmentService.postInterest(sc.purchaseId, body).subscribe({
next: (res) => {
this.intPosting = false;
this.toast(
`${r.type === 'COUPON' ? 'Coupon' : 'Provision'} of ${r.intDate} posted — voucher ${res.resourceIdentifier ?? ''}.`
);
this.retrieveInterestSchedule();
this.loadDueInterest();
},
error: (err) => {
this.intPosting = false;
this.toast(apiErrorMessage(err));
}
});
}
/** 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 --------------------
+477 -28
View File
@@ -17,7 +17,7 @@
*/
export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE';
export type HoldingStatus = 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF';
export type HoldingStatus = 'PENDING_SETTLEMENT' | 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF';
export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE';
export type TxnType =
| 'PURCHASE'
@@ -123,6 +123,20 @@ 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;
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
settlementDate?: string;
/** Journal entry transaction posted when the deal was settled. */
glTransactionId?: string;
}
export interface InvestmentTxn {
@@ -248,6 +262,80 @@ 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) : '';
}
/** First half-yearly coupon date after `onStr` (coupons fall on the maturity day/month and six months
* either side), never later than maturity. */
export function nextHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
const last = lastHalfYearlyCouponDate(maturityStr, onStr);
const mat = parseDDMMYYYY(maturityStr);
if (!last || isNaN(mat.getTime())) {
return '';
}
const l = parseDDMMYYYY(last);
const year = l.getFullYear() + (l.getMonth() >= 6 ? 1 : 0);
const month = (l.getMonth() + 6) % 12;
const next = new Date(year, month, Math.min(mat.getDate(), new Date(year, month + 1, 0).getDate()));
return formatDDMMYYYY(next > mat ? mat : next);
}
/** The two coupon days of a half-yearly security, e.g. "14 Feb / 14 Aug". */
export function couponDatesLabel(maturityStr: string): string {
const mat = parseDDMMYYYY(maturityStr);
if (isNaN(mat.getTime())) {
return '';
}
const label = (month: number): string => {
const day = Math.min(mat.getDate(), new Date(2001, month + 1, 0).getDate());
return `${day} ${new Date(2001, month, 1).toLocaleString('en-US', { month: 'short' })}`;
};
const first = mat.getMonth() % 6;
return `${label(first)} / ${label(first + 6)}`;
}
export function formatDDMMYYYY(d: Date): string {
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
}
@@ -1404,99 +1492,331 @@ 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'
};
/** Broken-period interest paid to the seller at purchase — an expense, not part of cost (RBI UCB Investment
* Directions para 152). */
export const BROKEN_PERIOD_INTEREST_GL: InvestmentGlOption = {
glCode: '500606',
glName: 'BROKEN PERIOD INTEREST PAID ON INVESTMENTS'
};
/** Charges on an investment deal — expenses on their own GLs, not part of the book value (Jitendra Sir). */
export const STAMP_DUTY_GL: InvestmentGlOption = { glCode: '500601', glName: 'STAMP DUTY ON INVESTMENTS' };
export const BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE ON INVESTMENTS' };
export const OTHER_CHARGES_GL: InvestmentGlOption = { glCode: '500603', glName: 'OTHER CHARGES ON INVESTMENTS' };
/** 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 +1881,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 {
@@ -1725,13 +2172,15 @@ export const LIEN_REASON_MASTER: LienReason[] = [
/** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported
* every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */
export interface DailyScriptRate {
/** Backend id once saved. */
id?: number;
rateDate: string;
scriptCode: string;
scriptName: string;
openRate: number;
openRate: number | null;
closeRate: number;
highRate: number;
lowRate: number;
highRate: number | null;
lowRate: number | null;
}
export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [
@@ -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) !== 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>
@@ -386,10 +545,11 @@
<th class="num">High</th>
<th class="num">Low</th>
<th class="num">Close</th>
<th></th>
</tr>
</thead>
<tbody>
@for (r of dailyScriptRates; track r.rateDate + r.scriptCode) {
@for (r of dailyScriptRates; track r.id ?? r.rateDate + r.scriptCode) {
<tr>
<td>{{ r.rateDate }}</td>
<td>
@@ -400,6 +560,13 @@
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
<td>
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropDailyRate(r)">Drop</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="pg-empty">No rates entered yet.</td>
</tr>
}
</tbody>
@@ -6,20 +6,17 @@
* 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,
SGL_BANK_MASTER,
SCRIPT_MARKET_DETAIL,
DAILY_SCRIPT_RATES,
LIEN_REASON_MASTER,
ScriptMaster,
BrokerMaster,
@@ -31,9 +28,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, toDailyScriptRate } from '../investment.service';
type MasterTab =
| 'party'
@@ -65,18 +67,19 @@ 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];
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
dailyScriptRates: DailyScriptRate[] = [...DAILY_SCRIPT_RATES];
/** Loaded from the backend (Daily Script Master) in ngOnInit. */
dailyScriptRates: DailyScriptRate[] = [];
lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
readonly instrumentTypes: InstrumentType[] = [
@@ -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,217 @@ export class InvestmentMastersComponent {
newSglBank: Partial<SglBankMaster> = {};
newDailyRate: Partial<DailyScriptRate> = {};
ngOnInit(): void {
this.loadDailyRates();
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 {
@@ -248,24 +436,56 @@ export class InvestmentMastersComponent {
this.toast('SGL Bank master added.');
}
private loadDailyRates(): void {
this.investmentService.getDailyRates().subscribe({
next: (rows) => (this.dailyScriptRates = rows.map(toDailyScriptRate)),
error: (err) => this.toast(`Could not load daily script rates: ${apiErrorMessage(err)}`)
});
}
addDailyRate(): void {
if (!this.newDailyRate.scriptCode || !this.newDailyRate.rateDate || this.newDailyRate.closeRate == null) {
const r = this.newDailyRate;
if (!r.scriptCode || !r.rateDate || r.closeRate == null) {
this.toast('Enter Rate Date, Script Code and Close Rate.');
return;
}
const script = this.scripts.find((s) => s.scriptCode === this.newDailyRate.scriptCode);
this.dailyScriptRates = [
{
scriptName: script?.scriptName || '',
openRate: this.newDailyRate.closeRate,
highRate: this.newDailyRate.closeRate,
lowRate: this.newDailyRate.closeRate,
...this.newDailyRate
} as DailyScriptRate,
...this.dailyScriptRates
];
this.newDailyRate = {};
this.toast('Daily script rate added.');
const script = this.scripts.find((s) => s.scriptCode === r.scriptCode);
if (!script?.id) {
this.toast(`Script ${r.scriptCode} is not saved in Script Master.`);
return;
}
const body = {
scriptId: script.id,
rateDate: r.rateDate,
closeRate: r.closeRate,
...(r.openRate != null ? { openRate: r.openRate } : {}),
...(r.highRate != null ? { highRate: r.highRate } : {}),
...(r.lowRate != null ? { lowRate: r.lowRate } : {}),
dateFormat: 'dd-MM-yyyy',
locale: 'en'
};
this.investmentService.createDailyRate(body).subscribe({
next: () => {
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} saved.`);
this.newDailyRate = {};
this.loadDailyRates();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
/** A wrong rate is corrected by dropping it and entering it again. */
dropDailyRate(r: DailyScriptRate): void {
if (!r.id) {
return;
}
this.investmentService.deleteDailyRate(r.id).subscribe({
next: () => {
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} dropped.`);
this.loadDailyRates();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
// -------------------- Export to Excel --------------------
@@ -279,46 +499,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(
+448
View File
@@ -0,0 +1,448 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import {
DailyScriptRate,
HoldingStatus,
InstrumentType,
InvestmentHolding,
PARTY_MASTER,
PartyMaster,
SCRIPT_MASTER,
ScriptMaster,
SlrType
} from './investment-data';
/** Investment party as returned by /investment/parties. */
export interface InvestmentPartyDto {
id: number;
partyCode: string;
bondType: InstrumentType;
partyName: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
bankType?: string;
glAccountId: number;
glCode: string;
glName: string;
rbiCode?: string;
limit?: number;
leiNo?: string;
cpBankName?: string;
cpBranchName?: string;
cpAccountNo?: string;
cpIfsc?: string;
active: boolean;
}
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
export interface InvestmentScriptDto {
id: number;
scriptCode: string;
bondType: InstrumentType;
slrType?: SlrType;
scriptDesc: string;
glAccountId: number;
glCode: string;
glName: string;
isin?: string;
faceValuePerUnit: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
companyName?: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
corpCode?: string;
active: boolean;
}
/** Purchase entry (holding) as returned by /investment/purchases (dates are DD-MM-YYYY). */
export interface InvestmentPurchaseDto {
id: number;
dealNo: string;
certNo: string;
bondType: InstrumentType;
scriptId: number;
scriptCode: string;
scriptDesc: string;
isin?: string;
partyId: number;
partyCode: string;
partyName: string;
dealDate: string;
dealTime?: string;
settlementType: string;
settlementDate: string;
faceValue: number;
purchaseRate: number;
units: number;
balanceUnits: number;
brokerage?: number;
stampDuty?: number;
otherCharges?: number;
bookValue: number;
premiumDiscount?: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
lastIntDate?: string;
accruedDays?: number;
accruedInterest?: number;
settlementAmount: number;
ytm?: number;
category: string;
subType?: string;
investmentGlAccountId: number;
investmentGlCode: string;
investmentGlName: string;
crGlAccountId: number;
crGlCode: string;
crGlName: string;
otherDetails?: string;
status: string;
glTransactionId?: string;
settledOnDate?: string;
}
export interface InvestmentGlAccount {
id: number;
glCode: string;
name: string;
}
/**
* Backend calls for the Investment masters (Bank / Party Master and Script Master). Results are also copied into the
* shared PARTY_MASTER / SCRIPT_MASTER lists so screens that still read those lists (Purchase Entry) offer the saved
* masters.
*/
@Injectable({ providedIn: 'root' })
export class InvestmentService {
private http = inject(HttpClient);
// -------------------- Bank / Party Master --------------------
getParties(): Observable<InvestmentPartyDto[]> {
return this.http.get<InvestmentPartyDto[]>('/investment/parties');
}
createParty(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/parties', body);
}
updateParty(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/parties/${id}`, body);
}
deleteParty(id: number): Observable<unknown> {
return this.http.delete(`/investment/parties/${id}`);
}
// -------------------- Script Master --------------------
getScripts(): Observable<InvestmentScriptDto[]> {
return this.http.get<InvestmentScriptDto[]>('/investment/scripts');
}
createScript(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/scripts', body);
}
updateScript(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/scripts/${id}`, body);
}
// -------------------- Purchase Entry --------------------
getPurchases(): Observable<InvestmentPurchaseDto[]> {
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
}
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
/** Interest lines of all holdings that are due now or within the next few days. */
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
}
/** Posts one interest line (coupon / provision) with its JV voucher; the voucher number comes back as resourceIdentifier. */
postInterest(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string; transactionId?: string }> {
return this.http.post<{ resourceIdentifier?: string; transactionId?: string }>(
`/investment/purchases/${purchaseId}/interest-postings`,
body
);
}
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
return this.http.get<InvestmentInterestScheduleDto>(
`/investment/purchases/${purchaseId}/interest-schedule${params}`
);
}
// -------------------- Daily Script Master --------------------
getDailyRates(scriptId?: number): Observable<InvestmentDailyRateDto[]> {
const params = scriptId ? `?scriptId=${scriptId}` : '';
return this.http.get<InvestmentDailyRateDto[]>(`/investment/daily-rates${params}`);
}
/** Newest Daily Script Master rate of a script on or before a date; null when none has been entered. */
getLatestDailyRate(scriptId: number, onDate: string): Observable<InvestmentDailyRateDto | null> {
return this.http.get<InvestmentDailyRateDto | null>(
`/investment/daily-rates/latest?scriptId=${scriptId}&onDate=${encodeURIComponent(onDate)}`
);
}
createDailyRate(body: object): Observable<{ resourceId: number }> {
return this.http.post<{ resourceId: number }>('/investment/daily-rates', body);
}
deleteDailyRate(rateId: number): Observable<unknown> {
return this.http.delete(`/investment/daily-rates/${rateId}`);
}
/** Settles a deal: posts it to GL dated its settlement date. Under maker-checker the response only says it awaits approval. */
settlePurchase(purchaseId: number): Observable<{ transactionId?: string; rollbackTransaction?: boolean }> {
return this.http.post<{ transactionId?: string; rollbackTransaction?: boolean }>(
`/investment/purchases/${purchaseId}/settle`,
{}
);
}
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
}
deletePurchase(id: number): Observable<unknown> {
return this.http.delete(`/investment/purchases/${id}`);
}
// -------------------- GL accounts --------------------
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
return this.http
.get<
Array<{ id: number; glCode: string; name: string; disabled: boolean }>
>('/glaccounts', { params: { type: '1', usage: '1', disabled: 'false' } })
.pipe(
map((list) =>
list
.map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
.sort((a, b) => a.glCode.localeCompare(b.glCode))
)
);
}
/** Loads both masters from the backend and refreshes the shared lists other screens read. */
syncSharedMasters(): Observable<{ parties: PartyMaster[]; scripts: ScriptMaster[] }> {
return forkJoin({ parties: this.getParties(), scripts: this.getScripts() }).pipe(
map(({ parties, scripts }) => ({ parties: parties.map(toPartyMaster), scripts: scripts.map(toScriptMaster) })),
tap(({ parties, scripts }) => {
PARTY_MASTER.splice(0, PARTY_MASTER.length, ...parties);
SCRIPT_MASTER.splice(0, SCRIPT_MASTER.length, ...scripts);
})
);
}
}
export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
return {
id: p.id,
partyCode: p.partyCode,
partyName: p.partyName,
partyType: 'BANK',
bondType: p.bondType,
address1: p.address1,
address2: p.address2,
address3: p.address3,
city: p.city,
pincode: p.pincode,
phone: p.phone,
fax: p.fax,
bankType: p.bankType,
glAccountId: p.glAccountId,
glCode: p.glCode,
glName: p.glName,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,
cpBankName: p.cpBankName,
cpBranchName: p.cpBranchName,
cpAccountNo: p.cpAccountNo,
cpIfsc: p.cpIfsc,
status: p.active ? 'ACTIVE' : 'INACTIVE'
};
}
export function toScriptMaster(s: InvestmentScriptDto): ScriptMaster {
return {
id: s.id,
scriptCode: s.scriptCode,
scriptName: s.scriptDesc,
instrumentType: s.bondType,
isin: s.isin ?? '',
faceValuePerUnit: s.faceValuePerUnit,
slrType: s.slrType,
glAccountId: s.glAccountId,
glCode: s.glCode,
glName: s.glName,
couponRate: s.intRate,
issueDate: s.issueDate,
maturityDate: s.maturityDate,
companyName: s.companyName,
address1: s.address1,
address2: s.address2,
address3: s.address3,
city: s.city,
pincode: s.pincode,
phone: s.phone,
fax: s.fax,
corpCode: s.corpCode,
active: s.active
};
}
/** A saved purchase as a row of the Holdings register. */
export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
return {
id: p.id,
dealNo: p.dealNo,
certNo: p.certNo,
instrumentType: p.bondType,
scriptCode: p.scriptCode,
scriptName: p.scriptDesc,
party: p.partyName,
partyCode: p.partyCode,
subType: p.subType ?? '',
faceValue: p.faceValue,
purchaseRate: p.purchaseRate,
units: p.balanceUnits,
purchaseDate: p.settlementDate,
maturityDate: p.maturityDate || p.dealDate,
interestRate: p.intRate ?? 0,
interestFreq: p.bondType === 'MUTUAL_FUND' ? 'CUMULATIVE' : 'HALF_YEARLY',
glCode: p.investmentGlCode,
glName: p.investmentGlName,
crToAc: p.crGlCode,
brokerCode: '',
status: p.status as HoldingStatus,
settlementDate: p.settlementDate,
glTransactionId: p.glTransactionId,
category: p.category,
bookValue: p.bookValue,
settlementAmount: p.settlementAmount
};
}
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
export interface InvestmentInterestRowDto {
type: 'COUPON' | 'PROVISION';
intDate: string;
fromDate: string;
toDate: string;
days: number;
grossInterest: number;
alreadyBooked: number;
amount: number;
due: boolean;
/** Posted to GL: its JV voucher and GL transaction. */
posted: boolean;
voucherNo?: string;
glTransactionId?: string;
postedOnDate?: string;
/** The next line to post and its date has come. */
postable: boolean;
/** Unposted and due within the reminder window (or past). */
dueSoon: boolean;
}
/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
export interface InvestmentInterestDueDto {
purchaseId: number;
dealNo: string;
certNo: string;
scriptCode: string;
scriptDesc: string;
lineType: 'COUPON' | 'PROVISION';
intDate: string;
grossInterest: number;
amount: number;
postable: boolean;
overdue: boolean;
}
export interface InvestmentInterestScheduleDto {
purchaseId: number;
dealNo: string;
certNo: string;
scriptCode: string;
scriptDesc: string;
faceValue: number;
unitValue: number;
balanceUnits: number;
purchaseRate: number;
bookValue: number;
intRate: number;
dealDate: string;
lastIntDate: string;
maturityDate: string;
purchasedAccruedInterest: number;
couponPerHalfYear: number;
asOnDate: string;
interestGlCode: string;
interestGlName: string;
bankGlAccountId: number;
bankGlCode: string;
bankGlName: string;
status: string;
rows: InvestmentInterestRowDto[];
}
/** A Daily Script Master row (see InvestmentDailyRateData in the backend). */
export interface InvestmentDailyRateDto {
id: number;
scriptId: number;
scriptCode: string;
scriptDesc: string;
bondType: string;
rateDate: string;
openRate: number | null;
highRate: number | null;
lowRate: number | null;
closeRate: number;
}
export function toDailyScriptRate(d: InvestmentDailyRateDto): DailyScriptRate {
return {
id: d.id,
rateDate: d.rateDate,
scriptCode: d.scriptCode,
scriptName: d.scriptDesc,
openRate: d.openRate,
closeRate: d.closeRate,
highRate: d.highRate,
lowRate: d.lowRate
};
}
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
export function apiErrorMessage(err: unknown): string {
const e = err as HttpErrorResponse;
const body = e?.error;
return body?.errors?.[0]?.defaultUserMessage || body?.defaultUserMessage || e?.message || 'Request failed.';
}