diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html
index 3315889c2..0879664f2 100644
--- a/src/app/investment/bond-investment/bond-investment.component.html
+++ b/src/app/investment/bond-investment/bond-investment.component.html
@@ -1552,60 +1552,204 @@
@if (activeTab === 'depreciation') {
-
Investment - Investment Depreciation (Mark-to-Market Provisioning)
+
Investment - Investment Depreciation (Market Valuation of AFS / HFT)
Valuation Date * Valuation Date
- Category
+ * Category
- -- All Categories --
- @for (c of categoryOptions; track c) {
- {{ c }}
- }
+ AFS (Available For Sale) — quarterly
+ HFT (Held For Trading) — monthly
- Holdings Requiring Provision
- Total Depreciation
+
+ 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.
+
- Process
+
+ {{ valuationLoading ? 'Valuing…' : 'Preview' }}
+
+
+ Post
+
Exit
+ @if (valuationError) {
+
{{ valuationError }}
+ }
+ @if (valuationPreview; as v) {
+ @if (v.missingRates.length) {
+
+ No rate on or before {{ v.valuationDate }} for: {{ v.missingRates.join(', ') }}. Enter it in Daily Script
+ Master (09) first.
+
+ }
+ @if (v.lastRunDate && !v.postable && !v.missingRates.length) {
+
+ The last {{ v.category }} valuation is dated {{ v.lastRunDate }}; a new one must be dated after it.
+
+ }
+
+
+
+ Class
+ Book Value
+ Market Value
+ Net Gain / Loss
+ IDR Needed
+
+
+
+ @for (c of v.classes; track c.assetClass) {
+
+ {{ valuationClassLabels[c.assetClass] }}
+ ₹{{ c.bookValue | number: '1.2-2' }}
+ ₹{{ c.marketValue | number: '1.2-2' }}
+ ₹{{ c.netDifference | number: '1.2-2' }}
+ ₹{{ c.required | number: '1.2-2' }}
+
+ }
+
+
+
+ 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).
+ }
+
+
+
+
+ Deal No
+ Script
+ Class
+ Units
+ Book Value
+ Rate / NAV
+ Rate Date
+ Market Value
+ Gain / Loss
+
+
+
+ @for (l of v.lines; track l.purchaseId) {
+
+
+ {{ l.dealNo }}
+
+ {{ l.scriptCode }} {{ l.scriptDesc }}
+ {{ valuationClassLabels[l.assetClass] }}
+ {{ l.units | number: '1.0-6' }}
+ ₹{{ l.bookValue | number: '1.2-2' }}
+ {{ l.rate | number: '1.2-4' }}
+ {{ l.rateDate }}
+ ₹{{ l.marketValue | number: '1.2-2' }}
+ ₹{{ l.difference | number: '1.2-2' }}
+
+ } @empty {
+
+ No {{ v.category }} holding with a rate on {{ v.valuationDate }}.
+
+ }
+
+
+ }
+
+
+
Valuation Runs
- Cert No
- Script
- Book Rate
- Market Price
- Depreciation
+ Date
+ Category
+ Govt Securities
+ Corporate Bonds
+ Mutual Funds
+ IDR Needed
+ Posted
+ Voucher
+
- @for (t of depreciationTargets; track t.holding.certNo) {
+ @for (r of valuationRuns; track r.id) {
+ {{ r.valuationDate }}
+ {{ r.category }}
+ ₹{{ r.classes[0].required | number: '1.2-2' }}
+ ₹{{ r.classes[1].required | number: '1.2-2' }}
+ ₹{{ r.classes[2].required | number: '1.2-2' }}
+ ₹{{ r.requiredTotal | number: '1.2-2' }}
+ ₹{{ r.delta | number: '1.2-2' }}
+ {{ r.voucherNo || '—' }}
- {{ t.holding.certNo }}
+ Lines
- {{ t.holding.scriptName }}
- {{ t.holding.purchaseRate }}
- {{ t.marketPrice }}
- ₹{{ t.depreciation | number: '1.2-2' }}
} @empty {
- No holdings require depreciation provisioning.
+ No valuation run yet.
}
+ @if (valuationOpened; as o) {
+
{{ o.category }} valuation of {{ o.valuationDate }}
+
+
+
+ Deal No
+ Script
+ Units
+ Book Value
+ Rate / NAV
+ Rate Date
+ Market Value
+ Gain / Loss
+
+
+
+ @for (l of o.lines; track l.purchaseId) {
+
+
+ {{ l.dealNo }}
+
+ {{ l.scriptCode }} {{ l.scriptDesc }}
+ {{ l.units | number: '1.0-6' }}
+ ₹{{ l.bookValue | number: '1.2-2' }}
+ {{ l.rate | number: '1.2-4' }}
+ {{ l.rateDate }}
+ ₹{{ l.marketValue | number: '1.2-2' }}
+ ₹{{ l.difference | number: '1.2-2' }}
+
+ } @empty {
+
+ No holding was valued (the run only wrote the reserve back).
+
+ }
+
+
+ }
}
@@ -1615,40 +1759,87 @@
Investment - Mutual Fund Transfer (Switch between schemes)
- From Holding *
-
- -- Select --
- @for (h of mfHoldings; track h.certNo) {
- {{ h.certNo }} — {{ h.scriptName }}
+ * From Fund (Deal No)
+
+ -- Select --
+ @for (h of mfHoldings; track h.id) {
+ {{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}
}
@if (mfFromHolding; as h) {
- Units Held
- NAV
+ Units Held
+ Book Value Held
}
+ * Switch Date
+ * Units to Switch
- To Product *
-
+ * NAV of this fund
+
+ @if (mfSwitchFromRate; as r) {
+ Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}
+ }
+
+
+ * Switch into Scheme
+
-- Select --
- @for (p of mfProductOptions; track p.productCode) {
- {{ p.productName }} ({{ p.amcName }})
+ @for (x of mfSwitchTargets; track x.scriptCode) {
+ {{ x.scriptCode }} — {{ x.scriptName }}
+ }
+
+
+
+ * NAV of the new scheme
+
+ @if (mfSwitchToRate; as r) {
+ Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}
+ }
+
+
+ * Sub Type of the new scheme
+
+ @for (s of mfSwitchSubTypes; track s.subTypeCode) {
+ {{ s.subTypeName }}
}
Units to Transfer *
- Transfer Date *
- Transfer Value (computed) Remarks
+ @if (mfFromHolding && mfSwitchToScript && mfSwitchValue) {
+ Accounting (dated {{ mfSwitchDate }}, no bank entry)
+
+ 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 }}
+
+
+ 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' }}
+ }
+
+ }
- Save
+
+ Save
+
Exit
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts
index d2c13ad57..edd0056dc 100644
--- a/src/app/investment/bond-investment/bond-investment.component.ts
+++ b/src/app/investment/bond-investment/bond-investment.component.ts
@@ -20,7 +20,6 @@ import {
SUBTYPE_MASTER,
BROKER_MASTER,
SGL_BANK_MASTER,
- MUTUAL_FUND_PRODUCTS,
LIEN_REASON_MASTER,
CATEGORY_OPTIONS,
DEAL_TYPE_OPTIONS,
@@ -73,6 +72,7 @@ import {
InvestmentSecurityTransferDto,
InvestmentNonSlrLimitDto,
InvestmentService,
+ InvestmentValuationDto,
NON_SLR_TYPES,
apiErrorMessage,
toHolding,
@@ -175,7 +175,7 @@ interface AccountingPreviewLine {
* 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,
* 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({
selector: 'mifosx-bond-investment',
@@ -278,7 +278,8 @@ export class BondInvestmentComponent implements OnInit {
'REDEEM',
'TRANSFER',
'AMORTIZE',
- 'WRITE_OFF'
+ 'WRITE_OFF',
+ 'SWITCH'
].includes(t.txnType)
);
this.txns = [
@@ -311,7 +312,6 @@ export class BondInvestmentComponent implements OnInit {
}
readonly brokerOptions = BROKER_MASTER;
readonly sglBankOptions = SGL_BANK_MASTER;
- readonly mfProductOptions = MUTUAL_FUND_PRODUCTS;
readonly lienReasonOptions = LIEN_REASON_MASTER;
readonly categoryOptions = CATEGORY_OPTIONS;
readonly dealTypeOptions = DEAL_TYPE_OPTIONS;
@@ -354,15 +354,9 @@ export class BondInvestmentComponent implements OnInit {
// GOI Category Transfer
- // Investment Depreciation
+ // Investment Depreciation (market valuation of AFS / HFT)
depreciationDate = this.asOnDate;
- depreciationCategory = '';
-
- // Mutual Fund Transfer
- mfFromCertNo = '';
- mfToProduct = '';
- mfTransferUnits: number | null = null;
- mfTransferDate = this.asOnDate;
+ depreciationCategory: 'AFS' | 'HFT' = 'AFS';
readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [
{ value: 'GOVT_SECURITY', label: 'GOVT SECURITY' },
@@ -373,6 +367,9 @@ export class BondInvestmentComponent implements OnInit {
setTab(tab: BondTab): void {
this.activeTab = tab;
+ if (tab === 'depreciation') {
+ this.loadValuationRuns();
+ }
}
setBondType(type: BondType): void {
@@ -1737,123 +1734,245 @@ export class BondInvestmentComponent implements OnInit {
});
}
- // -------------------- Investment Depreciation --------------------
- /** 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 }> {
- return this.holdings
- .filter((h) => h.status === 'ACTIVE' && h.instrumentType !== 'MUTUAL_FUND')
- .map((h) => {
- const md = this.scriptOptions.find((s) => s.scriptCode === h.scriptCode);
- const marketPrice = md ? h.purchaseRate * 0.995 : h.purchaseRate;
- const depreciation =
- marketPrice < h.purchaseRate ? Math.round((h.faceValue * (h.purchaseRate - marketPrice)) / 100) : 0;
- return { holding: h, marketPrice: +marketPrice.toFixed(2), depreciation };
- })
- .filter((r) => r.depreciation > 0);
+ // -------------------- Investment Depreciation (market valuation, MTM) --------------------
+ /**
+ * RBI: AFS holdings are valued at market quarterly and HFT monthly (or more often); HTM is carried at cost. The backend
+ * values each security at the Daily Script Master rate, nets gains and losses within each class, and posts only the
+ * change in the Investment Depreciation Reserve. The book value of the holdings does not change.
+ */
+ valuationPreview: InvestmentValuationDto | null = null;
+ valuationError = '';
+ valuationLoading = false;
+ valuationPosting = false;
+ valuationRuns: InvestmentValuationDto[] = [];
+ /** A past run opened from the history, with its lines. */
+ valuationOpened: InvestmentValuationDto | null = null;
+
+ readonly valuationClassLabels: Record = {
+ 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 {
- return this.depreciationTargets.reduce((s, r) => s + r.depreciation, 0);
- }
-
- runDepreciation(): void {
- const targets = this.depreciationTargets;
- if (targets.length === 0) {
- this.toast('No holdings require depreciation provisioning.');
+ previewValuation(): void {
+ this.valuationPreview = null;
+ this.valuationError = '';
+ if (!this.depreciationDate || !this.depreciationCategory) {
+ this.valuationError = 'Enter the valuation date and choose AFS or HFT.';
return;
}
- const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
- certNo: t.holding.certNo,
- instrumentType: t.holding.instrumentType,
- txnType: 'AMORTIZE',
- voucherNo: `INV/DEPR/${this.depreciationDate.replace(/-/g, '')}-${i + 1}`,
- txnDate: this.depreciationDate,
- amount: t.depreciation,
- particulars: `Investment depreciation provision - ${t.holding.scriptName} (market ${t.marketPrice})`
- }));
- this.txns = [
- ...newTxns,
- ...this.txns
- ];
- this.toast(
- `Depreciation provisioned for ${targets.length} holding(s). Total ₹${this.depreciationTotal.toLocaleString('en-IN')}.`
- );
+ this.valuationLoading = true;
+ this.investmentService.previewValuation(this.depreciationDate, this.depreciationCategory).subscribe({
+ next: (v) => {
+ this.valuationLoading = false;
+ this.valuationPreview = v;
+ },
+ error: (err) => {
+ this.valuationLoading = false;
+ this.valuationError = apiErrorMessage(err);
+ }
+ });
}
- // -------------------- 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[] {
- 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 {
- 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;
- if (!h || !this.mfTransferUnits) {
+ if (!h || !this.mfSwitchUnits || !h.units) {
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;
- if (!h || !this.mfToProduct || !this.mfTransferUnits) {
- this.toast('Select source holding, target product and units to transfer.');
+ const to = this.mfSwitchToScript;
+ 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;
}
- if (this.mfTransferUnits > h.units) {
- this.toast(`Units cannot exceed units held (${h.units}).`);
+ if (this.mfSwitchSaving) {
return;
}
- const value = this.mfTransferValue;
- h.units = +(h.units - this.mfTransferUnits).toFixed(3);
- h.faceValue = Math.round(h.units * h.purchaseRate);
- if (h.units <= 0) {
- h.status = 'SOLD';
- }
- const product = this.mfProductOptions.find((p) => p.productName === this.mfToProduct);
- const newCert = `MF-${5000 + this.holdings.filter((x) => x.instrumentType === 'MUTUAL_FUND').length + 1}`;
- this.holdings = [
- {
- certNo: newCert,
- instrumentType: 'MUTUAL_FUND',
- scriptCode: product?.productCode || this.mfToProduct,
- scriptName: this.mfToProduct,
- party: product?.amcName || '',
- subType: product?.category || 'DEBT',
- faceValue: value,
- purchaseRate: h.purchaseRate,
- units: this.mfTransferUnits,
- purchaseDate: this.mfTransferDate,
- maturityDate: addPeriod(this.mfTransferDate, 1, 0),
- interestRate: h.interestRate,
- interestFreq: 'CUMULATIVE',
- glCode: '310050',
- glName: 'INVESTMENT IN MUTUAL FUNDS',
- crToAc: h.crToAc,
- brokerCode: '',
- status: 'ACTIVE'
- },
- ...this.holdings
- ];
- this.txns = [
- {
- 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;
+ this.mfSwitchSaving = true;
+ this.investmentService
+ .switchFund(h.id, {
+ switchDate: this.mfSwitchDate,
+ units: this.mfSwitchUnits,
+ fromNav: this.mfSwitchFromNav,
+ toScriptId: to.id,
+ toNav: this.mfSwitchToNav,
+ subType: this.mfSwitchSubType,
+ ...(this.mfSwitchRemarks ? { remarks: this.mfSwitchRemarks } : {}),
+ dateFormat: 'dd-MM-yyyy',
+ locale: 'en'
+ })
+ .subscribe({
+ next: (res) => {
+ this.mfSwitchSaving = false;
+ this.toast(
+ `Switched ${this.mfSwitchUnits} units of deal ${h.dealNo} into ${to.scriptCode} — new deal ${res.resourceIdentifier ?? ''}.`
+ );
+ this.mfSwitchPurchaseId = null;
+ this.mfSwitchUnits = null;
+ this.mfSwitchFromNav = null;
+ this.mfSwitchToScriptCode = '';
+ this.mfSwitchToNav = null;
+ this.mfSwitchRemarks = '';
+ this.mfSwitchFromRate = null;
+ this.mfSwitchToRate = null;
+ this.loadPurchases();
+ },
+ error: (err) => {
+ this.mfSwitchSaving = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
}
// -------------------- Helpers --------------------
diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts
index a9d718630..815f88058 100644
--- a/src/app/investment/investment-data.ts
+++ b/src/app/investment/investment-data.ts
@@ -29,6 +29,7 @@ export type TxnType =
| 'WRITE_OFF'
| 'AMORTIZE'
| 'TRANSFER'
+ | 'SWITCH'
| 'PART_PAYMENT'
| 'PREMATURE'
| 'RENEWAL';
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
index 5eb2b2889..bac963d46 100644
--- a/src/app/investment/investment.service.ts
+++ b/src/app/investment/investment.service.ts
@@ -481,6 +481,36 @@ export class InvestmentService {
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 {
+ return this.http.get('/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 {
+ return this.http.get('/investment/valuations');
+ }
+
+ getValuation(id: number): Observable {
+ return this.http.get(`/investment/valuations/${id}`);
+ }
+
/** Interest lines already posted (coupons and provisions), newest first. */
getInterestPostings(): Observable {
return this.http.get('/investment/interest-postings');
@@ -826,6 +856,20 @@ export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
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';
// A Govt Security redeemed below book writes off its premium; a non-SLR shortfall is a loss (500605).
const pl =
@@ -1040,7 +1084,7 @@ export interface InvestmentSaleDto {
bondType: string;
scriptCode: string;
scriptDesc: string;
- saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE';
+ saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE' | 'SWITCH';
partyId?: number;
partyName?: string;
dealDate: string;
@@ -1075,6 +1119,47 @@ export interface InvestmentSaleDto {
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. */
export interface InvestmentSecurityTransferDto {
id: number;