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>
This commit is contained in:
2026-10-14 10:28:19 +05:30
co-authored by Claude Opus 5.5
parent 6a04a92113
commit c803e20628
4 changed files with 66 additions and 16 deletions
@@ -137,11 +137,12 @@
<th class="num">Units</th> <th class="num">Units</th>
<th class="num">Face Value</th> <th class="num">Face Value</th>
<th class="num">Rate</th> <th class="num">Rate</th>
<th>Purchase Date</th> <th>Settlement Date</th>
<th>Maturity Date</th> <th>Maturity Date</th>
<th class="num col-in">Accrued Interest</th> <th class="num col-in">Accrued Interest</th>
<th class="num col-net">Maturity Value</th> <th class="num col-net">Maturity Value</th>
<th>Status</th> <th>Status</th>
<th>Settlement</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -160,12 +161,23 @@
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td> <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 class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
<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> </td>
</tr> </tr>
} @empty { } @empty {
<tr> <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> </tr>
} }
</tbody> </tbody>
@@ -384,7 +396,7 @@
} }
<!-- 3. Accounting preview --> <!-- 3. Accounting preview -->
<div class="pos-section-label">Accounting Preview (posted after checker approval)</div> <div class="pos-section-label">Accounting Preview (posted on Settle, dated the settlement date)</div>
@if (purchaseAccountingPreview.length) { @if (purchaseAccountingPreview.length) {
<table class="pg-table"> <table class="pg-table">
<thead> <thead>
@@ -1284,7 +1296,7 @@
<td>{{ t.certNo }}</td> <td>{{ t.certNo }}</td>
<td>{{ t.txnType }}</td> <td>{{ t.txnType }}</td>
<td>{{ t.particulars }}</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> <td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
</tr> </tr>
} @empty { } @empty {
@@ -239,17 +239,19 @@ export class BondInvestmentComponent implements OnInit {
}); });
} }
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn { private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
const posted = !!d.glTransactionId;
return { return {
certNo: d.certNo, certNo: d.certNo,
instrumentType: d.bondType, instrumentType: d.bondType,
txnType: 'PURCHASE', txnType: 'PURCHASE',
voucherNo: `INV/PUR/${d.dealNo}`, voucherNo: `INV/PUR/${d.dealNo}`,
txnDate: d.dealDate, txnDate: d.settlementDate,
amount: d.settlementAmount, amount: d.settlementAmount,
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`, particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
glCode: d.investmentGlCode, glCode: posted ? d.investmentGlCode : '',
glName: d.investmentGlName glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
}; };
} }
@@ -573,20 +575,19 @@ export class BondInvestmentComponent implements OnInit {
return approxYtm(f.intRate, f.purchaseRate || 100, yearsBetween(f.issueDate, this.purchaseMaturityDate)); 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. */
/** Interest owed to the seller from the last coupon date up to the deal date, on a 30/360 basis —
* interest is counted to the event date, not the date the money moves (Jitendra Sir). */
get purchaseAccruedInterest(): number { get purchaseAccruedInterest(): number {
const f = this.purchaseForm; const f = this.purchaseForm;
if (!f.intRate || !f.lastIntDt || !f.dealDate) { if (!f.intRate || !f.lastIntDt || !f.dealDate) {
return 0; return 0;
} }
return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * days360(f.lastIntDt, f.dealDate)) / 360); return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * this.purchaseAccruedDays) / 360);
} }
get purchaseAccruedDays(): number { get purchaseAccruedDays(): number {
const f = this.purchaseForm; const f = this.purchaseForm;
return f.lastIntDt && f.dealDate ? days360(f.lastIntDt, f.dealDate) : 0; // 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. */ /** Premium (+) or discount (−) paid over face value. */
@@ -842,7 +843,9 @@ export class BondInvestmentComponent implements OnInit {
}; };
this.investmentService.createPurchase(body).subscribe({ this.investmentService.createPurchase(body).subscribe({
next: (res) => { next: (res) => {
this.toast(`Deal ${res.resourceIdentifier} saved. GL posting will follow once accounting is approved.`); this.toast(
`Deal ${res.resourceIdentifier} saved — pending settlement. Settle it from Holdings on or after the settlement date.`
);
this.activeTab = 'holdings'; this.activeTab = 'holdings';
this.loadPurchases(); this.loadPurchases();
}, },
@@ -941,6 +944,24 @@ export class BondInvestmentComponent implements OnInit {
this.toast(`Deal for Cert No ${h.certNo} retrieved.`); this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
} }
/** 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.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 { dropPurchase(): void {
const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo); const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo);
if (!h?.id) { if (!h?.id) {
+5 -1
View File
@@ -17,7 +17,7 @@
*/ */
export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE'; 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 InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE';
export type TxnType = export type TxnType =
| 'PURCHASE' | 'PURCHASE'
@@ -133,6 +133,10 @@ export interface InvestmentHolding {
partyCode?: string; partyCode?: string;
bookValue?: number; bookValue?: number;
settlementAmount?: 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 { export interface InvestmentTxn {
+14 -1
View File
@@ -12,6 +12,7 @@ import { Observable, forkJoin } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { import {
DailyScriptRate, DailyScriptRate,
HoldingStatus,
InstrumentType, InstrumentType,
InvestmentHolding, InvestmentHolding,
PARTY_MASTER, PARTY_MASTER,
@@ -119,6 +120,8 @@ export interface InvestmentPurchaseDto {
crGlName: string; crGlName: string;
otherDetails?: string; otherDetails?: string;
status: string; status: string;
glTransactionId?: string;
settledOnDate?: string;
} }
export interface InvestmentGlAccount { export interface InvestmentGlAccount {
@@ -200,6 +203,14 @@ export class InvestmentService {
return this.http.delete(`/investment/daily-rates/${rateId}`); 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 }> { createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body); return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
} }
@@ -316,7 +327,9 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
glName: p.investmentGlName, glName: p.investmentGlName,
crToAc: p.crGlCode, crToAc: p.crGlCode,
brokerCode: '', brokerCode: '',
status: 'ACTIVE', status: p.status as HoldingStatus,
settlementDate: p.settlementDate,
glTransactionId: p.glTransactionId,
category: p.category, category: p.category,
bookValue: p.bookValue, bookValue: p.bookValue,
settlementAmount: p.settlementAmount settlementAmount: p.settlementAmount