+ 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' }}
+
+ }
+ }
-
-
-
-
+
+
+
+
+
Pending Sales — settle on or after the settlement date
+
+
+
+
Deal No
+
Script
+
Buyer
+
Units
+
Rate
+
Settlement Date
+
Net Amount
+
Profit / Loss
+
+
+
+
+ @for (s of pendingSalesForType; track s.id) {
+
+
+ {{ 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' }}
+
+
+
+
+
+ } @empty {
+
+
No pending sales.
+
+ }
+
+
}
@if (activeTab === 'writeoff') {
-
Investment - Write Off Entry Gov/Bond
+
+ Investment - Write Off Entry Gov/Bond (book the loss when the value has fallen)
+
@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.
+
+ Investment - Amortize Entry (premium of HTM securities written off evenly to maturity, every 31 March)
+
- @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.
+
+ 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;