feat(investment): bucket wise report, script rating and non-SLR limits on purchase
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user