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:
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user