feat(investment): non-SLR bonds and mutual funds, rate upload, UAT fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 11:14:41 +05:30
co-authored by Claude Opus 5.5
parent 8e98077158
commit 13d04353e1
13 changed files with 486 additions and 111 deletions
@@ -9,6 +9,7 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { firstValueFrom } from 'rxjs';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
BROKER_MASTER,
@@ -33,6 +34,7 @@ import {
SLR_TYPE_OPTIONS,
INVESTMENT_GL_LOOKUP,
ELIGIBLE_RATINGS,
parseDDMMYYYY,
RATING_AGENCY_OPTIONS,
RATING_OPTIONS,
periodBetween
@@ -310,7 +312,19 @@ export class InvestmentMastersComponent implements OnInit {
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
get scriptRatingEligible(): boolean {
return !!this.scriptForm.rating && ELIGIBLE_RATINGS.includes(this.scriptForm.rating);
const r = this.scriptForm.rating;
return !!r && ELIGIBLE_RATINGS.includes(r) && this.scriptIsShortTerm === /^A[1-4]\+?$/.test(r);
}
/** A CP / CD runs one year or less and takes a short-term rating (A1+ to A4); a bond takes a long-term one. */
get scriptIsShortTerm(): boolean {
const f = this.scriptForm;
if (!f.issueDate || !f.maturityDate) {
return false;
}
const issue = parseDDMMYYYY(f.issueDate);
const limit = new Date(issue.getFullYear() + 1, issue.getMonth(), issue.getDate());
return parseDDMMYYYY(f.maturityDate) <= limit;
}
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
@@ -361,6 +375,8 @@ export class InvestmentMastersComponent implements OnInit {
glAccountId: Number(f.glAccountId),
isin: f.isin ?? '',
intRate: f.couponRate ?? null,
// Units = face value / face value per unit (100 for G-Secs by default; bonds can be 1,000 or 10,00,000).
...(f.faceValuePerUnit ? { faceValuePerUnit: Number(f.faceValuePerUnit) } : {}),
issueDate: f.issueDate ?? '',
maturityDate: f.maturityDate ?? '',
companyName: f.companyName ?? '',
@@ -541,6 +557,113 @@ export class InvestmentMastersComponent implements OnInit {
});
}
// -------------------- Daily rate upload (CSV / Excel) --------------------
/** Result of the last upload, one line per row of the file. */
uploadResults: Array<{ row: number; text: string; ok: boolean }> = [];
uploading = false;
/**
* Uploads the day's rates / NAVs from a CSV or Excel file (Jitendra Sir): columns Script Code (or ISIN), Rate Date
* (DD-MM-YYYY), Close, and optionally Open, High, Low; the first row is the header. Each row is saved on its own,
* so one bad row does not stop the rest.
*/
async uploadDailyRates(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) {
return;
}
this.uploadResults = [];
let rows: string[][];
try {
rows = file.name.toLowerCase().endsWith('.xlsx') ? await this.readXlsx(file) : this.readCsv(await file.text());
} catch {
this.toast('Could not read the file. Use CSV or Excel (.xlsx).');
return;
}
const data = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ''));
if (!data.length) {
this.toast('The file has no rate rows.');
return;
}
this.uploading = true;
for (let i = 0; i < data.length; i++) {
const [
code,
rateDate,
close,
open,
high,
low
] = data[i].map((c) => c.trim());
const script = this.scripts.find(
(s) =>
s.scriptCode.toUpperCase() === code.toUpperCase() || (!!s.isin && s.isin.toUpperCase() === code.toUpperCase())
);
const num = (v?: string) => (v ? Number(v.replace(/,/g, '')) : null);
if (!script?.id || !rateDate || num(close) == null || isNaN(num(close) as number)) {
this.uploadResults.push({
row: i + 2,
text: `${code || '?'}: script not found or date / close missing`,
ok: false
});
continue;
}
const body = {
scriptId: script.id,
rateDate,
closeRate: num(close),
...(num(open) != null ? { openRate: num(open) } : {}),
...(num(high) != null ? { highRate: num(high) } : {}),
...(num(low) != null ? { lowRate: num(low) } : {}),
dateFormat: 'dd-MM-yyyy',
locale: 'en'
};
try {
await firstValueFrom(this.investmentService.createDailyRate(body, true));
this.uploadResults.push({ row: i + 2, text: `${script.scriptCode} ${rateDate}: saved`, ok: true });
} catch (err) {
this.uploadResults.push({
row: i + 2,
text: `${script.scriptCode} ${rateDate}: ${apiErrorMessage(err)}`,
ok: false
});
}
}
this.uploading = false;
const saved = this.uploadResults.filter((r) => r.ok).length;
this.toast(`${saved} of ${data.length} rate(s) saved.`);
this.loadDailyRates();
}
private readCsv(text: string): string[][] {
return text.split(/\r?\n/).map((line) => line.split(',').map((c) => c.replace(/^"|"$/g, '')));
}
private async readXlsx(file: File): Promise<string[][]> {
const ExcelJS = await import('exceljs');
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(await file.arrayBuffer());
const sheet = wb.worksheets[0];
const rows: string[][] = [];
sheet.eachRow({ includeEmpty: false }, (row) => {
const cells: string[] = [];
for (let c = 1; c <= 6; c++) {
const v = row.getCell(c).value;
cells.push(
v instanceof Date
? `${String(v.getDate()).padStart(2, '0')}-${String(v.getMonth() + 1).padStart(2, '0')}-${v.getFullYear()}`
: v == null
? ''
: String(v)
);
}
rows.push(cells);
});
return rows;
}
/** A wrong rate is corrected by dropping it and entering it again. */
dropDailyRate(r: DailyScriptRate): void {
if (!r.id) {