Compare commits

...
Author SHA1 Message Date
anshul 8ed8f92b00 feat(investment): sell, redemption, write-off, GOI transfer and amortize screens 2026-10-15 18:45:21 +05:30
anshul c9feeae3cb feat(investment): interest in transaction log, accrued interest since last coupon, investment-only GL list 2026-10-14 17:40:08 +05:30
anshul 0404197c1b feat(investment): interest voucher posting UI, home pending alert and day-end reminder 2026-10-14 15:11:43 +05:30
anshul 8a6becff86 Merge origin/dev-latest into feature/investment 2026-10-14 10:49:37 +05:30
anshulandClaude Opus 5.5 c803e20628 feat(investment): settle purchases from Holdings; broken-period interest to settlement date
- Purchases are saved as Pending Settlement. Holdings show the status and
  a Settle button (from the settlement date), and the GL transaction id
  once settled.
- The transaction log shows "Not posted (pending settlement)" until a
  deal is settled, and dates the purchase on its settlement date.
- Broken-period interest is counted up to the settlement date.
- Accounting Preview title and the Holdings Settlement Date column
  reflect settlement-date posting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:28:19 +05:30
anshulandClaude Opus 5.5 6a04a92113 feat(investment): Daily Script Master, market rate on purchase, broken-period interest as expense
- 09 Daily Script Master now saves to and loads from the backend (sample
  rows removed), with Drop to correct a wrong rate.
- Purchase Entry takes the rate (NAV for a mutual fund) from the newest
  Daily Script Master rate on or before the deal date, shows which rate
  was used, warns when the rate entered is outside the day's low-high,
  and clears the rate when the script has none.
- Broken-period interest paid to the seller is debited to expense GL
  500606 instead of Interest Receivable (RBI UCB Investment Directions,
  para 152, confirmed by Jitendra Sir); Interest Entry labels it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:15 +05:30
anshulandClaude Opus 5.5 88f34f58e3 feat(investment): deal time, interest cycle and charges on Purchase Entry
As per Jitendra Sir's review:
- Deal Time is now a mandatory field next to Deal Date.
- New Interest Cycle section: the two coupon days, last and next coupon,
  next coupon amount and days to it.
- Stamp duty, brokerage and other charges sit on the main form. They are
  expenses on their own GLs (500601/500602/500603), so they are added to
  the settlement amount but no longer to the book value, and each shows
  as its own debit line in the accounting preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:54:33 +05:30
anshul 88c4242692 Merge origin/dev-latest into feature/investment 2026-10-12 10:35:53 +05:30
anshulandClaude Opus 5.5 ca01e9b65b feat(investment): Interest Entry screen with coupon and provision schedule
Replace the dummy interest calculation tab with the real Interest Entry:
pick Type / Script / Deal No and Retrieve the holding's schedule from
the backend - every half-yearly coupon and every 31 Mar / 30 Sep closing
provision up to maturity, with days (30/360), gross interest, amount
already booked, net income and the Dr/Cr entry per line. Display only;
nothing is posted to GL yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:36:01 +05:30
anshulandClaude Opus 5.5 a4e0a22205 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>
2026-10-08 12:38:10 +05:30
10 changed files with 3949 additions and 1207 deletions
@@ -386,6 +386,23 @@
</div> </div>
</div> </div>
} }
<!-- Investment interest vouchers that are due but not posted. A reminder only: unlike the interest
above it never stops the close (Jitendra Sir); the voucher can still be posted afterwards. -->
@if (preview?.investmentInterestPending?.length) {
<div class="bd-warn">
<mat-icon>notifications_active</mat-icon>
<div>
<strong>Reminder: investment interest vouchers are pending.</strong>
@for (t of preview.investmentInterestPending; track t.purchaseId) {
<div>
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.intDate }} · ₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }}
</div>
}
Post them under Investment → Govt Security / Bond → 03 Interest Entry. You can still close the day.
</div>
</div>
}
@if (preview?.savingsInterestPending?.length) { @if (preview?.savingsInterestPending?.length) {
<div class="bd-warn"> <div class="bd-warn">
<mat-icon>warning</mat-icon> <mat-icon>warning</mat-icon>
+17
View File
@@ -47,6 +47,23 @@
</div> </div>
</div> </div>
<!-- Pending investment interest vouchers (coupon / provision due now or within two days) -->
@if (pendingInvestmentInterest.length) {
<a class="investment-interest-alert" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }">
<span class="iia-icon">🔔</span>
<span class="iia-text">
<strong>{{ pendingInvestmentInterest.length }} investment interest voucher(s) pending</strong>
@for (t of pendingInvestmentInterest; track t.purchaseId) {
<span class="iia-item">
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</span>
}
</span>
<span class="iia-action">Open Interest Entry →</span>
</a>
}
<!-- Quick Action Cards --> <!-- Quick Action Cards -->
<div class="quick-actions"> <div class="quick-actions">
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard> <a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
+36
View File
@@ -424,3 +424,39 @@ $accent: #0ea5e9;
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.investment-interest-alert {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 12px 0;
padding: 12px 16px;
border: 1px solid #f5c26b;
border-left: 4px solid #e08e0b;
border-radius: 8px;
background: #fff8e6;
color: #5c3d00;
text-decoration: none;
.iia-icon {
font-size: 20px;
line-height: 1.2;
}
.iia-text {
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
}
.iia-item {
font-size: 13px;
}
.iia-action {
align-self: center;
font-weight: 600;
white-space: nowrap;
}
}
+14
View File
@@ -26,6 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service'
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service'; import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { DscService } from 'app/users/dsc/dsc.service'; import { DscService } from 'app/users/dsc/dsc.service';
import { InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service';
/** Custom Components */ /** Custom Components */
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component'; import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
@@ -109,6 +110,7 @@ export class HomeComponent implements OnInit, AfterViewInit {
this.tenant = this.tenantIdentifier(); this.tenant = this.tenantIdentifier();
this.setFilteredActivities(); this.setFilteredActivities();
this.loadDscStatus(); this.loadDscStatus();
this.loadPendingInvestmentInterest();
if (!this.authenticationService.hasDialogBeenShown()) { if (!this.authenticationService.hasDialogBeenShown()) {
this.dialog.open(WarningDialogComponent, { this.dialog.open(WarningDialogComponent, {
disableClose: false, disableClose: false,
@@ -118,6 +120,18 @@ export class HomeComponent implements OnInit, AfterViewInit {
} }
} }
/** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
private readonly investmentService = inject(InvestmentService);
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
private loadPendingInvestmentInterest(): void {
this.investmentService.getDueInterest().subscribe({
next: (due) => (this.pendingInvestmentInterest = due ?? []),
error: () => (this.pendingInvestmentInterest = [])
});
}
/** /**
* Surface a small "DSC verified" badge in the welcome banner when the * Surface a small "DSC verified" badge in the welcome banner when the
* signed-in user has at least one ACTIVE DSC certificate. Silent on * signed-in user has at least one ACTIVE DSC certificate. Silent on
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+482 -28
View File
@@ -17,7 +17,7 @@
*/ */
export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE'; export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE';
export type HoldingStatus = 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF'; export type HoldingStatus = 'PENDING_SETTLEMENT' | 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF';
export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE'; export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE';
export type TxnType = export type TxnType =
| 'PURCHASE' | 'PURCHASE'
@@ -28,6 +28,7 @@ export type TxnType =
| 'REDEEM' | 'REDEEM'
| 'WRITE_OFF' | 'WRITE_OFF'
| 'AMORTIZE' | 'AMORTIZE'
| 'TRANSFER'
| 'PART_PAYMENT'; | 'PART_PAYMENT';
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = { export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
@@ -123,6 +124,22 @@ export interface InvestmentHolding {
* never re-counts days up to this date, so re-running a Calculation & Post for an overlapping * never re-counts days up to this date, so re-running a Calculation & Post for an overlapping
* or repeated period always yields 0 instead of double-posting the same interest. */ * or repeated period always yields 0 instead of double-posting the same interest. */
interestPostedTillDate?: string; interestPostedTillDate?: string;
/** RBI investment category chosen at purchase — HTM / AFS / HFT. */
category?: string;
/** SGL / CSGL account the security is held in (custody, not an accounting GL). */
sglAccountNo?: string;
/** Backend id / deal number / party code once the purchase is saved (absent on sample rows). */
id?: number;
dealNo?: string;
partyCode?: string;
bookValue?: number;
settlementAmount?: number;
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
settlementDate?: string;
/** Last coupon date before the purchase; coupons fall every six months from it. */
lastIntDate?: string;
/** Journal entry transaction posted when the deal was settled. */
glTransactionId?: string;
} }
export interface InvestmentTxn { export interface InvestmentTxn {
@@ -248,6 +265,80 @@ export function accruedInterestBetween(h: InvestmentHolding, fromStr: string, to
return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25)); return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25));
} }
/** Days between two DD-MM-YYYY dates on a 30/360 basis (every month 30 days), as the bank's existing
* CBS counts them — e.g. 10-08 → 30-09 = 50 days, 10-08 → 07-10 = 57 days. */
export function days360(fromStr: string, toStr: string): number {
const a = parseDDMMYYYY(fromStr);
const b = parseDDMMYYYY(toStr);
if (isNaN(a.getTime()) || isNaN(b.getTime()) || b <= a) {
return 0;
}
const d1 = Math.min(a.getDate(), 30);
const d2 = Math.min(b.getDate(), 30);
return (b.getFullYear() - a.getFullYear()) * 360 + (b.getMonth() - a.getMonth()) * 30 + (d2 - d1);
}
/** Most recent half-yearly coupon date on or before `onStr`, derived from the maturity date (coupons
* fall on the maturity day/month and six months either side of it). */
export function lastHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
const mat = parseDDMMYYYY(maturityStr);
const on = parseDDMMYYYY(onStr);
if (isNaN(mat.getTime()) || isNaN(on.getTime())) {
return '';
}
const day = mat.getDate();
const couponOn = (year: number, month: number): Date => {
const lastDay = new Date(year, month + 1, 0).getDate();
return new Date(year, month, Math.min(day, lastDay));
};
const months = [
mat.getMonth() % 6,
(mat.getMonth() % 6) + 6
];
let best: Date | undefined;
for (const year of [
on.getFullYear() - 1,
on.getFullYear()
]) {
for (const m of months) {
const c = couponOn(year, m);
if (c <= on && (!best || c > best)) {
best = c;
}
}
}
return best ? formatDDMMYYYY(best) : '';
}
/** First half-yearly coupon date after `onStr` (coupons fall on the maturity day/month and six months
* either side), never later than maturity. */
export function nextHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
const last = lastHalfYearlyCouponDate(maturityStr, onStr);
const mat = parseDDMMYYYY(maturityStr);
if (!last || isNaN(mat.getTime())) {
return '';
}
const l = parseDDMMYYYY(last);
const year = l.getFullYear() + (l.getMonth() >= 6 ? 1 : 0);
const month = (l.getMonth() + 6) % 12;
const next = new Date(year, month, Math.min(mat.getDate(), new Date(year, month + 1, 0).getDate()));
return formatDDMMYYYY(next > mat ? mat : next);
}
/** The two coupon days of a half-yearly security, e.g. "14 Feb / 14 Aug". */
export function couponDatesLabel(maturityStr: string): string {
const mat = parseDDMMYYYY(maturityStr);
if (isNaN(mat.getTime())) {
return '';
}
const label = (month: number): string => {
const day = Math.min(mat.getDate(), new Date(2001, month + 1, 0).getDate());
return `${day} ${new Date(2001, month, 1).toLocaleString('en-US', { month: 'short' })}`;
};
const first = mat.getMonth() % 6;
return `${label(first)} / ${label(first + 6)}`;
}
export function formatDDMMYYYY(d: Date): string { export function formatDDMMYYYY(d: Date): string {
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`; return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
} }
@@ -1404,99 +1495,333 @@ export const CASH_BANK_GL_MASTER: InvestmentGlOption[] = [
{ glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' } { glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' }
]; ];
/** Investment GLs a script / party can be mapped to. PROPOSED codes awaiting approval — the
* backend will take these from the chart of accounts, never hard-coded. */
export const INVESTMENT_GL_OPTIONS: InvestmentGlOption[] = [
{ glCode: '100701', glName: 'INVESTMENT IN CENTRAL GOVT SECURITIES' },
{ glCode: '100702', glName: 'INVESTMENT IN STATE GOVT SECURITIES' },
{ glCode: '100703', glName: 'INVESTMENT IN BONDS / DEBENTURES' },
{ glCode: '100704', glName: 'FIXED DEPOSITS WITH OTHER BANKS' },
{ glCode: '100705', glName: 'INVESTMENT IN MUTUAL FUNDS' },
{ glCode: '100706', glName: 'INVESTMENT IN SHARES' }
];
/** Fallback investment GL when a script has none mapped. */
export const INVESTMENT_GL_BY_TYPE: Record<InstrumentType, string> = {
GOVT_SECURITY: '100701',
NON_SLR: '100703',
BOND: '100703',
FD: '100704',
MUTUAL_FUND: '100705',
SHARE: '100706'
};
/** Broken-period interest paid to the seller at purchase — an expense, not part of cost (RBI UCB Investment
* Directions para 152). */
export const BROKEN_PERIOD_INTEREST_GL: InvestmentGlOption = {
glCode: '500606',
glName: 'BROKEN PERIOD INTEREST PAID ON INVESTMENTS'
};
/** Charges on an investment deal — expenses on their own GLs, not part of the book value (Jitendra Sir). */
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 = {
glCode: '100603',
glName: 'INTEREST RECEIVABLE ON INVESTMENTS (PROVISION)'
};
/** Bank accounts a purchase can be paid from — these GLs already exist in the chart of accounts. */
export const SETTLEMENT_BANK_GL_OPTIONS: InvestmentGlOption[] = [
{ glCode: '100201', glName: 'IDBI BANK' },
{ glCode: '100202', glName: 'YES BANK' }
];
/** Every GL the Investment screens can show by name. */
export const INVESTMENT_GL_LOOKUP: InvestmentGlOption[] = [
...INVESTMENT_GL_OPTIONS,
INTEREST_RECEIVABLE_GL,
...SETTLEMENT_BANK_GL_OPTIONS
];
/** SLR classification on the Script Master, as in the bank's existing CBS ("GOI" = Central Govt). */
export type SlrType = 'GOI' | 'STATE_GOVT' | 'TREASURY' | 'NON_SLR';
export const SLR_TYPE_OPTIONS: Array<{ value: SlrType; label: string }> = [
{ value: 'GOI', label: 'GOI' },
{ value: 'STATE_GOVT', label: 'STATE GOVT' },
{ value: 'TREASURY', label: 'TREASURY' },
{ value: 'NON_SLR', label: 'NON SLR' }
];
export interface ScriptMaster { export interface ScriptMaster {
/** Backend id once the script is saved (absent on the sample rows). */
id?: number;
scriptCode: string; scriptCode: string;
scriptName: string; scriptName: string;
instrumentType: InstrumentType; instrumentType: InstrumentType;
isin: string; isin: string;
faceValuePerUnit: number; faceValuePerUnit: number;
// Fields below follow the bank's existing CBS Script Master; a purchase picks them up instead
// of the dealer re-typing them on every deal.
slrType?: SlrType;
/** Investment GL this script is booked to. */
glAccountId?: number;
glCode?: string;
glName?: string;
active?: boolean;
/** Interest (coupon) rate % per annum. */
couponRate?: number;
issueDate?: string;
maturityDate?: string;
// Issuer details — used for Bond / Mutual Fund scripts.
companyName?: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
corpCode?: string;
}
/** Period between two DD-MM-YYYY dates as years / months / days, as shown on the Script Master. */
export function periodBetween(fromStr?: string, toStr?: string): { years: number; months: number; days: number } {
const none = { years: 0, months: 0, days: 0 };
if (!fromStr || !toStr) {
return none;
}
const from = parseDDMMYYYY(fromStr);
const to = parseDDMMYYYY(toStr);
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to < from) {
return none;
}
let years = to.getFullYear() - from.getFullYear();
let months = to.getMonth() - from.getMonth();
let days = to.getDate() - from.getDate();
if (days < 0) {
months -= 1;
days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
}
if (months < 0) {
years -= 1;
months += 12;
}
return { years, months, days };
} }
export const SCRIPT_MASTER: ScriptMaster[] = [ export const SCRIPT_MASTER: ScriptMaster[] = [
{ scriptCode: 'FD-CUM', scriptName: 'Cumulative Fixed Deposit', instrumentType: 'FD', isin: '', faceValuePerUnit: 1 }, {
scriptCode: 'FD-CUM',
scriptName: 'Cumulative Fixed Deposit',
instrumentType: 'FD',
isin: '',
faceValuePerUnit: 1,
slrType: 'NON_SLR',
glCode: '100704'
},
{ {
scriptCode: 'FD-MIP', scriptCode: 'FD-MIP',
scriptName: 'Monthly Interest Payout FD', scriptName: 'Monthly Interest Payout FD',
instrumentType: 'FD', instrumentType: 'FD',
isin: '', isin: '',
faceValuePerUnit: 1 faceValuePerUnit: 1,
slrType: 'NON_SLR',
glCode: '100704'
}, },
{ {
scriptCode: 'IN0020230012', scriptCode: 'IN0020230012',
scriptName: '7.26% GOI 2033', scriptName: '7.26% GOI 2033',
instrumentType: 'GOVT_SECURITY', instrumentType: 'GOVT_SECURITY',
isin: 'IN0020230012', isin: 'IN0020230012',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'GOI',
glCode: '100701',
couponRate: 7.26,
issueDate: '15-01-2023',
maturityDate: '15-01-2033'
}, },
{ {
scriptCode: 'IN0020220089', scriptCode: 'IN0020220089',
scriptName: '7.10% GOI 2029', scriptName: '7.10% GOI 2029',
instrumentType: 'GOVT_SECURITY', instrumentType: 'GOVT_SECURITY',
isin: 'IN0020220089', isin: 'IN0020220089',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'GOI',
glCode: '100701',
couponRate: 7.1,
issueDate: '10-03-2022',
maturityDate: '10-03-2029'
},
{
scriptCode: 'IN0020210056',
scriptName: '6.79% GOI 2027',
instrumentType: 'GOVT_SECURITY',
isin: 'IN0020210056',
faceValuePerUnit: 100,
slrType: 'GOI',
glCode: '100701',
couponRate: 6.79,
issueDate: '05-06-2021',
maturityDate: '05-06-2027'
},
{
scriptCode: 'IN0020240034',
scriptName: '7.18% GOI 2037',
instrumentType: 'GOVT_SECURITY',
isin: 'IN0020240034',
faceValuePerUnit: 100,
slrType: 'GOI',
glCode: '100701',
couponRate: 7.18,
issueDate: '22-04-2024',
maturityDate: '22-04-2037'
},
{
scriptCode: 'MH-SDL-2031',
scriptName: '8.05% Maharashtra SDL 2031',
instrumentType: 'GOVT_SECURITY',
isin: '',
faceValuePerUnit: 100,
slrType: 'STATE_GOVT',
glCode: '100702',
couponRate: 8.05,
issueDate: '15-02-2021',
maturityDate: '15-02-2031'
},
{
scriptCode: 'GJ-SDL-2026',
scriptName: '7.85% Gujarat SDL 2026',
instrumentType: 'GOVT_SECURITY',
isin: '',
faceValuePerUnit: 100,
slrType: 'STATE_GOVT',
glCode: '100702',
couponRate: 7.85,
issueDate: '18-09-2016',
maturityDate: '18-09-2026'
}, },
{ {
scriptCode: 'TBILL-364D', scriptCode: 'TBILL-364D',
scriptName: '364 Days Treasury Bill', scriptName: '364 Days Treasury Bill',
instrumentType: 'GOVT_SECURITY', instrumentType: 'GOVT_SECURITY',
isin: '', isin: '',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'TREASURY',
glCode: '100701',
couponRate: 0,
issueDate: '02-07-2026',
maturityDate: '02-07-2027'
},
{
scriptCode: 'TBILL-91D',
scriptName: '91 Days Treasury Bill',
instrumentType: 'GOVT_SECURITY',
isin: '',
faceValuePerUnit: 100,
slrType: 'TREASURY',
glCode: '100701',
couponRate: 0,
issueDate: '15-05-2026',
maturityDate: '13-08-2026'
}, },
{ {
scriptCode: 'REC-7.5-2031', scriptCode: 'REC-7.5-2031',
scriptName: 'REC Ltd Bond 7.50% 2031', scriptName: 'REC Ltd Bond 7.50% 2031',
instrumentType: 'NON_SLR', instrumentType: 'NON_SLR',
isin: 'INE020B08JN0', isin: 'INE020B08JN0',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'NON_SLR',
glCode: '100703',
couponRate: 7.5,
issueDate: '11-08-2021',
maturityDate: '11-08-2031',
companyName: 'REC Limited',
city: 'New Delhi'
}, },
{ {
scriptCode: 'NABARD-7.35-2030', scriptCode: 'NABARD-7.35-2030',
scriptName: 'NABARD Bond 7.35% 2030', scriptName: 'NABARD Bond 7.35% 2030',
instrumentType: 'NON_SLR', instrumentType: 'NON_SLR',
isin: 'INE261F09EU1', isin: 'INE261F09EU1',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'NON_SLR',
glCode: '100703',
couponRate: 7.35,
issueDate: '20-09-2020',
maturityDate: '20-09-2030',
companyName: 'NABARD',
city: 'Mumbai'
}, },
{ {
scriptCode: 'LTF-8.1-2028', scriptCode: 'LTF-8.1-2028',
scriptName: 'L&T Finance NCD 8.10% 2028', scriptName: 'L&T Finance NCD 8.10% 2028',
instrumentType: 'BOND', instrumentType: 'BOND',
isin: 'INE027E07BR5', isin: 'INE027E07BR5',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'NON_SLR',
glCode: '100703',
couponRate: 8.1,
companyName: 'L&T Finance Ltd',
city: 'Mumbai'
}, },
{ {
scriptCode: 'BAF-7.95-2027', scriptCode: 'BAF-7.95-2027',
scriptName: 'Bajaj Finance NCD 7.95% 2027', scriptName: 'Bajaj Finance NCD 7.95% 2027',
instrumentType: 'BOND', instrumentType: 'BOND',
isin: 'INE296A07SP0', isin: 'INE296A07SP0',
faceValuePerUnit: 100 faceValuePerUnit: 100,
slrType: 'NON_SLR',
glCode: '100703',
couponRate: 7.95,
companyName: 'Bajaj Finance Ltd',
city: 'Pune'
}, },
{ {
scriptCode: 'SBI-OVN', scriptCode: 'SBI-OVN',
scriptName: 'SBI Overnight Fund', scriptName: 'SBI Overnight Fund',
instrumentType: 'MUTUAL_FUND', instrumentType: 'MUTUAL_FUND',
isin: 'INF200KA1Q40', isin: 'INF200KA1Q40',
faceValuePerUnit: 1000 faceValuePerUnit: 1000,
slrType: 'NON_SLR',
glCode: '100705',
companyName: 'SBI Mutual Fund',
city: 'Mumbai'
}, },
{ {
scriptCode: 'HDFC-LIQ', scriptCode: 'HDFC-LIQ',
scriptName: 'HDFC Liquid Fund', scriptName: 'HDFC Liquid Fund',
instrumentType: 'MUTUAL_FUND', instrumentType: 'MUTUAL_FUND',
isin: 'INF179KA1WR9', isin: 'INF179KA1WR9',
faceValuePerUnit: 1000 faceValuePerUnit: 1000,
slrType: 'NON_SLR',
glCode: '100705',
companyName: 'HDFC Mutual Fund',
city: 'Mumbai'
}, },
{ {
scriptCode: 'SBIN', scriptCode: 'SBIN',
scriptName: 'State Bank of India', scriptName: 'State Bank of India',
instrumentType: 'SHARE', instrumentType: 'SHARE',
isin: 'INE062A01020', isin: 'INE062A01020',
faceValuePerUnit: 1 faceValuePerUnit: 1,
slrType: 'NON_SLR',
glCode: '100706'
}, },
{ {
scriptCode: 'HDFCBANK', scriptCode: 'HDFCBANK',
scriptName: 'HDFC Bank Ltd', scriptName: 'HDFC Bank Ltd',
instrumentType: 'SHARE', instrumentType: 'SHARE',
isin: 'INE040A01034', isin: 'INE040A01034',
faceValuePerUnit: 1 faceValuePerUnit: 1,
slrType: 'NON_SLR',
glCode: '100706'
} }
]; ];
@@ -1561,23 +1886,150 @@ export const BROKER_MASTER: BrokerMaster[] = [
]; ];
export interface PartyMaster { export interface PartyMaster {
/** Backend id once the party is saved (absent on the sample rows). */
id?: number;
partyCode: string; partyCode: string;
partyName: string; partyName: string;
partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY'; partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY';
panNo: string; panNo?: string;
// Fields below follow the bank's existing CBS Bank / Party Master.
bondType?: InstrumentType;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
bankType?: string;
/** GL this party is mapped to. */
glAccountId?: number;
glCode?: string;
glName?: string;
rbiCode?: string;
/** Exposure limit for this party (₹). */
limit?: number | null;
leiNo?: string;
// Counterparty's bank details
cpBankName?: string;
cpBranchName?: string;
cpAccountNo?: string;
cpIfsc?: string;
/** Absent means ACTIVE. An INACTIVE party is not offered for new deals. */
status?: 'ACTIVE' | 'INACTIVE';
} }
/** Sample Bank Type options until the bank confirms its list. */
export const BANK_TYPE_OPTIONS = [
'SCHEDULED COMMERCIAL BANK',
'STATE CO-OPERATIVE BANK',
'DISTRICT CENTRAL CO-OPERATIVE BANK',
'URBAN CO-OPERATIVE BANK',
'RBI / GOVERNMENT',
'OTHER'
];
export const PARTY_MASTER: PartyMaster[] = [ export const PARTY_MASTER: PartyMaster[] = [
{ partyCode: 'PTY-001', partyName: 'Jila Sahakari Kendriya Bank', partyType: 'BANK', panNo: 'AABCJ1234K' }, {
{ partyCode: 'PTY-002', partyName: 'State Cooperative Bank Ltd', partyType: 'BANK', panNo: 'AABCS5678L' }, partyCode: 'PTY-001',
{ partyCode: 'PTY-003', partyName: 'RBI - SGL', partyType: 'GOVT', panNo: 'AAACR4849R' }, partyName: 'Jila Sahakari Kendriya Bank',
{ partyCode: 'PTY-004', partyName: 'REC Limited', partyType: 'CORPORATE', panNo: 'AAACR0641G' }, partyType: 'BANK',
{ partyCode: 'PTY-005', partyName: 'NABARD', partyType: 'CORPORATE', panNo: 'AAACN0272B' }, bondType: 'FD',
{ partyCode: 'PTY-006', partyName: 'L&T Finance Ltd', partyType: 'CORPORATE', panNo: 'AAACL0140P' }, glCode: '100704',
{ partyCode: 'PTY-007', partyName: 'SBI Mutual Fund', partyType: 'AMC', panNo: 'AAATS4325L' }, city: 'Vidisha',
{ partyCode: 'PTY-008', partyName: 'HDFC Mutual Fund', partyType: 'AMC', panNo: 'AAATH1234M' }, bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
{ partyCode: 'PTY-009', partyName: 'NSE/BSE', partyType: 'EXCHANGE', panNo: 'AAACN0028G' }, status: 'ACTIVE'
{ partyCode: 'PTY-010', partyName: 'District Cooperative Bank', partyType: 'COOP_SOCIETY', panNo: 'AABCD9988K' } },
{
partyCode: 'PTY-002',
partyName: 'State Cooperative Bank Ltd',
partyType: 'BANK',
bondType: 'FD',
glCode: '100704',
city: 'Bhopal',
bankType: 'STATE CO-OPERATIVE BANK',
status: 'ACTIVE'
},
{
partyCode: 'PTY-003',
partyName: 'RBI - SGL',
partyType: 'GOVT',
bondType: 'GOVT_SECURITY',
glCode: '100701',
city: 'Mumbai',
bankType: 'RBI / GOVERNMENT',
status: 'ACTIVE'
},
{
partyCode: 'PTY-004',
partyName: 'REC Limited',
partyType: 'CORPORATE',
bondType: 'NON_SLR',
glCode: '100703',
city: 'New Delhi',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-005',
partyName: 'NABARD',
partyType: 'CORPORATE',
bondType: 'NON_SLR',
glCode: '100703',
city: 'Mumbai',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-006',
partyName: 'L&T Finance Ltd',
partyType: 'CORPORATE',
bondType: 'BOND',
glCode: '100703',
city: 'Mumbai',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-007',
partyName: 'SBI Mutual Fund',
partyType: 'AMC',
bondType: 'MUTUAL_FUND',
glCode: '100705',
city: 'Mumbai',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-008',
partyName: 'HDFC Mutual Fund',
partyType: 'AMC',
bondType: 'MUTUAL_FUND',
glCode: '100705',
city: 'Mumbai',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-009',
partyName: 'NSE/BSE',
partyType: 'EXCHANGE',
bondType: 'SHARE',
glCode: '100706',
city: 'Mumbai',
bankType: 'OTHER',
status: 'ACTIVE'
},
{
partyCode: 'PTY-010',
partyName: 'District Cooperative Bank',
partyType: 'COOP_SOCIETY',
bondType: 'SHARE',
glCode: '100706',
city: 'Vidisha',
bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
status: 'ACTIVE'
}
]; ];
export interface SubTypeMaster { export interface SubTypeMaster {
@@ -1725,13 +2177,15 @@ export const LIEN_REASON_MASTER: LienReason[] = [
/** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported /** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported
* every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */ * every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */
export interface DailyScriptRate { export interface DailyScriptRate {
/** Backend id once saved. */
id?: number;
rateDate: string; rateDate: string;
scriptCode: string; scriptCode: string;
scriptName: string; scriptName: string;
openRate: number; openRate: number | null;
closeRate: number; closeRate: number;
highRate: number; highRate: number | null;
lowRate: number; lowRate: number | null;
} }
export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [ export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [
@@ -106,38 +106,112 @@
</button> </button>
</div> </div>
<!-- PARTY MASTER --> <!-- PARTY MASTER (layout follows the bank's existing CBS Bank / Party Master) -->
@if (activeTab === 'party') { @if (activeTab === 'party') {
<div class="pg-card"> <div class="pg-card">
<div class="pos-section-label">Add Bank / Party</div> <div class="pos-section-label">Bank / Party Master — Entry</div>
<div class="inv-form-grid"> <div class="inv-form-grid">
<label class="inv-form-field"
><span>Party Name *</span><input type="text" [(ngModel)]="newParty.partyName"
/></label>
<label class="inv-form-field"> <label class="inv-form-field">
<span>Party Type *</span> <span>* Bond Type</span>
<select [(ngModel)]="newParty.partyType"> <select [(ngModel)]="partyForm.bondType">
<option [ngValue]="undefined">-- Select --</option> <option [ngValue]="undefined">-- Select --</option>
<option value="BANK">BANK</option> @for (t of instrumentTypes; track t) {
<option value="CORPORATE">CORPORATE</option> <option [value]="t">{{ t }}</option>
<option value="GOVT">GOVT</option> }
<option value="AMC">AMC</option>
<option value="EXCHANGE">EXCHANGE</option>
<option value="COOP_SOCIETY">COOP SOCIETY</option>
</select> </select>
</label> </label>
<label class="inv-form-field"><span>PAN No</span><input type="text" [(ngModel)]="newParty.panNo" /></label> <label class="inv-form-field"
><span>* Party Code</span><input type="text" [value]="partyForm.partyCode || nextPartyCode" readonly
/></label>
<label class="inv-form-field"
><span>* Party Name</span><input type="text" [(ngModel)]="partyForm.partyName"
/></label>
<label class="inv-form-field"
><span>Address Line 1 (Building / Flat No.)</span
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="partyForm.address1"
/></label>
<label class="inv-form-field"
><span>Address Line 2 (Street / Area)</span
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="partyForm.address2"
/></label>
<label class="inv-form-field"
><span>Address Line 3 (Locality / Landmark)</span
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="partyForm.address3"
/></label>
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="partyForm.city" /></label>
<label class="inv-form-field"
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="partyForm.pincode"
/></label>
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="partyForm.phone" /></label>
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="partyForm.fax" /></label>
<label class="inv-form-field">
<span>Bank Type</span>
<select [(ngModel)]="partyForm.bankType">
<option [ngValue]="undefined">-- Select --</option>
@for (b of bankTypeOptions; track b) {
<option [value]="b">{{ b }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>* GL A/c No.</span>
<select [(ngModel)]="partyForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
</select>
</label>
<label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
<label class="inv-form-field"
><span>Limit (₹)</span><input type="number" min="0" [(ngModel)]="partyForm.limit"
/></label>
<label class="inv-form-field"><span>LEI No</span><input type="text" [(ngModel)]="partyForm.leiNo" /></label>
<label class="inv-form-field">
<span>Status</span>
<select [(ngModel)]="partyForm.status">
<option [ngValue]="undefined">ACTIVE</option>
<option value="INACTIVE">INACTIVE</option>
</select>
</label>
</div>
<div class="pos-section-label">Counterparty's Bank Details</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Name of Bank</span><input type="text" [(ngModel)]="partyForm.cpBankName"
/></label>
<label class="inv-form-field"
><span>Name of Branch</span><input type="text" [(ngModel)]="partyForm.cpBranchName"
/></label>
<label class="inv-form-field"
><span>Account No.</span><input type="text" [(ngModel)]="partyForm.cpAccountNo"
/></label>
<label class="inv-form-field"
><span>IFSC</span><input type="text" maxlength="11" [(ngModel)]="partyForm.cpIfsc"
/></label>
</div> </div>
<div class="inv-form-actions"> <div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="addParty()">Save</button> <label class="inv-form-field"
><span>Search Party Code</span><input type="text" placeholder="e.g. PTY-001" [(ngModel)]="partySearchCode"
/></label>
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveParty()">Save</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchParty()">Search</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropParty()">Drop</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Exit</button>
</div> </div>
<table class="pg-table"> <table class="pg-table">
<thead> <thead>
<tr> <tr>
<th>Party Code</th> <th>Party Code</th>
<th>Bond Type</th>
<th>Party Name</th> <th>Party Name</th>
<th>Type</th> <th>City</th>
<th>PAN No</th> <th>Bank Type</th>
<th>GL A/c</th>
<th class="num">Limit</th>
<th>Counterparty Bank / IFSC</th>
<th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -146,9 +220,14 @@
<td> <td>
<span class="pg-chip">{{ p.partyCode }}</span> <span class="pg-chip">{{ p.partyCode }}</span>
</td> </td>
<td>{{ p.bondType || '—' }}</td>
<td>{{ p.partyName }}</td> <td>{{ p.partyName }}</td>
<td>{{ p.partyType }}</td> <td>{{ p.city || '—' }}</td>
<td>{{ p.panNo }}</td> <td>{{ p.bankType || '—' }}</td>
<td>{{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }}</td>
<td class="num">{{ (p.limit ?? null) !== null ? '₹' + (p.limit | number: '1.0-0') : '—' }}</td>
<td>{{ p.cpBankName ? p.cpBankName + (p.cpIfsc ? ' / ' + p.cpIfsc : '') : '—' }}</td>
<td>{{ p.status ?? 'ACTIVE' }}</td>
</tr> </tr>
} }
</tbody> </tbody>
@@ -156,42 +235,118 @@
</div> </div>
} }
<!-- SCRIPT MASTER --> <!-- SCRIPT MASTER (layout follows the bank's existing CBS Script Master) -->
@if (activeTab === 'script') { @if (activeTab === 'script') {
<div class="pg-card"> <div class="pg-card">
<div class="pos-section-label">Add Script</div> <div class="pos-section-label">Script Master — Entry</div>
<div class="inv-form-grid"> <div class="inv-form-grid">
<label class="inv-form-field"
><span>Script Code</span><input type="text" [(ngModel)]="newScript.scriptCode"
/></label>
<label class="inv-form-field"
><span>Script Name *</span><input type="text" [(ngModel)]="newScript.scriptName"
/></label>
<label class="inv-form-field"> <label class="inv-form-field">
<span>Instrument Type *</span> <span>* Bond Type</span>
<select [(ngModel)]="newScript.instrumentType"> <select [(ngModel)]="scriptForm.instrumentType" [disabled]="!!scriptForm.scriptCode">
<option [ngValue]="undefined">-- Select --</option> <option [ngValue]="undefined">-- Select --</option>
@for (t of instrumentTypes; track t) { @for (t of instrumentTypes; track t) {
<option [value]="t">{{ t }}</option> <option [value]="t">{{ t }}</option>
} }
</select> </select>
</label> </label>
<label class="inv-form-field"><span>ISIN</span><input type="text" [(ngModel)]="newScript.isin" /></label> <label class="inv-form-field">
<span>SLR</span>
<select [(ngModel)]="scriptForm.slrType">
<option [ngValue]="undefined">-- Select --</option>
@for (o of slrTypeOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
}
</select>
</label>
<label class="inv-form-field" <label class="inv-form-field"
><span>Face Value / Unit</span><input type="number" [(ngModel)]="newScript.faceValuePerUnit" ><span>* Script Code</span><input type="text" [value]="scriptForm.scriptCode || nextScriptCode" readonly
/></label>
<label class="inv-form-field"
><span>* Script Desc</span
><input type="text" placeholder="e.g. 7.50%GOI2034" [(ngModel)]="scriptForm.scriptName"
/></label>
<label class="inv-form-field">
<span>* GL A/c No.</span>
<select [(ngModel)]="scriptForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
/></label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %</span
><input type="number" step="0.01" min="0" [(ngModel)]="scriptForm.couponRate"
/></label>
<label class="inv-form-field"
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Issue Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Redum / Mat Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.maturityDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Period (Year)</span><input type="text" [value]="scriptPeriod.years" readonly
/></label>
<label class="inv-form-field"
><span>Period (Month)</span><input type="text" [value]="scriptPeriod.months" readonly
/></label>
<label class="inv-form-field"
><span>Period (Days)</span><input type="text" [value]="scriptPeriod.days" readonly
/></label>
</div>
<div class="pos-section-label">Company Details (Bond / Mutual Fund)</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Company Name</span><input type="text" [(ngModel)]="scriptForm.companyName"
/></label>
<label class="inv-form-field"
><span>Address Line 1 (Building / Flat No.)</span
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="scriptForm.address1"
/></label>
<label class="inv-form-field"
><span>Address Line 2 (Street / Area)</span
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="scriptForm.address2"
/></label>
<label class="inv-form-field"
><span>Address Line 3 (Locality / Landmark)</span
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="scriptForm.address3"
/></label>
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="scriptForm.city" /></label>
<label class="inv-form-field"
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="scriptForm.pincode"
/></label>
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="scriptForm.phone" /></label>
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="scriptForm.fax" /></label>
<label class="inv-form-field"
><span>Corp. Code</span><input type="text" [(ngModel)]="scriptForm.corpCode"
/></label> /></label>
</div> </div>
<div class="inv-form-actions"> <div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="addScript()">Save</button> <label class="inv-form-field"
><span>Search Script Code</span><input type="text" placeholder="e.g. G00001" [(ngModel)]="scriptSearchCode"
/></label>
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveScript()">Save</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchScript()">Search</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Exit</button>
</div> </div>
<table class="pg-table"> <table class="pg-table">
<thead> <thead>
<tr> <tr>
<th>Script Code</th> <th>Script Code</th>
<th>Script Name</th> <th>Bond Type</th>
<th>Instrument</th> <th>SLR</th>
<th>Script Desc</th>
<th>GL A/c</th>
<th>ISIN</th> <th>ISIN</th>
<th class="num">Face Value/Unit</th> <th class="num">Int Rate %</th>
<th>Issue Date</th>
<th>Maturity</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -200,10 +355,14 @@
<td> <td>
<span class="pg-chip">{{ s.scriptCode }}</span> <span class="pg-chip">{{ s.scriptCode }}</span>
</td> </td>
<td>{{ s.scriptName }}</td>
<td>{{ s.instrumentType }}</td> <td>{{ s.instrumentType }}</td>
<td>{{ s.slrType || '—' }}</td>
<td>{{ s.scriptName }}</td>
<td>{{ s.glCode || '—' }}</td>
<td>{{ s.isin || '—' }}</td> <td>{{ s.isin || '—' }}</td>
<td class="num">{{ s.faceValuePerUnit }}</td> <td class="num">{{ s.couponRate ?? '—' }}</td>
<td>{{ s.issueDate || '—' }}</td>
<td>{{ s.maturityDate || '—' }}</td>
</tr> </tr>
} }
</tbody> </tbody>
@@ -386,10 +545,11 @@
<th class="num">High</th> <th class="num">High</th>
<th class="num">Low</th> <th class="num">Low</th>
<th class="num">Close</th> <th class="num">Close</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (r of dailyScriptRates; track r.rateDate + r.scriptCode) { @for (r of dailyScriptRates; track r.id ?? r.rateDate + r.scriptCode) {
<tr> <tr>
<td>{{ r.rateDate }}</td> <td>{{ r.rateDate }}</td>
<td> <td>
@@ -400,6 +560,13 @@
<td class="num">{{ r.highRate | number: '1.2-2' }}</td> <td class="num">{{ r.highRate | number: '1.2-2' }}</td>
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td> <td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td> <td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
<td>
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropDailyRate(r)">Drop</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="pg-empty">No rates entered yet.</td>
</tr> </tr>
} }
</tbody> </tbody>
@@ -6,20 +6,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component'; import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import { import {
SCRIPT_MASTER,
BROKER_MASTER, BROKER_MASTER,
PARTY_MASTER,
SUBTYPE_MASTER, SUBTYPE_MASTER,
CONTROL_PARAMETERS, CONTROL_PARAMETERS,
MUTUAL_FUND_PRODUCTS, MUTUAL_FUND_PRODUCTS,
SGL_BANK_MASTER, SGL_BANK_MASTER,
SCRIPT_MARKET_DETAIL, SCRIPT_MARKET_DETAIL,
DAILY_SCRIPT_RATES,
LIEN_REASON_MASTER, LIEN_REASON_MASTER,
ScriptMaster, ScriptMaster,
BrokerMaster, BrokerMaster,
@@ -31,9 +28,14 @@ import {
ScriptMarketDetail, ScriptMarketDetail,
DailyScriptRate, DailyScriptRate,
LienReason, LienReason,
InstrumentType InstrumentType,
BANK_TYPE_OPTIONS,
SLR_TYPE_OPTIONS,
INVESTMENT_GL_LOOKUP,
periodBetween
} from '../investment-data'; } from '../investment-data';
import { exportWorkbook, ExportSheetSpec } from '../investment-export'; import { exportWorkbook, ExportSheetSpec } from '../investment-export';
import { InvestmentGlAccount, InvestmentService, apiErrorMessage, toDailyScriptRate } from '../investment.service';
type MasterTab = type MasterTab =
| 'party' | 'party'
@@ -51,6 +53,10 @@ type MasterTab =
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy * Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
* lists, each with an inline Add-row form so the demo stays interactive without a backend. * lists, each with an inline Add-row form so the demo stays interactive without a backend.
*/ */
/** Detail GLs under the 100700 INVESTMENTS head; 100707 holds premium only, never a party's or script's GL. */
const INVESTMENT_GL_HEAD_PREFIX = '1007';
const PREMIUM_ON_INVESTMENTS_GL_CODE = '100707';
@Component({ @Component({
selector: 'mifosx-investment-masters', selector: 'mifosx-investment-masters',
templateUrl: './investment-masters.component.html', templateUrl: './investment-masters.component.html',
@@ -65,18 +71,19 @@ type MasterTab =
DateDdmmyyyyPickerComponent DateDdmmyyyyPickerComponent
] ]
}) })
export class InvestmentMastersComponent { export class InvestmentMastersComponent implements OnInit {
activeTab: MasterTab = 'party'; activeTab: MasterTab = 'party';
scripts: ScriptMaster[] = [...SCRIPT_MASTER]; scripts: ScriptMaster[] = [];
brokers: BrokerMaster[] = [...BROKER_MASTER]; brokers: BrokerMaster[] = [...BROKER_MASTER];
parties: PartyMaster[] = [...PARTY_MASTER]; parties: PartyMaster[] = [];
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER]; subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS]; controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS]; mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER]; sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL]; marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
dailyScriptRates: DailyScriptRate[] = [...DAILY_SCRIPT_RATES]; /** Loaded from the backend (Daily Script Master) in ngOnInit. */
dailyScriptRates: DailyScriptRate[] = [];
lienReasons: LienReason[] = [...LIEN_REASON_MASTER]; lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
readonly instrumentTypes: InstrumentType[] = [ readonly instrumentTypes: InstrumentType[] = [
@@ -106,8 +113,19 @@ export class InvestmentMastersComponent {
toastVisible = false; toastVisible = false;
private toastTimer: ReturnType<typeof setTimeout> | undefined; private toastTimer: ReturnType<typeof setTimeout> | undefined;
newParty: Partial<PartyMaster> = {}; // Bank / Party Master and Script Master follow the bank's existing CBS screens (one entry form
newScript: Partial<ScriptMaster> = {}; // 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;
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
investmentGlOptions: InvestmentGlAccount[] = [];
newSubType: Partial<SubTypeMaster> = {}; newSubType: Partial<SubTypeMaster> = {};
newBroker: Partial<BrokerMaster> = {}; newBroker: Partial<BrokerMaster> = {};
newControlParam: Partial<ControlParameter> = {}; newControlParam: Partial<ControlParameter> = {};
@@ -116,43 +134,220 @@ export class InvestmentMastersComponent {
newSglBank: Partial<SglBankMaster> = {}; newSglBank: Partial<SglBankMaster> = {};
newDailyRate: Partial<DailyScriptRate> = {}; newDailyRate: Partial<DailyScriptRate> = {};
ngOnInit(): void {
this.loadDailyRates();
this.investmentService.getAssetGlAccounts().subscribe({
next: (gls) =>
(this.investmentGlOptions = gls.filter(
(g) => g.glCode.startsWith(INVESTMENT_GL_HEAD_PREFIX) && g.glCode !== PREMIUM_ON_INVESTMENTS_GL_CODE
)),
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
this.reloadMasters();
}
setTab(tab: MasterTab): void { setTab(tab: MasterTab): void {
this.activeTab = tab; this.activeTab = tab;
} }
addParty(): void { /** Re-reads both masters from the backend (also refreshes the lists the Purchase screen uses). */
if (!this.newParty.partyName || !this.newParty.partyType) { private reloadMasters(): void {
this.toast('Enter Party Name and Type.'); this.investmentService.syncSharedMasters().subscribe({
return; next: ({ parties, scripts }) => {
} this.parties = parties;
this.parties = [ this.scripts = scripts;
{ },
partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`, error: (err) => this.toast(`Could not load masters: ${apiErrorMessage(err)}`)
panNo: '', });
...this.newParty
} as PartyMaster,
...this.parties
];
this.newParty = {};
this.toast('Party/Bank master added.');
} }
addScript(): void { glName(glCode?: string): string {
if (!this.newScript.scriptName || !this.newScript.instrumentType) { return (
this.toast('Enter Script Name and Instrument Type.'); 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; return;
} }
this.scripts = [ const body = {
{ bondType: f.bondType,
scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`, partyName: f.partyName,
isin: '', address1: f.address1 ?? '',
faceValuePerUnit: 100, address2: f.address2 ?? '',
...this.newScript address3: f.address3 ?? '',
} as ScriptMaster, city: f.city ?? '',
...this.scripts pincode: f.pincode ?? '',
]; phone: f.phone ?? '',
this.newScript = {}; fax: f.fax ?? '',
this.toast('Script master added.'); 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 { addSubType(): void {
@@ -248,24 +443,56 @@ export class InvestmentMastersComponent {
this.toast('SGL Bank master added.'); this.toast('SGL Bank master added.');
} }
private loadDailyRates(): void {
this.investmentService.getDailyRates().subscribe({
next: (rows) => (this.dailyScriptRates = rows.map(toDailyScriptRate)),
error: (err) => this.toast(`Could not load daily script rates: ${apiErrorMessage(err)}`)
});
}
addDailyRate(): void { addDailyRate(): void {
if (!this.newDailyRate.scriptCode || !this.newDailyRate.rateDate || this.newDailyRate.closeRate == null) { const r = this.newDailyRate;
if (!r.scriptCode || !r.rateDate || r.closeRate == null) {
this.toast('Enter Rate Date, Script Code and Close Rate.'); this.toast('Enter Rate Date, Script Code and Close Rate.');
return; return;
} }
const script = this.scripts.find((s) => s.scriptCode === this.newDailyRate.scriptCode); const script = this.scripts.find((s) => s.scriptCode === r.scriptCode);
this.dailyScriptRates = [ if (!script?.id) {
{ this.toast(`Script ${r.scriptCode} is not saved in Script Master.`);
scriptName: script?.scriptName || '', return;
openRate: this.newDailyRate.closeRate, }
highRate: this.newDailyRate.closeRate, const body = {
lowRate: this.newDailyRate.closeRate, scriptId: script.id,
...this.newDailyRate rateDate: r.rateDate,
} as DailyScriptRate, closeRate: r.closeRate,
...this.dailyScriptRates ...(r.openRate != null ? { openRate: r.openRate } : {}),
]; ...(r.highRate != null ? { highRate: r.highRate } : {}),
this.newDailyRate = {}; ...(r.lowRate != null ? { lowRate: r.lowRate } : {}),
this.toast('Daily script rate added.'); dateFormat: 'dd-MM-yyyy',
locale: 'en'
};
this.investmentService.createDailyRate(body).subscribe({
next: () => {
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} saved.`);
this.newDailyRate = {};
this.loadDailyRates();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
/** A wrong rate is corrected by dropping it and entering it again. */
dropDailyRate(r: DailyScriptRate): void {
if (!r.id) {
return;
}
this.investmentService.deleteDailyRate(r.id).subscribe({
next: () => {
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} dropped.`);
this.loadDailyRates();
},
error: (err) => this.toast(apiErrorMessage(err))
});
} }
// -------------------- Export to Excel -------------------- // -------------------- Export to Excel --------------------
@@ -279,46 +506,121 @@ export class InvestmentMastersComponent {
return one( return one(
[ [
'Party Code', 'Party Code',
'Bond Type',
'Party Name', 'Party Name',
'Type', 'Address',
'PAN No' 'City',
'Pincode',
'Phone',
'Fax',
'Bank Type',
'GL A/c No',
'RBI Code',
'Limit',
'LEI No',
'Counterparty Bank',
'Branch',
'Account No',
'IFSC',
'Status'
], ],
[ [
12, 12,
14,
30, 30,
16, 36,
14 14,
10,
14,
14,
24,
12,
12,
14,
22,
24,
20,
18,
14,
10
], ],
this.parties.map((p) => [ this.parties.map((p) => [
p.partyCode, p.partyCode,
p.bondType ?? '',
p.partyName, 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': case 'script':
return one( return one(
[ [
'Script Code', 'Script Code',
'Script Name', 'Bond Type',
'Instrument', 'SLR',
'Script Desc',
'GL A/c No',
'ISIN', 'ISIN',
'Face Value/Unit' 'Int Rate %',
'Issue Date',
'Maturity Date',
'Period (Y/M/D)',
'Face Value/Unit',
'Company Name',
'Corp Code'
], ],
[ [
14,
16, 16,
12,
30, 30,
12,
16, 16,
16, 10,
16 14,
14,
14,
14,
24,
12
], ],
this.scripts.map((s) => [ this.scripts.map((s) => {
s.scriptCode, const p = periodBetween(s.issueDate, s.maturityDate);
s.scriptName, return [
s.instrumentType, s.scriptCode,
s.isin, s.instrumentType,
s.faceValuePerUnit 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': case 'subtype':
return one( return one(
+653
View File
@@ -0,0 +1,653 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import {
DailyScriptRate,
HoldingStatus,
InstrumentType,
InvestmentHolding,
PARTY_MASTER,
PartyMaster,
SCRIPT_MASTER,
ScriptMaster,
SlrType
} from './investment-data';
/** Investment party as returned by /investment/parties. */
export interface InvestmentPartyDto {
id: number;
partyCode: string;
bondType: InstrumentType;
partyName: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
bankType?: string;
glAccountId: number;
glCode: string;
glName: string;
rbiCode?: string;
limit?: number;
leiNo?: string;
cpBankName?: string;
cpBranchName?: string;
cpAccountNo?: string;
cpIfsc?: string;
active: boolean;
}
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
export interface InvestmentScriptDto {
id: number;
scriptCode: string;
bondType: InstrumentType;
slrType?: SlrType;
scriptDesc: string;
glAccountId: number;
glCode: string;
glName: string;
isin?: string;
faceValuePerUnit: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
companyName?: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
pincode?: string;
phone?: string;
fax?: string;
corpCode?: string;
active: boolean;
}
/** Purchase entry (holding) as returned by /investment/purchases (dates are DD-MM-YYYY). */
export interface InvestmentPurchaseDto {
id: number;
dealNo: string;
certNo: string;
bondType: InstrumentType;
scriptId: number;
scriptCode: string;
scriptDesc: string;
isin?: string;
partyId: number;
partyCode: string;
partyName: string;
dealDate: string;
dealTime?: string;
settlementType: string;
settlementDate: string;
faceValue: number;
purchaseRate: number;
units: number;
balanceUnits: number;
brokerage?: number;
stampDuty?: number;
otherCharges?: number;
bookValue: number;
premiumDiscount?: number;
intRate?: number;
issueDate?: string;
maturityDate?: string;
lastIntDate?: string;
accruedDays?: number;
accruedInterest?: number;
settlementAmount: number;
ytm?: number;
category: string;
subType?: string;
investmentGlAccountId: number;
investmentGlCode: string;
investmentGlName: string;
crGlAccountId: number;
crGlCode: string;
crGlName: string;
otherDetails?: string;
status: string;
glTransactionId?: string;
settledOnDate?: string;
}
export interface InvestmentGlAccount {
id: number;
glCode: string;
name: string;
}
/**
* Backend calls for the Investment masters (Bank / Party Master and Script Master). Results are also copied into the
* shared PARTY_MASTER / SCRIPT_MASTER lists so screens that still read those lists (Purchase Entry) offer the saved
* masters.
*/
@Injectable({ providedIn: 'root' })
export class InvestmentService {
private http = inject(HttpClient);
// -------------------- Bank / Party Master --------------------
getParties(): Observable<InvestmentPartyDto[]> {
return this.http.get<InvestmentPartyDto[]>('/investment/parties');
}
createParty(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/parties', body);
}
updateParty(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/parties/${id}`, body);
}
deleteParty(id: number): Observable<unknown> {
return this.http.delete(`/investment/parties/${id}`);
}
// -------------------- Script Master --------------------
getScripts(): Observable<InvestmentScriptDto[]> {
return this.http.get<InvestmentScriptDto[]>('/investment/scripts');
}
createScript(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/scripts', body);
}
updateScript(id: number, body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.put<{ resourceId: number; resourceIdentifier: string }>(`/investment/scripts/${id}`, body);
}
// -------------------- Purchase Entry --------------------
getPurchases(): Observable<InvestmentPurchaseDto[]> {
return this.http.get<InvestmentPurchaseDto[]>('/investment/purchases');
}
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
/** GOI Category Transfers, newest first. */
getTransfers(): Observable<InvestmentSecurityTransferDto[]> {
return this.http.get<InvestmentSecurityTransferDto[]>('/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<InvestmentSaleDto[]> {
return this.http.get<InvestmentSaleDto[]>('/investment/sales');
}
/** What a sale would come to before it is saved (dealDate dd-MM-yyyy). */
quoteSale(params: Record<string, string | number>): Observable<InvestmentSaleDto> {
return this.http.get<InvestmentSaleDto>('/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<unknown> {
return this.http.delete(`/investment/sales/${saleId}`);
}
/** Premium amortisation schedule of a holding (06 Amortize Entry). */
getAmortisation(purchaseId: number): Observable<InvestmentAmortisationDto> {
return this.http.get<InvestmentAmortisationDto>(`/investment/amortisation/${purchaseId}`);
}
/** Holdings due for amortisation on a 31 March (07 Amortize Yearly Process). */
getAmortisationDue(fyEnd: string): Observable<InvestmentAmortisationDueDto[]> {
return this.http.get<InvestmentAmortisationDueDto[]>(`/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<InvestmentSaleDto> {
return this.http.get<InvestmentSaleDto>(`/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<InvestmentInterestPostingDto[]> {
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
}
/** Interest lines of all holdings that are due now or within the next few days. */
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
}
/** Posts one interest line (coupon / provision) with its JV voucher; the voucher number comes back as resourceIdentifier. */
postInterest(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string; transactionId?: string }> {
return this.http.post<{ resourceIdentifier?: string; transactionId?: string }>(
`/investment/purchases/${purchaseId}/interest-postings`,
body
);
}
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable<InvestmentInterestScheduleDto> {
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
return this.http.get<InvestmentInterestScheduleDto>(
`/investment/purchases/${purchaseId}/interest-schedule${params}`
);
}
// -------------------- Daily Script Master --------------------
getDailyRates(scriptId?: number): Observable<InvestmentDailyRateDto[]> {
const params = scriptId ? `?scriptId=${scriptId}` : '';
return this.http.get<InvestmentDailyRateDto[]>(`/investment/daily-rates${params}`);
}
/** Newest Daily Script Master rate of a script on or before a date; null when none has been entered. */
getLatestDailyRate(scriptId: number, onDate: string): Observable<InvestmentDailyRateDto | null> {
return this.http.get<InvestmentDailyRateDto | null>(
`/investment/daily-rates/latest?scriptId=${scriptId}&onDate=${encodeURIComponent(onDate)}`
);
}
createDailyRate(body: object): Observable<{ resourceId: number }> {
return this.http.post<{ resourceId: number }>('/investment/daily-rates', body);
}
deleteDailyRate(rateId: number): Observable<unknown> {
return this.http.delete(`/investment/daily-rates/${rateId}`);
}
/** Settles a deal: posts it to GL dated its settlement date. Under maker-checker the response only says it awaits approval. */
settlePurchase(purchaseId: number): Observable<{ transactionId?: string; rollbackTransaction?: boolean }> {
return this.http.post<{ transactionId?: string; rollbackTransaction?: boolean }>(
`/investment/purchases/${purchaseId}/settle`,
{}
);
}
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
}
deletePurchase(id: number): Observable<unknown> {
return this.http.delete(`/investment/purchases/${id}`);
}
// -------------------- GL accounts --------------------
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
return this.http
.get<
Array<{ id: number; glCode: string; name: string; disabled: boolean }>
>('/glaccounts', { params: { type: '1', usage: '1', disabled: 'false' } })
.pipe(
map((list) =>
list
.map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
.sort((a, b) => a.glCode.localeCompare(b.glCode))
)
);
}
/** Loads both masters from the backend and refreshes the shared lists other screens read. */
syncSharedMasters(): Observable<{ parties: PartyMaster[]; scripts: ScriptMaster[] }> {
return forkJoin({ parties: this.getParties(), scripts: this.getScripts() }).pipe(
map(({ parties, scripts }) => ({ parties: parties.map(toPartyMaster), scripts: scripts.map(toScriptMaster) })),
tap(({ parties, scripts }) => {
PARTY_MASTER.splice(0, PARTY_MASTER.length, ...parties);
SCRIPT_MASTER.splice(0, SCRIPT_MASTER.length, ...scripts);
})
);
}
}
export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
return {
id: p.id,
partyCode: p.partyCode,
partyName: p.partyName,
partyType: 'BANK',
bondType: p.bondType,
address1: p.address1,
address2: p.address2,
address3: p.address3,
city: p.city,
pincode: p.pincode,
phone: p.phone,
fax: p.fax,
bankType: p.bankType,
glAccountId: p.glAccountId,
glCode: p.glCode,
glName: p.glName,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,
cpBankName: p.cpBankName,
cpBranchName: p.cpBranchName,
cpAccountNo: p.cpAccountNo,
cpIfsc: p.cpIfsc,
status: p.active ? 'ACTIVE' : 'INACTIVE'
};
}
export function toScriptMaster(s: InvestmentScriptDto): ScriptMaster {
return {
id: s.id,
scriptCode: s.scriptCode,
scriptName: s.scriptDesc,
instrumentType: s.bondType,
isin: s.isin ?? '',
faceValuePerUnit: s.faceValuePerUnit,
slrType: s.slrType,
glAccountId: s.glAccountId,
glCode: s.glCode,
glName: s.glName,
couponRate: s.intRate,
issueDate: s.issueDate,
maturityDate: s.maturityDate,
companyName: s.companyName,
address1: s.address1,
address2: s.address2,
address3: s.address3,
city: s.city,
pincode: s.pincode,
phone: s.phone,
fax: s.fax,
corpCode: s.corpCode,
active: s.active
};
}
/** A saved purchase as a row of the Holdings register. */
export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
return {
id: p.id,
dealNo: p.dealNo,
certNo: p.certNo,
instrumentType: p.bondType,
scriptCode: p.scriptCode,
scriptName: p.scriptDesc,
party: p.partyName,
partyCode: p.partyCode,
subType: p.subType ?? '',
// 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,
maturityDate: p.maturityDate || p.dealDate,
interestRate: p.intRate ?? 0,
interestFreq: p.bondType === 'MUTUAL_FUND' ? 'CUMULATIVE' : 'HALF_YEARLY',
glCode: p.investmentGlCode,
glName: p.investmentGlName,
crToAc: p.crGlCode,
brokerCode: '',
status: (p.status === 'REDEEMED' ? 'MATURED' : p.status) as HoldingStatus,
settlementDate: p.settlementDate,
lastIntDate: p.lastIntDate,
glTransactionId: p.glTransactionId,
category: p.category,
bookValue: p.bookValue,
settlementAmount: p.settlementAmount
};
}
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
export interface InvestmentInterestRowDto {
type: 'COUPON' | 'PROVISION';
intDate: string;
fromDate: string;
toDate: string;
days: number;
grossInterest: number;
alreadyBooked: number;
amount: number;
due: boolean;
/** Posted to GL: its JV voucher and GL transaction. */
posted: boolean;
voucherNo?: string;
glTransactionId?: string;
postedOnDate?: string;
/** The next line to post and its date has come. */
postable: boolean;
/** Unposted and due within the reminder window (or past). */
dueSoon: boolean;
}
/** 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;
purchaseId: number;
dealNo: string;
certNo: string;
bondType: string;
scriptDesc: string;
lineType: 'COUPON' | 'PROVISION';
intDate: string;
fromDate: string;
toDate: string;
grossInterest: number;
alreadyBooked: number;
amount: number;
drGlCode: string;
drGlName: string;
incomeGlCode: string;
incomeGlName: string;
glTransactionId: string;
voucherNo: string;
postedOnDate: string;
}
export interface InvestmentInterestDueDto {
purchaseId: number;
dealNo: string;
certNo: string;
scriptCode: string;
scriptDesc: string;
lineType: 'COUPON' | 'PROVISION';
intDate: string;
grossInterest: number;
amount: number;
postable: boolean;
overdue: boolean;
}
export interface InvestmentInterestScheduleDto {
purchaseId: number;
dealNo: string;
certNo: string;
scriptCode: string;
scriptDesc: string;
faceValue: number;
unitValue: number;
balanceUnits: number;
purchaseRate: number;
bookValue: number;
intRate: number;
dealDate: string;
lastIntDate: string;
maturityDate: string;
purchasedAccruedInterest: number;
couponPerHalfYear: number;
asOnDate: string;
interestGlCode: string;
interestGlName: string;
bankGlAccountId: number;
bankGlCode: string;
bankGlName: string;
status: string;
rows: InvestmentInterestRowDto[];
}
/** A Daily Script Master row (see InvestmentDailyRateData in the backend). */
export interface InvestmentDailyRateDto {
id: number;
scriptId: number;
scriptCode: string;
scriptDesc: string;
bondType: string;
rateDate: string;
openRate: number | null;
highRate: number | null;
lowRate: number | null;
closeRate: number;
}
export function toDailyScriptRate(d: InvestmentDailyRateDto): DailyScriptRate {
return {
id: d.id,
rateDate: d.rateDate,
scriptCode: d.scriptCode,
scriptName: d.scriptDesc,
openRate: d.openRate,
closeRate: d.closeRate,
highRate: d.highRate,
lowRate: d.lowRate
};
}
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
export function apiErrorMessage(err: unknown): string {
const e = err as HttpErrorResponse;
const body = e?.error;
return body?.errors?.[0]?.defaultUserMessage || body?.defaultUserMessage || e?.message || 'Request failed.';
}