feat(investment): MTM valuation and mutual fund switch screens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 15:03:59 +05:30
co-authored by Claude Opus 5.5
parent 13d04353e1
commit d26cb89c5a
4 changed files with 551 additions and 155 deletions
@@ -1552,60 +1552,204 @@
<!-- ===================== 09 INVESTMENT DEPRECIATION ===================== --> <!-- ===================== 09 INVESTMENT DEPRECIATION ===================== -->
@if (activeTab === 'depreciation') { @if (activeTab === 'depreciation') {
<div class="pg-card"> <div class="pg-card">
<div class="pos-section-label">Investment - Investment Depreciation (Mark-to-Market Provisioning)</div> <div class="pos-section-label">Investment - Investment Depreciation (Market Valuation of AFS / HFT)</div>
<div class="inv-form-grid"> <div class="inv-form-grid">
<label class="inv-form-field" <label class="inv-form-field"
><span>Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy ><span>* Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy
></label> ></label>
<label class="inv-form-field"> <label class="inv-form-field">
<span>Category</span> <span>* Category</span>
<select [(ngModel)]="depreciationCategory"> <select [(ngModel)]="depreciationCategory">
<option value="">-- All Categories --</option> <option value="AFS">AFS (Available For Sale) — quarterly</option>
@for (c of categoryOptions; track c) { <option value="HFT">HFT (Held For Trading) — monthly</option>
<option [value]="c">{{ c }}</option>
}
</select> </select>
</label> </label>
<label class="inv-form-field"
><span>Holdings Requiring Provision</span><input type="text" [value]="depreciationTargets.length" readonly
/></label>
<label class="inv-form-field"
><span>Total Depreciation</span
><input type="text" [value]="'₹' + (depreciationTotal | number: '1.2-2')" readonly
/></label>
</div> </div>
<p class="inv-hint">
Each security still held is valued at the latest Daily Script Master rate / NAV on or before the date. Gains and
losses are netted within Govt Securities, Corporate Bonds and Mutual Funds separately; a net loss is provided in
the Investment Depreciation Reserve and a net gain is ignored. The book value does not change. HTM is not
valued.
</p>
<div class="inv-form-actions"> <div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="runDepreciation()">Process</button> <button type="button" class="pg-btn pg-btn--primary" [disabled]="valuationLoading" (click)="previewValuation()">
{{ valuationLoading ? 'Valuing…' : 'Preview' }}
</button>
<button
type="button"
class="pg-btn pg-btn--primary"
[disabled]="!valuationPreview?.postable || valuationPosting"
(click)="postValuation()"
>
Post
</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 (valuationError) {
<p class="pg-sub" style="padding: 0 16px; color: #b42318">{{ valuationError }}</p>
}
@if (valuationPreview; as v) {
@if (v.missingRates.length) {
<p class="inv-hint inv-hint--bad">
No rate on or before {{ v.valuationDate }} for: {{ v.missingRates.join(', ') }}. Enter it in Daily Script
Master (09) first.
</p>
}
@if (v.lastRunDate && !v.postable && !v.missingRates.length) {
<p class="inv-hint inv-hint--bad">
The last {{ v.category }} valuation is dated {{ v.lastRunDate }}; a new one must be dated after it.
</p>
}
<table class="pg-table">
<thead>
<tr>
<th>Class</th>
<th class="num">Book Value</th>
<th class="num">Market Value</th>
<th class="num">Net Gain / Loss</th>
<th class="num col-out">IDR Needed</th>
</tr>
</thead>
<tbody>
@for (c of v.classes; track c.assetClass) {
<tr>
<td>{{ valuationClassLabels[c.assetClass] }}</td>
<td class="num">₹{{ c.bookValue | number: '1.2-2' }}</td>
<td class="num">₹{{ c.marketValue | number: '1.2-2' }}</td>
<td class="num">₹{{ c.netDifference | number: '1.2-2' }}</td>
<td class="num col-out">₹{{ c.required | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
<p class="pg-sub" style="padding: 0 16px">
IDR needed ₹{{ v.requiredTotal | number: '1.2-2' }} · held from the last run ₹{{
v.previousTotal | number: '1.2-2'
}}
·
@if (v.delta > 0) {
Voucher dated {{ v.valuationDate }}: Dr {{ v.provisionGlCode }} Provision for Depreciation / Cr
{{ v.reserveGlCode }} IDR ₹{{ v.delta | number: '1.2-2' }}
} @else if (v.delta < 0) {
Write-back dated {{ v.valuationDate }}: Dr {{ v.reserveGlCode }} IDR / Cr {{ v.provisionGlCode }} Provision
for Depreciation ₹{{ -v.delta | number: '1.2-2' }}
} @else {
No change in the IDR, so no voucher (the valuation is still recorded).
}
</p>
<table class="pg-table">
<thead>
<tr>
<th>Deal No</th>
<th>Script</th>
<th>Class</th>
<th class="num">Units</th>
<th class="num">Book Value</th>
<th class="num">Rate / NAV</th>
<th>Rate Date</th>
<th class="num">Market Value</th>
<th class="num">Gain / Loss</th>
</tr>
</thead>
<tbody>
@for (l of v.lines; track l.purchaseId) {
<tr>
<td>
<span class="pg-chip">{{ l.dealNo }}</span>
</td>
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
<td>{{ valuationClassLabels[l.assetClass] }}</td>
<td class="num">{{ l.units | number: '1.0-6' }}</td>
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
<td>{{ l.rateDate }}</td>
<td class="num">₹{{ l.marketValue | number: '1.2-2' }}</td>
<td class="num" [class.col-out]="l.difference < 0">₹{{ l.difference | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="9" class="pg-empty">No {{ v.category }} holding with a rate on {{ v.valuationDate }}.</td>
</tr>
}
</tbody>
</table>
}
</div>
<div class="pg-card">
<div class="pos-section-label">Valuation Runs</div>
<table class="pg-table"> <table class="pg-table">
<thead> <thead>
<tr> <tr>
<th>Cert No</th> <th>Date</th>
<th>Script</th> <th>Category</th>
<th class="num">Book Rate</th> <th class="num">Govt Securities</th>
<th class="num">Market Price</th> <th class="num">Corporate Bonds</th>
<th class="num col-out">Depreciation</th> <th class="num">Mutual Funds</th>
<th class="num">IDR Needed</th>
<th class="num">Posted</th>
<th>Voucher</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (t of depreciationTargets; track t.holding.certNo) { @for (r of valuationRuns; track r.id) {
<tr> <tr>
<td>{{ r.valuationDate }}</td>
<td>{{ r.category }}</td>
<td class="num">₹{{ r.classes[0].required | number: '1.2-2' }}</td>
<td class="num">₹{{ r.classes[1].required | number: '1.2-2' }}</td>
<td class="num">₹{{ r.classes[2].required | number: '1.2-2' }}</td>
<td class="num">₹{{ r.requiredTotal | number: '1.2-2' }}</td>
<td class="num">₹{{ r.delta | number: '1.2-2' }}</td>
<td>{{ r.voucherNo || '—' }}</td>
<td> <td>
<span class="pg-chip">{{ t.holding.certNo }}</span> <button type="button" class="pg-btn pg-btn--ghost" (click)="openValuationRun(r.id)">Lines</button>
</td> </td>
<td>{{ t.holding.scriptName }}</td>
<td class="num">{{ t.holding.purchaseRate }}</td>
<td class="num">{{ t.marketPrice }}</td>
<td class="num col-out">₹{{ t.depreciation | number: '1.2-2' }}</td>
</tr> </tr>
} @empty { } @empty {
<tr> <tr>
<td colspan="5" class="pg-empty">No holdings require depreciation provisioning.</td> <td colspan="9" class="pg-empty">No valuation run yet.</td>
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
@if (valuationOpened; as o) {
<div class="pos-section-label">{{ o.category }} valuation of {{ o.valuationDate }}</div>
<table class="pg-table">
<thead>
<tr>
<th>Deal No</th>
<th>Script</th>
<th class="num">Units</th>
<th class="num">Book Value</th>
<th class="num">Rate / NAV</th>
<th>Rate Date</th>
<th class="num">Market Value</th>
<th class="num">Gain / Loss</th>
</tr>
</thead>
<tbody>
@for (l of o.lines; track l.purchaseId) {
<tr>
<td>
<span class="pg-chip">{{ l.dealNo }}</span>
</td>
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
<td class="num">{{ l.units | number: '1.0-6' }}</td>
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
<td>{{ l.rateDate }}</td>
<td class="num">₹{{ l.marketValue | number: '1.2-2' }}</td>
<td class="num" [class.col-out]="l.difference < 0">₹{{ l.difference | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="pg-empty">No holding was valued (the run only wrote the reserve back).</td>
</tr>
}
</tbody>
</table>
}
</div> </div>
} }
@@ -1615,40 +1759,87 @@
<div class="pos-section-label">Investment - Mutual Fund Transfer (Switch between schemes)</div> <div class="pos-section-label">Investment - Mutual Fund Transfer (Switch between schemes)</div>
<div class="inv-form-grid"> <div class="inv-form-grid">
<label class="inv-form-field"> <label class="inv-form-field">
<span>From Holding *</span> <span>* From Fund (Deal No)</span>
<select [(ngModel)]="mfFromCertNo"> <select [(ngModel)]="mfSwitchPurchaseId" (ngModelChange)="onMfSwitchDealChange()">
<option value="">-- Select --</option> <option [ngValue]="null">-- Select --</option>
@for (h of mfHoldings; track h.certNo) { @for (h of mfHoldings; track h.id) {
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option> <option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
} }
</select> </select>
</label> </label>
@if (mfFromHolding; as h) { @if (mfFromHolding; as h) {
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label> <label class="inv-form-field"
<label class="inv-form-field"><span>NAV</span><input type="text" [value]="h.purchaseRate" readonly /></label> ><span>Units Held</span><input type="text" [value]="h.units | number: '1.0-6'" readonly
/></label>
<label class="inv-form-field"
><span>Book Value Held</span
><input type="text" [value]="'₹' + (carryingValue(h) | number: '1.2-2')" readonly
/></label>
} }
<label class="inv-form-field"
><span>* Switch Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="mfSwitchDate" (ngModelChange)="loadMfSwitchRates()"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Units to Switch</span><input type="number" [(ngModel)]="mfSwitchUnits" min="0" step="0.001"
/></label>
<label class="inv-form-field"> <label class="inv-form-field">
<span>To Product *</span> <span>* NAV of this fund</span>
<select [(ngModel)]="mfToProduct"> <input type="number" [(ngModel)]="mfSwitchFromNav" min="0" step="0.0001" />
@if (mfSwitchFromRate; as r) {
<small>Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}</small>
}
</label>
<label class="inv-form-field">
<span>* Switch into Scheme</span>
<select [(ngModel)]="mfSwitchToScriptCode" (ngModelChange)="onMfSwitchTargetChange()">
<option value="">-- Select --</option> <option value="">-- Select --</option>
@for (p of mfProductOptions; track p.productCode) { @for (x of mfSwitchTargets; track x.scriptCode) {
<option [value]="p.productName">{{ p.productName }} ({{ p.amcName }})</option> <option [value]="x.scriptCode">{{ x.scriptCode }} — {{ x.scriptName }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>* NAV of the new scheme</span>
<input type="number" [(ngModel)]="mfSwitchToNav" min="0" step="0.0001" />
@if (mfSwitchToRate; as r) {
<small>Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}</small>
}
</label>
<label class="inv-form-field">
<span>* Sub Type of the new scheme</span>
<select [(ngModel)]="mfSwitchSubType">
@for (s of mfSwitchSubTypes; track s.subTypeCode) {
<option [value]="s.subTypeName">{{ s.subTypeName }}</option>
} }
</select> </select>
</label> </label>
<label class="inv-form-field" <label class="inv-form-field"
><span>Units to Transfer *</span><input type="number" [(ngModel)]="mfTransferUnits" min="0" step="0.001" ><span>Remarks</span><input type="text" maxlength="400" [(ngModel)]="mfSwitchRemarks"
/></label>
<label class="inv-form-field"
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="mfTransferDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Transfer Value (computed)</span
><input type="text" [value]="'₹' + (mfTransferValue | number: '1.2-2')" readonly
/></label> /></label>
</div> </div>
@if (mfFromHolding && mfSwitchToScript && mfSwitchValue) {
<div class="pos-section-label">Accounting (dated {{ mfSwitchDate }}, no bank entry)</div>
<p class="pg-sub" style="padding: 0 16px">
Switch value ₹{{ mfSwitchValue | number: '1.2-2' }} · cost of the units ₹{{
mfSwitchCost | number: '1.2-2'
}}
· new units {{ mfSwitchNewUnits | number: '1.0-3' }} of {{ mfSwitchToScript.scriptCode }}
</p>
<p class="pg-sub" style="padding: 0 16px">
Dr {{ mfSwitchToScript.glCode }} Investment (new scheme) ₹{{ mfSwitchValue | number: '1.2-2' }} · Cr
{{ mfFromHolding.glCode }} Investment (this fund) ₹{{ mfSwitchCost | number: '1.2-2' }}
@if (mfSwitchGain > 0) {
· Cr 400402 Interest on Bonds ₹{{ mfSwitchGain | number: '1.2-2' }}
} @else if (mfSwitchGain < 0) {
· Dr 500605 Loss on Investments ₹{{ -mfSwitchGain | number: '1.2-2' }}
}
</p>
}
<div class="inv-form-actions"> <div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="saveMfTransfer()">Save</button> <button type="button" class="pg-btn pg-btn--primary" [disabled]="mfSwitchSaving" (click)="saveMfSwitch()">
Save
</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>
</div> </div>
@@ -20,7 +20,6 @@ import {
SUBTYPE_MASTER, SUBTYPE_MASTER,
BROKER_MASTER, BROKER_MASTER,
SGL_BANK_MASTER, SGL_BANK_MASTER,
MUTUAL_FUND_PRODUCTS,
LIEN_REASON_MASTER, LIEN_REASON_MASTER,
CATEGORY_OPTIONS, CATEGORY_OPTIONS,
DEAL_TYPE_OPTIONS, DEAL_TYPE_OPTIONS,
@@ -73,6 +72,7 @@ import {
InvestmentSecurityTransferDto, InvestmentSecurityTransferDto,
InvestmentNonSlrLimitDto, InvestmentNonSlrLimitDto,
InvestmentService, InvestmentService,
InvestmentValuationDto,
NON_SLR_TYPES, NON_SLR_TYPES,
apiErrorMessage, apiErrorMessage,
toHolding, toHolding,
@@ -175,7 +175,7 @@ interface AccountingPreviewLine {
* Combined Govt Security / Non-SLR / Bond / Mutual Fund treasury screen, mirroring the legacy * Combined Govt Security / Non-SLR / Bond / Mutual Fund treasury screen, mirroring the legacy
* "03 Govt./Bond Entry" submenu in full: Purchase, Redemption, Interest Calculation, Sell, * "03 Govt./Bond Entry" submenu in full: Purchase, Redemption, Interest Calculation, Sell,
* Write-off, Amortize, Amortize Yearly Process, GOI Category Transfer, Investment Depreciation * Write-off, Amortize, Amortize Yearly Process, GOI Category Transfer, Investment Depreciation
* and Mutual Fund Transfer. Dummy in-memory data; Save appends to the local arrays. * (market valuation) and Mutual Fund Transfer (switch), all on the backend.
*/ */
@Component({ @Component({
selector: 'mifosx-bond-investment', selector: 'mifosx-bond-investment',
@@ -278,7 +278,8 @@ export class BondInvestmentComponent implements OnInit {
'REDEEM', 'REDEEM',
'TRANSFER', 'TRANSFER',
'AMORTIZE', 'AMORTIZE',
'WRITE_OFF' 'WRITE_OFF',
'SWITCH'
].includes(t.txnType) ].includes(t.txnType)
); );
this.txns = [ this.txns = [
@@ -311,7 +312,6 @@ export class BondInvestmentComponent implements OnInit {
} }
readonly brokerOptions = BROKER_MASTER; readonly brokerOptions = BROKER_MASTER;
readonly sglBankOptions = SGL_BANK_MASTER; readonly sglBankOptions = SGL_BANK_MASTER;
readonly mfProductOptions = MUTUAL_FUND_PRODUCTS;
readonly lienReasonOptions = LIEN_REASON_MASTER; readonly lienReasonOptions = LIEN_REASON_MASTER;
readonly categoryOptions = CATEGORY_OPTIONS; readonly categoryOptions = CATEGORY_OPTIONS;
readonly dealTypeOptions = DEAL_TYPE_OPTIONS; readonly dealTypeOptions = DEAL_TYPE_OPTIONS;
@@ -354,15 +354,9 @@ export class BondInvestmentComponent implements OnInit {
// GOI Category Transfer // GOI Category Transfer
// Investment Depreciation // Investment Depreciation (market valuation of AFS / HFT)
depreciationDate = this.asOnDate; depreciationDate = this.asOnDate;
depreciationCategory = ''; depreciationCategory: 'AFS' | 'HFT' = 'AFS';
// Mutual Fund Transfer
mfFromCertNo = '';
mfToProduct = '';
mfTransferUnits: number | null = null;
mfTransferDate = this.asOnDate;
readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [ readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [
{ value: 'GOVT_SECURITY', label: 'GOVT SECURITY' }, { value: 'GOVT_SECURITY', label: 'GOVT SECURITY' },
@@ -373,6 +367,9 @@ export class BondInvestmentComponent implements OnInit {
setTab(tab: BondTab): void { setTab(tab: BondTab): void {
this.activeTab = tab; this.activeTab = tab;
if (tab === 'depreciation') {
this.loadValuationRuns();
}
} }
setBondType(type: BondType): void { setBondType(type: BondType): void {
@@ -1737,123 +1734,245 @@ export class BondInvestmentComponent implements OnInit {
}); });
} }
// -------------------- Investment Depreciation -------------------- // -------------------- Investment Depreciation (market valuation, MTM) --------------------
/** AFS/HFT holdings whose market price is below book value must be marked down (RBI norms). */ /**
get depreciationTargets(): Array<{ holding: InvestmentHolding; marketPrice: number; depreciation: number }> { * RBI: AFS holdings are valued at market quarterly and HFT monthly (or more often); HTM is carried at cost. The backend
return this.holdings * values each security at the Daily Script Master rate, nets gains and losses within each class, and posts only the
.filter((h) => h.status === 'ACTIVE' && h.instrumentType !== 'MUTUAL_FUND') * change in the Investment Depreciation Reserve. The book value of the holdings does not change.
.map((h) => { */
const md = this.scriptOptions.find((s) => s.scriptCode === h.scriptCode); valuationPreview: InvestmentValuationDto | null = null;
const marketPrice = md ? h.purchaseRate * 0.995 : h.purchaseRate; valuationError = '';
const depreciation = valuationLoading = false;
marketPrice < h.purchaseRate ? Math.round((h.faceValue * (h.purchaseRate - marketPrice)) / 100) : 0; valuationPosting = false;
return { holding: h, marketPrice: +marketPrice.toFixed(2), depreciation }; valuationRuns: InvestmentValuationDto[] = [];
}) /** A past run opened from the history, with its lines. */
.filter((r) => r.depreciation > 0); valuationOpened: InvestmentValuationDto | null = null;
readonly valuationClassLabels: Record<string, string> = {
GOVT_SECURITY: 'Govt Securities',
CORPORATE_BOND: 'Corporate Bonds',
MUTUAL_FUND: 'Mutual Funds'
};
loadValuationRuns(): void {
this.investmentService.getValuations().subscribe({
next: (runs) => (this.valuationRuns = runs),
error: (err) => this.toast(`Could not load valuation runs: ${apiErrorMessage(err)}`)
});
} }
get depreciationTotal(): number { previewValuation(): void {
return this.depreciationTargets.reduce((s, r) => s + r.depreciation, 0); this.valuationPreview = null;
} this.valuationError = '';
if (!this.depreciationDate || !this.depreciationCategory) {
runDepreciation(): void { this.valuationError = 'Enter the valuation date and choose AFS or HFT.';
const targets = this.depreciationTargets;
if (targets.length === 0) {
this.toast('No holdings require depreciation provisioning.');
return; return;
} }
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({ this.valuationLoading = true;
certNo: t.holding.certNo, this.investmentService.previewValuation(this.depreciationDate, this.depreciationCategory).subscribe({
instrumentType: t.holding.instrumentType, next: (v) => {
txnType: 'AMORTIZE', this.valuationLoading = false;
voucherNo: `INV/DEPR/${this.depreciationDate.replace(/-/g, '')}-${i + 1}`, this.valuationPreview = v;
txnDate: this.depreciationDate, },
amount: t.depreciation, error: (err) => {
particulars: `Investment depreciation provision - ${t.holding.scriptName} (market ${t.marketPrice})` this.valuationLoading = false;
})); this.valuationError = apiErrorMessage(err);
this.txns = [ }
...newTxns, });
...this.txns
];
this.toast(
`Depreciation provisioned for ${targets.length} holding(s). Total ₹${this.depreciationTotal.toLocaleString('en-IN')}.`
);
} }
// -------------------- Mutual Fund Transfer -------------------- /** Posts the change in IDR worked out by the preview; the backend values the holdings again with the same rules. */
postValuation(): void {
const v = this.valuationPreview;
if (!v?.postable || this.valuationPosting) {
return;
}
this.valuationPosting = true;
this.investmentService
.runValuation({ valuationDate: v.valuationDate, category: v.category, dateFormat: 'dd-MM-yyyy', locale: 'en' })
.subscribe({
next: (res) => {
this.valuationPosting = false;
this.toast(
v.delta === 0
? `Valuation of ${v.valuationDate} recorded; the IDR needed is unchanged, so no voucher.`
: `Valuation of ${v.valuationDate} posted — ${res.resourceIdentifier ?? ''}.`
);
this.valuationPreview = null;
this.loadValuationRuns();
},
error: (err) => {
this.valuationPosting = false;
this.toast(apiErrorMessage(err));
}
});
}
openValuationRun(id: number | undefined): void {
if (id == null) {
return;
}
this.investmentService.getValuation(id).subscribe({
next: (v) => (this.valuationOpened = v),
error: (err) => this.toast(apiErrorMessage(err))
});
}
// -------------------- Mutual Fund Transfer (switch) --------------------
/**
* Units of one scheme are redeemed at its NAV and put into another scheme the same day; no money comes to the bank.
* Voucher: Dr new scheme's investment GL (value) / Cr old scheme's GL (cost of the units); gain 400402, loss 500605.
*/
mfSwitchPurchaseId: number | null = null;
mfSwitchUnits: number | null = null;
mfSwitchFromNav: number | null = null;
mfSwitchToScriptCode = '';
mfSwitchToNav: number | null = null;
mfSwitchSubType = 'DEBT - LIQUID';
mfSwitchDate = this.asOnDate;
mfSwitchRemarks = '';
mfSwitchSaving = false;
/** Latest Daily Script Master NAVs on the switch date, as hints. */
mfSwitchFromRate: InvestmentDailyRateDto | null = null;
mfSwitchToRate: InvestmentDailyRateDto | null = null;
get mfHoldings(): InvestmentHolding[] { get mfHoldings(): InvestmentHolding[] {
return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE'); return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE' && !!h.id);
} }
get mfFromHolding(): InvestmentHolding | undefined { get mfFromHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.mfFromCertNo); return this.holdings.find((h) => h.id === this.mfSwitchPurchaseId);
} }
get mfTransferValue(): number { /** Other active mutual fund schemes saved in Script Master. */
get mfSwitchTargets(): ScriptMaster[] {
const from = this.mfFromHolding;
return this.scriptOptions.filter(
(x) => !!x.id && x.active !== false && x.instrumentType === 'MUTUAL_FUND' && x.scriptCode !== from?.scriptCode
);
}
get mfSwitchToScript(): ScriptMaster | undefined {
return this.scriptOptions.find((x) => x.scriptCode === this.mfSwitchToScriptCode);
}
get mfSwitchSubTypes(): typeof SUBTYPE_MASTER {
return SUBTYPE_MASTER.filter((s) => s.instrumentType === 'MUTUAL_FUND');
}
/** Units × NAV of the scheme left. */
get mfSwitchValue(): number {
return this.mfSwitchUnits && this.mfSwitchFromNav
? Math.round(this.mfSwitchUnits * this.mfSwitchFromNav * 100) / 100
: 0;
}
/** The units' share of the book value still held, as the backend works it out for a sale. */
get mfSwitchCost(): number {
const h = this.mfFromHolding; const h = this.mfFromHolding;
if (!h || !this.mfTransferUnits) { if (!h || !this.mfSwitchUnits || !h.units) {
return 0; return 0;
} }
return Math.round(this.mfTransferUnits * h.purchaseRate); const book = this.carryingValue(h);
return this.mfSwitchUnits >= h.units ? book : Math.round(((book * this.mfSwitchUnits) / h.units) * 100) / 100;
} }
saveMfTransfer(): void { get mfSwitchGain(): number {
return Math.round((this.mfSwitchValue - this.mfSwitchCost) * 100) / 100;
}
get mfSwitchNewUnits(): number {
return this.mfSwitchToNav ? Math.round((this.mfSwitchValue / this.mfSwitchToNav) * 1000) / 1000 : 0;
}
onMfSwitchDealChange(): void {
this.mfSwitchUnits = this.mfFromHolding?.units ?? null;
this.mfSwitchFromNav = null;
this.loadMfSwitchRates();
}
/** Fills the NAVs from Daily Script Master for the switch date when one has been entered. */
loadMfSwitchRates(): void {
const fromId = this.scriptOptions.find((x) => x.scriptCode === this.mfFromHolding?.scriptCode)?.id;
const toId = this.mfSwitchToScript?.id;
this.mfSwitchFromRate = null;
this.mfSwitchToRate = null;
if (fromId && this.mfSwitchDate) {
this.investmentService.getLatestDailyRate(fromId, this.mfSwitchDate).subscribe({
next: (r) => {
this.mfSwitchFromRate = r;
this.mfSwitchFromNav = this.mfSwitchFromNav ?? r?.closeRate ?? null;
},
error: () => (this.mfSwitchFromRate = null)
});
}
if (toId && this.mfSwitchDate) {
this.investmentService.getLatestDailyRate(toId, this.mfSwitchDate).subscribe({
next: (r) => {
this.mfSwitchToRate = r;
this.mfSwitchToNav = this.mfSwitchToNav ?? r?.closeRate ?? null;
},
error: () => (this.mfSwitchToRate = null)
});
}
}
onMfSwitchTargetChange(): void {
this.mfSwitchToNav = null;
this.loadMfSwitchRates();
}
saveMfSwitch(): void {
const h = this.mfFromHolding; const h = this.mfFromHolding;
if (!h || !this.mfToProduct || !this.mfTransferUnits) { const to = this.mfSwitchToScript;
this.toast('Select source holding, target product and units to transfer.'); if (
!h?.id ||
!to?.id ||
!this.mfSwitchUnits ||
!this.mfSwitchFromNav ||
!this.mfSwitchToNav ||
!this.mfSwitchDate
) {
this.toast('Select the fund, the units, both NAVs, the scheme to switch into and the date.');
return; return;
} }
if (this.mfTransferUnits > h.units) { if (this.mfSwitchSaving) {
this.toast(`Units cannot exceed units held (${h.units}).`);
return; return;
} }
const value = this.mfTransferValue; this.mfSwitchSaving = true;
h.units = +(h.units - this.mfTransferUnits).toFixed(3); this.investmentService
h.faceValue = Math.round(h.units * h.purchaseRate); .switchFund(h.id, {
if (h.units <= 0) { switchDate: this.mfSwitchDate,
h.status = 'SOLD'; units: this.mfSwitchUnits,
} fromNav: this.mfSwitchFromNav,
const product = this.mfProductOptions.find((p) => p.productName === this.mfToProduct); toScriptId: to.id,
const newCert = `MF-${5000 + this.holdings.filter((x) => x.instrumentType === 'MUTUAL_FUND').length + 1}`; toNav: this.mfSwitchToNav,
this.holdings = [ subType: this.mfSwitchSubType,
{ ...(this.mfSwitchRemarks ? { remarks: this.mfSwitchRemarks } : {}),
certNo: newCert, dateFormat: 'dd-MM-yyyy',
instrumentType: 'MUTUAL_FUND', locale: 'en'
scriptCode: product?.productCode || this.mfToProduct, })
scriptName: this.mfToProduct, .subscribe({
party: product?.amcName || '', next: (res) => {
subType: product?.category || 'DEBT', this.mfSwitchSaving = false;
faceValue: value, this.toast(
purchaseRate: h.purchaseRate, `Switched ${this.mfSwitchUnits} units of deal ${h.dealNo} into ${to.scriptCode} — new deal ${res.resourceIdentifier ?? ''}.`
units: this.mfTransferUnits, );
purchaseDate: this.mfTransferDate, this.mfSwitchPurchaseId = null;
maturityDate: addPeriod(this.mfTransferDate, 1, 0), this.mfSwitchUnits = null;
interestRate: h.interestRate, this.mfSwitchFromNav = null;
interestFreq: 'CUMULATIVE', this.mfSwitchToScriptCode = '';
glCode: '310050', this.mfSwitchToNav = null;
glName: 'INVESTMENT IN MUTUAL FUNDS', this.mfSwitchRemarks = '';
crToAc: h.crToAc, this.mfSwitchFromRate = null;
brokerCode: '', this.mfSwitchToRate = null;
status: 'ACTIVE' this.loadPurchases();
}, },
...this.holdings error: (err) => {
]; this.mfSwitchSaving = false;
this.txns = [ this.toast(apiErrorMessage(err));
{ }
certNo: newCert, });
instrumentType: 'MUTUAL_FUND',
txnType: 'PURCHASE',
voucherNo: `INV/MF-TRF/${newCert}`,
txnDate: this.mfTransferDate,
amount: value,
particulars: `Mutual Fund switch from ${h.scriptName} to ${this.mfToProduct} (${this.mfTransferUnits} units)`
},
...this.txns
];
this.toast(`Transferred ${this.mfTransferUnits} units to ${this.mfToProduct} as ${newCert}.`);
this.mfFromCertNo = '';
this.mfToProduct = '';
this.mfTransferUnits = null;
} }
// -------------------- Helpers -------------------- // -------------------- Helpers --------------------
+1
View File
@@ -29,6 +29,7 @@ export type TxnType =
| 'WRITE_OFF' | 'WRITE_OFF'
| 'AMORTIZE' | 'AMORTIZE'
| 'TRANSFER' | 'TRANSFER'
| 'SWITCH'
| 'PART_PAYMENT' | 'PART_PAYMENT'
| 'PREMATURE' | 'PREMATURE'
| 'RENEWAL'; | 'RENEWAL';
+86 -1
View File
@@ -481,6 +481,36 @@ export class InvestmentService {
return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/redeem`, body); return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/redeem`, body);
} }
/** Mutual fund switch: units leave this fund at its NAV and a new deal opens in the other scheme; the new deal no comes back. */
switchFund(purchaseId: number, body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>(
`/investment/purchases/${purchaseId}/switch`,
body
);
}
// -------------------- Market valuation (MTM) --------------------
/** What a valuation of AFS / HFT holdings on a date would find and post, without saving. */
previewValuation(valuationDate: string, category: string): Observable<InvestmentValuationDto> {
return this.http.get<InvestmentValuationDto>('/investment/valuations/preview', {
params: { valuationDate, category }
});
}
/** Runs the valuation and posts the change in the Investment Depreciation Reserve. */
runValuation(body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/valuations', body);
}
/** Valuation runs, newest first (without their lines). */
getValuations(): Observable<InvestmentValuationDto[]> {
return this.http.get<InvestmentValuationDto[]>('/investment/valuations');
}
getValuation(id: number): Observable<InvestmentValuationDto> {
return this.http.get<InvestmentValuationDto>(`/investment/valuations/${id}`);
}
/** Interest lines already posted (coupons and provisions), newest first. */ /** Interest lines already posted (coupons and provisions), newest first. */
getInterestPostings(): Observable<InvestmentInterestPostingDto[]> { getInterestPostings(): Observable<InvestmentInterestPostingDto[]> {
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings'); return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
@@ -826,6 +856,20 @@ export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})` glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
}; };
} }
if (x.saleType === 'SWITCH') {
const result = x.profitLoss >= 0 ? `profit ₹${x.profitLoss}` : `loss ₹${-x.profitLoss}`;
return {
certNo: x.certNo,
instrumentType: x.bondType as InstrumentType,
txnType: 'SWITCH',
voucherNo: x.voucherNo ?? `SWT/${x.dealNo}/${x.id}`,
txnDate: x.settlementDate,
amount: x.consideration,
particulars: `Switched ${x.units} units @ NAV ${x.saleRate} - Deal ${x.dealNo} - ${x.scriptDesc} (${result}) - ${x.remarks ?? ''}`,
glCode: x.investmentGlCode,
glName: `Switched to another scheme, no bank entry (txn ${x.glTransactionId})`
};
}
const redeem = x.saleType === 'REDEEM'; const redeem = x.saleType === 'REDEEM';
// A Govt Security redeemed below book writes off its premium; a non-SLR shortfall is a loss (500605). // A Govt Security redeemed below book writes off its premium; a non-SLR shortfall is a loss (500605).
const pl = const pl =
@@ -1040,7 +1084,7 @@ export interface InvestmentSaleDto {
bondType: string; bondType: string;
scriptCode: string; scriptCode: string;
scriptDesc: string; scriptDesc: string;
saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE'; saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE' | 'SWITCH';
partyId?: number; partyId?: number;
partyName?: string; partyName?: string;
dealDate: string; dealDate: string;
@@ -1075,6 +1119,47 @@ export interface InvestmentSaleDto {
settledOnDate?: string; settledOnDate?: string;
} }
/** A market valuation (MTM) of AFS or HFT holdings: a preview (no id) or a posted run. */
export interface InvestmentValuationDto {
id?: number;
valuationDate: string;
category: string;
requiredTotal: number;
/** IDR held from the category's previous run. */
previousTotal: number;
/** More provision when positive, a write-back when negative. */
delta: number;
provisionGlCode: string;
reserveGlCode: string;
glTransactionId?: string;
voucherNo?: string;
lastRunDate?: string;
missingRates: string[];
postable: boolean;
classes: Array<{
assetClass: 'GOVT_SECURITY' | 'CORPORATE_BOND' | 'MUTUAL_FUND';
bookValue: number;
marketValue: number;
netDifference: number;
required: number;
}>;
lines: Array<{
purchaseId: number;
dealNo: string;
certNo: string;
bondType: string;
scriptCode: string;
scriptDesc: string;
assetClass: string;
units: number;
bookValue: number;
rate: number;
rateDate: string;
marketValue: number;
difference: number;
}>;
}
/** A GOI Category Transfer: a holding moved to another security of the same terms. */ /** A GOI Category Transfer: a holding moved to another security of the same terms. */
export interface InvestmentSecurityTransferDto { export interface InvestmentSecurityTransferDto {
id: number; id: number;