feat(investment): due date diary, script status, deal no by deal date, matured-only redemption, keep typed rate and business-rule error title

This commit is contained in:
2026-10-22 17:39:50 +05:30
parent 6890584b16
commit d89f13471c
11 changed files with 423 additions and 13 deletions
@@ -664,10 +664,13 @@
<span>* Deal No</span>
<select [(ngModel)]="redeemPurchaseId" (ngModelChange)="onRedeemDealChange()">
<option [ngValue]="null">-- Select --</option>
@for (h of saleableHoldings; track h.certNo) {
@for (h of redeemableHoldings; track h.certNo) {
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptName }} (matures {{ h.maturityDate }})</option>
}
</select>
@if (!redeemableHoldings.length) {
<small>No holding has reached its maturity date yet.</small>
}
</label>
@if (redeemQuote; as q) {
<label class="inv-form-field"
@@ -190,9 +190,10 @@ export class BondInvestmentComponent implements OnInit {
/** Script and party dropdowns offer the masters saved in the backend. */
ngOnInit(): void {
// The home-screen interest alert links here with ?tab=interest.
if (this.route.snapshot.queryParamMap.get('tab') === 'interest') {
this.activeTab = 'interest';
// The home-screen alert and the Due Date Diary link here with ?tab=interest / redemption / amortize.
const tab = this.route.snapshot.queryParamMap.get('tab');
if (tab === 'interest' || tab === 'redemption' || tab === 'amortize') {
this.activeTab = tab;
}
this.investmentService.syncSharedMasters().subscribe({
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
@@ -470,9 +471,9 @@ export class BondInvestmentComponent implements OnInit {
return INSTRUMENT_LABELS[type];
}
/** Preview of the deal number the backend will assign: FY prefix (e.g. 202627) + running number. */
private nextDealNo(): string {
const d = parseDDMMYYYY(this.asOnDate);
/** Preview of the deal number the backend will assign: FY of the deal date (e.g. 202627) + running number. */
private nextDealNo(dealDate?: string): string {
const d = parseDDMMYYYY(dealDate || this.asOnDate);
const fyStart = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
const prefix = `${fyStart}${String((fyStart + 1) % 100).padStart(2, '0')}`;
const nums = (this.holdings ?? [])
@@ -668,6 +669,11 @@ export class BondInvestmentComponent implements OnInit {
/** Re-suggests the last coupon date when the deal date changes. */
onDealDateChange(): void {
const f = this.purchaseForm;
// A new deal takes its number from the deal date's financial year; a saved deal keeps its own.
if (f.dealDate && !this.holdings.some((h) => h.dealNo === f.dealNo)) {
f.dealNo = this.nextDealNo(f.dealDate);
}
this.suggestLastIntDate();
this.loadMarketRate();
}
@@ -754,6 +760,9 @@ export class BondInvestmentComponent implements OnInit {
*/
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
/** The rate this screen last filled in from the Daily Script Master, to tell it apart from one the user typed. */
private autoFilledRate: number | null = null;
private loadMarketRate(): void {
const s = this.purchaseScript;
const f = this.purchaseForm;
@@ -764,12 +773,16 @@ export class BondInvestmentComponent implements OnInit {
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
next: (rate) => {
this.purchaseMarketRate = rate;
// No market rate: clear the field rather than leave another script's rate in it.
const current = this.isMutualFund ? f.nav : f.purchaseRate;
// A market rate fills the field. Without one, only a rate this screen filled in earlier (another script's or
// another day's) is cleared; a rate the user typed stays.
const next = rate ? rate.closeRate : current === this.autoFilledRate ? null : current;
if (this.isMutualFund) {
f.nav = rate ? rate.closeRate : null;
f.nav = next;
} else {
f.purchaseRate = rate ? rate.closeRate : null;
f.purchaseRate = next;
}
this.autoFilledRate = rate ? rate.closeRate : null;
},
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
});
@@ -1080,6 +1093,12 @@ export class BondInvestmentComponent implements OnInit {
return this.activeHoldingsForType.filter((h) => !!h.id);
}
/** Redemption is at maturity, so only holdings that have reached it are offered. */
get redeemableHoldings(): InvestmentHolding[] {
const today = parseDDMMYYYY(this.asOnDate);
return this.saleableHoldings.filter((h) => !!h.maturityDate && parseDDMMYYYY(h.maturityDate) <= today);
}
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
onRedeemDealChange(): void {
this.redeemQuote = null;