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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user