feat(investment): bucket wise report, script rating and non-SLR limits on purchase
This commit is contained in:
@@ -1528,7 +1528,7 @@
|
||||
<button mat-menu-item routerLink="/investment/interest-report" tabindex="0">
|
||||
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||
<button mat-menu-item routerLink="/investment/bucket-report" tabindex="0">
|
||||
<fa-icon icon="piggy-bank" class="menu-item-icon"></fa-icon> Bucket Wise Investment
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/deaf" tabindex="0">
|
||||
@@ -2400,7 +2400,7 @@
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Bucket Wise Investment')"
|
||||
routerLink="/investment/reports"
|
||||
routerLink="/investment/bucket-report"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="layer-group" class="gl-icon"></fa-icon> Bucket Wise Investment
|
||||
|
||||
@@ -336,6 +336,19 @@
|
||||
<label class="inv-form-field"
|
||||
><span>Sub Type</span><input type="text" [value]="purchaseForm.subType || '—'" readonly
|
||||
/></label>
|
||||
@if (purchaseIsRated) {
|
||||
<label class="inv-form-field"
|
||||
><span>Rating</span
|
||||
><input
|
||||
type="text"
|
||||
[value]="
|
||||
purchaseScript?.rating
|
||||
? purchaseScript?.rating + (purchaseScript?.listed === false ? ' · Unlisted' : '')
|
||||
: '— (enter in Script Master)'
|
||||
"
|
||||
readonly
|
||||
/></label>
|
||||
}
|
||||
@if (!isMutualFund) {
|
||||
<label class="inv-form-field"
|
||||
><span>Int Rate %</span><input type="text" [value]="purchaseForm.intRate ?? '—'" readonly
|
||||
@@ -403,6 +416,52 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (purchaseRatingProblem) {
|
||||
<p class="inv-hint inv-hint--bad">{{ purchaseRatingProblem }}</p>
|
||||
}
|
||||
@if (isNonSlrPurchase && nonSlrLimit; as l) {
|
||||
<div class="pos-section-label">RBI Limit — non-SLR investments (as on {{ l.asOnDate }})</div>
|
||||
@if (l.nonSlrLimit !== null && l.nonSlrLimit !== undefined) {
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Limit</th>
|
||||
<th class="num">Allowed</th>
|
||||
<th class="num">Now</th>
|
||||
<th class="num">After this deal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Non-SLR — 10% of total deposits ₹{{ l.totalDeposits | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ l.nonSlrLimit | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ l.nonSlrOutstanding | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ nonSlrCheck?.nonSlrAfter | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Unlisted — 10% of non-SLR held on {{ l.asOnDate }} ₹{{ l.nonSlrBase | number: '1.2-2' }}
|
||||
@if (!l.listed) {
|
||||
<br /><small>This security is unlisted.</small>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ l.unlistedLimit | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ l.unlistedOutstanding | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ nonSlrCheck?.unlistedAfter | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
@if (nonSlrCheck; as c) {
|
||||
<p class="inv-hint" [class.inv-hint--bad]="!c.withinLimits">
|
||||
{{ c.message }}
|
||||
@if (l.nonSlrLimit === null || l.nonSlrLimit === undefined) {
|
||||
Enter it on the FD screen (RBI Limit section).
|
||||
}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- 3. Accounting preview -->
|
||||
<div class="pos-section-label">Accounting Preview (posted on Settle, dated the settlement date)</div>
|
||||
@if (purchaseAccountingPreview.length) {
|
||||
@@ -466,14 +525,17 @@
|
||||
}
|
||||
</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>
|
||||
<!-- Bonds, CPs and CDs take their rating from Script Master (shown above), where RBI's minimum is checked. -->
|
||||
@if (!purchaseIsRated) {
|
||||
<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">
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
PAYMENT_MODE_OPTIONS,
|
||||
DELIVERY_MODE_OPTIONS,
|
||||
BOND_RATING_OPTIONS,
|
||||
ELIGIBLE_RATINGS,
|
||||
INT_TYPE_OPTIONS,
|
||||
AUTHORITY_OPTIONS,
|
||||
INVESTMENT_GL_BY_TYPE,
|
||||
@@ -70,6 +71,7 @@ import {
|
||||
InvestmentAmortisationDueDto,
|
||||
InvestmentSaleDto,
|
||||
InvestmentSecurityTransferDto,
|
||||
InvestmentNonSlrLimitDto,
|
||||
InvestmentService,
|
||||
apiErrorMessage,
|
||||
toHolding
|
||||
@@ -222,6 +224,7 @@ export class BondInvestmentComponent implements OnInit {
|
||||
this.holdings = purchases.map(toHolding);
|
||||
this.txns = purchases.map((d) => this.purchaseTxn(d));
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.loadNonSlrLimit();
|
||||
this.loadInterestLog();
|
||||
},
|
||||
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
|
||||
@@ -374,7 +377,10 @@ export class BondInvestmentComponent implements OnInit {
|
||||
// Master-driven dropdown sources
|
||||
readonly scriptOptions = SCRIPT_MASTER;
|
||||
readonly partyOptions = PARTY_MASTER;
|
||||
readonly subTypeOptions = SUBTYPE_MASTER;
|
||||
/** Sub types of the chosen bond type only — a mutual fund gets the debt ones, the only kind RBI allows. */
|
||||
get subTypeOptions(): typeof SUBTYPE_MASTER {
|
||||
return SUBTYPE_MASTER.filter((s) => s.instrumentType === this.purchaseForm.bondType);
|
||||
}
|
||||
readonly brokerOptions = BROKER_MASTER;
|
||||
readonly sglBankOptions = SGL_BANK_MASTER;
|
||||
readonly mfProductOptions = MUTUAL_FUND_PRODUCTS;
|
||||
@@ -444,6 +450,98 @@ export class BondInvestmentComponent implements OnInit {
|
||||
setBondType(type: BondType): void {
|
||||
this.bondType = type;
|
||||
this.purchaseForm.bondType = type;
|
||||
this.purchaseForm.subType = this.defaultSubType(type);
|
||||
this.loadNonSlrLimit();
|
||||
}
|
||||
|
||||
private defaultSubType(type: BondType): string {
|
||||
return type === 'GOVT_SECURITY'
|
||||
? 'SLR'
|
||||
: type === 'NON_SLR'
|
||||
? 'NON SLR'
|
||||
: type === 'MUTUAL_FUND'
|
||||
? 'DEBT - LIQUID'
|
||||
: 'CORPORATE BOND';
|
||||
}
|
||||
|
||||
// -------------------- RBI non-SLR limits --------------------
|
||||
/** Holdings and limits from the backend for the chosen script and deal date; the new deal is added on screen. */
|
||||
nonSlrLimit: InvestmentNonSlrLimitDto | null = null;
|
||||
|
||||
/** Bonds, CPs, CDs and mutual funds are non-SLR investments, limited by RBI to 10% of deposits. */
|
||||
get isNonSlrPurchase(): boolean {
|
||||
return [
|
||||
'NON_SLR',
|
||||
'BOND',
|
||||
'MUTUAL_FUND'
|
||||
].includes(this.purchaseForm.bondType);
|
||||
}
|
||||
|
||||
loadNonSlrLimit(): void {
|
||||
const f = this.purchaseForm;
|
||||
if (!this.isNonSlrPurchase || !f.dealDate) {
|
||||
this.nonSlrLimit = null;
|
||||
return;
|
||||
}
|
||||
this.investmentService.getNonSlrLimit(this.purchaseScript?.id ?? null, f.dealDate, 0).subscribe({
|
||||
next: (l: InvestmentNonSlrLimitDto) => (this.nonSlrLimit = l),
|
||||
error: () => (this.nonSlrLimit = null)
|
||||
});
|
||||
}
|
||||
|
||||
/** Bonds, CPs and CDs must carry a credit rating at or above the RBI minimum. */
|
||||
get purchaseIsRated(): boolean {
|
||||
return [
|
||||
'NON_SLR',
|
||||
'BOND'
|
||||
].includes(this.purchaseForm.bondType);
|
||||
}
|
||||
|
||||
/** RBI minimum rating: "A" for bonds, "A2" for CP / CD; empty when the script is not rated yet. */
|
||||
get purchaseRatingProblem(): string {
|
||||
const s = this.purchaseScript;
|
||||
if (!this.purchaseIsRated || !s) {
|
||||
return '';
|
||||
}
|
||||
if (!s.rating) {
|
||||
return `Enter the credit rating of ${s.scriptCode} in Script Master first.`;
|
||||
}
|
||||
return ELIGIBLE_RATINGS.includes(s.rating)
|
||||
? ''
|
||||
: `${s.scriptCode} is rated ${s.rating}; RBI allows bonds rated A or higher and CP / CD rated A2 or higher.`;
|
||||
}
|
||||
|
||||
/** The RBI limits with this deal's book value added — the same checks the backend makes on Save. */
|
||||
get nonSlrCheck(): { nonSlrAfter: number; unlistedAfter: number; withinLimits: boolean; message: string } | null {
|
||||
const l = this.nonSlrLimit;
|
||||
if (!l) {
|
||||
return null;
|
||||
}
|
||||
const amount = this.purchaseBookValue || 0;
|
||||
const nonSlrAfter = l.nonSlrOutstanding + amount;
|
||||
const unlistedAfter = l.unlistedOutstanding + (l.listed ? 0 : amount);
|
||||
const rupees = (v: number) =>
|
||||
'₹' + v.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const fail = (message: string) => ({ nonSlrAfter, unlistedAfter, withinLimits: false, message });
|
||||
if (l.nonSlrLimit === null || l.nonSlrLimit === undefined) {
|
||||
return fail(l.message);
|
||||
}
|
||||
if (nonSlrAfter > l.nonSlrLimit) {
|
||||
return fail(
|
||||
`Non-SLR investments would be ${rupees(nonSlrAfter)}, above the RBI limit of ${rupees(l.nonSlrLimit)}.`
|
||||
);
|
||||
}
|
||||
if (l.unlistedOutstanding > l.unlistedLimit) {
|
||||
return fail(
|
||||
`Unlisted non-SLR investments are ${rupees(l.unlistedOutstanding)}, above their limit of ${rupees(l.unlistedLimit)}; no further non-SLR investment until they are within it.`
|
||||
);
|
||||
}
|
||||
if (!l.listed && unlistedAfter > l.unlistedLimit) {
|
||||
return fail(
|
||||
`Unlisted non-SLR investments would be ${rupees(unlistedAfter)}, above the RBI limit of ${rupees(l.unlistedLimit)}.`
|
||||
);
|
||||
}
|
||||
return { nonSlrAfter, unlistedAfter, withinLimits: true, message: 'Within the RBI limits.' };
|
||||
}
|
||||
|
||||
get holdingsForType(): InvestmentHolding[] {
|
||||
@@ -508,7 +606,7 @@ export class BondInvestmentComponent implements OnInit {
|
||||
faceValue: null,
|
||||
purchaseRate: 100,
|
||||
partyCode: '',
|
||||
subType: type === 'GOVT_SECURITY' ? 'SLR' : type === 'NON_SLR' ? 'NON SLR' : 'CORPORATE BOND',
|
||||
subType: this.defaultSubType(type),
|
||||
tbillGlAc: '',
|
||||
crToAc: SETTLEMENT_BANK_GL_OPTIONS[0]?.glCode ?? '',
|
||||
premiumAmt: null,
|
||||
@@ -676,6 +774,7 @@ export class BondInvestmentComponent implements OnInit {
|
||||
}
|
||||
this.suggestLastIntDate();
|
||||
this.loadMarketRate();
|
||||
this.loadNonSlrLimit();
|
||||
}
|
||||
|
||||
private suggestLastIntDate(): void {
|
||||
@@ -743,14 +842,19 @@ export class BondInvestmentComponent implements OnInit {
|
||||
f.periodMonth = p.months;
|
||||
f.periodDays = p.days;
|
||||
}
|
||||
if (s.slrType) {
|
||||
// A mutual fund keeps its debt sub type; the SLR flag of its script is not a sub type.
|
||||
if (s.slrType && !this.isMutualFund) {
|
||||
f.subType = s.slrType === 'NON_SLR' ? 'NON SLR' : 'SLR';
|
||||
}
|
||||
if (this.isMutualFund) {
|
||||
f.productName = s.scriptName;
|
||||
}
|
||||
if (this.purchaseIsRated) {
|
||||
f.bondRating = s.rating ?? '';
|
||||
}
|
||||
this.suggestLastIntDate();
|
||||
this.loadMarketRate();
|
||||
this.loadNonSlrLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||
<h1 class="pg-title">Bucket Wise Investment</h1>
|
||||
<p class="pg-sub">
|
||||
Govt Securities, bonds and FDs held on a date, split by the time left to maturity. Click a bucket to see the
|
||||
holdings in it.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="!report || generating" (click)="exportExcel()">
|
||||
{{ generating ? 'Generating…' : 'Export to Excel' }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>As on</span><mifosx-date-ddmmyyyy [(ngModel)]="asOnDate"></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
</div>
|
||||
<div class="diary-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||
{{ loading ? 'Loading…' : 'Show' }}
|
||||
</button>
|
||||
</div>
|
||||
@if (error) {
|
||||
<p class="diary-error">{{ error }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (report; as r) {
|
||||
<div class="diary-summary">
|
||||
<div class="diary-tile">
|
||||
<span class="diary-tile-l">Total Investment</span
|
||||
><span class="diary-tile-v">₹{{ r.total.totalAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="diary-tile">
|
||||
<span class="diary-tile-l">Govt Securities (book value)</span
|
||||
><span class="diary-tile-v">₹{{ r.total.govtBookValue | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (hasBonds) {
|
||||
<div class="diary-tile">
|
||||
<span class="diary-tile-l">Bonds / Non-SLR (book value)</span
|
||||
><span class="diary-tile-v">₹{{ r.total.bondBookValue | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="diary-tile">
|
||||
<span class="diary-tile-l">FD with banks</span
|
||||
><span class="diary-tile-v">₹{{ r.total.fdPrincipal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">As on {{ r.asOnDate }} · by time left to maturity</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bucket</th>
|
||||
<th class="num">Govt Sec</th>
|
||||
<th class="num">Govt Sec Face Value</th>
|
||||
<th class="num">Govt Sec Book Value</th>
|
||||
@if (hasBonds) {
|
||||
<th class="num">Bonds</th>
|
||||
<th class="num">Bonds Book Value</th>
|
||||
}
|
||||
<th class="num">FD</th>
|
||||
<th class="num">FD Principal</th>
|
||||
<th class="num">Total Amount</th>
|
||||
<th class="num">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (b of r.buckets; track b.code) {
|
||||
<tr class="report-pick" [class.report-pick--on]="bucket === b.code" (click)="pickBucket(b)">
|
||||
<td>
|
||||
<strong>{{ b.label }}</strong>
|
||||
</td>
|
||||
<td class="num">{{ b.govtCount }}</td>
|
||||
<td class="num">₹{{ b.govtFaceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ b.govtBookValue | number: '1.2-2' }}</td>
|
||||
@if (hasBonds) {
|
||||
<td class="num">{{ b.bondCount }}</td>
|
||||
<td class="num">₹{{ b.bondBookValue | number: '1.2-2' }}</td>
|
||||
}
|
||||
<td class="num">{{ b.fdCount }}</td>
|
||||
<td class="num">₹{{ b.fdPrincipal | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ b.totalAmount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ share(b) | number: '1.2-2' }}%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="report-total">
|
||||
<td>Total</td>
|
||||
<td class="num">{{ r.total.govtCount }}</td>
|
||||
<td class="num">₹{{ r.total.govtFaceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.total.govtBookValue | number: '1.2-2' }}</td>
|
||||
@if (hasBonds) {
|
||||
<td class="num">{{ r.total.bondCount }}</td>
|
||||
<td class="num">₹{{ r.total.bondBookValue | number: '1.2-2' }}</td>
|
||||
}
|
||||
<td class="num">{{ r.total.fdCount }}</td>
|
||||
<td class="num">₹{{ r.total.fdPrincipal | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.total.totalAmount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.total.totalAmount > 0 ? '100.00%' : '—' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Bucket</span>
|
||||
<select [(ngModel)]="bucket">
|
||||
<option value="">All buckets</option>
|
||||
@for (b of r.buckets; track b.code) {
|
||||
<option [value]="b.code">{{ b.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Investment</span>
|
||||
<select [(ngModel)]="instrument">
|
||||
<option value="">All</option>
|
||||
<option value="GOVT_SECURITY">Govt Security</option>
|
||||
@if (hasBonds) {
|
||||
<option value="BOND">Bond / Non-SLR</option>
|
||||
}
|
||||
<option value="FD">FD with banks</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bucket</th>
|
||||
<th>Investment</th>
|
||||
<th>Deal No</th>
|
||||
<th>Particulars</th>
|
||||
<th>Category</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Maturity</th>
|
||||
<th>Time Left</th>
|
||||
<th class="num">Face Value / Principal</th>
|
||||
<th class="num">Book Value / Principal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (x of rows; track x.instrument + x.refId) {
|
||||
<tr>
|
||||
<td>{{ bucketLabel(x.bucket) }}</td>
|
||||
<td>{{ instrumentLabel(x.instrument) }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||
</td>
|
||||
<td>{{ x.particulars }}</td>
|
||||
<td>{{ x.category || '—' }}</td>
|
||||
<td class="num">{{ x.rate !== null && x.rate !== undefined ? x.rate + '%' : '—' }}</td>
|
||||
<td>{{ x.maturityDate }}</td>
|
||||
<td>
|
||||
@if (x.daysToMaturity < 0) {
|
||||
<span class="inv-badge diary-badge--overdue">{{ timeLeft(x) }}</span>
|
||||
} @else {
|
||||
{{ timeLeft(x) }}
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ x.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ x.bookValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="10" class="pg-empty">No investment held in this selection.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@if (rows.length) {
|
||||
<tfoot>
|
||||
<tr class="report-total">
|
||||
<td colspan="8">Total ({{ rows.length }})</td>
|
||||
<td class="num">₹{{ rowsTotal.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ rowsTotal.bookValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 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 { 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 { todayDDMMYYYY } from '../investment-data';
|
||||
import { exportWorkbook } from '../investment-export';
|
||||
import {
|
||||
InvestmentBucketDto,
|
||||
InvestmentBucketReportDto,
|
||||
InvestmentBucketRowDto,
|
||||
InvestmentService,
|
||||
apiErrorMessage
|
||||
} from '../investment.service';
|
||||
|
||||
type InstrumentFilter = '' | InvestmentBucketRowDto['instrument'];
|
||||
|
||||
const INSTRUMENT_LABEL: Record<InvestmentBucketRowDto['instrument'], string> = {
|
||||
GOVT_SECURITY: 'Govt Security',
|
||||
BOND: 'Bond / Non-SLR',
|
||||
FD: 'FD'
|
||||
};
|
||||
|
||||
/**
|
||||
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity — up to
|
||||
* 1 year, over 1 to 3, over 3 to 5 and over 5 years. Clicking a bucket lists the holdings in it.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-investment-bucket-report',
|
||||
templateUrl: './investment-bucket-report.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss',
|
||||
'../investment-diary/investment-diary.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
DateDdmmyyyyPickerComponent
|
||||
]
|
||||
})
|
||||
export class InvestmentBucketReportComponent implements OnInit {
|
||||
private readonly investmentService = inject(InvestmentService);
|
||||
|
||||
asOnDate = todayDDMMYYYY();
|
||||
bucket = '';
|
||||
instrument: InstrumentFilter = '';
|
||||
report: InvestmentBucketReportDto | null = null;
|
||||
loading = false;
|
||||
generating = false;
|
||||
error = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
if (!this.asOnDate) {
|
||||
this.error = 'Enter the As on date.';
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.investmentService.getBucketReport(this.asOnDate).subscribe({
|
||||
next: (r: InvestmentBucketReportDto) => {
|
||||
this.report = r;
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
this.report = null;
|
||||
this.loading = false;
|
||||
this.error = apiErrorMessage(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Bonds / Non-SLR columns only when any are held. */
|
||||
get hasBonds(): boolean {
|
||||
return (this.report?.total.bondCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
get rows(): InvestmentBucketRowDto[] {
|
||||
const all: InvestmentBucketRowDto[] = this.report?.rows ?? [];
|
||||
return all.filter(
|
||||
(r: InvestmentBucketRowDto) =>
|
||||
(!this.bucket || r.bucket === this.bucket) && (!this.instrument || r.instrument === this.instrument)
|
||||
);
|
||||
}
|
||||
|
||||
get rowsTotal(): { faceValue: number; bookValue: number } {
|
||||
return this.rows.reduce(
|
||||
(t: { faceValue: number; bookValue: number }, r: InvestmentBucketRowDto) => ({
|
||||
faceValue: t.faceValue + r.faceValue,
|
||||
bookValue: t.bookValue + r.bookValue
|
||||
}),
|
||||
{ faceValue: 0, bookValue: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
pickBucket(b: InvestmentBucketDto): void {
|
||||
this.bucket = this.bucket === b.code ? '' : b.code;
|
||||
}
|
||||
|
||||
bucketLabel(code: string): string {
|
||||
return this.report?.buckets.find((b: InvestmentBucketDto) => b.code === code)?.label ?? code;
|
||||
}
|
||||
|
||||
instrumentLabel(i: InvestmentBucketRowDto['instrument']): string {
|
||||
return INSTRUMENT_LABEL[i];
|
||||
}
|
||||
|
||||
/** Share of the bucket in the total amount, in %. */
|
||||
share(b: InvestmentBucketDto): number {
|
||||
const total = this.report?.total.totalAmount ?? 0;
|
||||
return total > 0 ? (b.totalAmount / total) * 100 : 0;
|
||||
}
|
||||
|
||||
/** Time left to maturity as years and months, or how long it is overdue. */
|
||||
timeLeft(r: InvestmentBucketRowDto): string {
|
||||
if (r.daysToMaturity < 0) {
|
||||
return `Overdue ${-r.daysToMaturity} days`;
|
||||
}
|
||||
const months = Math.floor(r.daysToMaturity / 30.4375);
|
||||
const years = Math.floor(months / 12);
|
||||
if (years === 0 && months === 0) {
|
||||
return `${r.daysToMaturity} days`;
|
||||
}
|
||||
return years > 0 ? `${years}y ${months % 12}m` : `${months}m`;
|
||||
}
|
||||
|
||||
async exportExcel(): Promise<void> {
|
||||
const report = this.report;
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
this.generating = true;
|
||||
try {
|
||||
const bucketRow = (b: InvestmentBucketDto): (string | number | null)[] => [
|
||||
b.label,
|
||||
b.govtCount,
|
||||
b.govtFaceValue,
|
||||
b.govtBookValue,
|
||||
...(this.hasBonds ? [
|
||||
b.bondCount,
|
||||
b.bondFaceValue,
|
||||
b.bondBookValue
|
||||
] : []),
|
||||
b.fdCount,
|
||||
b.fdPrincipal,
|
||||
b.totalAmount,
|
||||
Number(this.share(b).toFixed(2))
|
||||
];
|
||||
await exportWorkbook(
|
||||
[
|
||||
{
|
||||
sheetName: 'Bucket Summary',
|
||||
title: 'Bucket Wise Investment',
|
||||
subtitle: `As on ${report.asOnDate} · by time left to maturity`,
|
||||
headers: [
|
||||
'Bucket',
|
||||
'Govt Sec (No.)',
|
||||
'Govt Sec Face Value',
|
||||
'Govt Sec Book Value',
|
||||
...(this.hasBonds ? [
|
||||
'Bonds (No.)',
|
||||
'Bonds Face Value',
|
||||
'Bonds Book Value'
|
||||
] : []),
|
||||
'FD (No.)',
|
||||
'FD Principal',
|
||||
'Total Amount',
|
||||
'Share %'
|
||||
],
|
||||
rows: [
|
||||
...report.buckets.map(bucketRow),
|
||||
bucketRow(report.total)
|
||||
]
|
||||
},
|
||||
{
|
||||
sheetName: 'Holdings',
|
||||
title: 'Bucket Wise Investment — Holdings',
|
||||
subtitle: `As on ${report.asOnDate}`,
|
||||
headers: [
|
||||
'Bucket',
|
||||
'Investment',
|
||||
'Deal No',
|
||||
'Particulars',
|
||||
'Category',
|
||||
'Rate %',
|
||||
'Maturity',
|
||||
'Time Left',
|
||||
'Face Value / Principal',
|
||||
'Book Value / Principal'
|
||||
],
|
||||
widths: [
|
||||
22,
|
||||
16,
|
||||
16,
|
||||
40,
|
||||
10,
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
20,
|
||||
20
|
||||
],
|
||||
rows: report.rows.map((r: InvestmentBucketRowDto) => [
|
||||
this.bucketLabel(r.bucket),
|
||||
this.instrumentLabel(r.instrument),
|
||||
r.dealNo,
|
||||
r.particulars,
|
||||
r.category ?? '',
|
||||
r.rate ?? null,
|
||||
r.maturityDate,
|
||||
this.timeLeft(r),
|
||||
r.faceValue,
|
||||
r.bookValue
|
||||
])
|
||||
}
|
||||
],
|
||||
`Bucket-Wise-Investment-${report.asOnDate}`
|
||||
);
|
||||
} finally {
|
||||
this.generating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1589,8 +1589,80 @@ export interface ScriptMaster {
|
||||
phone?: string;
|
||||
fax?: string;
|
||||
corpCode?: string;
|
||||
/** Credit rating — long-term for bonds, short-term for CP / CD (Non-SLR and Bond scripts). */
|
||||
rating?: string;
|
||||
ratingAgency?: string;
|
||||
/** Listed on an exchange or proposed to be listed; unlisted ones have their own RBI limit. */
|
||||
listed?: boolean;
|
||||
}
|
||||
|
||||
/** Credit ratings on the Script Master, best first: long-term for bonds, short-term for CP / CD. */
|
||||
export const RATING_OPTIONS: Array<{ group: string; values: string[] }> = [
|
||||
{
|
||||
group: 'Long-term (bonds)',
|
||||
values: [
|
||||
'AAA',
|
||||
'AA+',
|
||||
'AA',
|
||||
'AA-',
|
||||
'A+',
|
||||
'A',
|
||||
'A-',
|
||||
'BBB+',
|
||||
'BBB',
|
||||
'BBB-',
|
||||
'BB+',
|
||||
'BB',
|
||||
'BB-',
|
||||
'B+',
|
||||
'B',
|
||||
'B-',
|
||||
'C',
|
||||
'D'
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'Short-term (CP / CD)',
|
||||
values: [
|
||||
'A1+',
|
||||
'A1',
|
||||
'A2+',
|
||||
'A2',
|
||||
'A3+',
|
||||
'A3',
|
||||
'A4+',
|
||||
'A4'
|
||||
]
|
||||
},
|
||||
{ group: 'None', values: ['UNRATED'] }
|
||||
];
|
||||
|
||||
/** RBI: bonds rated "A" or higher, CP / CD rated "A2" or higher. */
|
||||
export const ELIGIBLE_RATINGS: string[] = [
|
||||
'AAA',
|
||||
'AA+',
|
||||
'AA',
|
||||
'AA-',
|
||||
'A+',
|
||||
'A',
|
||||
'A-',
|
||||
'A1+',
|
||||
'A1',
|
||||
'A2+',
|
||||
'A2'
|
||||
];
|
||||
|
||||
/** Credit rating agencies registered with SEBI. */
|
||||
export const RATING_AGENCY_OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: 'CRISIL', label: 'CRISIL' },
|
||||
{ value: 'ICRA', label: 'ICRA' },
|
||||
{ value: 'CARE', label: 'CARE' },
|
||||
{ value: 'INDIA_RATINGS', label: 'India Ratings' },
|
||||
{ value: 'ACUITE', label: 'Acuite' },
|
||||
{ value: 'BRICKWORK', label: 'Brickwork' },
|
||||
{ value: 'INFOMERICS', label: 'Infomerics' }
|
||||
];
|
||||
|
||||
/** 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 };
|
||||
@@ -2052,7 +2124,11 @@ export const SUBTYPE_MASTER: SubTypeMaster[] = [
|
||||
{ subTypeCode: '4', subTypeName: 'EQUITY', instrumentType: 'SHARE' },
|
||||
{ subTypeCode: '5', subTypeName: 'CO-OP SHARE', instrumentType: 'SHARE' },
|
||||
{ subTypeCode: '6', subTypeName: 'DEBT - OVERNIGHT', instrumentType: 'MUTUAL_FUND' },
|
||||
{ subTypeCode: '7', subTypeName: 'DEBT - LIQUID', instrumentType: 'MUTUAL_FUND' }
|
||||
{ subTypeCode: '7', subTypeName: 'DEBT - LIQUID', instrumentType: 'MUTUAL_FUND' },
|
||||
{ subTypeCode: '8', subTypeName: 'DEBT - MONEY MARKET', instrumentType: 'MUTUAL_FUND' },
|
||||
{ subTypeCode: '9', subTypeName: 'DEBT - BANKING & PSU', instrumentType: 'MUTUAL_FUND' },
|
||||
{ subTypeCode: '10', subTypeName: 'DEBT - CORPORATE BOND', instrumentType: 'MUTUAL_FUND' },
|
||||
{ subTypeCode: '11', subTypeName: 'DEBT - GILT', instrumentType: 'MUTUAL_FUND' }
|
||||
];
|
||||
|
||||
export interface ControlParameter {
|
||||
|
||||
@@ -87,3 +87,11 @@
|
||||
font-weight: 700;
|
||||
border-top: 2px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.report-pick {
|
||||
cursor: pointer;
|
||||
|
||||
&--on td {
|
||||
background: #eff6ff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,52 @@
|
||||
><span>Period (Days)</span><input type="text" [value]="scriptPeriod.days" readonly
|
||||
/></label>
|
||||
</div>
|
||||
@if (scriptIsRated) {
|
||||
<div class="pos-section-label">Credit Rating (RBI: bonds "A" or higher, CP / CD "A2" or higher)</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>* Rating</span>
|
||||
<select [(ngModel)]="scriptForm.rating">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
@for (g of ratingOptions; track g.group) {
|
||||
<optgroup [label]="g.group">
|
||||
@for (r of g.values; track r) {
|
||||
<option [value]="r">{{ r }}</option>
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Rating Agency</span>
|
||||
<select [(ngModel)]="scriptForm.ratingAgency">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
@for (a of ratingAgencyOptions; track a.value) {
|
||||
<option [value]="a.value">{{ a.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Listing</span>
|
||||
<select
|
||||
[ngModel]="scriptForm.listed === false ? 'UNLISTED' : 'LISTED'"
|
||||
(ngModelChange)="scriptForm.listed = $event !== 'UNLISTED'"
|
||||
>
|
||||
<option value="LISTED">Listed / proposed to be listed</option>
|
||||
<option value="UNLISTED">Unlisted</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@if (scriptForm.rating) {
|
||||
<p class="inv-hint" [class.inv-hint--bad]="!scriptRatingEligible">
|
||||
{{
|
||||
scriptRatingEligible
|
||||
? 'Rating meets the RBI minimum; this security can be bought.'
|
||||
: 'Below the RBI minimum rating; a purchase of this security will be blocked.'
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
<div class="pos-section-label">Company Details (Bond / Mutual Fund)</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
@@ -381,6 +427,7 @@
|
||||
<th class="num">Int Rate %</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Maturity</th>
|
||||
<th>Rating</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -398,6 +445,7 @@
|
||||
<td class="num">{{ s.couponRate ?? '—' }}</td>
|
||||
<td>{{ s.issueDate || '—' }}</td>
|
||||
<td>{{ s.maturityDate || '—' }}</td>
|
||||
<td>{{ s.rating ? s.rating + (s.listed === false ? ' · Unlisted' : '') : '—' }}</td>
|
||||
<td>{{ s.active === false ? 'INACTIVE' : 'ACTIVE' }}</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
BANK_TYPE_OPTIONS,
|
||||
SLR_TYPE_OPTIONS,
|
||||
INVESTMENT_GL_LOOKUP,
|
||||
ELIGIBLE_RATINGS,
|
||||
RATING_AGENCY_OPTIONS,
|
||||
RATING_OPTIONS,
|
||||
periodBetween
|
||||
} from '../investment-data';
|
||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||
@@ -124,6 +127,8 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
saving = false;
|
||||
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
||||
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
|
||||
readonly ratingOptions = RATING_OPTIONS;
|
||||
readonly ratingAgencyOptions = RATING_AGENCY_OPTIONS;
|
||||
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
|
||||
investmentGlOptions: InvestmentGlAccount[] = [];
|
||||
/** Income GLs under 400400 INCOME FROM INVESTMENTS, for an FD bank's interest. */
|
||||
@@ -295,6 +300,19 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
].includes(this.scriptForm.instrumentType ?? '');
|
||||
}
|
||||
|
||||
/** Corporate bonds, CPs and CDs carry a credit rating and a listed / unlisted flag (RBI non-SLR rules). */
|
||||
get scriptIsRated(): boolean {
|
||||
return [
|
||||
'NON_SLR',
|
||||
'BOND'
|
||||
].includes(this.scriptForm.instrumentType ?? '');
|
||||
}
|
||||
|
||||
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
|
||||
get scriptRatingEligible(): boolean {
|
||||
return !!this.scriptForm.rating && ELIGIBLE_RATINGS.includes(this.scriptForm.rating);
|
||||
}
|
||||
|
||||
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
|
||||
get nextScriptCode(): string {
|
||||
const prefixByType: Record<InstrumentType, string> = {
|
||||
@@ -332,6 +350,10 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
this.toast('Enter ISIN, Int Rate, Issue Date and Maturity Date for this security.');
|
||||
return;
|
||||
}
|
||||
if (this.scriptIsRated && !f.rating) {
|
||||
this.toast('Enter the credit rating of this bond / CP / CD.');
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
bondType: f.instrumentType,
|
||||
slrType: f.slrType ?? '',
|
||||
@@ -350,6 +372,9 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
phone: f.phone ?? '',
|
||||
fax: f.fax ?? '',
|
||||
corpCode: f.corpCode ?? '',
|
||||
rating: this.scriptIsRated ? (f.rating ?? '') : '',
|
||||
ratingAgency: this.scriptIsRated ? (f.ratingAgency ?? '') : '',
|
||||
listed: !this.scriptIsRated || f.listed !== false,
|
||||
active: f.active !== false,
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
|
||||
@@ -19,6 +19,7 @@ import { InvestmentReportsComponent } from './investment-reports/investment-repo
|
||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
||||
import { InvestmentInterestReportComponent } from './investment-interest-report/investment-interest-report.component';
|
||||
import { InvestmentBucketReportComponent } from './investment-bucket-report/investment-bucket-report.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
@@ -66,6 +67,11 @@ const routes: Routes = [
|
||||
component: InvestmentInterestReportComponent,
|
||||
data: { title: 'Interest Received & Receivable', breadcrumb: 'Interest Received & Receivable' }
|
||||
},
|
||||
{
|
||||
path: 'bucket-report',
|
||||
component: InvestmentBucketReportComponent,
|
||||
data: { title: 'Bucket Wise Investment', breadcrumb: 'Bucket Wise Investment' }
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
component: InvestmentReportsComponent,
|
||||
|
||||
@@ -260,3 +260,14 @@
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.inv-hint {
|
||||
margin: 8px 0;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
color: #15803d;
|
||||
|
||||
&--bad {
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,44 @@ export interface InvestmentInterestReportDto {
|
||||
rows: InvestmentInterestReportRowDto[];
|
||||
}
|
||||
|
||||
/** One maturity bucket of the Bucket Wise Investment report, or its total. */
|
||||
export interface InvestmentBucketDto {
|
||||
code: string;
|
||||
label: string;
|
||||
govtCount: number;
|
||||
govtFaceValue: number;
|
||||
govtBookValue: number;
|
||||
bondCount: number;
|
||||
bondFaceValue: number;
|
||||
bondBookValue: number;
|
||||
fdCount: number;
|
||||
fdPrincipal: number;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
/** One Govt Security, bond or FD held on the as-on date, with the bucket it falls in. */
|
||||
export interface InvestmentBucketRowDto {
|
||||
bucket: string;
|
||||
instrument: 'GOVT_SECURITY' | 'BOND' | 'FD';
|
||||
refId: number;
|
||||
dealNo: string;
|
||||
particulars: string;
|
||||
category?: string;
|
||||
rate?: number;
|
||||
maturityDate: string;
|
||||
daysToMaturity: number;
|
||||
faceValue: number;
|
||||
bookValue: number;
|
||||
}
|
||||
|
||||
/** Holdings on a date split by the time left to maturity. */
|
||||
export interface InvestmentBucketReportDto {
|
||||
asOnDate: string;
|
||||
buckets: InvestmentBucketDto[];
|
||||
total: InvestmentBucketDto;
|
||||
rows: InvestmentBucketRowDto[];
|
||||
}
|
||||
|
||||
/** One dated event in the Due Date Diary. */
|
||||
export interface InvestmentDiaryRowDto {
|
||||
date: string;
|
||||
@@ -250,9 +288,29 @@ export interface InvestmentScriptDto {
|
||||
phone?: string;
|
||||
fax?: string;
|
||||
corpCode?: string;
|
||||
rating?: string;
|
||||
ratingAgency?: string;
|
||||
listed: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/** Where a non-SLR purchase stands against the RBI limits, from /investment/purchases/non-slr-limit. */
|
||||
export interface InvestmentNonSlrLimitDto {
|
||||
asOnDate: string;
|
||||
totalDeposits?: number;
|
||||
nonSlrLimit?: number;
|
||||
nonSlrOutstanding: number;
|
||||
nonSlrAfter: number;
|
||||
nonSlrBase: number;
|
||||
unlistedLimit: number;
|
||||
unlistedOutstanding: number;
|
||||
unlistedAfter: number;
|
||||
newAmount: number;
|
||||
listed: boolean;
|
||||
withinLimits: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Purchase entry (holding) as returned by /investment/purchases (dates are DD-MM-YYYY). */
|
||||
export interface InvestmentPurchaseDto {
|
||||
id: number;
|
||||
@@ -468,6 +526,15 @@ export class InvestmentService {
|
||||
);
|
||||
}
|
||||
|
||||
/** RBI non-SLR limits for a purchase of `amount` (book value) in a script on a deal date (DD-MM-YYYY). */
|
||||
getNonSlrLimit(scriptId: number | null, dealDate: string, amount: number): Observable<InvestmentNonSlrLimitDto> {
|
||||
const params: Record<string, string> = { dealDate, amount: String(amount || 0) };
|
||||
if (scriptId) {
|
||||
params['scriptId'] = String(scriptId);
|
||||
}
|
||||
return this.http.get<InvestmentNonSlrLimitDto>('/investment/purchases/non-slr-limit', { params });
|
||||
}
|
||||
|
||||
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
||||
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
|
||||
}
|
||||
@@ -532,6 +599,11 @@ export class InvestmentService {
|
||||
return this.http.get<InvestmentInterestReportDto>('/investment/interest-report', { params: { fromDate, toDate } });
|
||||
}
|
||||
|
||||
/** Bucket Wise Investment: holdings on a date (DD-MM-YYYY) split by the time left to maturity. */
|
||||
getBucketReport(asOnDate: string): Observable<InvestmentBucketReportDto> {
|
||||
return this.http.get<InvestmentBucketReportDto>('/investment/bucket-report', { params: { asOnDate } });
|
||||
}
|
||||
|
||||
/** Due Date Diary: dated events of all active investments between two dates (DD-MM-YYYY). */
|
||||
getDiary(fromDate: string, toDate: string): Observable<InvestmentDiaryDto> {
|
||||
return this.http.get<InvestmentDiaryDto>('/investment/diary', { params: { fromDate, toDate } });
|
||||
@@ -643,6 +715,9 @@ export function toScriptMaster(s: InvestmentScriptDto): ScriptMaster {
|
||||
phone: s.phone,
|
||||
fax: s.fax,
|
||||
corpCode: s.corpCode,
|
||||
rating: s.rating,
|
||||
ratingAgency: s.ratingAgency,
|
||||
listed: s.listed,
|
||||
active: s.active
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user