feat(investment): wire Bank/Party, Script Master and Purchase Entry to the backend
- Bank / Party Master and Script Master follow the bank's existing CBS screens (address, bank type, GL A/c, RBI code, limit, LEI, counterparty bank details; SLR type, auto script code, ISIN, interest rate, issue / maturity date, auto period, company details) and save to /investment/parties and /investment/scripts. GL dropdowns list enabled ASSET detail accounts from the chart of accounts. - Purchase Entry simplified to the mandatory fields; script details, settlement date, last interest date, units, book value, premium/discount, accrued interest (30/360 up to the deal date), settlement amount and YTM are filled automatically; optional fields sit under "More Details"; an Accounting Preview shows the GL entry. Save / Retrieve / Drop use /investment/purchases and the Holdings register lists saved purchases. - New InvestmentService for the investment API calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,12 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||
import {
|
||||
SCRIPT_MASTER,
|
||||
BROKER_MASTER,
|
||||
PARTY_MASTER,
|
||||
SUBTYPE_MASTER,
|
||||
CONTROL_PARAMETERS,
|
||||
MUTUAL_FUND_PRODUCTS,
|
||||
@@ -31,9 +29,14 @@ import {
|
||||
ScriptMarketDetail,
|
||||
DailyScriptRate,
|
||||
LienReason,
|
||||
InstrumentType
|
||||
InstrumentType,
|
||||
BANK_TYPE_OPTIONS,
|
||||
SLR_TYPE_OPTIONS,
|
||||
INVESTMENT_GL_LOOKUP,
|
||||
periodBetween
|
||||
} from '../investment-data';
|
||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||
import { InvestmentGlAccount, InvestmentService, apiErrorMessage } from '../investment.service';
|
||||
|
||||
type MasterTab =
|
||||
| 'party'
|
||||
@@ -65,12 +68,12 @@ type MasterTab =
|
||||
DateDdmmyyyyPickerComponent
|
||||
]
|
||||
})
|
||||
export class InvestmentMastersComponent {
|
||||
export class InvestmentMastersComponent implements OnInit {
|
||||
activeTab: MasterTab = 'party';
|
||||
|
||||
scripts: ScriptMaster[] = [...SCRIPT_MASTER];
|
||||
scripts: ScriptMaster[] = [];
|
||||
brokers: BrokerMaster[] = [...BROKER_MASTER];
|
||||
parties: PartyMaster[] = [...PARTY_MASTER];
|
||||
parties: PartyMaster[] = [];
|
||||
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
|
||||
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
|
||||
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
|
||||
@@ -106,8 +109,19 @@ export class InvestmentMastersComponent {
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
newParty: Partial<PartyMaster> = {};
|
||||
newScript: Partial<ScriptMaster> = {};
|
||||
// Bank / Party Master and Script Master follow the bank's existing CBS screens (one entry form
|
||||
// with Save / Search / Cancel / Drop / Exit) and are stored in the backend through
|
||||
// InvestmentService; the other master tabs are still sample data.
|
||||
private readonly investmentService = inject(InvestmentService);
|
||||
partyForm: Partial<PartyMaster> = {};
|
||||
partySearchCode = '';
|
||||
scriptForm: Partial<ScriptMaster> = {};
|
||||
scriptSearchCode = '';
|
||||
saving = false;
|
||||
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
||||
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
|
||||
/** Enabled ASSET detail GLs from the chart of accounts. */
|
||||
investmentGlOptions: InvestmentGlAccount[] = [];
|
||||
newSubType: Partial<SubTypeMaster> = {};
|
||||
newBroker: Partial<BrokerMaster> = {};
|
||||
newControlParam: Partial<ControlParameter> = {};
|
||||
@@ -116,43 +130,216 @@ export class InvestmentMastersComponent {
|
||||
newSglBank: Partial<SglBankMaster> = {};
|
||||
newDailyRate: Partial<DailyScriptRate> = {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.investmentService.getAssetGlAccounts().subscribe({
|
||||
next: (gls) => (this.investmentGlOptions = gls),
|
||||
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
||||
});
|
||||
this.reloadMasters();
|
||||
}
|
||||
|
||||
setTab(tab: MasterTab): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
addParty(): void {
|
||||
if (!this.newParty.partyName || !this.newParty.partyType) {
|
||||
this.toast('Enter Party Name and Type.');
|
||||
return;
|
||||
}
|
||||
this.parties = [
|
||||
{
|
||||
partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`,
|
||||
panNo: '',
|
||||
...this.newParty
|
||||
} as PartyMaster,
|
||||
...this.parties
|
||||
];
|
||||
this.newParty = {};
|
||||
this.toast('Party/Bank master added.');
|
||||
/** Re-reads both masters from the backend (also refreshes the lists the Purchase screen uses). */
|
||||
private reloadMasters(): void {
|
||||
this.investmentService.syncSharedMasters().subscribe({
|
||||
next: ({ parties, scripts }) => {
|
||||
this.parties = parties;
|
||||
this.scripts = scripts;
|
||||
},
|
||||
error: (err) => this.toast(`Could not load masters: ${apiErrorMessage(err)}`)
|
||||
});
|
||||
}
|
||||
|
||||
addScript(): void {
|
||||
if (!this.newScript.scriptName || !this.newScript.instrumentType) {
|
||||
this.toast('Enter Script Name and Instrument Type.');
|
||||
glName(glCode?: string): string {
|
||||
return (
|
||||
this.investmentGlOptions.find((g) => g.glCode === glCode)?.name ??
|
||||
INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ??
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- Bank / Party Master --------------------
|
||||
/** Preview of the code the backend will assign; the saved code always comes from the backend. */
|
||||
get nextPartyCode(): string {
|
||||
const nums = this.parties.map((p) => parseInt(p.partyCode.replace(/\D/g, ''), 10)).filter((n) => !isNaN(n));
|
||||
return `PTY-${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
saveParty(): void {
|
||||
const f = this.partyForm;
|
||||
if (!f.bondType || !f.partyName || !f.glAccountId) {
|
||||
this.toast('Enter Bond Type, Party Name and GL A/c No.');
|
||||
return;
|
||||
}
|
||||
this.scripts = [
|
||||
{
|
||||
scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`,
|
||||
isin: '',
|
||||
faceValuePerUnit: 100,
|
||||
...this.newScript
|
||||
} as ScriptMaster,
|
||||
...this.scripts
|
||||
];
|
||||
this.newScript = {};
|
||||
this.toast('Script master added.');
|
||||
const body = {
|
||||
bondType: f.bondType,
|
||||
partyName: f.partyName,
|
||||
address1: f.address1 ?? '',
|
||||
address2: f.address2 ?? '',
|
||||
address3: f.address3 ?? '',
|
||||
city: f.city ?? '',
|
||||
pincode: f.pincode ?? '',
|
||||
phone: f.phone ?? '',
|
||||
fax: f.fax ?? '',
|
||||
bankType: f.bankType ?? '',
|
||||
glAccountId: Number(f.glAccountId),
|
||||
rbiCode: f.rbiCode ?? '',
|
||||
limit: f.limit ?? null,
|
||||
leiNo: f.leiNo ?? '',
|
||||
cpBankName: f.cpBankName ?? '',
|
||||
cpBranchName: f.cpBranchName ?? '',
|
||||
cpAccountNo: f.cpAccountNo ?? '',
|
||||
cpIfsc: f.cpIfsc ?? '',
|
||||
active: f.status !== 'INACTIVE',
|
||||
locale: 'en'
|
||||
};
|
||||
const request = f.id ? this.investmentService.updateParty(f.id, body) : this.investmentService.createParty(body);
|
||||
this.saving = true;
|
||||
request.subscribe({
|
||||
next: (res) => {
|
||||
this.saving = false;
|
||||
this.toast(`Party ${res.resourceIdentifier ?? f.partyCode} ${f.id ? 'updated' : 'saved'}.`);
|
||||
this.partyForm = {};
|
||||
this.reloadMasters();
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchParty(): void {
|
||||
const p = this.parties.find((x) => x.partyCode.toUpperCase() === this.partySearchCode.trim().toUpperCase());
|
||||
if (!p) {
|
||||
this.toast(`Party Code ${this.partySearchCode} not found.`);
|
||||
return;
|
||||
}
|
||||
this.partyForm = { ...p };
|
||||
this.toast(`Party ${p.partyCode} loaded.`);
|
||||
}
|
||||
|
||||
cancelParty(): void {
|
||||
this.partyForm = {};
|
||||
this.partySearchCode = '';
|
||||
}
|
||||
|
||||
dropParty(): void {
|
||||
const f = this.partyForm;
|
||||
if (!f.id) {
|
||||
this.toast('Search a saved party first, then Drop.');
|
||||
return;
|
||||
}
|
||||
this.investmentService.deleteParty(f.id).subscribe({
|
||||
next: () => {
|
||||
this.toast(`Party ${f.partyCode} dropped.`);
|
||||
this.partyForm = {};
|
||||
this.reloadMasters();
|
||||
},
|
||||
error: (err) => this.toast(apiErrorMessage(err))
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Script Master --------------------
|
||||
/** Coupon / maturity terms only apply to debt securities. */
|
||||
get scriptIsDebtSecurity(): boolean {
|
||||
return [
|
||||
'GOVT_SECURITY',
|
||||
'NON_SLR',
|
||||
'BOND'
|
||||
].includes(this.scriptForm.instrumentType ?? '');
|
||||
}
|
||||
|
||||
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
|
||||
get nextScriptCode(): string {
|
||||
const prefixByType: Record<InstrumentType, string> = {
|
||||
GOVT_SECURITY: 'G',
|
||||
NON_SLR: 'N',
|
||||
BOND: 'B',
|
||||
MUTUAL_FUND: 'M',
|
||||
FD: 'F',
|
||||
SHARE: 'S'
|
||||
};
|
||||
const type = this.scriptForm.instrumentType;
|
||||
if (!type) {
|
||||
return '';
|
||||
}
|
||||
const prefix = prefixByType[type];
|
||||
const pattern = new RegExp(`^${prefix}(\\d{5})$`);
|
||||
const nums = this.scripts
|
||||
.map((s) => pattern.exec(s.scriptCode)?.[1])
|
||||
.filter((n): n is string => !!n)
|
||||
.map((n) => parseInt(n, 10));
|
||||
return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(5, '0')}`;
|
||||
}
|
||||
|
||||
get scriptPeriod(): { years: number; months: number; days: number } {
|
||||
return periodBetween(this.scriptForm.issueDate, this.scriptForm.maturityDate);
|
||||
}
|
||||
|
||||
saveScript(): void {
|
||||
const f = this.scriptForm;
|
||||
if (!f.instrumentType || !f.scriptName || !f.glAccountId) {
|
||||
this.toast('Enter Bond Type, Script Desc and GL A/c No.');
|
||||
return;
|
||||
}
|
||||
if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate)) {
|
||||
this.toast('Enter ISIN, Int Rate and Issue Date for this security.');
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
bondType: f.instrumentType,
|
||||
slrType: f.slrType ?? '',
|
||||
scriptDesc: f.scriptName,
|
||||
glAccountId: Number(f.glAccountId),
|
||||
isin: f.isin ?? '',
|
||||
intRate: f.couponRate ?? null,
|
||||
issueDate: f.issueDate ?? '',
|
||||
maturityDate: f.maturityDate ?? '',
|
||||
companyName: f.companyName ?? '',
|
||||
address1: f.address1 ?? '',
|
||||
address2: f.address2 ?? '',
|
||||
address3: f.address3 ?? '',
|
||||
city: f.city ?? '',
|
||||
pincode: f.pincode ?? '',
|
||||
phone: f.phone ?? '',
|
||||
fax: f.fax ?? '',
|
||||
corpCode: f.corpCode ?? '',
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
};
|
||||
const request = f.id ? this.investmentService.updateScript(f.id, body) : this.investmentService.createScript(body);
|
||||
this.saving = true;
|
||||
request.subscribe({
|
||||
next: (res) => {
|
||||
this.saving = false;
|
||||
this.toast(`Script ${res.resourceIdentifier ?? f.scriptCode} ${f.id ? 'updated' : 'saved'}.`);
|
||||
this.scriptForm = {};
|
||||
this.reloadMasters();
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchScript(): void {
|
||||
const s = this.scripts.find((x) => x.scriptCode.toUpperCase() === this.scriptSearchCode.trim().toUpperCase());
|
||||
if (!s) {
|
||||
this.toast(`Script Code ${this.scriptSearchCode} not found.`);
|
||||
return;
|
||||
}
|
||||
this.scriptForm = { ...s };
|
||||
this.toast(`Script ${s.scriptCode} loaded.`);
|
||||
}
|
||||
|
||||
cancelScript(): void {
|
||||
this.scriptForm = {};
|
||||
this.scriptSearchCode = '';
|
||||
}
|
||||
|
||||
addSubType(): void {
|
||||
@@ -279,46 +466,121 @@ export class InvestmentMastersComponent {
|
||||
return one(
|
||||
[
|
||||
'Party Code',
|
||||
'Bond Type',
|
||||
'Party Name',
|
||||
'Type',
|
||||
'PAN No'
|
||||
'Address',
|
||||
'City',
|
||||
'Pincode',
|
||||
'Phone',
|
||||
'Fax',
|
||||
'Bank Type',
|
||||
'GL A/c No',
|
||||
'RBI Code',
|
||||
'Limit',
|
||||
'LEI No',
|
||||
'Counterparty Bank',
|
||||
'Branch',
|
||||
'Account No',
|
||||
'IFSC',
|
||||
'Status'
|
||||
],
|
||||
[
|
||||
12,
|
||||
14,
|
||||
30,
|
||||
16,
|
||||
14
|
||||
36,
|
||||
14,
|
||||
10,
|
||||
14,
|
||||
14,
|
||||
24,
|
||||
12,
|
||||
12,
|
||||
14,
|
||||
22,
|
||||
24,
|
||||
20,
|
||||
18,
|
||||
14,
|
||||
10
|
||||
],
|
||||
this.parties.map((p) => [
|
||||
p.partyCode,
|
||||
p.bondType ?? '',
|
||||
p.partyName,
|
||||
p.partyType,
|
||||
p.panNo
|
||||
[
|
||||
p.address1,
|
||||
p.address2,
|
||||
p.address3
|
||||
]
|
||||
.filter((x) => !!x)
|
||||
.join(', '),
|
||||
p.city ?? '',
|
||||
p.pincode ?? '',
|
||||
p.phone ?? '',
|
||||
p.fax ?? '',
|
||||
p.bankType ?? '',
|
||||
p.glCode ?? '',
|
||||
p.rbiCode ?? '',
|
||||
p.limit ?? null,
|
||||
p.leiNo ?? '',
|
||||
p.cpBankName ?? '',
|
||||
p.cpBranchName ?? '',
|
||||
p.cpAccountNo ?? '',
|
||||
p.cpIfsc ?? '',
|
||||
p.status ?? 'ACTIVE'
|
||||
])
|
||||
);
|
||||
case 'script':
|
||||
return one(
|
||||
[
|
||||
'Script Code',
|
||||
'Script Name',
|
||||
'Instrument',
|
||||
'Bond Type',
|
||||
'SLR',
|
||||
'Script Desc',
|
||||
'GL A/c No',
|
||||
'ISIN',
|
||||
'Face Value/Unit'
|
||||
'Int Rate %',
|
||||
'Issue Date',
|
||||
'Maturity Date',
|
||||
'Period (Y/M/D)',
|
||||
'Face Value/Unit',
|
||||
'Company Name',
|
||||
'Corp Code'
|
||||
],
|
||||
[
|
||||
14,
|
||||
16,
|
||||
12,
|
||||
30,
|
||||
12,
|
||||
16,
|
||||
16,
|
||||
16
|
||||
10,
|
||||
14,
|
||||
14,
|
||||
14,
|
||||
14,
|
||||
24,
|
||||
12
|
||||
],
|
||||
this.scripts.map((s) => [
|
||||
s.scriptCode,
|
||||
s.scriptName,
|
||||
s.instrumentType,
|
||||
s.isin,
|
||||
s.faceValuePerUnit
|
||||
])
|
||||
this.scripts.map((s) => {
|
||||
const p = periodBetween(s.issueDate, s.maturityDate);
|
||||
return [
|
||||
s.scriptCode,
|
||||
s.instrumentType,
|
||||
s.slrType ?? '',
|
||||
s.scriptName,
|
||||
s.glCode ?? '',
|
||||
s.isin,
|
||||
s.couponRate ?? null,
|
||||
s.issueDate ?? '',
|
||||
s.maturityDate ?? '',
|
||||
s.issueDate && s.maturityDate ? `${p.years}/${p.months}/${p.days}` : '',
|
||||
s.faceValuePerUnit,
|
||||
s.companyName ?? '',
|
||||
s.corpCode ?? ''
|
||||
];
|
||||
})
|
||||
);
|
||||
case 'subtype':
|
||||
return one(
|
||||
|
||||
Reference in New Issue
Block a user