From d26cb89c5a656f870a17e765e76b7ff79a518551 Mon Sep 17 00:00:00 2001 From: anshul Date: Tue, 27 Oct 2026 15:03:59 +0530 Subject: [PATCH] feat(investment): MTM valuation and mutual fund switch screens Co-Authored-By: Claude Opus 5.5 --- .../bond-investment.component.html | 285 ++++++++++++--- .../bond-investment.component.ts | 333 ++++++++++++------ src/app/investment/investment-data.ts | 1 + src/app/investment/investment.service.ts | 87 ++++- 4 files changed, 551 insertions(+), 155 deletions(-) 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') {
- +
- -
+

+ 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. +

- + +
+ @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. +

+ } + + + + + + + + + + + + @for (c of v.classes; track c.assetClass) { + + + + + + + + } + +
ClassBook ValueMarket ValueNet Gain / LossIDR Needed
{{ 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). + } +

+ + + + + + + + + + + + + + + + @for (l of v.lines; track l.purchaseId) { + + + + + + + + + + + + } @empty { + + + + } + +
Deal NoScriptClassUnitsBook ValueRate / NAVRate DateMarket ValueGain / Loss
+ {{ 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' }}
No {{ v.category }} holding with a rate on {{ v.valuationDate }}.
+ } +
+
+ - - - - - + + + + + + + + + - @for (t of depreciationTargets; track t.holding.certNo) { + @for (r of valuationRuns; track r.id) { + + + + + + + + - - - - } @empty { - + }
Cert NoScriptBook RateMarket PriceDepreciationDateCategoryGovt SecuritiesCorporate BondsMutual FundsIDR NeededPostedVoucher
{{ 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 }} + {{ t.holding.scriptName }}{{ t.holding.purchaseRate }}{{ t.marketPrice }}₹{{ t.depreciation | number: '1.2-2' }}
No holdings require depreciation provisioning.No valuation run yet.
+ @if (valuationOpened; as o) { + + + + + + + + + + + + + + + + @for (l of o.lines; track l.purchaseId) { + + + + + + + + + + + } @empty { + + + + } + +
Deal NoScriptUnitsBook ValueRate / NAVRate DateMarket ValueGain / Loss
+ {{ 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' }}
No holding was valued (the run only wrote the reserve back).
+ }
} @@ -1615,40 +1759,87 @@
@if (mfFromHolding; as h) { - - + + } + + + + + - -
+ @if (mfFromHolding && mfSwitchToScript && mfSwitchValue) { + +

+ 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' }} + } +

+ }
- +
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;