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>
This commit is contained in:
@@ -626,20 +626,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ===================== 03 INTEREST ENTRY / CALCULATION ===================== -->
|
<!-- ===================== 03 INTEREST ENTRY ===================== -->
|
||||||
@if (activeTab === 'interest') {
|
@if (activeTab === 'interest') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Govt./Bond Interest calculation</div>
|
<div class="pos-section-label">Interest Entry — coupon and 31 Mar / 30 Sep provision schedule</div>
|
||||||
<div class="inv-form-grid">
|
<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">
|
<label class="inv-form-field">
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<select [(ngModel)]="intCalcType" (ngModelChange)="onIntCalcTypeChange()">
|
<select [(ngModel)]="intType" (ngModelChange)="onIntFilterChange()">
|
||||||
<option value="">-- All Types --</option>
|
<option value="">-- All Types --</option>
|
||||||
@for (o of bondTypeOptions; track o.value) {
|
@for (o of bondTypeOptions; track o.value) {
|
||||||
<option [value]="o.value">{{ o.label }}</option>
|
<option [value]="o.value">{{ o.label }}</option>
|
||||||
@@ -648,67 +642,149 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Script</span>
|
<span>Script</span>
|
||||||
<select [(ngModel)]="intCalcScript">
|
<select [(ngModel)]="intScript" (ngModelChange)="onIntFilterChange()">
|
||||||
<option value="">-- All Scripts --</option>
|
<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>
|
<option [value]="s.scriptCode">{{ s.scriptCode }} — {{ s.scriptName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<label class="inv-form-field">
|
||||||
<span>Post To (Interest Income GL)</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="intCalcGlCode">
|
<select [(ngModel)]="intPurchaseId" (ngModelChange)="intSchedule = null">
|
||||||
@for (g of interestIncomeGlOptions; track g.glCode) {
|
<option [ngValue]="null">-- Select --</option>
|
||||||
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
@for (h of intDealOptions; track h.id) {
|
||||||
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} (Cert {{ h.certNo }})</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>As On Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intAsOnDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<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>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
@if (intCalcExcludedNpi.length > 0) {
|
|
||||||
|
@if (intSchedule; as sc) {
|
||||||
|
<div class="pos-section-label">Purchase Details</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Deal No / Cert No</span><input type="text" [value]="sc.dealNo + ' / ' + sc.certNo" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Script</span><input type="text" [value]="sc.scriptCode + ' — ' + sc.scriptDesc" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Face Value Held</span><input type="text" [value]="'₹' + (sc.faceValue | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Interest Rate</span><input type="text" [value]="sc.intRate + '%'" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Coupon per Half-Year</span
|
||||||
|
><input type="text" [value]="'₹' + (sc.couponPerHalfYear | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Deal Date</span><input type="text" [value]="sc.dealDate" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Last Interest Date</span><input type="text" [value]="sc.lastIntDate" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Maturity Date</span><input type="text" [value]="sc.maturityDate" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Accrued Interest Paid at Purchase</span
|
||||||
|
><input type="text" [value]="'₹' + (sc.purchasedAccruedInterest | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Book Value</span><input type="text" [value]="'₹' + (sc.bookValue | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pos-section-label">
|
||||||
|
Interest Schedule ({{ sc.rows.length }} lines) — due up to {{ sc.asOnDate }}: ₹{{
|
||||||
|
intDueTotal | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Days are 30/360. A provision books the interest earned since the last coupon; the coupon then clears what is
|
||||||
|
already booked. Display only — nothing is posted to GL yet.
|
||||||
|
</p>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Int Date</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th class="num">Days</th>
|
||||||
|
<th class="num">Int %</th>
|
||||||
|
<th class="num">Gross Interest</th>
|
||||||
|
<th class="num">Already Booked</th>
|
||||||
|
<th class="num col-in">Amount</th>
|
||||||
|
<th>Accounting</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of sc.rows; track r.intDate) {
|
||||||
|
<tr [style.opacity]="r.due ? 1 : 0.6">
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}</span>
|
||||||
|
@if (r.due) {
|
||||||
|
<span class="pg-chip">Due</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>{{ r.intDate }}</td>
|
||||||
|
<td>{{ r.fromDate }}</td>
|
||||||
|
<td>{{ r.toDate }}</td>
|
||||||
|
<td class="num">{{ r.days }}</td>
|
||||||
|
<td class="num">{{ sc.intRate }}</td>
|
||||||
|
<td class="num">₹{{ r.grossInterest | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ r.alreadyBooked | number: '1.2-2' }}</td>
|
||||||
|
<td class="num col-in">₹{{ r.amount | number: '1.2-2' }}</td>
|
||||||
|
<td style="white-space: nowrap; font-size: 12px">
|
||||||
|
@if (r.type === 'COUPON') {
|
||||||
|
<div>Dr Bank ₹{{ r.grossInterest | number: '1.0-0' }}</div>
|
||||||
|
@if (r.alreadyBooked > 0) {
|
||||||
|
<div>
|
||||||
|
Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.0-0' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div>
|
||||||
|
} @else {
|
||||||
|
<div>Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.0-0' }}</div>
|
||||||
|
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="pg-empty">
|
||||||
|
No interest lines — check the rate, last interest date and maturity date.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="pg-sub" style="padding: 8px 16px 12px">
|
||||||
|
Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }}. Posting to GL will be enabled after
|
||||||
|
approval.
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
<p class="pg-sub" style="padding: 0 16px 12px">
|
<p class="pg-sub" style="padding: 0 16px 12px">
|
||||||
{{ intCalcExcludedNpi.length }} holding(s) excluded — classified Non-Performing, interest is not posted on
|
Select a Deal No and press Retrieve to see its interest schedule.
|
||||||
accrual basis (collectible on cash basis only, via Interest Receive in the Investment Interest module).
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import {
|
|||||||
BOND_RATING_OPTIONS,
|
BOND_RATING_OPTIONS,
|
||||||
INT_TYPE_OPTIONS,
|
INT_TYPE_OPTIONS,
|
||||||
AUTHORITY_OPTIONS,
|
AUTHORITY_OPTIONS,
|
||||||
INTEREST_INCOME_GL_MASTER,
|
|
||||||
INTEREST_INCOME_GL_BY_TYPE,
|
|
||||||
INVESTMENT_GL_BY_TYPE,
|
INVESTMENT_GL_BY_TYPE,
|
||||||
INVESTMENT_GL_LOOKUP,
|
INVESTMENT_GL_LOOKUP,
|
||||||
INTEREST_RECEIVABLE_GL,
|
INTEREST_RECEIVABLE_GL,
|
||||||
@@ -42,8 +40,6 @@ import {
|
|||||||
InterestFreq,
|
InterestFreq,
|
||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
accruedInterest,
|
accruedInterest,
|
||||||
accruedInterestBetween,
|
|
||||||
canAccrueInterest,
|
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
maturityAmount,
|
maturityAmount,
|
||||||
addPeriod,
|
addPeriod,
|
||||||
@@ -52,7 +48,13 @@ import {
|
|||||||
todayDDMMYYYY
|
todayDDMMYYYY
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
import { InvestmentPurchaseDto, InvestmentService, apiErrorMessage, toHolding } from '../investment.service';
|
import {
|
||||||
|
InvestmentInterestScheduleDto,
|
||||||
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage,
|
||||||
|
toHolding
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
|
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
|
||||||
type BondTab =
|
type BondTab =
|
||||||
@@ -286,17 +288,16 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
redeemDate = this.asOnDate;
|
redeemDate = this.asOnDate;
|
||||||
redeemRemarks = '';
|
redeemRemarks = '';
|
||||||
|
|
||||||
// Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) —
|
// 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every
|
||||||
// posts interest accrued strictly within [intCalcFromDate, intCalcToDate], same period-based
|
// half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only:
|
||||||
// basis as the Investment Interest module's Govt./Bond Interest Calculation.
|
// nothing is posted to GL until the posting rules are approved.
|
||||||
intCalcFromDate = addPeriod(this.asOnDate, 0, -3);
|
intType: BondType | '' = '';
|
||||||
intCalcToDate = this.asOnDate;
|
intScript = '';
|
||||||
intCalcType: BondType | '' = '';
|
intPurchaseId: number | null = null;
|
||||||
intCalcScript = '';
|
intAsOnDate = this.asOnDate;
|
||||||
intCalcDealNo = '';
|
intSchedule: InvestmentInterestScheduleDto | null = null;
|
||||||
intCalcAll = false;
|
intLoading = false;
|
||||||
intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
|
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
|
||||||
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
|
|
||||||
|
|
||||||
// Write-off
|
// Write-off
|
||||||
writeoffCertNo = '';
|
writeoffCertNo = '';
|
||||||
@@ -892,78 +893,53 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Interest calculation --------------------
|
// -------------------- Interest calculation --------------------
|
||||||
/** Holdings the Interest Calculation "Process" run would cover, given the filters. A
|
/** Saved holdings for the Deal No dropdown, narrowed by Type and Script. */
|
||||||
* Non-Performing holding is always excluded — same accrual-basis rule as the Investment
|
get intDealOptions(): InvestmentHolding[] {
|
||||||
* Interest module (it's still collectible on cash basis via Interest Receive there). */
|
return this.holdings.filter(
|
||||||
get intCalcTargets(): InvestmentHolding[] {
|
(h) =>
|
||||||
const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
|
h.id != null &&
|
||||||
if (this.intCalcAll) {
|
h.status === 'ACTIVE' &&
|
||||||
return base;
|
(!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.intPurchaseId != null && !this.intDealOptions.some((h) => h.id === this.intPurchaseId)) {
|
||||||
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
|
this.intPurchaseId = null;
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
this.intSchedule = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
glName(glCode: string): string {
|
retrieveInterestSchedule(): void {
|
||||||
return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
|
if (this.intPurchaseId == null) {
|
||||||
}
|
this.toast('Select a Deal No first.');
|
||||||
|
|
||||||
/** 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.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toDate = this.intCalcToDate || this.asOnDate;
|
this.intLoading = true;
|
||||||
const fromDate = this.intCalcFromDate || toDate;
|
this.investmentService.getInterestSchedule(this.intPurchaseId, this.intAsOnDate || undefined).subscribe({
|
||||||
const glCode = this.intCalcGlCode;
|
next: (schedule) => {
|
||||||
const glName = this.glName(glCode);
|
this.intSchedule = schedule;
|
||||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
this.intLoading = false;
|
||||||
const amt = this.interestAccrued(h);
|
},
|
||||||
h.interestPosted = (h.interestPosted ?? 0) + amt;
|
error: (err) => {
|
||||||
h.interestPostedTillDate = toDate;
|
this.intSchedule = null;
|
||||||
return {
|
this.intLoading = false;
|
||||||
certNo: h.certNo,
|
this.toast(apiErrorMessage(err));
|
||||||
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.txns = [
|
}
|
||||||
...newTxns,
|
|
||||||
...this.txns
|
/** Total of the lines falling due up to the as-on date. */
|
||||||
];
|
get intDueTotal(): number {
|
||||||
this.intCalcFromDate = toDate;
|
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
|
||||||
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')}.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Sell / Redeem Entry --------------------
|
// -------------------- Sell / Redeem Entry --------------------
|
||||||
|
|||||||
@@ -169,6 +169,14 @@ export class InvestmentService {
|
|||||||
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
|
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
|
||||||
|
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
|
||||||
|
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
|
||||||
|
return this.http.get<InvestmentInterestScheduleDto>(
|
||||||
|
`/investment/purchases/${purchaseId}/interest-schedule${params}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
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);
|
||||||
}
|
}
|
||||||
@@ -292,6 +300,42 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
|
||||||
|
export interface InvestmentInterestRowDto {
|
||||||
|
type: 'COUPON' | 'PROVISION';
|
||||||
|
intDate: string;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
days: number;
|
||||||
|
grossInterest: number;
|
||||||
|
alreadyBooked: number;
|
||||||
|
amount: number;
|
||||||
|
due: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvestmentInterestScheduleDto {
|
||||||
|
purchaseId: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo: string;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
faceValue: number;
|
||||||
|
unitValue: number;
|
||||||
|
balanceUnits: number;
|
||||||
|
purchaseRate: number;
|
||||||
|
bookValue: number;
|
||||||
|
intRate: number;
|
||||||
|
dealDate: string;
|
||||||
|
lastIntDate: string;
|
||||||
|
maturityDate: string;
|
||||||
|
purchasedAccruedInterest: number;
|
||||||
|
couponPerHalfYear: number;
|
||||||
|
asOnDate: string;
|
||||||
|
interestGlCode: string;
|
||||||
|
interestGlName: string;
|
||||||
|
rows: InvestmentInterestRowDto[];
|
||||||
|
}
|
||||||
|
|
||||||
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
|
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
|
||||||
export function apiErrorMessage(err: unknown): string {
|
export function apiErrorMessage(err: unknown): string {
|
||||||
const e = err as HttpErrorResponse;
|
const e = err as HttpErrorResponse;
|
||||||
|
|||||||
Reference in New Issue
Block a user