From 8ed8f92b000ce3cee8f12e49a4dc4caf30ce51e3 Mon Sep 17 00:00:00 2001 From: anshul Date: Thu, 15 Oct 2026 18:45:21 +0530 Subject: [PATCH] feat(investment): sell, redemption, write-off, GOI transfer and amortize screens --- .../bond-investment.component.html | 622 +++++++++---- .../bond-investment.component.ts | 856 ++++++++++-------- src/app/investment/investment-data.ts | 3 + src/app/investment/investment.service.ts | 179 +++- 4 files changed, 1102 insertions(+), 558 deletions(-) diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html index 37c544297..faae95c05 100644 --- a/src/app/investment/bond-investment/bond-investment.component.html +++ b/src/app/investment/bond-investment/bond-investment.component.html @@ -136,6 +136,7 @@ Party Units Face Value + Book Value Rate Settlement Date Maturity Date @@ -155,6 +156,7 @@ {{ h.party }} {{ h.units | number }} ₹{{ h.faceValue | number: '1.2-2' }} + ₹{{ carryingValue(h) | number: '1.2-2' }} {{ h.interestRate }}% {{ h.purchaseDate }} {{ h.maturityDate }} @@ -177,7 +179,7 @@ } @empty { - No {{ instrumentLabel(bondType) }} holdings yet. + No {{ instrumentLabel(bondType) }} holdings yet. } @@ -650,40 +652,79 @@ @if (activeTab === 'redemption') {
- +
- @if (redeemHolding; as h) { - + @if (redeemQuote; as q) { + + + + + } - - -
+ @if (redeemError) { +

{{ redeemError }}

+ } + @if (redeemQuote; as q) { + +

+ Dr {{ redeemBankGlCode }} Bank ₹{{ q.netAmount | number: '1.2-2' }} · Cr {{ q.investmentGlCode }} Investment + ₹{{ q.bookValueSold - (splitPremium(q) ? q.premiumCleared! : 0) | number: '1.2-2' }} + @if (splitPremium(q)) { + · Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }} + } + @if (q.profitLoss > 0) { + · Cr 400405 Profit on Redemption ₹{{ q.profitLoss | number: '1.2-2' }} + } @else if (q.profitLoss < 0) { + · Dr 500604 Premium Amortisation ₹{{ -q.profitLoss | number: '1.2-2' }} + } +

+ }
- +
@@ -920,175 +961,240 @@ @if (activeTab === 'sell') {
- +
- - - -
-
- - - - - - - - - - - - - - - - - - - +
+ @if (saleIsHtm) { +

+ HTM securities are not sold except under liquidity stress, with the Board's permission and the reason + recorded. +

+ } + @if (saleQuoteError) { +

{{ saleQuoteError }}

+ } + @if (saleQuote; as q) { + +
+ + + + + +
+

+ On Settle: Dr Bank ₹{{ q.netAmount | number: '1.2-2' }} + @if (q.brokerage) { + · Dr 500602 Brokerage ₹{{ q.brokerage | number: '1.2-2' }} + } + · Cr {{ q.investmentGlCode }} Investment ₹{{ + q.bookValueSold - (splitPremium(q) ? q.premiumCleared! : 0) | number: '1.2-2' + }} + @if (splitPremium(q)) { + · Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }} + } + @if (q.profitLoss > 0) { + · Cr 400405 Profit ₹{{ q.profitLoss | number: '1.2-2' }} + } @else if (q.profitLoss < 0) { + · Dr 500605 Loss ₹{{ -q.profitLoss | number: '1.2-2' }} + } +

+ @if (q.accruedInterest) { +

+ Interest — manual entry: the buyer also pays interest for {{ q.accruedDays }} days since + the last coupon, ₹{{ q.accruedInterest | number: '1.2-2' }}. It is not in this voucher; when it reaches the + bank, pass a journal entry: Dr Bank ₹{{ q.accruedInterest | number: '1.2-2' }} + @if (q.receivableCleared) { + · Cr 100603 Int. Receivable ₹{{ q.receivableCleared | number: '1.2-2' }} (already provisioned) + } + · Cr Int. Income ₹{{ q.interestIncome | number: '1.2-2' }} +

+ } + }
- - - - + + +
+ + + + + + + + + + + + + + + + + + @for (s of pendingSalesForType; track s.id) { + + + + + + + + + + + + } @empty { + + + + } + +
Deal NoScriptBuyerUnitsRateSettlement DateNet AmountProfit / Loss
+ {{ s.dealNo }} + {{ s.scriptCode }} — {{ s.scriptDesc }}{{ s.partyName }}{{ s.units | number: '1.0-6' }}{{ s.saleRate }}{{ s.settlementDate }}₹{{ s.netAmount | number: '1.2-2' }}₹{{ s.profitLoss | number: '1.2-2' }} + + +
No pending sales.
} @if (activeTab === 'writeoff') {
- +
@if (writeoffHolding; as h) { + + + + } -
+ @if (writeoffHolding && writeoffAmount) { +

+ Voucher: Dr 500605 Loss on Investments ₹{{ writeoffAmount | number: '1.2-2' }} / Cr + {{ writeoffHolding!.glCode }} Investment ₹{{ writeoffAmount | number: '1.2-2' }}. The loss is an expense of + the year; the units stay held and later sales or the redemption work from the lower book value. +

+ }
- +
@@ -1097,33 +1203,104 @@ @if (activeTab === 'amortize') {
- +
- @if (amortizeHolding; as h) { + @if (amortSchedule; as d) { + + + } -
+ @if (!premiumHoldings.length) { +

+ No active HTM holding was bought above face value, so there is no premium to amortise. +

+ } + @if (amortSchedule; as d) { +

+ Each 31 March: premium still to amortise × days in the period ÷ days left to maturity. Voucher: Dr 500604 + Amortisation of Premium / Cr {{ d.premiumGlCode }}. The last part is written off with the redemption. +

+ + + + + + + + + + + + + + @for (r of d.rows; track r.fyEnd) { + + + + + + + + + + } + +
Year EndPeriodDaysAmortisationPremium LeftStatus
{{ r.kind === 'MATURITY' ? 'Maturity ' + r.fyEnd : r.fyEnd }}{{ r.fromDate }} → {{ r.toDate }}{{ r.days }}₹{{ r.amount | number: '1.2-2' }} + {{ + r.premiumAfter === null || r.premiumAfter === undefined + ? '' + : '₹' + (r.premiumAfter | number: '1.2-2') + }} + + @if (r.posted) { + Posted {{ r.voucherNo }} + } @else if (r.kind === 'MATURITY') { + With redemption + } @else { + Projected + } + + @if (r.postable) { + + } +
+ }
-
@@ -1132,49 +1309,65 @@ @if (activeTab === 'amortizeYearly') {
- +
- + +
- + - - - - + + + + - @for (t of amortizeYearlyTargets; track t.holding.certNo) { + @for (d of amortizeDue; track d.purchaseId) { - - - - - + + + + + } @empty { - + } @@ -1185,47 +1378,74 @@ @if (activeTab === 'goiTransfer') {
- +
@if (goiHolding; as h) { - + + + + @if (goiToScript; as to) { + + } + } - - -
+ @if (goiHolding && !goiTargetScripts.length) { +

+ No other active security with the same coupon rate, maturity date and unit value. Add it in Script Master + first. +

+ } + @if (goiHolding && goiToScript; as to) { +

+ @if (to.glCode === goiHolding!.glCode) { + Same GL — the deal moves to {{ to.scriptCode }}; no accounting entry is needed. + } @else { + Voucher on transfer: Dr {{ to.glCode }} / Cr {{ goiHolding!.glCode }} at the book value of the holding; any + premium not yet amortised stays in 100707. + } + Units, book value and the interest schedule stay the same. +

+ }
- +
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts index 157970240..fc1bb99c7 100644 --- a/src/app/investment/bond-investment/bond-investment.component.ts +++ b/src/app/investment/bond-investment/bond-investment.component.ts @@ -10,9 +10,12 @@ import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { forkJoin, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component'; import { SCRIPT_MASTER, + ScriptMaster, PARTY_MASTER, SUBTYPE_MASTER, BROKER_MASTER, @@ -34,6 +37,7 @@ import { STAMP_DUTY_GL, BROKERAGE_GL, OTHER_CHARGES_GL, + PREMIUM_ON_INVESTMENTS_GL, SETTLEMENT_BANK_GL_OPTIONS, periodBetween, parseDDMMYYYY, @@ -62,6 +66,10 @@ import { InvestmentInterestRowDto, InvestmentInterestScheduleDto, InvestmentPurchaseDto, + InvestmentAmortisationDto, + InvestmentAmortisationDueDto, + InvestmentSaleDto, + InvestmentSecurityTransferDto, InvestmentService, apiErrorMessage, toHolding @@ -156,37 +164,6 @@ interface AccountingPreviewLine { amount: number; } -interface BondSellForm { - /** Legacy screen has SELL / REDEEM / MUTUAL FUND SELL radio at the top. */ - mode: 'SELL' | 'REDEEM' | 'MUTUAL_FUND_SELL'; - /** Purchase Search — locate the original purchase by Deal No or ISIN No. */ - searchBy: 'DEAL_NO' | 'ISIN_NO'; - searchValue: string; - bondType: BondType; - scriptCode: string; - certNo: string; - buyerCode: string; - subType: string; - isin: string; - tbillGlAc: string; - dealDate: string; - dealTime: string; - settlementType: 'T+0' | 'T+1' | 'T+2'; - settlementDt: string; - faceValue: number | null; - sellRate: number | null; - tdsAmt: number | null; - unitVal: number | null; - payAmt: number | null; - sellUnit: number | null; - brokerage: number | null; - dbToAc: string; - amortizeAmt: number | null; - recommAuthority: string; - sanctionAuthority: string; - callMoney: boolean; -} - /** * 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, @@ -252,18 +229,103 @@ export class BondInvestmentComponent implements OnInit { /** Posted coupons / provisions join the purchases in the transaction log, latest date first. */ private loadInterestLog(): void { - this.investmentService.getInterestPostings().subscribe({ - next: (postings) => { - const others = this.txns.filter((t) => !(t.txnType === 'INTEREST' && t.voucherNo.startsWith('JV/'))); - this.txns = [ - ...others, - ...postings.map((x) => this.interestTxn(x)) - ].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime()); - }, - error: () => undefined + forkJoin({ + postings: this.investmentService + .getInterestPostings() + .pipe(catchError(() => of([] as InvestmentInterestPostingDto[]))), + sales: this.investmentService.getSales().pipe(catchError(() => of([] as InvestmentSaleDto[]))), + transfers: this.investmentService.getTransfers().pipe(catchError(() => of([] as InvestmentSecurityTransferDto[]))) + }).subscribe(({ postings, sales, transfers }) => { + this.sales = sales; + const others = this.txns.filter( + (t) => ![ + 'INTEREST', + 'SELL', + 'REDEEM', + 'TRANSFER', + 'AMORTIZE', + 'WRITE_OFF' + ].includes(t.txnType) + ); + this.txns = [ + ...others, + ...postings.map((x) => this.interestTxn(x)), + ...sales.map((x) => this.saleTxn(x)), + ...transfers.map((x) => this.transferTxn(x)) + ].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime()); }); } + /** A security transfer shows the book value carried to the new security. */ + private transferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn { + return { + certNo: x.certNo, + instrumentType: x.bondType as InstrumentType, + txnType: 'TRANSFER', + voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`, + txnDate: x.transferDate, + amount: x.bookValue, + particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`, + glCode: x.toGlCode, + glName: x.glTransactionId + ? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})` + : `${x.toGlName} (same GL, no entry)` + }; + } + + /** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */ + private saleTxn(x: InvestmentSaleDto): InvestmentTxn { + if (x.saleType === 'AMORTISE') { + return { + certNo: x.certNo, + instrumentType: x.bondType as InstrumentType, + txnType: 'AMORTIZE', + voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`, + txnDate: x.settlementDate, + amount: x.bookValueSold, + particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`, + glCode: '500604', + glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})` + }; + } + if (x.saleType === 'WRITEOFF') { + return { + certNo: x.certNo, + instrumentType: x.bondType as InstrumentType, + txnType: 'WRITE_OFF', + voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`, + txnDate: x.settlementDate, + amount: x.bookValueSold, + particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`, + glCode: '500605', + glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})` + }; + } + const redeem = x.saleType === 'REDEEM'; + const pl = + x.profitLoss >= 0 + ? `profit ₹${x.profitLoss}` + : redeem + ? `premium written off ₹${-x.profitLoss}` + : `loss ₹${-x.profitLoss}`; + return { + certNo: x.certNo, + instrumentType: x.bondType as InstrumentType, + txnType: x.saleType, + voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`, + txnDate: x.settlementDate, + amount: x.netAmount, + particulars: redeem + ? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})` + : `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`, + glCode: x.status === 'SETTLED' ? x.bankGlCode : '', + glName: + x.status === 'SETTLED' + ? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})` + : 'Not posted (pending settlement)' + }; + } + /** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */ private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn { const coupon = x.lineType === 'COUPON'; @@ -331,13 +393,9 @@ export class BondInvestmentComponent implements OnInit { private toastTimer: ReturnType | undefined; purchaseForm: BondPurchaseForm = this.blankPurchaseForm(); - sellForm: BondSellForm = this.blankSellForm(); readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS; // Redemption Entry - redeemCertNo = ''; - redeemDate = this.asOnDate; - redeemRemarks = ''; // 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every // half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only: @@ -358,21 +416,8 @@ export class BondInvestmentComponent implements OnInit { readonly interestReceivableGl = INTEREST_RECEIVABLE_GL; // Write-off - writeoffCertNo = ''; - writeoffReason = ''; - - // Amortize - amortizeCertNo = ''; - amortizeAmt: number | null = null; - - // Amortize Yearly Process - amortizeYear = String(new Date().getFullYear()); // GOI Category Transfer - goiCertNo = ''; - goiFromCategory = ''; - goiToCategory = ''; - goiTransferDate = this.asOnDate; // Investment Depreciation depreciationDate = this.asOnDate; @@ -412,8 +457,13 @@ export class BondInvestmentComponent implements OnInit { return this.txns.filter((t) => t.instrumentType === this.bondType); } + /** Scripts offered for a new deal — inactive ones are hidden, except the one a retrieved deal already uses. */ get scriptsForType(): typeof SCRIPT_MASTER { - return this.scriptOptions.filter((s) => s.instrumentType === this.purchaseForm.bondType); + return this.scriptOptions.filter( + (s) => + s.instrumentType === this.purchaseForm.bondType && + (s.active !== false || s.scriptCode === this.purchaseForm.scriptCode) + ); } instrumentLabel(type: InstrumentType): string { @@ -512,37 +562,6 @@ export class BondInvestmentComponent implements OnInit { }; } - private blankSellForm(): BondSellForm { - return { - mode: 'SELL', - searchBy: 'DEAL_NO', - searchValue: '', - bondType: this.bondType ?? 'GOVT_SECURITY', - scriptCode: '', - certNo: '', - buyerCode: '', - subType: '', - isin: '', - tbillGlAc: '', - dealDate: this.asOnDate, - dealTime: '', - settlementType: 'T+0', - settlementDt: this.asOnDate, - faceValue: null, - sellRate: null, - tdsAmt: null, - unitVal: 100, - payAmt: null, - sellUnit: null, - brokerage: null, - dbToAc: '310070', - amortizeAmt: null, - recommAuthority: '', - sanctionAuthority: '', - callMoney: false - }; - } - get isMutualFund(): boolean { return this.purchaseForm.bondType === 'MUTUAL_FUND'; } @@ -800,8 +819,18 @@ export class BondInvestmentComponent implements OnInit { return []; } const invGl = this.purchaseInvestmentGlCode; + // Premium over face value sits in its own GL until it is amortised (Jitendra Sir). + const premium = Math.max(0, Math.min(this.purchasePremiumDiscount, book)); const lines: AccountingPreviewLine[] = [ - { side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }]; + { side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book - premium }]; + if (premium > 0) { + lines.push({ + side: 'Dr', + glCode: PREMIUM_ON_INVESTMENTS_GL.glCode, + glName: PREMIUM_ON_INVESTMENTS_GL.glName, + amount: premium + }); + } // Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir). if (accrued > 0) { lines.push({ @@ -1031,38 +1060,62 @@ export class BondInvestmentComponent implements OnInit { } // -------------------- Redemption Entry -------------------- - get redeemHolding(): InvestmentHolding | undefined { - return this.holdingsForType.find((h) => h.certNo === this.redeemCertNo); + redeemPurchaseId: number | null = null; + redeemQuote: InvestmentSaleDto | null = null; + redeemError = ''; + redeemBankGlCode = ''; + redeemRemarks = ''; + redeeming = false; + + /** Settled holdings of this type still held — the ones that can be sold or redeemed. */ + get saleableHoldings(): InvestmentHolding[] { + return this.activeHoldingsForType.filter((h) => !!h.id); } - get redeemAmount(): number { - const h = this.redeemHolding; - return h ? maturityAmount(h) + accruedInterest(h, this.redeemDate) - h.faceValue + h.faceValue : 0; - } - - saveRedemption(): void { - const h = this.redeemHolding; - if (!h) { - this.toast('Select a certificate to redeem.'); + /** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */ + onRedeemDealChange(): void { + this.redeemQuote = null; + this.redeemError = ''; + if (!this.redeemPurchaseId) { return; } - h.status = 'MATURED'; - this.txns = [ - { - certNo: h.certNo, - instrumentType: h.instrumentType, - txnType: 'REDEEM', - voucherNo: `INV/RED/${h.certNo}`, - txnDate: this.redeemDate, - amount: this.redeemAmount, - particulars: `Redemption on maturity - ${h.scriptName}${this.redeemRemarks ? ' - ' + this.redeemRemarks : ''}` + this.investmentService.redemptionQuote(this.redeemPurchaseId).subscribe({ + next: (q) => { + this.redeemQuote = q; + this.redeemBankGlCode = q.bankGlCode; }, - ...this.txns - ]; - this.toast(`${h.certNo} redeemed. Proceeds ₹${this.redeemAmount.toLocaleString('en-IN')}.`); - this.redeemCertNo = ''; - this.redeemRemarks = ''; - this.activeTab = 'holdings'; + error: (err) => (this.redeemError = apiErrorMessage(err)) + }); + } + + /** Posts the face value received at maturity, dated the maturity date, and closes the holding. */ + redeem(): void { + const q = this.redeemQuote; + if (!q || this.redeeming) { + return; + } + const bankGlAccountId = this.glIdByCode.get(this.redeemBankGlCode) ?? q.bankGlAccountId; + this.redeeming = true; + this.investmentService + .redeem(q.purchaseId, { + bankGlAccountId, + ...(this.redeemRemarks ? { remarks: this.redeemRemarks } : {}), + locale: 'en' + }) + .subscribe({ + next: (res) => { + this.redeeming = false; + this.toast(`Deal ${q.dealNo} redeemed — voucher ${res.resourceIdentifier ?? ''}.`); + this.redeemPurchaseId = null; + this.redeemQuote = null; + this.redeemRemarks = ''; + this.loadPurchases(); + }, + error: (err) => { + this.redeeming = false; + this.toast(apiErrorMessage(err)); + } + }); } // -------------------- Interest calculation -------------------- @@ -1174,274 +1227,367 @@ export class BondInvestmentComponent implements OnInit { return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0); } - // -------------------- Sell / Redeem Entry -------------------- - /** Locates the original purchase by Deal No / Cert No or ISIN and fills the sell form. */ - searchPurchaseForSell(): void { - const val = this.sellForm.searchValue.trim(); - if (!val) { - this.toast('Enter a Deal No / Cert No or ISIN to search.'); - return; - } - const h = - this.sellForm.searchBy === 'DEAL_NO' - ? this.holdings.find((x) => x.certNo === val) - : this.holdings.find((x) => this.scriptOptions.find((s) => s.scriptCode === x.scriptCode)?.isin === val); - if (!h) { - this.toast('No matching purchase found.'); - return; - } - this.sellForm = { - ...this.sellForm, - bondType: h.instrumentType as BondType, - scriptCode: h.scriptCode, - certNo: h.certNo, - subType: h.subType, - isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '', - faceValue: h.faceValue, - unitVal: h.instrumentType === 'MUTUAL_FUND' ? h.purchaseRate : 100, - sellUnit: h.units + // -------------------- Sell Entry -------------------- + sale = this.blankSale(); + saleQuote: InvestmentSaleDto | null = null; + saleQuoteError = ''; + savingSale = false; + sales: InvestmentSaleDto[] = []; + + private blankSale() { + return { + purchaseId: null as number | null, + partyCode: '', + dealDate: this.asOnDate, + dealTime: '', + settlementType: 'T+0', + units: null as number | null, + saleRate: null as number | null, + brokerage: null as number | null, + bankGlCode: '', + sanctionAuthority: '', + remarks: '' }; - this.toast(`Purchase ${h.certNo} loaded.`); } - get sellHolding(): InvestmentHolding | undefined { - return this.holdings.find((h) => h.certNo === this.sellForm.certNo); + resetSale(): void { + this.sale = this.blankSale(); + this.saleQuote = null; + this.saleQuoteError = ''; } - /** Consideration = units sold × sell rate, scaled by the instrument's unit convention. */ - get sellConsideration(): number { - const f = this.sellForm; - if (!f.sellUnit || !f.sellRate) { - return 0; - } - const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100; - return Math.round((f.sellUnit * f.sellRate) / perUnitDivisor); + get saleHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.id === this.sale.purchaseId); } - /** Sale Diff — profit/loss versus what the holding was bought at. */ - get sellSaleDiff(): number { - const h = this.sellHolding; - const f = this.sellForm; - if (!h || !f.sellUnit || !f.sellRate) { - return 0; - } - const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100; - const cost = Math.round((f.sellUnit * h.purchaseRate) / perUnitDivisor); - return this.sellConsideration - cost; + /** An HTM security is sold only with the Board's permission, recorded with its reason. */ + get saleIsHtm(): boolean { + return (this.saleHolding?.category ?? '').startsWith('HTM'); } - /** Premium Loss — the unamortised premium written off when a holding is sold early. */ - get sellPremiumLoss(): number { - const h = this.sellHolding; - if (!h || h.purchaseRate <= 100 || h.instrumentType === 'MUTUAL_FUND') { - return 0; - } - return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100); + get pendingSalesForType(): InvestmentSaleDto[] { + return this.sales.filter((s) => s.status === 'PENDING_SETTLEMENT' && s.bondType === this.bondType); } - get sellAccruedInterest(): number { - const h = this.sellHolding; - return h ? accruedInterest(h, this.sellForm.dealDate || this.asOnDate) : 0; + onSaleDealChange(): void { + const h = this.saleHolding; + this.sale.units = h ? h.units : null; + this.sale.bankGlCode = h?.crToAc ?? ''; + this.refreshSaleQuote(); } - get sellAccruedDays(): number { - const h = this.sellHolding; - if (!h) { - return 0; - } - return Math.max(0, Math.round(yearsBetween(h.purchaseDate, this.sellForm.dealDate || this.asOnDate) * 365)); - } - - /** Pay Amt — net amount receivable: consideration + accrued interest − TDS − brokerage. */ - get sellPayAmount(): number { - const f = this.sellForm; - return this.sellConsideration + this.sellAccruedInterest - (f.tdsAmt || 0) - (f.brokerage || 0); - } - - saveSell(): void { - const f = this.sellForm; - const h = this.sellHolding; - if (!h || !f.sellRate || !f.sellUnit) { - this.toast('Load a purchase, then enter Sell Rate and Sell Unit.'); + /** Price, interest from the buyer, cost and profit / loss worked out by the backend, as Settle will post them. */ + refreshSaleQuote(): void { + const f = this.sale; + this.saleQuote = null; + this.saleQuoteError = ''; + if (!f.purchaseId || !f.units || !f.saleRate || !f.dealDate) { return; } - if (f.sellUnit > h.units) { - this.toast(`Sell Unit cannot exceed units held (${h.units}).`); - return; - } - h.units -= f.sellUnit; - h.faceValue = Math.round(h.units * (f.unitVal || 100)); - if (h.units <= 0) { - h.status = f.mode === 'REDEEM' ? 'MATURED' : 'SOLD'; - } - this.txns = [ - { - certNo: h.certNo, - instrumentType: h.instrumentType, - txnType: f.mode === 'REDEEM' ? 'REDEEM' : 'SELL', - voucherNo: `INV/${f.mode === 'REDEEM' ? 'RED' : 'SEL'}/${h.certNo}-${this.txns.length + 1}`, - txnDate: f.dealDate, - amount: this.sellPayAmount, - particulars: `${f.mode.replace(/_/g, ' ')} - ${h.scriptName} @ ${f.sellRate}${f.buyerCode ? ' to ' + f.buyerCode : ''}${f.callMoney ? ' [CALL MONEY]' : ''}` - }, - ...this.txns - ]; - this.toast( - `${h.certNo} ${f.mode === 'REDEEM' ? 'redeemed' : 'sold'}. Pay Amt ₹${this.sellPayAmount.toLocaleString('en-IN')}.` - ); - this.sellForm = this.blankSellForm(); - this.activeTab = 'holdings'; - } - - cancelSell(): void { - this.sellForm = this.blankSellForm(); - this.toast('Sell entry cancelled — form reset.'); - } - - exitSell(): void { - this.sellForm = this.blankSellForm(); - this.activeTab = 'holdings'; - } - - // -------------------- Write-off -------------------- - get writeoffHolding(): InvestmentHolding | undefined { - return this.holdingsForType.find((h) => h.certNo === this.writeoffCertNo); - } - - saveWriteoff(): void { - const h = this.writeoffHolding; - if (!h || !this.writeoffReason) { - this.toast('Select a certificate and enter a write-off reason.'); - return; - } - h.status = 'WRITTEN_OFF'; - this.txns = [ - { - certNo: h.certNo, - instrumentType: h.instrumentType, - txnType: 'WRITE_OFF', - voucherNo: `INV/WOF/${h.certNo}`, - txnDate: this.asOnDate, - amount: h.faceValue, - particulars: `Written off - ${this.writeoffReason}` - }, - ...this.txns - ]; - this.toast(`${h.certNo} written off.`); - this.writeoffCertNo = ''; - this.writeoffReason = ''; - this.activeTab = 'holdings'; - } - - // -------------------- Amortize -------------------- - get amortizeHolding(): InvestmentHolding | undefined { - return this.holdingsForType.find((h) => h.certNo === this.amortizeCertNo); - } - - /** Straight-line premium amortisation for one year on the selected holding. */ - get suggestedAmortizeAmt(): number { - const h = this.amortizeHolding; - if (!h || h.purchaseRate <= 100) { - return 0; - } - const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate)); - return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years); - } - - saveAmortize(): void { - const h = this.amortizeHolding; - const amt = this.amortizeAmt ?? this.suggestedAmortizeAmt; - if (!h || !amt) { - this.toast('Select a certificate and enter the amortization amount.'); - return; - } - this.txns = [ - { - certNo: h.certNo, - instrumentType: h.instrumentType, - txnType: 'AMORTIZE', - voucherNo: `INV/AMZ/${h.certNo}-${this.txns.length + 1}`, - txnDate: this.asOnDate, - amount: amt, - particulars: `Premium/discount amortized - ${h.scriptName}` - }, - ...this.txns - ]; - this.toast(`Amortization of ₹${amt.toLocaleString('en-IN')} booked for ${h.certNo}.`); - this.amortizeCertNo = ''; - this.amortizeAmt = null; - } - - // -------------------- Amortize Yearly Process -------------------- - /** All premium-carrying holdings the yearly batch would amortise. */ - get amortizeYearlyTargets(): Array<{ holding: InvestmentHolding; amount: number }> { - return this.holdings - .filter((h) => h.status === 'ACTIVE' && h.purchaseRate > 100 && h.instrumentType !== 'MUTUAL_FUND') - .map((h) => { - const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate)); - return { holding: h, amount: Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years) }; + const bankGlAccountId = this.glIdByCode.get(f.bankGlCode); + this.investmentService + .quoteSale({ + purchaseId: f.purchaseId, + dealDate: f.dealDate, + settlementType: f.settlementType, + units: f.units, + saleRate: f.saleRate, + ...(f.brokerage ? { brokerage: f.brokerage } : {}), + ...(bankGlAccountId ? { bankGlAccountId } : {}) + }) + .subscribe({ + next: (q) => (this.saleQuote = q), + error: (err) => (this.saleQuoteError = apiErrorMessage(err)) }); } - get amortizeYearlyTotal(): number { - return this.amortizeYearlyTargets.reduce((s, t) => s + t.amount, 0); - } - - runAmortizeYearly(): void { - const targets = this.amortizeYearlyTargets; - if (targets.length === 0) { - this.toast('No premium-carrying holdings to amortise.'); + /** Saves the sale pending; it reaches the GL when settled on its settlement date. */ + saveSale(): void { + const f = this.sale; + const party = this.partyOptions.find((p) => p.partyCode === f.partyCode); + const bankGlAccountId = this.glIdByCode.get(f.bankGlCode); + if (!f.purchaseId || !party?.id || !f.units || !f.saleRate || !bankGlAccountId) { + this.toast('Select the deal, buyer and bank, and enter units and sale rate.'); return; } - const newTxns: InvestmentTxn[] = targets.map((t, i) => ({ - certNo: t.holding.certNo, - instrumentType: t.holding.instrumentType, - txnType: 'AMORTIZE', - voucherNo: `INV/AMZ-YR/${this.amortizeYear}-${i + 1}`, - txnDate: this.asOnDate, - amount: t.amount, - particulars: `Yearly amortization process ${this.amortizeYear} - ${t.holding.scriptName}` - })); - this.txns = [ - ...newTxns, - ...this.txns - ]; - this.toast( - `Yearly amortization run for ${targets.length} holding(s). Total ₹${this.amortizeYearlyTotal.toLocaleString('en-IN')}.` - ); + if (this.saleIsHtm && (!f.sanctionAuthority.trim() || !f.remarks.trim())) { + this.toast('HTM sale needs the Board sanction authority and the reason in Remarks.'); + return; + } + const body = { + purchaseId: f.purchaseId, + partyId: party.id, + dealDate: f.dealDate, + dealTime: f.dealTime || '', + settlementType: f.settlementType, + units: f.units, + saleRate: f.saleRate, + ...(f.brokerage ? { brokerage: f.brokerage } : {}), + bankGlAccountId, + ...(f.sanctionAuthority.trim() ? { sanctionAuthority: f.sanctionAuthority.trim() } : {}), + ...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}), + dateFormat: 'dd-MM-yyyy', + locale: 'en' + }; + this.savingSale = true; + this.investmentService.createSale(body).subscribe({ + next: () => { + this.savingSale = false; + this.toast(`Sale saved — settle it on ${this.saleQuote?.settlementDate ?? 'the settlement date'} to post it.`); + this.sale = this.blankSale(); + this.saleQuote = null; + this.loadInterestLog(); + }, + error: (err) => { + this.savingSale = false; + this.toast(apiErrorMessage(err)); + } + }); + } + + settleSale(s: InvestmentSaleDto): void { + if (!s.id) { + return; + } + this.investmentService.settleSale(s.id).subscribe({ + next: (res) => { + this.toast(`Sale of deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`); + this.loadPurchases(); + }, + error: (err) => this.toast(apiErrorMessage(err)) + }); + } + + dropSale(s: InvestmentSaleDto): void { + if (!s.id) { + return; + } + this.investmentService.deleteSale(s.id).subscribe({ + next: () => { + this.toast(`Pending sale of deal ${s.dealNo} dropped.`); + this.loadInterestLog(); + }, + error: (err) => this.toast(apiErrorMessage(err)) + }); + } + + // -------------------- Write-off -------------------- + writeoffPurchaseId: number | null = null; + writeoffAmount: number | null = null; + writeoffReason = ''; + writingOff = false; + + get writeoffHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.id === this.writeoffPurchaseId); + } + + /** True when a sale or redemption credits the unamortised premium to its own GL (100707), apart from the investment GL. */ + splitPremium(q: InvestmentSaleDto): boolean { + return !!q.premiumCleared && q.premiumCleared > 0 && !!q.premiumGlCode && q.premiumGlCode !== q.investmentGlCode; + } + + /** Book value of a holding as it stands: cost less what sales and write-offs have taken off. */ + carryingValue(h: InvestmentHolding): number { + const taken = this.sales + .filter((x) => x.purchaseId === h.id && x.status === 'SETTLED') + .reduce((sum, x) => sum + x.bookValueSold, 0); + return Math.round(((h.bookValue ?? 0) - taken) * 100) / 100; + } + + /** Writes the loss off at once: Dr 500605 Loss on Investments / Cr Investment; the holding stays at the lower value. */ + saveWriteoff(): void { + const h = this.writeoffHolding; + const amount = this.writeoffAmount; + if (!h?.id || !amount || amount <= 0 || !this.writeoffReason.trim()) { + this.toast('Select the deal, enter the loss amount and the reason.'); + return; + } + if (amount >= this.carryingValue(h)) { + this.toast('The write-off must be less than the book value; sell or redeem the security to close it.'); + return; + } + this.writingOff = true; + this.investmentService.writeOff(h.id, { amount, remarks: this.writeoffReason.trim(), locale: 'en' }).subscribe({ + next: (res) => { + this.writingOff = false; + this.toast(`Deal ${h.dealNo}: ₹${amount} written off — voucher ${res.resourceIdentifier ?? ''}.`); + this.writeoffPurchaseId = null; + this.writeoffAmount = null; + this.writeoffReason = ''; + this.loadPurchases(); + }, + error: (err) => { + this.writingOff = false; + this.toast(apiErrorMessage(err)); + } + }); + } + + // -------------------- Amortize -------------------- + amortizePurchaseId: number | null = null; + amortSchedule: InvestmentAmortisationDto | null = null; + amortizing = false; + + /** Active HTM holdings bought above face value — the ones whose premium is amortised. */ + get premiumHoldings(): InvestmentHolding[] { + return this.saleableHoldings.filter((h) => (h.category ?? '').startsWith('HTM') && h.purchaseRate > 100); + } + + onAmortizeDealChange(): void { + this.amortSchedule = null; + if (this.amortizePurchaseId) { + this.investmentService.getAmortisation(this.amortizePurchaseId).subscribe({ + next: (d) => (this.amortSchedule = d), + error: (err) => this.toast(apiErrorMessage(err)) + }); + } + } + + /** Posts the next due 31 March of the selected holding: Dr 500604 / Cr premium GL. */ + amortizeRow(fyEnd: string): void { + const d = this.amortSchedule; + if (!d) { + return; + } + this.amortizing = true; + this.investmentService + .amortise({ purchaseId: d.purchaseId, fyEnd, locale: 'en', dateFormat: 'dd-MM-yyyy' }) + .subscribe({ + next: (res) => { + this.amortizing = false; + this.toast(`Deal ${d.dealNo}: premium amortised for ${fyEnd} — voucher ${res.resourceIdentifier ?? ''}.`); + this.onAmortizeDealChange(); + this.loadPurchases(); + }, + error: (err) => { + this.amortizing = false; + this.toast(apiErrorMessage(err)); + } + }); + } + + // -------------------- Amortize Yearly Process -------------------- + /** Latest 31 March on or before today. */ + amortizeFyEnd = (() => { + const t = new Date(); + const y = t.getMonth() > 2 || (t.getMonth() === 2 && t.getDate() === 31) ? t.getFullYear() : t.getFullYear() - 1; + return `31-03-${y}`; + })(); + amortizeDue: InvestmentAmortisationDueDto[] = []; + amortizeDueLoaded = false; + + get amortizeDueTotal(): number { + return Math.round(this.amortizeDue.reduce((sum, d) => sum + d.amount, 0) * 100) / 100; + } + + loadAmortizeDue(): void { + this.amortizeDueLoaded = false; + if (!/^31-03-\d{4}$/.test(this.amortizeFyEnd)) { + this.toast('Amortisation is done as on 31 March: enter the year end as 31-03-YYYY.'); + return; + } + this.investmentService.getAmortisationDue(this.amortizeFyEnd).subscribe({ + next: (d) => { + this.amortizeDue = d; + this.amortizeDueLoaded = true; + }, + error: (err) => this.toast(apiErrorMessage(err)) + }); + } + + /** Posts the year's amortisation for every holding listed; one voucher per holding. */ + runAmortizeYearly(): void { + if (!this.amortizeDue.length) { + this.toast('No holding has premium amortisation due for this year end.'); + return; + } + this.amortizing = true; + this.investmentService.amortise({ fyEnd: this.amortizeFyEnd, locale: 'en', dateFormat: 'dd-MM-yyyy' }).subscribe({ + next: (res) => { + this.amortizing = false; + this.toast( + `Amortisation for ${this.amortizeFyEnd} posted for ${this.amortizeDue.length} holding(s) — vouchers ${res.resourceIdentifier ?? ''}.` + ); + this.loadAmortizeDue(); + this.loadPurchases(); + }, + error: (err) => { + this.amortizing = false; + this.toast(apiErrorMessage(err)); + } + }); } // -------------------- GOI Category Transfer -------------------- + // -------------------- GOI Category Transfer -------------------- + goiPurchaseId: number | null = null; + goiToScriptCode = ''; + goiRemarks = ''; + goiSaving = false; + get goiHolding(): InvestmentHolding | undefined { - return this.holdings.find((h) => h.certNo === this.goiCertNo); + return this.holdings.find((h) => h.id === this.goiPurchaseId); } + private get goiFromScript(): ScriptMaster | undefined { + return this.scriptOptions.find((x) => x.scriptCode === this.goiHolding?.scriptCode); + } + + /** Securities the holding can move to: same type, coupon rate, maturity and unit value — only the security changes. */ + get goiTargetScripts(): ScriptMaster[] { + const from = this.goiFromScript; + if (!from) { + return []; + } + return this.scriptOptions.filter( + (x) => + !!x.id && + x.active !== false && + x.scriptCode !== from.scriptCode && + x.instrumentType === from.instrumentType && + x.couponRate === from.couponRate && + x.maturityDate === from.maturityDate && + x.faceValuePerUnit === from.faceValuePerUnit + ); + } + + get goiToScript(): ScriptMaster | undefined { + return this.scriptOptions.find((x) => x.scriptCode === this.goiToScriptCode); + } + + onGoiDealChange(): void { + this.goiToScriptCode = ''; + } + + /** Replaces the holding's security with the new one; the voucher moves the book value only if the GL differs. */ saveGoiTransfer(): void { const h = this.goiHolding; - if (!h || !this.goiFromCategory || !this.goiToCategory) { - this.toast('Select a certificate and both From / To categories.'); + const to = this.goiToScript; + if (!h?.id || !to?.id || this.goiSaving) { + this.toast('Select the deal and the security to transfer to.'); return; } - if (this.goiFromCategory === this.goiToCategory) { - this.toast('From and To categories must differ.'); - return; - } - this.txns = [ - { - certNo: h.certNo, - instrumentType: h.instrumentType, - txnType: 'AMORTIZE', - voucherNo: `INV/GOI-TRF/${h.certNo}-${this.txns.length + 1}`, - txnDate: this.goiTransferDate, - amount: h.faceValue, - particulars: `GOI category transfer ${this.goiFromCategory} → ${this.goiToCategory} - ${h.scriptName}` - }, - ...this.txns - ]; - this.toast(`${h.certNo} transferred from ${this.goiFromCategory} to ${this.goiToCategory}.`); - this.goiCertNo = ''; - this.goiFromCategory = ''; - this.goiToCategory = ''; + this.goiSaving = true; + this.investmentService + .transferSecurity(h.id, { + scriptId: to.id, + ...(this.goiRemarks ? { remarks: this.goiRemarks } : {}), + locale: 'en' + }) + .subscribe({ + next: (res) => { + this.goiSaving = false; + this.toast(`Deal ${h.dealNo} transferred to ${to.scriptCode} — ${res.resourceIdentifier ?? ''}.`); + this.goiPurchaseId = null; + this.goiToScriptCode = ''; + this.goiRemarks = ''; + this.loadPurchases(); + }, + error: (err) => { + this.goiSaving = false; + this.toast(apiErrorMessage(err)); + } + }); } // -------------------- Investment Depreciation -------------------- diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts index 34e830788..77d7e95a7 100644 --- a/src/app/investment/investment-data.ts +++ b/src/app/investment/investment-data.ts @@ -28,6 +28,7 @@ export type TxnType = | 'REDEEM' | 'WRITE_OFF' | 'AMORTIZE' + | 'TRANSFER' | 'PART_PAYMENT'; export const INSTRUMENT_LABELS: Record = { @@ -1526,6 +1527,8 @@ export const BROKEN_PERIOD_INTEREST_GL: InvestmentGlOption = { export const STAMP_DUTY_GL: InvestmentGlOption = { glCode: '500601', glName: 'STAMP DUTY ON INVESTMENTS' }; export const BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE ON INVESTMENTS' }; export const OTHER_CHARGES_GL: InvestmentGlOption = { glCode: '500603', glName: 'OTHER CHARGES ON INVESTMENTS' }; +/** Premium paid over face value, held until amortised to maturity (Jitendra Sir). */ +export const PREMIUM_ON_INVESTMENTS_GL: InvestmentGlOption = { glCode: '100707', glName: 'PREMIUM ON INVESTMENTS' }; /** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */ export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = { diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts index 2ff933c63..4a450142e 100644 --- a/src/app/investment/investment.service.ts +++ b/src/app/investment/investment.service.ts @@ -175,6 +175,68 @@ export class InvestmentService { } /** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */ + /** GOI Category Transfers, newest first. */ + getTransfers(): Observable { + return this.http.get('/investment/transfers'); + } + + /** Moves a holding to another security of the same terms; a voucher is raised only when the GL changes. */ + transferSecurity(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> { + return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/transfer`, body); + } + + /** Sales and redemptions, newest first (pending sales included). */ + getSales(): Observable { + return this.http.get('/investment/sales'); + } + + /** What a sale would come to before it is saved (dealDate dd-MM-yyyy). */ + quoteSale(params: Record): Observable { + return this.http.get('/investment/sales/quote', { params }); + } + + createSale(body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> { + return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/sales', body); + } + + /** Posts a pending sale on or after its settlement date; the voucher number comes back as resourceIdentifier. */ + settleSale(saleId: number): Observable<{ resourceIdentifier?: string }> { + return this.http.post<{ resourceIdentifier?: string }>(`/investment/sales/${saleId}/settle`, {}); + } + + deleteSale(saleId: number): Observable { + return this.http.delete(`/investment/sales/${saleId}`); + } + + /** Premium amortisation schedule of a holding (06 Amortize Entry). */ + getAmortisation(purchaseId: number): Observable { + return this.http.get(`/investment/amortisation/${purchaseId}`); + } + + /** Holdings due for amortisation on a 31 March (07 Amortize Yearly Process). */ + getAmortisationDue(fyEnd: string): Observable { + return this.http.get(`/investment/amortisation/due?fyEnd=${fyEnd}`); + } + + /** Posts amortisation for a 31 March: one holding when purchaseId is given, else every holding due. */ + amortise(body: object): Observable<{ resourceIdentifier?: string }> { + return this.http.post<{ resourceIdentifier?: string }>('/investment/amortisation', body); + } + + /** Writes part of a holding's book value off as a loss (Dr Loss / Cr Investment); the units stay held. */ + writeOff(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> { + return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/write-off`, body); + } + + /** What the redemption at maturity will post; refused before maturity. */ + redemptionQuote(purchaseId: number): Observable { + return this.http.get(`/investment/purchases/${purchaseId}/redemption-quote`); + } + + redeem(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> { + return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/redeem`, body); + } + /** Interest lines already posted (coupons and provisions), newest first. */ getInterestPostings(): Observable { return this.http.get('/investment/interest-postings'); @@ -334,7 +396,8 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding { party: p.partyName, partyCode: p.partyCode, subType: p.subType ?? '', - faceValue: p.faceValue, + // Face value still held after any part sale. + faceValue: p.units ? Math.round((p.faceValue * p.balanceUnits * 100) / p.units) / 100 : p.faceValue, purchaseRate: p.purchaseRate, units: p.balanceUnits, purchaseDate: p.settlementDate, @@ -345,7 +408,7 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding { glName: p.investmentGlName, crToAc: p.crGlCode, brokerCode: '', - status: p.status as HoldingStatus, + status: (p.status === 'REDEEMED' ? 'MATURED' : p.status) as HoldingStatus, settlementDate: p.settlementDate, lastIntDate: p.lastIntDate, glTransactionId: p.glTransactionId, @@ -378,6 +441,118 @@ export interface InvestmentInterestRowDto { } /** A holding's next interest line that is due (or due within the reminder window) — a task to post. */ +/** A sale (before maturity) or redemption (at maturity) of a holding; also the shape of a quote before saving. */ +/** One line of a holding's premium amortisation schedule (31 March, or the remainder at maturity). */ +export interface InvestmentAmortisationRowDto { + kind: 'YEAR' | 'MATURITY'; + fyEnd: string; + fromDate: string; + toDate: string; + days: number; + amount: number; + premiumAfter?: number; + posted: boolean; + voucherNo?: string; + glTransactionId?: string; + postable: boolean; +} + +export interface InvestmentAmortisationDto { + purchaseId: number; + dealNo: string; + certNo: string; + scriptCode: string; + scriptDesc: string; + category: string; + settlementDate: string; + maturityDate: string; + faceValue: number; + premium: number; + unamortisedPremium: number; + bookValue: number; + premiumGlCode: string; + rows: InvestmentAmortisationRowDto[]; +} + +/** A holding whose premium amortisation falls due on a 31 March. */ +export interface InvestmentAmortisationDueDto { + purchaseId: number; + dealNo: string; + certNo: string; + scriptCode: string; + scriptDesc: string; + fyEnd: string; + fromDate: string; + days: number; + unamortisedPremium: number; + amount: number; +} + +export interface InvestmentSaleDto { + id?: number; + purchaseId: number; + dealNo: string; + certNo: string; + bondType: string; + scriptCode: string; + scriptDesc: string; + saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE'; + partyId?: number; + partyName?: string; + dealDate: string; + dealTime?: string; + settlementType?: string; + settlementDate: string; + units: number; + faceValue: number; + saleRate: number; + consideration: number; + accruedDays: number; + accruedInterest: number; + receivableCleared: number; + interestIncome: number; + brokerage: number; + bookValueSold: number; + /** Part of the book value taken off that was unamortised premium. */ + premiumCleared?: number; + /** GL the premium share is credited to (100707, or the investment GL on older deals). */ + premiumGlCode?: string; + profitLoss: number; + netAmount: number; + bankGlAccountId: number; + bankGlCode: string; + bankGlName: string; + investmentGlCode: string; + sanctionAuthority?: string; + remarks?: string; + status: 'PENDING_SETTLEMENT' | 'SETTLED'; + glTransactionId?: string; + voucherNo?: string; + settledOnDate?: string; +} + +/** A GOI Category Transfer: a holding moved to another security of the same terms. */ +export interface InvestmentSecurityTransferDto { + id: number; + purchaseId: number; + dealNo: string; + certNo: string; + bondType: string; + fromScriptCode: string; + fromScriptDesc: string; + toScriptCode: string; + toScriptDesc: string; + fromGlCode: string; + toGlCode: string; + toGlName: string; + transferDate: string; + units: number; + bookValue: number; + remarks?: string; + glTransactionId?: string; + voucherNo?: string; +} + /** An interest line already posted to GL with its JV voucher. */ export interface InvestmentInterestPostingDto { id: number;
Cert NoDeal No ScriptFace ValuePurchase RateMaturity DateYearly AmortizationPeriodDaysPremium LeftAmortisation
- {{ t.holding.certNo }} + {{ d.dealNo }} {{ t.holding.scriptName }}₹{{ t.holding.faceValue | number: '1.2-2' }}{{ t.holding.purchaseRate }}{{ t.holding.maturityDate }}₹{{ t.amount | number: '1.2-2' }}{{ d.scriptCode }} {{ d.scriptDesc }}{{ d.fromDate }} → {{ d.fyEnd }}{{ d.days }}₹{{ d.unamortisedPremium | number: '1.2-2' }}₹{{ d.amount | number: '1.2-2' }}
No premium-carrying holdings require amortization. + {{ + amortizeDueLoaded + ? 'No holding has amortisation due for ' + amortizeFyEnd + '.' + : 'Enter the year end and click Show Due.' + }} +