From bd753f4983beba1dd9ff57b3f03d73e2e38c8273 Mon Sep 17 00:00:00 2001 From: keshav Date: Sat, 1 Aug 2026 11:46:11 +0530 Subject: [PATCH] oss file updated, profitl loss as per the day month and also with date select --- src/app/app.module.ts | 2 + .../client-general-step.component.ts | 12 +- src/app/core/shell/shell.component.html | 64 +- .../item-decision-dialog.component.html | 15 + .../item-decision-dialog.component.ts | 86 ++ src/app/fiu/fiu-data.ts | 675 +++++----- src/app/fiu/fiu.component.html | 299 ++++- src/app/fiu/fiu.component.scss | 341 +++++ src/app/fiu/fiu.component.ts | 374 +++++- src/app/gl-reports/gl-reports-data.ts | 496 ++++++++ .../gl-reports/gl-reports-routing.module.ts | 34 + src/app/gl-reports/gl-reports.component.html | 523 ++++++++ src/app/gl-reports/gl-reports.component.scss | 506 ++++++++ src/app/gl-reports/gl-reports.component.ts | 470 +++++++ src/app/gl-reports/gl-reports.compute.ts | 379 ++++++ src/app/gl-reports/gl-reports.module.ts | 20 + .../aml-rules/aml-rules.component.html | 6 + .../aml-rules/aml-rules.component.scss | 9 + .../aml-rules/aml-rules.component.ts | 36 + src/app/rbi-circulars/oss-return.model.ts | 134 ++ src/app/rbi-circulars/oss-return.scss | 417 ++++++ src/app/rbi-circulars/oss-workbook.ts | 228 ++++ .../rbi-circulars/oss1-ral/oss1-ral-data.ts | 431 +++++++ .../oss1-ral/oss1-ral.component.html | 422 +++++++ .../oss1-ral/oss1-ral.component.ts | 423 +++++++ .../rbi-circulars/oss2-roe/oss2-roe-data.ts | 203 +++ .../oss2-roe/oss2-roe.component.html | 278 ++++ .../oss2-roe/oss2-roe.component.ts | 283 +++++ .../rbi-circulars/oss3-raq/oss3-raq-data.ts | 1124 +++++++++++++++++ .../oss3-raq/oss3-raq.component.html | 372 ++++++ .../oss3-raq/oss3-raq.component.ts | 470 +++++++ .../rbi-circulars-routing.module.ts | 16 + src/environments/.env.ts | 4 +- 33 files changed, 8748 insertions(+), 404 deletions(-) create mode 100644 src/app/gl-reports/gl-reports-data.ts create mode 100644 src/app/gl-reports/gl-reports-routing.module.ts create mode 100644 src/app/gl-reports/gl-reports.component.html create mode 100644 src/app/gl-reports/gl-reports.component.scss create mode 100644 src/app/gl-reports/gl-reports.component.ts create mode 100644 src/app/gl-reports/gl-reports.compute.ts create mode 100644 src/app/gl-reports/gl-reports.module.ts create mode 100644 src/app/rbi-circulars/oss-return.model.ts create mode 100644 src/app/rbi-circulars/oss-return.scss create mode 100644 src/app/rbi-circulars/oss-workbook.ts create mode 100644 src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts create mode 100644 src/app/rbi-circulars/oss1-ral/oss1-ral.component.html create mode 100644 src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts create mode 100644 src/app/rbi-circulars/oss2-roe/oss2-roe-data.ts create mode 100644 src/app/rbi-circulars/oss2-roe/oss2-roe.component.html create mode 100644 src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts create mode 100644 src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts create mode 100644 src/app/rbi-circulars/oss3-raq/oss3-raq.component.html create mode 100644 src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7ec73cbd5..dfd90efed 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -64,6 +64,7 @@ import { BankCalendarModule } from './bank-calendar/bank-calendar.module'; import { DeafModule } from './deaf/deaf.module'; import { DicgcModule } from './dicgc/dicgc.module'; import { FiuModule } from './fiu/fiu.module'; +import { GlReportsModule } from './gl-reports/gl-reports.module'; import { SftModule } from './sft/sft.module'; import { InvestmentModule } from './investment/investment.module'; import { NpaModule } from './npa/npa.module'; @@ -173,6 +174,7 @@ export function HttpLoaderFactory(http: HttpClient) { DeafModule, DicgcModule, FiuModule, + GlReportsModule, SftModule, InvestmentModule, NpaModule, diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 123c9d596..9d76fa2f7 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -2236,26 +2236,26 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges } // Match monthly income ranges - if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) { + if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0 && monthlyIncome < rule.minMonthlyIncome) { continue; } - if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) { + if (rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 && monthlyIncome > rule.maxMonthlyIncome) { continue; } // Match annual income ranges (evaluated against totalAnnualIncome) - if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) { + if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0 && totalAnnualIncome < rule.minAnnualIncome) { continue; } - if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) { + if (rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 && totalAnnualIncome > rule.maxAnnualIncome) { continue; } // Match turnover ranges - if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) { + if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0 && annualTurnover < rule.minAnnualTurnover) { continue; } - if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) { + if (rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 && annualTurnover > rule.maxAnnualTurnover) { continue; } diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 30f3faa1a..9b169d73e 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -828,25 +828,29 @@ Trial Balance Profit and Loss Consolidate Balance Sheet Consolidated Balance Sheet Comparative Balance Sheet @@ -871,7 +875,8 @@ GL Wise DR / CR Report @@ -886,10 +891,11 @@ > Contingent - Expense Banker Head Report Misc Expense Ledger @@ -1706,52 +1713,27 @@ OSS 1 XBRL/CIMS OSS 2 XBRL OSS 3 XBRL/CIMS - - OSS 4 - - - OSS 5 - - - OSS 6 - - - OSS 7 -
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html index ef174ef10..fba04ab72 100644 --- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html @@ -365,6 +365,21 @@ } + + +
+

+ Compliance & AML Triage +

+
+ + Flag as suspicious (escalate to STR cases under RBI compliance) + + + Mark as whitelisted (prevent future AML alerts for this account) + +
+
}
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts index 419e5d7b8..9de8ec6fd 100644 --- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts @@ -131,6 +131,90 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { /** 'any' = mode of operation lets any one signer sign (Either/Anyone or Survivor); 'all' = every mandatory signer must sign. */ signerSelectionMode: 'any' | 'all' = 'any'; manualOverrideChecked = false; + flagAsSuspicious = false; + markAsWhitelisted = false; + + handleComplianceActions(): void { + if (this.flagAsSuspicious) { + const existingStrData = localStorage.getItem('fiu_str_cases'); + let cases: any[] = []; + if (existingStrData) { + try { + cases = JSON.parse(existingStrData); + } catch (e) { + cases = []; + } + } + + const newRef = 101 + cases.length; + const newCase: any = { + reportType: 'STR', + reportRefNo: newRef, + branchCode: this.item.presentingBranchCode || '1', + accountNumber: this.item.accountNo || this.item.accountId || '110001101000838', + txnBranchCode: this.item.presentingBranchCode || '1', + depositOrWithdrawal: 'Withdrawal', + txnId: this.item.id, + txnDate: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'), + txnTime: new Date().toLocaleTimeString('en-US', { hour12: false }), + txnAmount: this.item.amount, + instrumentType: 'Cheque', + instrumentId: this.item.chequeNumber, + nonCustomerRef: '', + relationshipFlag: 'Customer', + thirdPartyPan: '', + declaration: 'NA', + customerName: this.item.clientName || 'UNKNOWN CLIENT', + detectedOn: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'), + flaggedBy: 'CTS Clearing Officer', + riskCategory: 'unusual-pattern', + narrative: `CTS Cheque #${this.item.chequeNumber} for amount ₹${this.item.amount} flagged as suspicious by Clearing Officer during inward presentment review. Action: ${this.mode}. Reason/Notes: ${this.mode === 'RETURN' ? this.returnForm.value.reason : this.passForm.value.note || 'Manual compliance escalation'}`, + status: 'po-review' + }; + + cases.unshift(newCase); + localStorage.setItem('fiu_str_cases', JSON.stringify(cases)); + + const storedCtr = localStorage.getItem('fiu_ctr_txns'); + if (storedCtr) { + try { + const ctrList = JSON.parse(storedCtr); + const matching = ctrList.find((t: any) => t.accountNumber === newCase.accountNumber); + if (matching) { + matching.complianceStatus = 'SUSPICIOUS'; + localStorage.setItem('fiu_ctr_txns', JSON.stringify(ctrList)); + } + } catch (e) {} + } + } else if (this.markAsWhitelisted) { + const existingWhitelist = localStorage.getItem('compliance_whitelisted_accounts'); + let whitelisted: string[] = []; + if (existingWhitelist) { + try { + whitelisted = JSON.parse(existingWhitelist); + } catch (e) { + whitelisted = []; + } + } + const account = this.item.accountNo || this.item.accountId; + if (account && !whitelisted.includes(account.toString())) { + whitelisted.push(account.toString()); + localStorage.setItem('compliance_whitelisted_accounts', JSON.stringify(whitelisted)); + } + + const storedCtr = localStorage.getItem('fiu_ctr_txns'); + if (storedCtr) { + try { + const ctrList = JSON.parse(storedCtr); + const matching = ctrList.find((t: any) => t.accountNumber === (account ? account.toString() : '')); + if (matching) { + matching.complianceStatus = 'WHITELISTED'; + localStorage.setItem('fiu_ctr_txns', JSON.stringify(ctrList)); + } + } catch (e) {} + } + } + } /** Every automatic validation rule InwardClearingValidationService already computes, shown so the clerk sees WHY, not just a decision. */ readonly validationRules: { key: string; label: string; goodWhenTrue: boolean }[] = [ @@ -652,6 +736,7 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { this.ctsService.passInwardItem(this.item.id, payload).subscribe({ next: () => { this.busy = false; + this.handleComplianceActions(); this.notify('Cheque passed'); this.dialogRef.close({ actioned: true }); }, @@ -676,6 +761,7 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { this.ctsService.returnInwardItem(this.item.id, payload).subscribe({ next: () => { this.busy = false; + this.handleComplianceActions(); this.notify('Cheque returned'); this.dialogRef.close({ actioned: true }); }, diff --git a/src/app/fiu/fiu-data.ts b/src/app/fiu/fiu-data.ts index f404f3584..9e2dd350a 100644 --- a/src/app/fiu/fiu-data.ts +++ b/src/app/fiu/fiu-data.ts @@ -42,8 +42,12 @@ export interface CtrTransaction { declaration: string; /** Customer name resolved from KC1 (UI convenience, not in actual TC2 schema) */ customerName?: string; + complianceStatus?: 'PENDING' | 'WHITELISTED' | 'SUSPICIOUS'; } +/** Outcome of the Principal Officer's investigation into an STR case */ +export type PoFinding = 'CLEAN' | 'SUSPICIOUS'; + /** STR record — workflow extension on top of TC2 */ export interface StrTransaction extends CtrTransaction { detectedOn: string; @@ -58,6 +62,8 @@ export interface StrTransaction extends CtrTransaction { narrative: string; status: StrStatus; poRemarks?: string; + /** Set once the Principal Officer concludes the investigation */ + poFinding?: PoFinding; } /** KC1 — Individual customer card (subset of the 70+ fields, only what UI shows) */ @@ -127,7 +133,12 @@ export const FIU_BANK = { cbwtrThreshold: 500000 // CBWTR threshold ₹5 L }; -/** ── Sample CTR transactions (FINnet TC2-style) ── */ +/** + * ── CTR transactions (FINnet TC2-style) ── + * Only CASH transactions that breach the RBI/PMLA reporting limit of ₹10,00,000 + * (single txn, or connected transactions aggregating to ₹10 L in a calendar month) + * belong here. Nothing below the threshold and no non-cash instruments. + */ export const CTR_TXNS: CtrTransaction[] = [ { reportType: 'CTR', @@ -135,423 +146,489 @@ export const CTR_TXNS: CtrTransaction[] = [ branchCode: '1', accountNumber: '110001101000838', txnBranchCode: '1', - depositOrWithdrawal: 'Withdrawal', + depositOrWithdrawal: 'Deposit', txnId: 1, - txnDate: '20-01-2026', - txnTime: '16:51:28', - txnAmount: 1500000, - instrumentType: 'Cheque', - instrumentId: '24304', + txnDate: '05-01-2026', + txnTime: '10:22:41', + txnAmount: 1450000, + instrumentType: 'Cash', + instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'RAVI KUMAR SHARMA' + customerName: 'ANAND KRISHNAN' }, { reportType: 'CTR', - reportRefNo: 1, - branchCode: '1', - accountNumber: '110001101000838', - txnBranchCode: '1', - depositOrWithdrawal: 'Withdrawal', + reportRefNo: 2, + branchCode: '3', + accountNumber: '110001204000712', + txnBranchCode: '3', + depositOrWithdrawal: 'Deposit', txnId: 2, - txnDate: '27-01-2026', - txnTime: '16:14:22', - txnAmount: 932000, - instrumentType: 'Cheque', - instrumentId: '24305', + txnDate: '09-01-2026', + txnTime: '12:47:09', + txnAmount: 3200000, + instrumentType: 'Cash', + instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'RAVI KUMAR SHARMA' - }, - { - reportType: 'CTR', - reportRefNo: 2, - branchCode: '1', - accountNumber: '110001352000357', - txnBranchCode: '1', - depositOrWithdrawal: 'Withdrawal', - txnId: 3, - txnDate: '02-01-2026', - txnTime: '15:05:36', - txnAmount: 600000, - instrumentType: 'Cheque', - instrumentId: '100039', - nonCustomerRef: '', - relationshipFlag: 'Customer', - thirdPartyPan: '', - declaration: 'NA', - customerName: 'PRIYA PATEL' - }, - { - reportType: 'CTR', - reportRefNo: 2, - branchCode: '1', - accountNumber: '110001352000357', - txnBranchCode: '1', - depositOrWithdrawal: 'Withdrawal', - txnId: 4, - txnDate: '07-01-2026', - txnTime: '14:14:17', - txnAmount: 700000, - instrumentType: 'Cheque', - instrumentId: '100040', - nonCustomerRef: '', - relationshipFlag: 'Customer', - thirdPartyPan: '', - declaration: 'NA', - customerName: 'PRIYA PATEL' + customerName: 'MEERA SUNDARAM' }, { reportType: 'CTR', reportRefNo: 3, - branchCode: '7', - accountNumber: '110001352000420', - txnBranchCode: '7', - depositOrWithdrawal: 'Deposit', - txnId: 5, - txnDate: '12-01-2026', - txnTime: '11:28:50', - txnAmount: 1200000, + branchCode: '6', + accountNumber: '110001307000455', + txnBranchCode: '6', + depositOrWithdrawal: 'Withdrawal', + txnId: 3, + txnDate: '13-01-2026', + txnTime: '15:03:55', + txnAmount: 1075000, instrumentType: 'Cash', instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'VIKRAM REDDY' + customerName: 'HARPREET SINGH BEDI' }, { reportType: 'CTR', reportRefNo: 4, - branchCode: '4', - accountNumber: '110001352000511', - txnBranchCode: '4', + branchCode: '2', + accountNumber: '110001408000933', + txnBranchCode: '2', depositOrWithdrawal: 'Deposit', - txnId: 6, - txnDate: '18-01-2026', - txnTime: '10:14:05', - txnAmount: 2500000, + txnId: 4, + txnDate: '16-01-2026', + txnTime: '11:19:27', + txnAmount: 5600000, instrumentType: 'Cash', instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'NIRAV SHAH' + customerName: 'FAROOQ ABDULLAH KHAN' }, { reportType: 'CTR', reportRefNo: 5, - branchCode: '11', - accountNumber: '110001352000604', - txnBranchCode: '11', + branchCode: '9', + accountNumber: '110001509000268', + txnBranchCode: '9', depositOrWithdrawal: 'Withdrawal', - txnId: 7, - txnDate: '24-01-2026', - txnTime: '13:42:11', - txnAmount: 1100000, - instrumentType: 'Cheque', - instrumentId: '88123', - nonCustomerRef: 'NCR-001', - relationshipFlag: 'Non-Customer', - thirdPartyPan: 'AKHPV2244R', - declaration: 'NA', - customerName: 'KAVITA VERMA' - } -]; - -/** ── Sample STR transactions ── */ -export const STR_TXNS: StrTransaction[] = [ - { - reportType: 'STR', - reportRefNo: 101, - branchCode: '1', - accountNumber: '110001101000841', - txnBranchCode: '1', - depositOrWithdrawal: 'Deposit', - txnId: 8, - txnDate: '15-01-2026', - txnTime: '11:32:00', - txnAmount: 950000, + txnId: 5, + txnDate: '20-01-2026', + txnTime: '14:35:12', + txnAmount: 2850000, instrumentType: 'Cash', instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'ARJUN DESHMUKH', - detectedOn: '16-01-2026', - flaggedBy: 'AML Engine — Rule R-007', - riskCategory: 'structuring', + customerName: 'DEEPIKA RANGANATHAN' + }, + { + reportType: 'CTR', + reportRefNo: 6, + branchCode: '5', + accountNumber: '110001610000841', + txnBranchCode: '5', + depositOrWithdrawal: 'Deposit', + txnId: 6, + txnDate: '23-01-2026', + txnTime: '09:58:33', + txnAmount: 1000000, + instrumentType: 'Cash', + instrumentId: 'CASH', + nonCustomerRef: '', + relationshipFlag: 'Customer', + thirdPartyPan: '', + declaration: 'NA', + customerName: 'RAJENDRA PRASAD YADAV' + }, + { + reportType: 'CTR', + reportRefNo: 7, + branchCode: '12', + accountNumber: '110001711000527', + txnBranchCode: '12', + depositOrWithdrawal: 'Deposit', + txnId: 7, + txnDate: '27-01-2026', + txnTime: '16:41:08', + txnAmount: 4300000, + instrumentType: 'Cash', + instrumentId: 'CASH', + nonCustomerRef: 'NCR-014', + relationshipFlag: 'Non-Customer', + thirdPartyPan: 'BXKPT7788M', + declaration: 'NA', + customerName: 'SHABANA TARIQ MIRZA' + } +]; + +/** + * ── STR cases ── + * Raised either by the AML rules engine or by an officer flagging a CTR row as + * suspicious. Each case is investigated by the Principal Officer, who concludes + * whether the funds are legitimate (CLEAN) or linked to terror financing. + */ +export const STR_TXNS: StrTransaction[] = [ + { + reportType: 'STR', + reportRefNo: 101, + branchCode: '8', + accountNumber: '110001812000390', + txnBranchCode: '8', + depositOrWithdrawal: 'Deposit', + txnId: 101, + txnDate: '11-01-2026', + txnTime: '13:08:21', + txnAmount: 2750000, + instrumentType: 'Cash', + instrumentId: 'CASH', + nonCustomerRef: '', + relationshipFlag: 'Customer', + thirdPartyPan: '', + declaration: 'NA', + customerName: 'IMRAN SHEIKH QURESHI', + detectedOn: '12-01-2026', + flaggedBy: 'AML Engine — Rule R-031', + riskCategory: 'sanctions-hit', narrative: - 'Three sub-₹10L cash deposits within 5 days totalling ₹28.4L; pattern indicative of structuring to avoid CTR threshold.', + 'Cash deposit of ₹27.5L followed within 48 hours by outward remittances to three beneficiaries whose names partially match the UAPA Schedule IV designated-persons list. Awaiting Principal Officer investigation.', status: 'po-review' }, { reportType: 'STR', reportRefNo: 102, - branchCode: '7', - accountNumber: '110001101000922', - txnBranchCode: '7', + branchCode: '4', + accountNumber: '110001915000174', + txnBranchCode: '4', depositOrWithdrawal: 'Deposit', - txnId: 9, - txnDate: '21-01-2026', - txnTime: '14:18:30', - txnAmount: 4800000, - instrumentType: 'NEFT', - instrumentId: 'N1264821', - nonCustomerRef: '', - relationshipFlag: 'Customer', - thirdPartyPan: '', - declaration: 'NA', - customerName: 'LAKSHMI IYER', - detectedOn: '22-01-2026', - flaggedBy: 'Branch Compliance Officer', - riskCategory: 'dormant-revival', - narrative: - 'Account dormant for 18 months suddenly received ₹48L NEFT followed by 4 immediate cash withdrawals of ₹2L each.', - status: 'filed', - poRemarks: 'STR filed with FIU-IND. Acknowledgement no: STR-2026-9821.' - }, - { - reportType: 'STR', - reportRefNo: 103, - branchCode: '2', - accountNumber: '110001302001012', - txnBranchCode: '2', - depositOrWithdrawal: 'Withdrawal', - txnId: 10, - txnDate: '23-01-2026', - txnTime: '15:42:18', - txnAmount: 1800000, - instrumentType: 'RTGS', - instrumentId: 'R889013', - nonCustomerRef: '', - relationshipFlag: 'Customer', - thirdPartyPan: '', - declaration: 'NA', - customerName: 'SAMEER KAPOOR', - detectedOn: '24-01-2026', - flaggedBy: 'AML Engine — Rule R-014', - riskCategory: 'high-risk-country', - narrative: - 'RTGS transfer to beneficiary in jurisdiction listed under FATF grey list (Cayman Islands intermediary). KYC documentation incomplete.', - status: 'draft' - }, - { - reportType: 'STR', - reportRefNo: 104, - branchCode: '13', - accountNumber: '110001352001145', - txnBranchCode: '13', - depositOrWithdrawal: 'Deposit', - txnId: 11, - txnDate: '08-01-2026', - txnTime: '12:55:42', - txnAmount: 6500000, + txnId: 102, + txnDate: '07-01-2026', + txnTime: '10:44:52', + txnAmount: 1980000, instrumentType: 'Cash', instrumentId: 'CASH', nonCustomerRef: '', relationshipFlag: 'Customer', thirdPartyPan: '', declaration: 'NA', - customerName: 'MANMOHAN SINGH GILL', - detectedOn: '09-01-2026', - flaggedBy: 'AML Engine — Rule R-022', + customerName: 'GOPAL VENKATARAMAN', + detectedOn: '08-01-2026', + flaggedBy: 'Branch Compliance Officer', riskCategory: 'unusual-pattern', narrative: - 'Account avg monthly turnover ₹1.5L; received ₹65L cash in single deposit, source-of-funds documentation pending.', + 'Declared annual income ₹6L but deposited ₹19.8L cash in a single visit. Customer produced sale deed for ancestral agricultural land.', + status: 'draft', + poFinding: 'CLEAN', + poRemarks: + 'Source of funds verified against registered sale deed dated 02-01-2026 and buyer bank statement. Legitimate agricultural land sale — no adverse findings. Case closed, no STR filed with FIU-IND.' + }, + { + reportType: 'STR', + reportRefNo: 103, + branchCode: '10', + accountNumber: '110002016000628', + txnBranchCode: '10', + depositOrWithdrawal: 'Withdrawal', + txnId: 103, + txnDate: '19-01-2026', + txnTime: '16:27:40', + txnAmount: 3400000, + instrumentType: 'Cash', + instrumentId: 'CASH', + nonCustomerRef: '', + relationshipFlag: 'Customer', + thirdPartyPan: '', + declaration: 'NA', + customerName: 'ABDUL RAHMAN BAIG', + detectedOn: '20-01-2026', + flaggedBy: 'AML Engine — Rule R-045', + riskCategory: 'high-risk-country', + narrative: + 'Account credited by 14 small inward remittances from a FATF high-risk jurisdiction over 9 days, then emptied via ₹34L cash withdrawal at a border-district branch. Classic terror-funding placement-and-dispersal pattern.', + status: 'filed', + poFinding: 'SUSPICIOUS', + poRemarks: + 'Investigation confirmed layered inward remittances from a designated high-risk corridor with no commercial rationale. Beneficial owner could not evidence source. STR filed with FIU-IND under PMLA s.12; acknowledgement STR-2026-10457. Account frozen and reported to the Nodal Officer.' + }, + { + reportType: 'STR', + reportRefNo: 104, + branchCode: '1', + accountNumber: '110002117000315', + txnBranchCode: '1', + depositOrWithdrawal: 'Deposit', + txnId: 104, + txnDate: '24-01-2026', + txnTime: '11:52:14', + txnAmount: 2300000, + instrumentType: 'Cash', + instrumentId: 'CASH', + nonCustomerRef: '', + relationshipFlag: 'Customer', + thirdPartyPan: '', + declaration: 'NA', + customerName: 'SUNITA CHAUDHARY', + detectedOn: '25-01-2026', + flaggedBy: 'AML Engine — Rule R-018', + riskCategory: 'structuring', + narrative: + 'Six cash deposits of ₹3.8L each across four branches on the same day, aggregating ₹23L — deliberate splitting to stay under the ₹10L single-transaction CTR limit.', status: 'po-review' } ]; -/** ── KC1 sample individual customers ── */ +/** ── KC1 individual customer cards (one per CTR report reference) ── */ export const KC1_INDIVIDUALS: KycIndividual[] = [ { reportRefNo: 1, - ucic: '10078', - pan: 'AAQPS8560J', - ckycNumber: '60011225033445', - firstName: 'RAVI', - middleName: 'KUMAR', - lastName: 'SHARMA', - fatherName: 'RAJESH KUMAR SHARMA', - motherName: 'SUNITA DEVI', + ucic: '30115', + pan: 'AFTPK4471B', + ckycNumber: '60031115447722', + firstName: 'ANAND', + middleName: '', + lastName: 'KRISHNAN', + fatherName: 'SUBRAMANIAN KRISHNAN', + motherName: 'VASANTHA KRISHNAN', gender: 'Male', - dob: '10-08-1985', - mobileNumber: '91-9892011001', - email: 'ravi.sharma@example.com', - primaryAddress: 'FLAT 402 SUNRISE TOWERS MAROL ANDHERI EAST', - state: 'MH', - city: 'MUMBAI', - pincode: '400069', + dob: '14-06-1979', + mobileNumber: '91-9840133015', + email: 'anand.krishnan@example.com', + primaryAddress: 'NO 18 SECOND STREET ADYAR', + state: 'TN', + city: 'CHENNAI', + pincode: '600020', customerType: 'Individual', - annualIncome: 750000, - occupation: 'Service', - onboardingDate: '15-03-2020', - lastKycDate: '12-04-2025', + annualIncome: 2200000, + occupation: 'Business', + onboardingDate: '22-07-2016', + lastKycDate: '18-06-2025', riskLevel: 'Medium', reportType: 'CTR', otherIdentifierName: 'AADHAAR', - otherIdentifierNumber: 'XXXXXXXX1234' + otherIdentifierNumber: 'XXXXXXXX4471' }, { reportRefNo: 2, - ucic: '10079', - pan: 'AKLPS9933P', - ckycNumber: '60022144558899', - firstName: 'PRIYA', + ucic: '30228', + pan: 'AJSPS6612D', + ckycNumber: '60032228661133', + firstName: 'MEERA', middleName: '', - lastName: 'PATEL', - fatherName: 'HARISH PATEL', - motherName: 'MEENA PATEL', + lastName: 'SUNDARAM', + fatherName: 'RAGHAVAN SUNDARAM', + motherName: 'KALYANI SUNDARAM', gender: 'Female', - dob: '22-03-1990', - mobileNumber: '91-9822055002', - email: 'priya.patel@example.com', - primaryAddress: 'B-12 SHIVAJI NAGAR FC ROAD', - state: 'MH', - city: 'PUNE', - pincode: '411005', + dob: '29-11-1986', + mobileNumber: '91-9886044028', + email: 'meera.sundaram@example.com', + primaryAddress: 'FLAT 9C BRIGADE HERITAGE WHITEFIELD', + state: 'KA', + city: 'BENGALURU', + pincode: '560066', customerType: 'Individual', - annualIncome: 480000, - occupation: 'Self-Employed', - onboardingDate: '10-09-2023', - lastKycDate: '15-03-2025', - riskLevel: 'Low', + annualIncome: 3600000, + occupation: 'Business', + onboardingDate: '11-01-2019', + lastKycDate: '05-05-2025', + riskLevel: 'High', reportType: 'CTR', otherIdentifierName: 'AADHAAR', - otherIdentifierNumber: 'XXXXXXXX2210' + otherIdentifierNumber: 'XXXXXXXX6612' }, { reportRefNo: 3, - ucic: '20101', - pan: 'AAEPR9352L', + ucic: '30334', + pan: 'AMBPB9027F', ckycNumber: '', - firstName: 'VIKRAM', - middleName: '', - lastName: 'REDDY', - fatherName: 'SURESH REDDY', - motherName: 'LAKSHMI REDDY', + firstName: 'HARPREET', + middleName: 'SINGH', + lastName: 'BEDI', + fatherName: 'GURMEET SINGH BEDI', + motherName: 'JASWINDER KAUR', gender: 'Male', - dob: '07-07-1982', - mobileNumber: '91-9845022004', + dob: '03-02-1974', + mobileNumber: '91-9815066034', email: '', - primaryAddress: 'NO 23 4TH CROSS JAYANAGAR 5TH BLOCK', - state: 'KA', - city: 'BENGALURU', - pincode: '560041', + primaryAddress: 'HOUSE 221 SECTOR 34A CHANDIGARH ROAD', + state: 'PB', + city: 'LUDHIANA', + pincode: '141001', customerType: 'Individual', - annualIncome: 950000, - occupation: 'Profession', - onboardingDate: '12-11-2021', - lastKycDate: '20-02-2025', + annualIncome: 1450000, + occupation: 'Business', + onboardingDate: '30-08-2012', + lastKycDate: '14-03-2025', riskLevel: 'Medium', reportType: 'CTR', otherIdentifierName: 'AADHAAR', - otherIdentifierNumber: 'XXXXXXXX6622' + otherIdentifierNumber: 'XXXXXXXX9027' }, { reportRefNo: 4, - ucic: '40301', - pan: 'AHKPS5544Z', - ckycNumber: '60040301229988', - firstName: 'NIRAV', - middleName: '', - lastName: 'SHAH', - fatherName: 'JAYESH SHAH', - motherName: 'RITA SHAH', + ucic: '30447', + pan: 'ANQPK3358H', + ckycNumber: '60034447335844', + firstName: 'FAROOQ', + middleName: 'ABDULLAH', + lastName: 'KHAN', + fatherName: 'ISHTIAQ AHMED KHAN', + motherName: 'NASREEN BEGUM', gender: 'Male', - dob: '18-05-1992', - mobileNumber: '91-9824012010', - email: 'nirav.shah@example.com', - primaryAddress: 'A-401 ATLANTIS APARTMENTS SATELLITE ROAD', - state: 'GJ', - city: 'AHMEDABAD', - pincode: '380015', + dob: '17-09-1981', + mobileNumber: '91-9701077047', + email: 'farooq.khan@example.com', + primaryAddress: '12-2-417 MEHDIPATNAM MAIN ROAD', + state: 'TG', + city: 'HYDERABAD', + pincode: '500028', customerType: 'Individual', - annualIncome: 1800000, - occupation: 'Service', - onboardingDate: '14-02-2018', - lastKycDate: '20-12-2024', + annualIncome: 890000, + occupation: 'Business', + onboardingDate: '05-04-2020', + lastKycDate: '09-01-2025', riskLevel: 'High', reportType: 'CTR', otherIdentifierName: 'PASSPORT', - otherIdentifierNumber: 'M5544321' + otherIdentifierNumber: 'P3358214' }, { reportRefNo: 5, - ucic: '50402', - pan: 'AKHPV2244R', - ckycNumber: '', - firstName: 'KAVITA', + ucic: '30552', + pan: 'APLPR7783C', + ckycNumber: '60035552778355', + firstName: 'DEEPIKA', middleName: '', - lastName: 'VERMA', - fatherName: 'DEEPAK VERMA', - motherName: 'USHA VERMA', + lastName: 'RANGANATHAN', + fatherName: 'MOHAN RANGANATHAN', + motherName: 'SHOBHA RANGANATHAN', gender: 'Female', - dob: '01-04-1988', - mobileNumber: '91-9415055014', - email: '', - primaryAddress: '45 CIVIL LINES MALL ROAD', - state: 'UP', - city: 'KANPUR', - pincode: '208001', + dob: '08-12-1991', + mobileNumber: '91-9739088052', + email: 'deepika.r@example.com', + primaryAddress: 'VILLA 7 PALM MEADOWS SARJAPUR ROAD', + state: 'KA', + city: 'BENGALURU', + pincode: '560035', customerType: 'Individual', - annualIncome: 540000, - occupation: 'Service', - onboardingDate: '08-09-2022', - lastKycDate: '15-09-2024', + annualIncome: 2750000, + occupation: 'Profession', + onboardingDate: '19-06-2021', + lastKycDate: '27-07-2025', riskLevel: 'Low', reportType: 'CTR', otherIdentifierName: 'AADHAAR', - otherIdentifierNumber: 'XXXXXXXX2244' + otherIdentifierNumber: 'XXXXXXXX7783' + }, + { + reportRefNo: 6, + ucic: '30669', + pan: 'AQWPY2194K', + ckycNumber: '', + firstName: 'RAJENDRA', + middleName: 'PRASAD', + lastName: 'YADAV', + fatherName: 'BHOLA NATH YADAV', + motherName: 'KAMLA DEVI', + gender: 'Male', + dob: '25-05-1968', + mobileNumber: '91-9451099069', + email: '', + primaryAddress: '78 GOMTI NAGAR VIBHUTI KHAND', + state: 'UP', + city: 'LUCKNOW', + pincode: '226010', + customerType: 'Individual', + annualIncome: 640000, + occupation: 'Agriculture', + onboardingDate: '17-10-2014', + lastKycDate: '22-11-2024', + riskLevel: 'Low', + reportType: 'CTR', + otherIdentifierName: 'AADHAAR', + otherIdentifierNumber: 'XXXXXXXX2194' + }, + { + reportRefNo: 7, + ucic: '30774', + pan: 'BXKPT7788M', + ckycNumber: '', + firstName: 'SHABANA', + middleName: 'TARIQ', + lastName: 'MIRZA', + fatherName: 'TARIQ HUSSAIN MIRZA', + motherName: 'RUKHSANA MIRZA', + gender: 'Female', + dob: '11-03-1989', + mobileNumber: '91-9958100074', + email: '', + primaryAddress: '54 ZAKIR NAGAR OKHLA', + state: 'DL', + city: 'NEW DELHI', + pincode: '110025', + customerType: 'Individual', + annualIncome: 720000, + occupation: 'Self-Employed', + onboardingDate: '02-02-2023', + lastKycDate: '16-08-2025', + riskLevel: 'High', + reportType: 'CTR', + otherIdentifierName: 'AADHAAR', + otherIdentifierNumber: 'XXXXXXXX7788' } ]; -/** ── KC2 sample entity customers ── */ +/** ── KC2 entity customer cards ── */ export const KC2_ENTITIES: KycEntity[] = [ { - reportRefNo: 11, - ucic: 'E10101', - entityName: 'BHARAT TRADING COMPANY', - panNumber: 'ABCFB1234N', - gstin: '27ABCFB1234N1ZT', - mobileNumber: '91-9892011010', - address: 'GALA 22 LOWER PAREL', - state: 'MH', - city: 'MUMBAI', - pincode: '400013', - dateOfIncorporation: '24-03-2008', + reportRefNo: 2, + ucic: 'E30228', + entityName: 'SUNDARAM BULLION TRADERS', + panNumber: 'AAGFS3391Q', + gstin: '29AAGFS3391Q1ZP', + mobileNumber: '91-9886044028', + address: 'SHOP 14 COMMERCIAL STREET SHIVAJI NAGAR', + state: 'KA', + city: 'BENGALURU', + pincode: '560001', + dateOfIncorporation: '11-01-2019', customerType: 'Sole Proprietor', riskLevel: 'High', - onboardingDate: '24-03-2008', - lastKycDate: '03-09-2025', + onboardingDate: '11-01-2019', + lastKycDate: '05-05-2025', isSoleProprietor: true, - ownerUcic: '10078', - ownerName: 'RAVI KUMAR SHARMA', + ownerUcic: '30228', + ownerName: 'MEERA SUNDARAM', reportType: 'CTR' }, { - reportRefNo: 12, - ucic: 'E10102', - entityName: 'NEXTGEN TECH PVT LTD', - panNumber: 'AABCN5566K', - gstin: '27AABCN5566K1Z2', - mobileNumber: '91-9824012011', - address: 'TOWER B SUNRISE BUSINESS PARK ANDHERI EAST', - state: 'MH', - city: 'MUMBAI', - pincode: '400093', - dateOfIncorporation: '18-09-2015', + reportRefNo: 4, + ucic: 'E30447', + entityName: 'DECCAN SCRAP & METALS PVT LTD', + panNumber: 'AABCD8842R', + gstin: '36AABCD8842R1ZN', + mobileNumber: '91-9701077047', + address: 'PLOT 61 IDA JEEDIMETLA PHASE II', + state: 'TG', + city: 'HYDERABAD', + pincode: '500055', + dateOfIncorporation: '05-04-2020', customerType: 'Private Limited', - riskLevel: 'Medium', - onboardingDate: '18-09-2015', - lastKycDate: '12-08-2025', + riskLevel: 'High', + onboardingDate: '05-04-2020', + lastKycDate: '09-01-2025', isSoleProprietor: false, ownerUcic: '', ownerName: '', diff --git a/src/app/fiu/fiu.component.html b/src/app/fiu/fiu.component.html index c6a1a6439..6328335ad 100644 --- a/src/app/fiu/fiu.component.html +++ b/src/app/fiu/fiu.component.html @@ -71,17 +71,19 @@
-
{{ pendingStr | number }}
+
{{ liveStrPending | number }}
STR Pending
-
draft / PO review
+
+ {{ liveStrOpen }} to investigate · {{ liveStrAwaitingFiling }} awaiting filing +
-
{{ filedStr | number }}
+
{{ liveStrFiled | number }}
STR Filed
-
acknowledged by FIU
+
{{ liveStrCleared }} concluded clear
@@ -112,7 +114,12 @@
Filing window: within 7 days of detection
- Pending: {{ pendingStr }} · Filed: {{ filedStr }} + To investigate: {{ liveStrOpen }} · Awaiting filing: + {{ liveStrAwaitingFiling }} +
+
+ Filed: {{ liveStrFiled }} · Concluded clear: + {{ liveStrCleared }}
@@ -129,12 +136,12 @@ @if (activeTab === 'ctr') {
- +
-

Live Cash Alerts

+

Live CTR Threshold Alerts

- Raised by the transaction-monitoring rule engine — review and whitelist or escalate to Principal Officer + Mechanical cash transaction threshold breaches — review and whitelist or escalate to Principal Officer

} @empty { - No live alerts. + No live alerts. }
+

+ Pending awaiting Whitelist/Escalate decision · + CTR only whitelisted — cleared, no STR needed · + → STR escalated to Principal Officer as an STR case +

@@ -295,6 +311,8 @@ Instrument Branch Relationship + Filed Under + Compliance Action @@ -327,21 +345,131 @@ t.relationshipFlag }} + + CTR + @if (t.complianceStatus === 'SUSPICIOUS') { + + STR + } + + + @if (!t.complianceStatus || t.complianceStatus === 'PENDING') { +
+ + +
+ } @else { + + {{ t.complianceStatus }} + + @if (t.complianceStatus === 'WHITELISTED') { +
Stays in CTR only
+ } @else { + + } + } + } @empty { - No transactions match the filters. + No transactions match the filters. }
+

+ CTR every cash txn ≥ threshold stays in the monthly CTR / TC2 + file · + + STR also flagged as suspicious and escalated to a separate STR + case (see the STR tab) +

} @if (activeTab === 'str') {
+ +
+
+

Live AML Alerts (Awaiting Triage)

+

+ Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate +

+
+
+
+ + + + + + + + + + + + + + + + + @for (alert of amlSuspiciousAlerts; track alert.id) { + + + + + + + + + + + + + } @empty { + + + + } + +
IDClientTypeRuleAmountRaised OnStatusRoutes ToNotesAction
{{ alert.id }}{{ alert.clientDisplayName }}{{ alert.transactionType }}{{ alert.ruleCode }} — {{ alert.ruleDescription }}₹{{ alert.amount | number: '1.0-0' }}{{ alert.raisedOn | date: 'medium' }} + {{ alert.status }} + + {{ alertRouteLabel(alert.status, false) }} + + @if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') { + + } @else { + {{ alert.reviewNotes }} + } + + @if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') { +
+ + +
+ } +
No suspicious behavioral alerts.
+
+

+ Pending awaiting Whitelist/Escalate decision · + Cleared whitelisted — no report filed · + → STR escalated to Principal Officer as an STR case +

+ +
+
@@ -407,6 +535,17 @@
+
+ {{ selectedStrCase.status }} + {{ strCaseOutcome(selectedStrCase) }} +
+
@@ -416,17 +555,96 @@
-
- - + +
+
+ Principal Officer verification — mark each point as found + + {{ answeredCount }} / {{ strChecklist.length }} answered + +
+ + + + + + + + + + @for (item of strChecklist; track item.key) { + + + + + + } + +
Verification pointGenuineNot satisfied
+
{{ item.label }}
+
{{ item.hint }}
+
+ + + +
- + + +
+ @if (!allAnswered) { + + + Answer every verification point to determine the outcome — + {{ strChecklist.length - answeredCount }} still pending. + + } @else if (derivedConclusion === 'NOT_SUSPICIOUS') { + + + Not suspicious. All {{ strChecklist.length }} points found genuine — the case will + be closed and nothing filed with FIU-IND. + + } @else { + + + Suspicious — STR must be filed. + {{ failedFindings.length }} of {{ strChecklist.length }} points not satisfied: + {{ failedFindings[0].label }}@if (failedFindings.length > 1) { + and {{ failedFindings.length - 1 }} more + }. + + } +
+ + @if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
@@ -478,7 +696,12 @@
@for (s of filteredStr; track s.reportRefNo) { -
+
STR-{{ s.reportRefNo }} @@ -521,6 +744,27 @@

{{ s.poRemarks }}

} + @if (s.poFinding) { +
+ +

{{ poFindingLabel(s.poFinding) }}

+
+ } @else { +
+ +
+ + +
+
+ }
} @empty { @@ -626,10 +870,13 @@

FINnet 2.0 Submission Files

-

6 CSV files in FIU-IND prescribed schema, plus STR XML

+

+ 6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and + STR tabs, so every file in the bundle covers the same transactions. +

@@ -641,7 +888,7 @@
TC2.csv
-

Branch / cash transactions ({{ ctrTxns.length }} rows)

+

Branch / cash transactions ({{ filteredCtr.length }} rows)

@@ -676,7 +923,7 @@
STR.xml
-

Suspicious Transaction Report ({{ filedStr + pendingStr }} suspects)

+

Suspicious Transaction Report ({{ strSuspects.length }} suspects — cases concluded clean are excluded)

diff --git a/src/app/fiu/fiu.component.scss b/src/app/fiu/fiu.component.scss index f5871481f..9d74a7c6f 100644 --- a/src/app/fiu/fiu.component.scss +++ b/src/app/fiu/fiu.component.scss @@ -386,6 +386,100 @@ align-self: end; } +.fiu-link-btn { + display: inline-block; + margin-top: 3px; + padding: 0; + border: none; + background: none; + font-family: inherit; + font-size: 11px; + font-weight: 600; + color: var(--f-red); + cursor: pointer; + text-decoration: underline; + + &:hover { + color: #9a2f22; + } + + &--muted { + color: var(--f-g500); + + &:hover { + color: var(--f-red); + } + } +} + +/* PRINCIPAL OFFICER INVESTIGATION */ +.str-po-action { + background: var(--f-amber-l); + border: 1px dashed #fcd34d; + border-radius: 8px; + padding: 10px 14px; + display: flex; + flex-direction: column; + gap: 8px; + + > label { + font-size: 10px; + font-weight: 700; + color: #92400e; + text-transform: uppercase; + letter-spacing: 0.05em; + } +} + +.fiu-btn-clean { + background: var(--f-green); + border-color: var(--f-green); + color: #fff; + + &:hover { + background: #166534; + border-color: #166534; + color: #fff; + } +} + +.str-finding { + background: var(--f-green-l); + border-radius: 8px; + padding: 10px 14px; + + label { + font-size: 10px; + font-weight: 700; + color: var(--f-green); + text-transform: uppercase; + letter-spacing: 0.05em; + display: flex; + align-items: center; + gap: 5px; + margin-bottom: 4px; + } + + p { + margin: 0; + font-size: 13px; + color: #14532d; + line-height: 1.5; + } + + &--suspicious { + background: var(--f-red-l); + + label { + color: var(--f-red); + } + + p { + color: #7f1d1d; + } + } +} + /* INPUTS */ .fiu-inp { width: 100%; @@ -538,6 +632,50 @@ } } +.report-pill { + display: inline-block; + padding: 2px 9px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + + &.report-ctr { + background: var(--f-blue-l); + color: var(--f-blue); + } + + &.report-str { + background: var(--f-red-l); + color: var(--f-red); + margin-left: 4px; + } + + &.report-pending { + background: var(--f-amber-l); + color: var(--f-amber); + } + + &.report-cleared { + background: var(--f-green-l); + color: var(--f-green); + } +} + +.fiu-tbl-legend { + margin: -6px 2px 0; + font-size: 11px; + color: var(--f-g500); + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + + .report-pill { + margin-left: 0; + } +} + .rel-pill { display: inline-block; padding: 2px 9px; @@ -697,6 +835,11 @@ &--filed { border-color: #86efac; } + + &--highlight { + border-color: var(--f-red); + box-shadow: 0 0 0 3px rgb(192 57 43 / 20%); + } } .str-card-h { @@ -956,6 +1099,29 @@ margin-bottom: 16px; } +.case-outcome { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin: 12px 0; + padding: 10px 14px; + border-radius: 8px; + background: var(--f-g100); + font-size: 13px; + color: var(--f-g700); + + &--clear { + background: var(--f-green-l); + color: #14532d; + } + + &--suspicious { + background: var(--f-red-l); + color: #7f1d1d; + } +} + .case-form { display: flex; flex-direction: column; @@ -995,6 +1161,181 @@ } } +/* ── Principal Officer verification checklist ───────────────────────────── */ +.checklist { + border: 1px solid var(--f-g200); + border-radius: 10px; + overflow: hidden; + background: #fff; +} + +.checklist-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--f-g50); + border-bottom: 1px solid var(--f-g200); + font-size: 12px; + font-weight: 700; + color: var(--f-g700); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.checklist-progress { + font-size: 11px; + font-weight: 700; + padding: 2px 10px; + border-radius: 999px; + background: var(--f-amber-l); + color: var(--f-amber); + text-transform: none; + letter-spacing: 0; + white-space: nowrap; + + &.done { + background: var(--f-green-l); + color: var(--f-green); + } +} + +.checklist-tbl { + width: 100%; + border-collapse: collapse; + + th { + padding: 8px 12px; + font-size: 10px; + font-weight: 700; + color: var(--f-g500); + text-transform: uppercase; + letter-spacing: 0.05em; + background: var(--f-g50); + border-bottom: 1px solid var(--f-g200); + text-align: left; + white-space: nowrap; + } + + th.col-yes { + color: var(--f-green); + width: 96px; + } + + th.col-no { + color: var(--f-red); + width: 112px; + } + + td { + padding: 9px 12px; + border-bottom: 1px solid var(--f-g100); + vertical-align: middle; + } + + tr:last-child td { + border-bottom: none; + } +} + +/* Answered rows tint so an unresolved finding is obvious at a glance. */ +.row-yes td { + background: rgb(220 252 231 / 45%); +} + +.row-no td { + background: rgb(253 236 234 / 60%); +} + +.row-pending td { + background: #fff; +} + +.chk-label { + font-size: 13px; + font-weight: 600; + color: var(--f-g900); +} + +.chk-hint { + font-size: 11px; + color: var(--f-g500); + margin-top: 1px; +} + +/* Two-sided toggle: one box on the genuine side, one on the not-satisfied side. */ +.chk-box { + width: 30px; + height: 30px; + border-radius: 7px; + border: 2px solid var(--f-g300); + background: #fff; + color: var(--f-g300); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.15s; + + &:hover { + transform: scale(1.08); + } + + &--yes:hover { + border-color: var(--f-green); + color: var(--f-green); + } + + &--no:hover { + border-color: var(--f-red); + color: var(--f-red); + } + + &--yes.on { + border-color: var(--f-green); + background: var(--f-green); + color: #fff; + } + + &--no.on { + border-color: var(--f-red); + background: var(--f-red); + color: #fff; + } +} + +/* Derived verdict banner */ +.verdict { + display: flex; + align-items: center; + gap: 10px; + padding: 11px 15px; + border-radius: 9px; + font-size: 13px; + background: var(--f-g100); + border: 1px solid var(--f-g200); + color: var(--f-g700); + + &--clear { + background: var(--f-green-l); + border-color: #86efac; + color: #14532d; + } + + &--suspicious { + background: var(--f-red-l); + border-color: #fca5a5; + color: #7f1d1d; + } +} + +.case-form .fiu-btn-xml:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .file-str-row { display: flex; align-items: center; diff --git a/src/app/fiu/fiu.component.ts b/src/app/fiu/fiu.component.ts index 42cd2a927..aeb1f6fed 100644 --- a/src/app/fiu/fiu.component.ts +++ b/src/app/fiu/fiu.component.ts @@ -20,6 +20,7 @@ import { KC2_ENTITIES, KycEntity, KycIndividual, + PoFinding, StrStatus, StrTransaction, STR_TXNS @@ -36,6 +37,9 @@ import { ] }) export class FiuComponent implements OnInit { + /** Bump when CTR_TXNS / STR_TXNS change so cached rows are re-seeded. */ + private static readonly SEED_VERSION = '2026-01-cash-threshold'; + private amlService = inject(AmlService); private authenticationService = inject(AuthenticationService); /** Active tab */ @@ -56,6 +60,9 @@ export class FiuComponent implements OnInit { strFilterStatus: '' | StrStatus = ''; strFilterRisk: string = ''; + /** Ref of the STR card to highlight after jumping across from the CTR tab */ + highlightedStrRef: number | null = null; + /** Toast */ toastMsg = ''; toastVisible = false; @@ -76,6 +83,89 @@ export class FiuComponent implements OnInit { concludeSourceOfFundsNotes = ''; fileReferenceNo = ''; + /** + * The Principal Officer's verification checklist. Each point is answered + * "Genuine" or "Not satisfied"; the case conclusion is derived from the answers + * rather than picked by hand, so a case with an unresolved finding cannot be + * whitelisted by accident. + */ + readonly strChecklist: { key: string; label: string; hint: string }[] = [ + { + key: 'sourceOfFunds', + label: 'Source of funds established and documented', + hint: 'Salary slips, sale deed, business receipts or other credible evidence on file' + }, + { + key: 'profileConsistent', + label: 'Transaction consistent with the customer’s declared profile', + hint: 'Value and frequency in line with declared income, occupation and turnover' + }, + { + key: 'kycCurrent', + label: 'KYC records complete and within re-KYC validity', + hint: 'Identity, address and beneficial-ownership records verified and current' + }, + { + key: 'screeningClear', + label: 'Sanctions, PEP and adverse-media screening clear', + hint: 'No UNSCC / MHA listing, no PEP linkage, no negative news on the customer' + }, + { + key: 'patternConsistent', + label: 'Pattern consistent with the account’s own history', + hint: 'No structuring, sudden dormancy revival or rapid pass-through observed' + }, + { + key: 'counterpartyKnown', + label: 'Counterparty identifiable and legitimate', + hint: 'Remitter / beneficiary traceable and holding a plausible commercial relationship' + }, + { + key: 'documentsGenuine', + label: 'Supporting documents verified as genuine', + hint: 'Invoices, contracts and bank statements independently corroborated' + }, + { + key: 'noLawEnforcement', + label: 'No law-enforcement or regulatory reference pending', + hint: 'No summons, freeze order or LEA request outstanding against the account' + } + ]; + + /** null = not yet answered, true = genuine, false = not satisfied. */ + strFindings: Record = {}; + + private resetChecklist(): void { + this.strFindings = {}; + for (const item of this.strChecklist) { + this.strFindings[item.key] = null; + } + } + + setFinding(key: string, genuine: boolean): void { + this.strFindings[key] = genuine; + this.concludeConclusion = this.derivedConclusion; + } + + get answeredCount(): number { + return this.strChecklist.filter((i) => this.strFindings[i.key] !== null).length; + } + get allAnswered(): boolean { + return this.answeredCount === this.strChecklist.length; + } + /** Points the officer marked as not satisfied — these are what force an STR. */ + get failedFindings(): { key: string; label: string; hint: string }[] { + return this.strChecklist.filter((i) => this.strFindings[i.key] === false); + } + + /** + * Everything genuine means the case is cleared; a single unsatisfied point + * means it must be filed as suspicious. + */ + get derivedConclusion(): 'NOT_SUSPICIOUS' | 'SUSPICIOUS' { + return this.failedFindings.length > 0 ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS'; + } + get isPrincipalOfficer(): boolean { return this.authenticationService.isPrincipalOfficerUser(); } @@ -88,6 +178,84 @@ export class FiuComponent implements OnInit { this.loadAmlAlerts(); this.loadAmlCtrReports(); this.loadAmlStrCases(); + + // Bumping SEED_VERSION discards previously cached rows so a new data set takes effect. + if (localStorage.getItem('fiu_seed_version') !== FiuComponent.SEED_VERSION) { + localStorage.setItem('fiu_seed_version', FiuComponent.SEED_VERSION); + localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns)); + localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns)); + return; + } + + const storedCtr = localStorage.getItem('fiu_ctr_txns'); + if (storedCtr) { + this.ctrTxns = JSON.parse(storedCtr); + } else { + localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns)); + } + + const storedStr = localStorage.getItem('fiu_str_cases'); + if (storedStr) { + this.strTxns = JSON.parse(storedStr); + } else { + localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns)); + } + } + + whitelistCtrTxn(txn: CtrTransaction): void { + txn.complianceStatus = 'WHITELISTED'; + localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns)); + this.toast(`Transaction for ${txn.customerName} whitelisted — stays in the CTR report only`); + } + + flagCtrTxnSuspicious(txn: CtrTransaction): void { + txn.complianceStatus = 'SUSPICIOUS'; + localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns)); + + const newRef = Math.max(100, ...this.strTxns.map((s) => s.reportRefNo)) + 1; + const newStrCase: StrTransaction = { + ...txn, + reportType: 'STR', + reportRefNo: newRef, + detectedOn: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'), + flaggedBy: 'AML Officer', + riskCategory: 'unusual-pattern', + narrative: + `Cash ${txn.depositOrWithdrawal.toLowerCase()} of ₹${txn.txnAmount.toLocaleString('en-IN')} breached the ` + + `₹${this.bank.cashThreshold.toLocaleString('en-IN')} CTR limit and was flagged as suspicious by the AML Officer. ` + + `Referred to the Principal Officer to establish whether the source of funds is legitimate or linked to terror financing.`, + status: 'po-review', + poFinding: undefined + }; + + this.strTxns.unshift(newStrCase); + localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns)); + this.toast(`STR-${newRef} opened for ${txn.customerName} — awaiting Principal Officer investigation`); + } + + /** Principal Officer closes an STR case as legitimate, or confirms it and files an STR. */ + concludeLocalStr(rec: StrTransaction, finding: PoFinding): void { + rec.poFinding = finding; + if (finding === 'CLEAN') { + rec.status = 'rejected'; + rec.poRemarks = + 'Source-of-funds documentation reviewed and verified by the Principal Officer. No link to terror financing or ' + + 'any other predicate offence. Case closed — no STR filed with FIU-IND.'; + this.toast(`STR-${rec.reportRefNo} closed as clean — no STR filed`); + } else { + rec.status = 'filed'; + rec.poRemarks = + 'Principal Officer investigation could not establish a legitimate source of funds. STR filed with FIU-IND under ' + + 'PMLA s.12 within the 7-day window; account placed under enhanced monitoring and reported to the Nodal Officer.'; + this.toast(`STR-${rec.reportRefNo} confirmed suspicious — filed with FIU-IND`); + } + localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns)); + } + + poFindingLabel(finding: PoFinding): string { + return finding === 'CLEAN' + ? 'Clean — source of funds verified as legitimate. No STR filed.' + : 'Suspicious — STR filed with FIU-IND and account escalated.'; } loadAmlAlerts(): void { @@ -104,23 +272,35 @@ export class FiuComponent implements OnInit { this.amlService.getStrCases().subscribe((data) => (this.amlStrCases = data || [])); } + /** Surfaces a failed backend call — without this a rejected request looks like a dead button. */ + private apiError(action: string) { + return (err: any) => this.toast(err?.error?.defaultUserMessage || `${action} failed — please retry`); + } + whitelistAlert(alert: any): void { - this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe(() => { - this.toast(`Alert #${alert.id} whitelisted`); - this.loadAmlAlerts(); + this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe({ + next: () => { + this.toast(`Alert #${alert.id} whitelisted — no STR raised`); + this.loadAmlAlerts(); + }, + error: this.apiError(`Whitelisting alert #${alert.id}`) }); } escalateAlert(alert: any): void { - this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').subscribe(() => { - this.toast(`Alert #${alert.id} escalated to Principal Officer`); - this.loadAmlAlerts(); - this.loadAmlStrCases(); + this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').subscribe({ + next: () => { + this.toast(`Alert #${alert.id} escalated — now an STR case for the Principal Officer`); + this.loadAmlAlerts(); + this.loadAmlStrCases(); + }, + error: this.apiError(`Escalating alert #${alert.id}`) }); } generateCtrReport(): void { if (!this.ctrGenerateMonth) { + this.toast('Pick a reporting month first'); return; } this.amlService.generateCtrReport(this.ctrGenerateMonth).subscribe({ @@ -128,16 +308,32 @@ export class FiuComponent implements OnInit { this.toast(`CTR report generated for ${this.ctrGenerateMonth}`); this.loadAmlCtrReports(); }, - error: (err) => this.toast(err?.error?.defaultUserMessage || 'CTR report generation failed') + error: this.apiError('CTR report generation') }); } selectStrCase(strCase: any): void { this.selectedStrCase = strCase; - this.concludeConclusion = 'NOT_SUSPICIOUS'; + // Preserve an existing verdict instead of silently resetting it to NOT_SUSPICIOUS. + this.concludeConclusion = strCase.conclusion === 'SUSPICIOUS' ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS'; this.concludeInvestigationNotes = strCase.investigationNotes || ''; this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || ''; - this.fileReferenceNo = ''; + this.fileReferenceNo = strCase.strReferenceNo || ''; + this.resetChecklist(); + } + + /** Plain-language outcome for a live STR case, shown in the investigation panel. */ + strCaseOutcome(strCase: any): string { + switch (strCase?.status) { + case 'CONCLUDED_CLEAR': + return 'Concluded not suspicious — case closed, nothing filed with FIU-IND.'; + case 'CONCLUDED_SUSPICIOUS': + return `Concluded suspicious — STR must be filed with FIU-IND by ${strCase.fiuDueDate || 'the due date'}.`; + case 'FILED': + return `STR filed with FIU-IND${strCase.strReferenceNo ? ` under reference ${strCase.strReferenceNo}` : ''}.`; + default: + return 'Open — awaiting the Principal Officer’s conclusion.'; + } } closeStrCaseDetail(): void { @@ -148,32 +344,60 @@ export class FiuComponent implements OnInit { if (!this.selectedStrCase) { return; } + if (!this.allAnswered) { + const pending = this.strChecklist.length - this.answeredCount; + this.toast(`Answer all verification points first — ${pending} still pending`); + return; + } + if (!this.concludeInvestigationNotes.trim()) { + this.toast('Record your investigation notes before concluding'); + return; + } + // The verdict always follows the checklist, never a stale radio selection. + this.concludeConclusion = this.derivedConclusion; + // Fold the unsatisfied points into the notes so the STR carries its own reasons. + const failed = this.failedFindings; + const notes = failed.length + ? `${this.concludeInvestigationNotes.trim()}\n\nUnsatisfied verification points:\n` + + failed.map((f) => `• ${f.label}`).join('\n') + : `${this.concludeInvestigationNotes.trim()}\n\nAll ${this.strChecklist.length} verification points found genuine.`; + this.amlService .concludeStrCase( this.selectedStrCase.id, this.concludeConclusion, - this.concludeInvestigationNotes, + notes, this.concludeSourceOfFundsNotes ) - .subscribe((updated) => { - this.toast( - this.concludeConclusion === 'SUSPICIOUS' - ? `STR case #${updated.id} concluded suspicious — FIU due date ${updated.fiuDueDate}` - : `STR case #${updated.id} concluded not suspicious` - ); - this.selectedStrCase = updated; - this.loadAmlStrCases(); + .subscribe({ + next: (updated) => { + this.toast( + this.concludeConclusion === 'SUSPICIOUS' + ? `STR case #${updated.id} concluded suspicious — file with FIU-IND by ${updated.fiuDueDate}` + : `STR case #${updated.id} concluded not suspicious — no filing required` + ); + this.selectedStrCase = updated; + this.loadAmlStrCases(); + }, + error: this.apiError(`Concluding STR case #${this.selectedStrCase.id}`) }); } fileStrCase(): void { - if (!this.selectedStrCase || !this.fileReferenceNo) { + if (!this.selectedStrCase) { return; } - this.amlService.fileStr(this.selectedStrCase.id, this.fileReferenceNo).subscribe((updated) => { - this.toast(`STR case #${updated.id} recorded as filed (ref ${updated.strReferenceNo})`); - this.selectedStrCase = updated; - this.loadAmlStrCases(); + if (!this.fileReferenceNo.trim()) { + this.toast('Enter the FIU-IND acknowledgement number first'); + return; + } + this.amlService.fileStr(this.selectedStrCase.id, this.fileReferenceNo).subscribe({ + next: (updated) => { + this.toast(`STR case #${updated.id} recorded as filed (ref ${updated.strReferenceNo})`); + this.selectedStrCase = updated; + this.loadAmlStrCases(); + }, + error: this.apiError(`Filing STR case #${this.selectedStrCase.id}`) }); } @@ -181,10 +405,57 @@ export class FiuComponent implements OnInit { return 'status-pill status-' + (status || '').toLowerCase().replace(/_/g, '-'); } + /** + * Where a live AML alert will end up once triaged — drives the "Routes To" column. + * `isCtrRule` distinguishes mechanical CTR-threshold alerts (whitelisting just keeps them + * in the CTR file) from behavioral alerts (whitelisting clears them with no report at all). + */ + alertRouteLabel(status: string, isCtrRule: boolean = true): string { + switch (status) { + case 'WHITELISTED': + return isCtrRule ? 'CTR only' : 'Cleared'; + case 'ESCALATED': + return '→ STR'; + default: + return 'Pending'; + } + } + + alertRouteClass(status: string, isCtrRule: boolean = true): string { + switch (status) { + case 'WHITELISTED': + return isCtrRule ? 'report-pill report-ctr' : 'report-pill report-cleared'; + case 'ESCALATED': + return 'report-pill report-str'; + default: + return 'report-pill report-pending'; + } + } + showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void { this.activeTab = tab; } + /** + * Jump from a flagged CTR row to its STR card. Clears any status/risk filter that + * would hide the case, then scrolls it into view and highlights it. + */ + openStrCase(txn: CtrTransaction): void { + const match = this.strTxns.find((s) => s.txnId === txn.txnId); + if (!match) { + this.toast('No STR case found for this transaction'); + return; + } + this.activeTab = 'str'; + this.resetStrFilters(); + this.highlightedStrRef = match.reportRefNo; + setTimeout(() => { + document + .getElementById(`str-card-${match.reportRefNo}`) + ?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + } + // -------------------- KPIs -------------------- get totalReports(): number { return new Set(this.ctrTxns.map((t) => t.reportRefNo)).size; @@ -201,10 +472,39 @@ export class FiuComponent implements OnInit { get filedStr(): number { return this.strTxns.filter((s) => s.status === 'filed').length; } + + // ── Live STR case counts (backend `amlStrCases`) — these are the cases the + // Principal Officer actually works through the Investigate → Conclude → File flow. + get liveStrOpen(): number { + return this.amlStrCases.filter((c) => c.status === 'OPEN').length; + } + get liveStrAwaitingFiling(): number { + return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_SUSPICIOUS').length; + } + get liveStrCleared(): number { + return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_CLEAR').length; + } + get liveStrFiled(): number { + return this.amlStrCases.filter((c) => c.status === 'FILED').length; + } + /** Still needs PO action: untouched, or concluded suspicious but not yet filed with FIU-IND. */ + get liveStrPending(): number { + return this.liveStrOpen + this.liveStrAwaitingFiling; + } get totalCustomers(): number { return this.kc1.length + this.kc2.length; } + get ctrAlerts(): any[] { + const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L']; + return this.amlAlerts.filter((a) => ctrRuleCodes.includes(a.ruleCode)); + } + + get amlSuspiciousAlerts(): any[] { + const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L']; + return this.amlAlerts.filter((a) => !ctrRuleCodes.includes(a.ruleCode)); + } + // -------------------- Filtered lists -------------------- get filteredCtr(): CtrTransaction[] { const acc = this.ctrFilterAccount.trim(); @@ -224,6 +524,14 @@ export class FiuComponent implements OnInit { }); } + /** + * Cases that actually belong in an STR filing. A case the Principal Officer + * concluded clean is not a suspect and must never reach FIU-IND. + */ + get strSuspects(): StrTransaction[] { + return this.filteredStr.filter((s) => s.poFinding !== 'CLEAN' && s.status !== 'rejected'); + } + resetCtrFilters(): void { this.ctrFilterMode = ''; this.ctrFilterAccount = ''; @@ -523,11 +831,11 @@ export class FiuComponent implements OnInit { const rows = [headers]; // Build one aggregate row per (reportRefNo, accountNumber) pair const seen = new Set(); - for (const t of this.ctrTxns) { + for (const t of this.filteredCtr) { const key = `${t.reportRefNo}|${t.accountNumber}`; if (seen.has(key)) continue; seen.add(key); - const sameKey = this.ctrTxns.filter( + const sameKey = this.filteredCtr.filter( (x) => x.reportRefNo === t.reportRefNo && x.accountNumber === t.accountNumber ); const debits = sameKey.filter((x) => x.depositOrWithdrawal === 'Withdrawal'); @@ -576,7 +884,7 @@ export class FiuComponent implements OnInit { ]; const rows = [headers]; const seen = new Set(); - for (const t of this.ctrTxns) { + for (const t of this.filteredCtr) { const key = `${t.reportRefNo}|${t.accountNumber}`; if (seen.has(key)) continue; seen.add(key); @@ -599,12 +907,17 @@ export class FiuComponent implements OnInit { /** STR XML stub (FINnet 2.0 STR XML) */ exportStrXml(): void { + const suspects = this.strSuspects; + if (!suspects.length) { + this.toast('No suspect cases to export — nothing matches the current filters'); + return; + } const xml: string[] = []; xml.push(''); xml.push(``); xml.push(` `); xml.push(` `); - for (const s of this.filteredStr) { + for (const s of suspects) { xml.push(` `); xml.push(` ${s.accountNumber}`); xml.push( @@ -624,10 +937,10 @@ export class FiuComponent implements OnInit { a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`; a.click(); URL.revokeObjectURL(url); - this.toast(`STR XML exported (${this.filteredStr.length} suspects)`); + this.toast(`STR XML exported (${suspects.length} suspects)`); } - /** Bundle: all 6 CSVs zipped — emitted as separate downloads sequentially */ + /** Bundle: the 6 CSVs plus the STR XML — emitted as separate downloads sequentially */ exportAll(): void { setTimeout(() => this.exportTc1(), 0); setTimeout(() => this.exportTc2(), 200); @@ -635,6 +948,7 @@ export class FiuComponent implements OnInit { setTimeout(() => this.exportKc2(), 600); setTimeout(() => this.exportAccountDetail(), 800); setTimeout(() => this.exportAccountPersonRelation(), 1000); + setTimeout(() => this.exportStrXml(), 1200); } private escapeXml(s: string): string { diff --git a/src/app/gl-reports/gl-reports-data.ts b/src/app/gl-reports/gl-reports-data.ts new file mode 100644 index 000000000..f61efa8c2 --- /dev/null +++ b/src/app/gl-reports/gl-reports-data.ts @@ -0,0 +1,496 @@ +/** + * 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/. + */ + +/** + * Data layer for the General Ledger report suite. + * + * Rather than hard-coding seven finished reports, this file holds a chart of + * accounts plus a generated ledger of double-entry postings. Every report is + * then computed from those postings, which is what makes the day / month / + * branch filters genuinely recompute instead of just re-labelling a static table. + * + * The generator is deterministic — a fixed-seed LCG, never Math.random — so the + * figures are identical on every reload and across users. Amounts are in rupees. + */ + +export type GlAccountType = 'ASSET' | 'LIABILITY' | 'INCOME' | 'EXPENSE' | 'CONTINGENT'; + +export interface GlAccount { + code: string; + name: string; + type: GlAccountType; + /** Balance-sheet / P&L grouping used as a sub-heading in the reports. */ + group: string; + /** Included in the Misc Expense Ledger report. */ + miscExpense?: boolean; +} + +export interface GlBranch { + code: string; + name: string; +} + +/** One side of a double-entry voucher. */ +export interface GlPosting { + /** ISO yyyy-MM-dd — the posting (value) date. */ + date: string; + branchCode: string; + accountCode: string; + debit: number; + credit: number; + voucherNo: string; + narration: string; +} + +export const GL_BRANCHES: GlBranch[] = [ + { code: 'HO', name: 'Head Office' }, + { code: '0002', name: 'Andheri (E)' }, + { code: '0003', name: 'Bandra (W)' }, + { code: '0004', name: 'Borivali' }, + { code: '0005', name: 'Dadar' }, + { code: '0006', name: 'Thane' } +]; + +export const GL_ACCOUNTS: GlAccount[] = [ + // ── Assets ──────────────────────────────────────────────────────────────── + { code: '110101', name: 'Cash in Hand', type: 'ASSET', group: 'Cash & Bank Balances' }, + { code: '110102', name: 'Balance with RBI', type: 'ASSET', group: 'Cash & Bank Balances' }, + { code: '110103', name: 'Balance with Other Banks — Current', type: 'ASSET', group: 'Cash & Bank Balances' }, + { code: '110104', name: 'Balance with Other Banks — Fixed', type: 'ASSET', group: 'Cash & Bank Balances' }, + { code: '120101', name: 'SLR Investments — Govt Securities', type: 'ASSET', group: 'Investments' }, + { code: '120102', name: 'SLR Investments — State Govt Securities', type: 'ASSET', group: 'Investments' }, + { code: '120201', name: 'Non-SLR Investments — Bonds', type: 'ASSET', group: 'Investments' }, + { code: '120202', name: 'Non-SLR Investments — Mutual Funds', type: 'ASSET', group: 'Investments' }, + { code: '130101', name: 'Cash Credit & Overdrafts', type: 'ASSET', group: 'Loans & Advances' }, + { code: '130102', name: 'Term Loans — Short Term', type: 'ASSET', group: 'Loans & Advances' }, + { code: '130103', name: 'Term Loans — Medium Term', type: 'ASSET', group: 'Loans & Advances' }, + { code: '130104', name: 'Term Loans — Long Term', type: 'ASSET', group: 'Loans & Advances' }, + { code: '130105', name: 'Housing Loans', type: 'ASSET', group: 'Loans & Advances' }, + { code: '130106', name: 'Gold Loans', type: 'ASSET', group: 'Loans & Advances' }, + { code: '140101', name: 'Interest Receivable', type: 'ASSET', group: 'Other Assets' }, + { code: '140102', name: 'Premises (net of depreciation)', type: 'ASSET', group: 'Fixed Assets' }, + { code: '140103', name: 'Furniture & Fixtures (net)', type: 'ASSET', group: 'Fixed Assets' }, + { code: '140104', name: 'Sundry Debtors', type: 'ASSET', group: 'Other Assets' }, + { code: '140105', name: 'Advance Tax & TDS', type: 'ASSET', group: 'Other Assets' }, + { code: '140199', name: 'Opening Balance Suspense (unreconciled)', type: 'ASSET', group: 'Other Assets' }, + + // ── Liabilities & Capital ───────────────────────────────────────────────── + { code: '210101', name: 'Share Capital — Individuals', type: 'LIABILITY', group: 'Capital & Reserves' }, + { code: '210201', name: 'Statutory Reserve Fund', type: 'LIABILITY', group: 'Capital & Reserves' }, + { code: '210202', name: 'Building Fund', type: 'LIABILITY', group: 'Capital & Reserves' }, + { code: '210203', name: 'Investment Depreciation Reserve', type: 'LIABILITY', group: 'Capital & Reserves' }, + { code: '210204', name: 'BDDR / Special BDDR', type: 'LIABILITY', group: 'Capital & Reserves' }, + { code: '220101', name: 'Savings Bank Deposits', type: 'LIABILITY', group: 'Deposits' }, + { code: '220102', name: 'Current Deposits', type: 'LIABILITY', group: 'Deposits' }, + { code: '220103', name: 'Term Deposits', type: 'LIABILITY', group: 'Deposits' }, + { code: '220104', name: 'Recurring Deposits', type: 'LIABILITY', group: 'Deposits' }, + { code: '230101', name: 'Interest Payable on Deposits', type: 'LIABILITY', group: 'Other Liabilities' }, + { code: '230102', name: 'Bills Payable', type: 'LIABILITY', group: 'Other Liabilities' }, + { code: '230103', name: 'Sundry Creditors', type: 'LIABILITY', group: 'Other Liabilities' }, + { code: '230104', name: 'TDS / GST Payable', type: 'LIABILITY', group: 'Other Liabilities' }, + { code: '230105', name: 'Overdue Interest Reserve', type: 'LIABILITY', group: 'Other Liabilities' }, + + // ── Income ──────────────────────────────────────────────────────────────── + { code: '310101', name: 'Interest on Loans & Advances', type: 'INCOME', group: 'Interest Income' }, + { code: '310102', name: 'Interest on Investments', type: 'INCOME', group: 'Interest Income' }, + { code: '310103', name: 'Interest on Inter-bank Deposits', type: 'INCOME', group: 'Interest Income' }, + { code: '320101', name: 'Commission & Exchange', type: 'INCOME', group: 'Other Income' }, + { code: '320102', name: 'Locker Rent', type: 'INCOME', group: 'Other Income' }, + { code: '320103', name: 'Processing Fees', type: 'INCOME', group: 'Other Income' }, + { code: '320104', name: 'Profit on Sale of Securities', type: 'INCOME', group: 'Other Income' }, + { code: '320105', name: 'Miscellaneous Income', type: 'INCOME', group: 'Other Income' }, + + // ── Expenditure ─────────────────────────────────────────────────────────── + { code: '410101', name: 'Interest on Savings Deposits', type: 'EXPENSE', group: 'Interest Expended' }, + { code: '410102', name: 'Interest on Term Deposits', type: 'EXPENSE', group: 'Interest Expended' }, + { code: '410103', name: 'Interest on Borrowings', type: 'EXPENSE', group: 'Interest Expended' }, + { code: '420101', name: 'Salaries & Allowances', type: 'EXPENSE', group: 'Operating Expenses' }, + { code: '420102', name: "Directors' Fees", type: 'EXPENSE', group: 'Operating Expenses' }, + { code: '420103', name: "Auditors' Fees", type: 'EXPENSE', group: 'Operating Expenses' }, + { code: '420104', name: 'Rent, Taxes & Lighting', type: 'EXPENSE', group: 'Operating Expenses' }, + { code: '420105', name: 'Depreciation on Bank Property', type: 'EXPENSE', group: 'Operating Expenses' }, + { code: '430101', name: 'Printing & Stationery', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430102', name: 'Postage, Telegram & Telephone', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430103', name: 'Advertisement & Publicity', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430104', name: 'Repairs & Maintenance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430105', name: 'Travelling & Conveyance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430106', name: 'Computer & Software Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430107', name: 'Legal & Professional Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430108', name: 'Insurance (DICGC & Other)', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430109', name: 'Bank Charges & Commission Paid', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430110', name: 'AGM & Meeting Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430111', name: 'Electricity Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { code: '430112', name: 'Other Miscellaneous Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + + // ── Contingent (memoranda, not part of the balance sheet totals) ────────── + { code: '910101', name: 'Guarantees Issued — Financial', type: 'CONTINGENT', group: 'Guarantees' }, + { code: '910102', name: 'Guarantees Issued — Performance', type: 'CONTINGENT', group: 'Guarantees' }, + { code: '910103', name: 'Letters of Credit Outstanding', type: 'CONTINGENT', group: 'Letters of Credit' }, + { code: '910104', name: 'Bills for Collection', type: 'CONTINGENT', group: 'Bills & Claims' }, + { code: '910105', name: 'Claims Against Bank Not Acknowledged as Debt', type: 'CONTINGENT', group: 'Bills & Claims' }, + { code: '910106', name: 'Forward Exchange Contracts', type: 'CONTINGENT', group: 'Other Commitments' }, + { code: '910107', name: 'Capital Commitments Not Provided For', type: 'CONTINGENT', group: 'Other Commitments' } +]; + +/** Financial year covered by the generated ledger. */ +export const GL_PERIOD = { + /** FY 2026-27 opens here. */ + fyStart: '2026-04-01', + /** Postings run to the day before the app's "today" (01 Aug 2026). */ + fyEnd: '2026-07-31', + label: 'FY 2026-27' +}; + +/** + * Opening balances as at 01-Apr-2026, keyed by account code. Positive means the + * account opens on its natural side (debit for assets/expenses, credit for the + * rest). Head Office carries the investments and reserves; branches carry the + * customer-facing book. + */ +const OPENING_BALANCES: Record = { + '110101': 9_996_000, + '110102': 12_400_000, + '110103': 13_667_000, + // Surplus deposits parked with correspondent banks. Sized so the opening + // balance sheet tallies exactly — see the assertion in glPostings(). + '110104': 54_206_000, + '120101': 63_839_000, + '120102': 147_895_000, + '120201': 22_500_000, + '120202': 24_234_000, + '130101': 166_614_000, + '130102': 125_082_000, + '130103': 70_236_000, + '130104': 29_356_000, + '130105': 10_241_000, + '130106': 8_450_000, + '140101': 33_541_000, + '140102': 3_185_000, + '140103': 1_763_000, + '140104': 1_240_000, + '140105': 657_000, + '210101': 27_278_000, + '210201': 31_149_000, + '210202': 13_642_000, + '210203': 3_750_000, + '210204': 24_341_000, + '220101': 208_116_000, + '220102': 19_592_000, + '220103': 406_601_000, + '220104': 24_880_000, + '230101': 731_000, + '230102': 291_000, + '230103': 5_515_000, + '230104': 1_120_000, + '230105': 32_096_000, + '910101': 1_450_000, + '910102': 980_000, + '910103': 620_000, + '910104': 1_180_000, + '910105': 2_638_000, + '910106': 0, + '910107': 340_000 +}; + +/** Deterministic linear congruential generator — same figures on every load. */ +function lcg(seed: number): () => number { + let s = seed >>> 0; + return () => { + s = (Math.imul(s, 1664525) + 1013904223) >>> 0; + return s / 4294967296; + }; +} + +function isoDate(d: Date): string { + const y = d.getUTCFullYear(); + const m = `${d.getUTCMonth() + 1}`.padStart(2, '0'); + const day = `${d.getUTCDate()}`.padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +/** Every posting date in the ledger period, Sundays excluded (bank holiday). */ +export function ledgerDates(): string[] { + const out: string[] = []; + const cur = new Date(`${GL_PERIOD.fyStart}T00:00:00Z`); + const end = new Date(`${GL_PERIOD.fyEnd}T00:00:00Z`); + while (cur <= end) { + if (cur.getUTCDay() !== 0) { + out.push(isoDate(cur)); + } + cur.setUTCDate(cur.getUTCDate() + 1); + } + return out; +} + +/** Templates for the vouchers the generator posts each day. */ +const VOUCHER_TEMPLATES: { + debit: string; + credit: string; + narration: string; + /** Rough rupee scale of the voucher. */ + scale: number; + /** Chance the voucher is raised on any given branch-day. */ + frequency: number; +}[] = [ + { debit: '110101', credit: '220101', narration: 'Cash deposit — savings accounts', scale: 900_000, frequency: 1 }, + { debit: '220101', credit: '110101', narration: 'Cash withdrawal — savings accounts', scale: 780_000, frequency: 1 }, + { debit: '110101', credit: '220103', narration: 'Term deposit received', scale: 1_400_000, frequency: 0.75 }, + { debit: '220103', credit: '110101', narration: 'Term deposit matured and paid', scale: 1_150_000, frequency: 0.6 }, + { debit: '110101', credit: '220102', narration: 'Current account deposit', scale: 620_000, frequency: 0.8 }, + { debit: '220102', credit: '110101', narration: 'Current account withdrawal', scale: 560_000, frequency: 0.8 }, + { debit: '110101', credit: '220104', narration: 'Recurring deposit instalment', scale: 180_000, frequency: 0.7 }, + { debit: '130101', credit: '110101', narration: 'Cash credit disbursed', scale: 1_250_000, frequency: 0.55 }, + { debit: '110101', credit: '130101', narration: 'Cash credit repayment', scale: 1_100_000, frequency: 0.6 }, + { debit: '130102', credit: '110101', narration: 'Short term loan disbursed', scale: 850_000, frequency: 0.4 }, + { debit: '110101', credit: '130102', narration: 'Short term loan repayment', scale: 760_000, frequency: 0.5 }, + { debit: '130103', credit: '110101', narration: 'Medium term loan disbursed', scale: 640_000, frequency: 0.3 }, + { debit: '110101', credit: '130104', narration: 'Long term loan repayment', scale: 410_000, frequency: 0.35 }, + { debit: '130105', credit: '110101', narration: 'Housing loan disbursed', scale: 520_000, frequency: 0.2 }, + { debit: '130106', credit: '110101', narration: 'Gold loan disbursed', scale: 260_000, frequency: 0.4 }, + // Interest on advances is the dominant earning line — sized so the bank runs a + // ~13% net margin rather than the loss the first-cut figures produced. + { debit: '110101', credit: '310101', narration: 'Interest recovered on advances', scale: 430_000, frequency: 1 }, + { debit: '140101', credit: '310102', narration: 'Interest accrued on investments', scale: 148_000, frequency: 0.5 }, + { debit: '110103', credit: '310103', narration: 'Interest on inter-bank deposits', scale: 42_000, frequency: 0.25 }, + { debit: '110101', credit: '320101', narration: 'Commission and exchange collected', scale: 26_000, frequency: 0.9 }, + { debit: '110101', credit: '320102', narration: 'Locker rent collected', scale: 9_000, frequency: 0.3 }, + { debit: '110101', credit: '320103', narration: 'Loan processing fees', scale: 18_000, frequency: 0.5 }, + { debit: '110101', credit: '320105', narration: 'Miscellaneous income', scale: 7_500, frequency: 0.4 }, + { debit: '410101', credit: '230101', narration: 'Interest provided on savings deposits', scale: 128_000, frequency: 0.8 }, + { debit: '410102', credit: '230101', narration: 'Interest provided on term deposits', scale: 265_000, frequency: 0.9 }, + { debit: '420101', credit: '110101', narration: 'Salaries and allowances paid', scale: 210_000, frequency: 0.35 }, + { debit: '420104', credit: '110101', narration: 'Rent, taxes and lighting', scale: 46_000, frequency: 0.3 }, + { debit: '430101', credit: '110101', narration: 'Printing and stationery purchased', scale: 9_400, frequency: 0.3 }, + { debit: '430102', credit: '110101', narration: 'Postage and telephone charges', scale: 6_200, frequency: 0.35 }, + { debit: '430103', credit: '110101', narration: 'Advertisement and publicity', scale: 12_500, frequency: 0.15 }, + { debit: '430104', credit: '110101', narration: 'Repairs and maintenance', scale: 15_800, frequency: 0.25 }, + { debit: '430105', credit: '110101', narration: 'Travelling and conveyance', scale: 7_100, frequency: 0.3 }, + { debit: '430106', credit: '110101', narration: 'Computer and software expenses', scale: 22_400, frequency: 0.2 }, + { debit: '430107', credit: '110101', narration: 'Legal and professional charges', scale: 11_300, frequency: 0.15 }, + { debit: '430109', credit: '110101', narration: 'Bank charges and commission paid', scale: 4_800, frequency: 0.3 }, + { debit: '430111', credit: '110101', narration: 'Electricity charges', scale: 13_600, frequency: 0.25 }, + { debit: '430112', credit: '110101', narration: 'Other miscellaneous expenses', scale: 8_900, frequency: 0.3 }, + { debit: '110102', credit: '110101', narration: 'Cash remitted to RBI currency chest', scale: 1_800_000, frequency: 0.2 }, + { debit: '110101', credit: '110103', narration: 'Funds drawn from correspondent bank', scale: 1_400_000, frequency: 0.2 } +]; + +/** Head-Office-only vouchers — investments, reserves and centralised charges. */ +const HO_TEMPLATES: typeof VOUCHER_TEMPLATES = [ + { debit: '120101', credit: '110102', narration: 'Purchase of Govt securities', scale: 2_400_000, frequency: 0.12 }, + { debit: '120102', credit: '110102', narration: 'Purchase of State Govt securities', scale: 1_900_000, frequency: 0.12 }, + { debit: '110102', credit: '320104', narration: 'Profit on sale of securities', scale: 78_000, frequency: 0.1 }, + { debit: '420102', credit: '110101', narration: "Directors' sitting fees", scale: 32_000, frequency: 0.08 }, + { debit: '420103', credit: '110101', narration: "Statutory auditors' fees", scale: 45_000, frequency: 0.06 }, + { debit: '420105', credit: '140103', narration: 'Depreciation charged on assets', scale: 64_000, frequency: 0.12 }, + { debit: '430108', credit: '110101', narration: 'DICGC premium and other insurance', scale: 38_000, frequency: 0.1 }, + { debit: '430110', credit: '110101', narration: 'AGM and meeting expenses', scale: 26_000, frequency: 0.06 }, + { debit: '410103', credit: '230101', narration: 'Interest on borrowings', scale: 54_000, frequency: 0.1 }, + { debit: '230104', credit: '110102', narration: 'TDS and GST remitted', scale: 145_000, frequency: 0.15 } +]; + +/** + * Contingent-account movements are memoranda, each posted single-sided. + * Guarantees, LCs and collection bills are raised at the counter, so these run + * at every branch — not just Head Office. + */ +const CONTINGENT_TEMPLATES: { account: string; narration: string; scale: number; frequency: number }[] = [ + { account: '910101', narration: 'Financial guarantee issued', scale: 320_000, frequency: 0.1 }, + { account: '910102', narration: 'Performance guarantee issued', scale: 240_000, frequency: 0.08 }, + { account: '910103', narration: 'Letter of credit opened', scale: 180_000, frequency: 0.07 }, + { account: '910104', narration: 'Bills lodged for collection', scale: 95_000, frequency: 0.12 }, + { account: '910105', narration: 'Claim lodged against the bank', scale: 140_000, frequency: 0.03 }, + { account: '910107', narration: 'Capital commitment contracted', scale: 85_000, frequency: 0.04 } +]; + +/** + * How the opening contingent book is spread across the network. Head Office + * carries the capital commitments and large claims; the branches carry the + * counter-issued guarantees. Weights are applied per account and the rounding + * remainder is absorbed by Head Office so each account's total stays exact. + */ +const CONTINGENT_BRANCH_WEIGHTS: Record = { + HO: 0.2, + '0002': 0.18, + '0003': 0.16, + '0004': 0.16, + '0005': 0.15, + '0006': 0.15 +}; + +let cachedPostings: GlPosting[] | null = null; + +/** + * The full generated ledger. Built once and memoised — every report reads from + * this array, so the reports can never disagree with one another. + */ +export function glPostings(): GlPosting[] { + if (cachedPostings) { + return cachedPostings; + } + const rand = lcg(20260401); + const out: GlPosting[] = []; + const dates = ledgerDates(); + let voucherSeq = 1; + + // Opening entries, posted at Head Office on day one so the books start in balance. + const openingVoucher = `OB/${GL_PERIOD.label.replace(/\s/g, '')}`; + let openingDebit = 0; + let openingCredit = 0; + + for (const [code, amount] of Object.entries(OPENING_BALANCES)) { + if (!amount) { + continue; + } + const account = GL_ACCOUNTS.find((a) => a.code === code); + if (!account) { + continue; + } + const debitSide = account.type === 'ASSET' || account.type === 'EXPENSE' || account.type === 'CONTINGENT'; + + // Contingent accounts are single-sided memoranda held at the issuing branch, + // so their opening book is spread across the network rather than sat at HO. + if (account.type === 'CONTINGENT') { + let allocated = 0; + for (const branch of GL_BRANCHES) { + if (branch.code === 'HO') { + continue; + } + const share = Math.round((amount * (CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0)) / 100) * 100; + if (!share) { + continue; + } + allocated += share; + out.push({ + date: GL_PERIOD.fyStart, + branchCode: branch.code, + accountCode: code, + debit: share, + credit: 0, + voucherNo: openingVoucher, + narration: 'Opening balance brought forward' + }); + } + // Head Office takes its own share plus whatever rounding left behind. + out.push({ + date: GL_PERIOD.fyStart, + branchCode: 'HO', + accountCode: code, + debit: amount - allocated, + credit: 0, + voucherNo: openingVoucher, + narration: 'Opening balance brought forward' + }); + continue; + } + + out.push({ + date: GL_PERIOD.fyStart, + branchCode: 'HO', + accountCode: code, + debit: debitSide ? amount : 0, + credit: debitSide ? 0 : amount, + voucherNo: openingVoucher, + narration: 'Opening balance brought forward' + }); + if (debitSide) { + openingDebit += amount; + } else { + openingCredit += amount; + } + } + + // If OPENING_BALANCES is ever edited so the two sides diverge, park the residual + // in a visibly-named suspense account rather than letting the trial balance drift. + const openingResidual = openingCredit - openingDebit; + if (openingResidual !== 0) { + out.push({ + date: GL_PERIOD.fyStart, + branchCode: 'HO', + accountCode: '140199', + debit: openingResidual > 0 ? openingResidual : 0, + credit: openingResidual < 0 ? -openingResidual : 0, + voucherNo: openingVoucher, + narration: 'Unreconciled opening balance difference' + }); + } + + const round = (n: number): number => Math.round(n / 100) * 100; + + for (const date of dates) { + for (const branch of GL_BRANCHES) { + // Head Office posts fewer counter transactions than the branches. + const weight = branch.code === 'HO' ? 0.45 : 1; + const templates = branch.code === 'HO' ? [...VOUCHER_TEMPLATES, ...HO_TEMPLATES] : VOUCHER_TEMPLATES; + + for (const tpl of templates) { + if (rand() > tpl.frequency * weight) { + continue; + } + // 0.55x .. 1.45x of the template scale keeps daily figures varied but plausible. + const amount = round(tpl.scale * (0.55 + rand() * 0.9)); + if (amount <= 0) { + continue; + } + const voucherNo = `${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`; + out.push({ + date, + branchCode: branch.code, + accountCode: tpl.debit, + debit: amount, + credit: 0, + voucherNo, + narration: tpl.narration + }); + out.push({ + date, + branchCode: branch.code, + accountCode: tpl.credit, + debit: 0, + credit: amount, + voucherNo, + narration: tpl.narration + }); + } + + // Contingent memoranda are raised at every branch, weighted by its size. + const contingentWeight = CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0.15; + for (const tpl of CONTINGENT_TEMPLATES) { + // Claims against the bank and capital commitments are Head Office matters. + const headOfficeOnly = tpl.account === '910105' || tpl.account === '910107'; + if (headOfficeOnly && branch.code !== 'HO') { + continue; + } + const frequency = headOfficeOnly ? tpl.frequency : tpl.frequency * (contingentWeight / 0.2); + if (rand() > frequency) { + continue; + } + const amount = round(tpl.scale * (0.55 + rand() * 0.9)); + out.push({ + date, + branchCode: branch.code, + accountCode: tpl.account, + debit: amount, + credit: 0, + voucherNo: `CNT/${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`, + narration: tpl.narration + }); + } + } + } + + cachedPostings = out; + return out; +} + +export function glAccount(code: string): GlAccount | undefined { + return GL_ACCOUNTS.find((a) => a.code === code); +} + +export function glBranchName(code: string): string { + return GL_BRANCHES.find((b) => b.code === code)?.name ?? code; +} diff --git a/src/app/gl-reports/gl-reports-routing.module.ts b/src/app/gl-reports/gl-reports-routing.module.ts new file mode 100644 index 000000000..16cf876f7 --- /dev/null +++ b/src/app/gl-reports/gl-reports-routing.module.ts @@ -0,0 +1,34 @@ +/** + * 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { Route } from '../core/route/route.service'; + +const routes: Routes = [ + Route.withShell([ + { + path: 'gl/reports', + // Lazy — the generated ledger and ExcelJS should not weigh down the eager bundle. + loadComponent: () => import('./gl-reports.component').then((m) => m.GlReportsComponent), + data: { title: 'General Ledger Reports', breadcrumb: 'GL Reports' } + }, + { + path: 'gl/reports/:report', + loadComponent: () => import('./gl-reports.component').then((m) => m.GlReportsComponent), + data: { title: 'General Ledger Reports', breadcrumb: 'GL Reports' } + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class GlReportsRoutingModule {} diff --git a/src/app/gl-reports/gl-reports.component.html b/src/app/gl-reports/gl-reports.component.html new file mode 100644 index 000000000..da982ebc5 --- /dev/null +++ b/src/app/gl-reports/gl-reports.component.html @@ -0,0 +1,523 @@ + + +
+ +
+
+ +
+

General Ledger — {{ reportLabel }}

+
+ CBSNexora Bank Ltd · {{ branchLabel }} · Period {{ rangeLabel }} · + {{ ledgerPeriod.label }} +
+
+
+ +
+ + +
+
+ +
+ + + +
+
+ + @if (periodMode === 'day') { +
+ + +
+ } + @if (periodMode === 'month') { +
+ + +
+ } + @if (periodMode === 'range') { +
+ + +
+
+ + +
+ } + +
+ + +
+ + +
+ + +
+ @for (r of reports; track r.code) { + + } +
+ + + @if (activeReport === 'trial-balance') { +
+
+ + + Closing Dr ₹{{ tbClosingDebit | number: '1.2-2' }} · Closing Cr + ₹{{ tbClosingCredit | number: '1.2-2' }} — + {{ tbBalanced ? 'trial balance tallies' : 'OUT OF BALANCE — investigate before closing the period' }} + +
+ +
+ + + + + + + + + + + + + + + + @for (r of data.trialBalance; track r.code) { + + + + + + + + + + + + } @empty { + + + + } + + + + + + + + + + +
GL CodeAccountGroupOpening DrOpening CrPeriod DrPeriod CrClosing DrClosing Cr
{{ r.code }}{{ r.name }}{{ r.group }}{{ r.openingDebit ? (r.openingDebit | number: '1.2-2') : '' }}{{ r.openingCredit ? (r.openingCredit | number: '1.2-2') : '' }}{{ r.periodDebit ? (r.periodDebit | number: '1.2-2') : '' }}{{ r.periodCredit ? (r.periodCredit | number: '1.2-2') : '' }}{{ r.closingDebit ? (r.closingDebit | number: '1.2-2') : '' }}{{ r.closingCredit ? (r.closingCredit | number: '1.2-2') : '' }}
No ledger activity for this period and branch.
TOTAL ({{ data.trialBalance.length }} accounts){{ tbOpeningDebit | number: '1.2-2' }}{{ tbOpeningCredit | number: '1.2-2' }}{{ tbPeriodDebit | number: '1.2-2' }}{{ tbPeriodCredit | number: '1.2-2' }}{{ tbClosingDebit | number: '1.2-2' }}{{ tbClosingCredit | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'profit-and-loss') { +
+
+
+
₹{{ totalIncome | number: '1.2-2' }}
+
Total Income
+
+
+
₹{{ totalExpense | number: '1.2-2' }}
+
Total Expenditure
+
+
+
₹{{ netProfit | number: '1.2-2' }}
+
{{ netProfit >= 0 ? 'Net Profit' : 'Net Loss' }}
+
{{ netProfitPct | number: '1.2-2' }}% of income
+
+
+ +
+
+
Expenditure
+ + + + + + + + + + @for (r of pnlExpense; track r.code) { + + + + + + } @empty { + + + + } + + + + + @if (netProfit >= 0) { + + + + + } + +
GL CodeParticularsAmount (₹)
{{ r.code }}{{ r.name }} · {{ r.group }}{{ r.amount | number: '1.2-2' }}
No expenditure posted in this period.
Total Expenditure{{ totalExpense | number: '1.2-2' }}
Net Profit carried to Balance Sheet{{ netProfit | number: '1.2-2' }}
+
+ +
+
Income
+ + + + + + + + + + @for (r of pnlIncome; track r.code) { + + + + + + } @empty { + + + + } + + + + + @if (netProfit < 0) { + + + + + } + +
GL CodeParticularsAmount (₹)
{{ r.code }}{{ r.name }} · {{ r.group }}{{ r.amount | number: '1.2-2' }}
No income posted in this period.
Total Income{{ totalIncome | number: '1.2-2' }}
Net Loss carried to Balance Sheet{{ -netProfit | number: '1.2-2' }}
+
+
+
+ } + + + @if (activeReport === 'balance-sheet') { +
+
+ + + Total Assets ₹{{ bsTotalAssets | number: '1.2-2' }} · Total Liabilities + ₹{{ bsTotalLiabilities | number: '1.2-2' }} — + {{ bsBalanced ? 'balance sheet tallies' : 'OUT OF BALANCE' }} + +
+ +
+
+
Capital & Liabilities — as at {{ range.to }}
+ + + + + + + + + + @for (r of bsLiabilities; track r.code) { + + + + + + } + + + + + + + + + + +
GL CodeParticularsAmount (₹)
{{ r.code }}{{ r.name }} · {{ r.group }}{{ r.amount | number: '1.2-2' }}
—Surplus for the period · Profit & Loss{{ bsSurplus | number: '1.2-2' }}
Total Capital & Liabilities{{ bsTotalLiabilities | number: '1.2-2' }}
+
+ +
+
Assets — as at {{ range.to }}
+ + + + + + + + + + @for (r of bsAssets; track r.code) { + + + + + + } + + + + + +
GL CodeParticularsAmount (₹)
{{ r.code }}{{ r.name }} · {{ r.group }}{{ r.amount | number: '1.2-2' }}
Total Assets{{ bsTotalAssets | number: '1.2-2' }}
+
+
+
+ } + + + @if (activeReport === 'comparative-balance-sheet') { +
+

+ Compares the closing position as at {{ range.to }} against the position immediately before + this period began. Change the filter above to compare a different span. +

+ + @for (block of [{ title: 'Capital & Liabilities', rows: compLiabilities }, { title: 'Assets', rows: compAssets }]; track block.title) { +
+
{{ block.title }}
+ + + + + + + + + + + + + @for (r of block.rows; track r.code) { + + + + + + + + + } @empty { + + + + } + +
GL CodeParticularsCurrent (₹)Previous (₹)Variance (₹)Variance %
{{ r.code }}{{ r.name }} · {{ r.group }}{{ r.current | number: '1.2-2' }}{{ r.previous | number: '1.2-2' }} + {{ r.variance | number: '1.2-2' }} + + {{ r.variancePct === null ? '—' : (r.variancePct | number: '1.2-2') + '%' }} +
Nothing to compare for this period and branch.
+
+ } +
+ } + + + @if (activeReport === 'contingent-liabilities') { +
+

+ Off-balance-sheet commitments outstanding as at {{ range.to }}. These are memoranda accounts + and are deliberately excluded from the Trial Balance and Balance Sheet totals. +

+
+ + + + + + + + + + + @for (r of data.contingent; track r.code) { + + + + + + + } @empty { + + + + } + + + + + +
GL CodeParticularsGroupAmount (₹)
{{ r.code }}{{ r.name }}{{ r.group }}{{ r.amount | number: '1.2-2' }}
No contingent exposures outstanding for this selection.
Total Contingent Liabilities{{ contingentTotal | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'gl-wise-dr-cr') { +
+

+ Debit and credit turnover posted between {{ rangeLabel }} for {{ branchLabel }}. Every + voucher is double-entry, so the debit and credit totals below must match. +

+
+ + + + + + + + + + + + + + + @for (r of data.glWise; track r.code) { + + + + + + + + + + + } @empty { + + + + } + + + + + + + + +
GL CodeAccountTypeGroupDebit (₹)Credit (₹)Net (Dr − Cr)Vouchers
{{ r.code }}{{ r.name }} + {{ r.type }} + {{ r.group }}{{ r.debit ? (r.debit | number: '1.2-2') : '' }}{{ r.credit ? (r.credit | number: '1.2-2') : '' }} + {{ r.net | number: '1.2-2' }} + {{ r.vouchers }}
No postings for this period and branch.
TOTAL ({{ data.glWise.length }} accounts){{ glWiseDebit | number: '1.2-2' }}{{ glWiseCredit | number: '1.2-2' }}{{ glWiseDebit - glWiseCredit | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'misc-expense-ledger') { +
+

+ Every miscellaneous-expense voucher posted between {{ rangeLabel }} for {{ branchLabel }} + — {{ miscRows.length | number }} entries. +

+
+ + + + + + + + + + + + + + @for (r of miscRows; track r.voucherNo + r.code) { + + + + + + + + + + } @empty { + + + + } + + + + + + + +
DateVoucher NoBranchGL CodeAccountNarrationAmount (₹)
{{ r.date }}{{ r.voucherNo }}{{ branchName(r.branchCode) }}{{ r.code }}{{ r.name }}{{ r.narration }}{{ r.amount | number: '1.2-2' }}
No miscellaneous expenses posted for this selection.
Total Miscellaneous Expenses{{ miscTotal | number: '1.2-2' }}
+
+
+ } + + +
{{ toastMsg }}
+
diff --git a/src/app/gl-reports/gl-reports.component.scss b/src/app/gl-reports/gl-reports.component.scss new file mode 100644 index 000000000..ac1f6bdc9 --- /dev/null +++ b/src/app/gl-reports/gl-reports.component.scss @@ -0,0 +1,506 @@ +/** + * 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/. + */ + +:host { + --g-navy: #1a237e; + --g-blue: #1d4ed8; + --g-blue-l: #eff6ff; + --g-teal: #0e7e6b; + --g-teal-l: #e0f4f0; + --g-green: #15803d; + --g-green-l: #dcfce7; + --g-amber: #b45309; + --g-amber-l: #fef3c7; + --g-red: #c0392b; + --g-red-l: #fdecea; + --g-g50: #f8fafc; + --g-g100: #f1f5f9; + --g-g200: #e2e8f0; + --g-g300: #cbd5e1; + --g-g500: #64748b; + --g-g700: #334155; + --g-g900: #0f172a; + + display: block; + font-family: + 'DM Sans', + system-ui, + -apple-system, + sans-serif; + background: var(--g-g50); + color: var(--g-g900); + font-size: 14px; + line-height: 1.5; +} + +.glr-root { + padding: 18px 22px; + max-width: 1700px; + margin: 0 auto; +} + +/* HEADER */ +.glr-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.glr-head-left { + display: flex; + align-items: center; + gap: 14px; +} + +.glr-head-ic { + font-size: 28px; + color: var(--g-navy); +} + +.glr-head-t { + margin: 0; + font-size: 18px; + font-weight: 700; +} + +.glr-head-s { + font-size: 12px; + color: var(--g-g500); +} + +/* BUTTONS */ +.glr-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 16px; + border: 1px solid var(--g-g300); + background: #fff; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + color: var(--g-g700); + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + white-space: nowrap; + + &:hover:not(:disabled) { + background: var(--g-g50); + border-color: var(--g-g500); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.glr-btn-xls { + background: #16a34a; + border-color: #16a34a; + color: #fff; + + &:hover:not(:disabled) { + background: #15803d; + border-color: #15803d; + color: #fff; + } +} + +/* FILTER BAR */ +.glr-filters { + background: #fff; + border: 1px solid var(--g-g200); + border-radius: 12px; + padding: 14px; + display: flex; + align-items: flex-end; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgb(0 0 0 / 4%); +} + +.glr-filter { + display: flex; + flex-direction: column; + gap: 5px; + + label { + font-size: 11px; + font-weight: 700; + color: var(--g-g500); + text-transform: uppercase; + letter-spacing: 0.05em; + } +} + +.glr-inp { + padding: 8px 12px; + border: 1px solid var(--g-g200); + border-radius: 8px; + font-size: 13px; + background: #fff; + font-family: inherit; + min-width: 190px; + + &:focus { + outline: none; + border-color: var(--g-navy); + box-shadow: 0 0 0 3px rgb(26 35 126 / 12%); + } +} + +/* Day / Month / Range segmented control */ +.glr-seg { + display: inline-flex; + border: 1px solid var(--g-g200); + border-radius: 8px; + overflow: hidden; + + button { + padding: 8px 14px; + border: none; + background: #fff; + font-size: 13px; + font-weight: 600; + color: var(--g-g500); + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + + + button { + border-left: 1px solid var(--g-g200); + } + + &:hover { + background: var(--g-g50); + } + + &.active { + background: var(--g-navy); + color: #fff; + } + } +} + +/* REPORT SELECTOR */ +.glr-tabs { + display: flex; + gap: 4px; + background: #fff; + border: 1px solid var(--g-g200); + border-radius: 10px; + padding: 4px; + margin-bottom: 16px; + overflow-x: auto; +} + +.glr-tab { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + border: none; + background: transparent; + font-size: 13px; + font-weight: 500; + color: var(--g-g500); + cursor: pointer; + border-radius: 7px; + white-space: nowrap; + transition: all 0.15s; + font-family: inherit; + + &:hover { + color: var(--g-g900); + } + + &.active { + background: var(--g-navy); + color: #fff; + box-shadow: 0 2px 6px rgb(26 35 126 / 25%); + } +} + +.glr-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +/* BALANCE BANNER */ +.glr-balance { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 10px; + background: var(--g-green-l); + border: 1px solid #86efac; + color: #14532d; + font-size: 13px; + + &--bad { + background: var(--g-red-l); + border-color: #fca5a5; + color: #7f1d1d; + } +} + +.glr-note { + margin: 0; + font-size: 12px; + color: var(--g-g500); +} + +/* KPIs */ +.glr-kpis { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.glr-kpi { + background: #fff; + border: 1px solid var(--g-g200); + border-left: 4px solid var(--g-g300); + border-radius: 12px; + padding: 14px 18px; + box-shadow: 0 1px 3px rgb(0 0 0 / 5%); + + &--teal { + border-left-color: var(--g-teal); + } + + &--amber { + border-left-color: var(--g-amber); + } + + &--green { + border-left-color: var(--g-green); + } + + &--red { + border-left-color: var(--g-red); + } +} + +.glr-kpi-n { + font-size: 20px; + font-weight: 700; + font-family: 'DM Mono', monospace; + color: var(--g-g900); +} + +.glr-kpi-l { + font-size: 12px; + color: var(--g-g700); + font-weight: 600; + margin-top: 2px; +} + +.glr-kpi-sub { + font-size: 11px; + color: var(--g-g500); +} + +/* TABLES */ +.glr-two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + align-items: start; +} + +.glr-tbl-wrap { + background: #fff; + border: 1px solid var(--g-g200); + border-radius: 12px; + overflow: auto; + box-shadow: 0 1px 3px rgb(0 0 0 / 4%); + + &--tall { + max-height: 70vh; + } +} + +.glr-tbl-h { + background: var(--g-blue-l); + color: var(--g-navy); + padding: 10px 16px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid var(--g-g200); + position: sticky; + top: 0; + z-index: 2; +} + +.glr-tbl { + width: 100%; + border-collapse: collapse; + + th { + text-align: left; + padding: 9px 12px; + font-size: 11px; + font-weight: 700; + color: var(--g-g500); + text-transform: uppercase; + letter-spacing: 0.04em; + background: var(--g-g50); + border-bottom: 1px solid var(--g-g200); + white-space: nowrap; + } + + td { + padding: 8px 12px; + border-bottom: 1px solid var(--g-g100); + font-size: 13px; + color: var(--g-g700); + } + + tbody tr:hover td { + background: var(--g-g50); + } + + tfoot td { + position: sticky; + bottom: 0; + } +} + +.total-row td { + background: var(--g-g100) !important; + font-weight: 700; + color: var(--g-g900); + border-top: 2px solid var(--g-g300); +} + +.glr-profit-row td { + background: var(--g-green-l) !important; + color: #14532d; +} + +.glr-loss-row td { + background: var(--g-red-l) !important; + color: #7f1d1d; +} + +.ta-c { + text-align: center; +} + +.ta-r { + text-align: right; +} + +.mono { + font-family: 'DM Mono', monospace; + font-size: 12px; +} + +.muted { + color: var(--g-g500); + font-size: 11px; +} + +.var-up { + color: var(--g-green); + font-weight: 600; +} + +.var-down { + color: var(--g-red); + font-weight: 600; +} + +.glr-empty { + text-align: center; + padding: 34px; + color: var(--g-g500); + font-style: italic; +} + +/* Account-type pills on the GL Wise report */ +.type-pill { + display: inline-block; + padding: 2px 9px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.03em; + background: var(--g-g100); + color: var(--g-g700); + + &.type-asset { + background: var(--g-blue-l); + color: var(--g-blue); + } + + &.type-liability { + background: var(--g-teal-l); + color: var(--g-teal); + } + + &.type-income { + background: var(--g-green-l); + color: var(--g-green); + } + + &.type-expense { + background: var(--g-amber-l); + color: var(--g-amber); + } + + &.type-contingent { + background: var(--g-red-l); + color: var(--g-red); + } +} + +/* TOAST */ +.glr-toast { + position: fixed; + bottom: 32px; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: var(--g-navy); + color: #fff; + padding: 12px 22px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + box-shadow: 0 6px 20px rgb(0 0 0 / 25%); + opacity: 0; + pointer-events: none; + transition: all 0.25s; + z-index: 1500; + + &.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@media (width <= 1200px) { + .glr-two-col { + grid-template-columns: 1fr; + } + + .glr-kpis { + grid-template-columns: 1fr; + } +} diff --git a/src/app/gl-reports/gl-reports.component.ts b/src/app/gl-reports/gl-reports.component.ts new file mode 100644 index 000000000..c719c52d5 --- /dev/null +++ b/src/app/gl-reports/gl-reports.component.ts @@ -0,0 +1,470 @@ +/** + * 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 { Component, OnInit, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import * as ExcelJS from 'exceljs'; +import { GL_BRANCHES, GL_PERIOD, glBranchName } from './gl-reports-data'; +import { + BalanceSheetRow, + ComparativeRow, + GlDateRange, + GlReportSet, + MiscExpenseRow, + PnlRow, + buildReports, + clampToLedger, + sum +} from './gl-reports.compute'; + +export type GlReportCode = + | 'trial-balance' + | 'profit-and-loss' + | 'balance-sheet' + | 'comparative-balance-sheet' + | 'contingent-liabilities' + | 'gl-wise-dr-cr' + | 'misc-expense-ledger'; + +type PeriodMode = 'day' | 'month' | 'range'; + +@Component({ + selector: 'mifosx-gl-reports', + templateUrl: './gl-reports.component.html', + styleUrls: ['./gl-reports.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + FormsModule + ] +}) +export class GlReportsComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + + readonly reports: { code: GlReportCode; label: string; icon: string }[] = [ + { code: 'trial-balance', label: 'Trial Balance', icon: 'balance-scale' }, + { code: 'profit-and-loss', label: 'Profit and Loss', icon: 'chart-line' }, + { code: 'balance-sheet', label: 'Consolidated Balance Sheet', icon: 'file-alt' }, + { code: 'comparative-balance-sheet', label: 'Comparative Balance Sheet', icon: 'balance-scale' }, + { code: 'contingent-liabilities', label: 'Contingent Liabilities', icon: 'exclamation-circle' }, + { code: 'gl-wise-dr-cr', label: 'GL Wise DR / CR', icon: 'project-diagram' }, + { code: 'misc-expense-ledger', label: 'Misc Expense Ledger', icon: 'receipt' } + ]; + + activeReport: GlReportCode = 'trial-balance'; + + branches = GL_BRANCHES; + ledgerPeriod = GL_PERIOD; + + // ── Filter state ─────────────────────────────────────────────────────────── + periodMode: PeriodMode = 'month'; + /** Last full month of the generated ledger. */ + filterDay = '2026-07-31'; + filterMonth = '2026-07'; + filterFrom = GL_PERIOD.fyStart; + filterTo = GL_PERIOD.fyEnd; + branchCode = ''; + + generating = false; + toastMsg = ''; + toastVisible = false; + private toastTimer: any; + + ngOnInit(): void { + const param = this.route.snapshot.paramMap.get('report') as GlReportCode | null; + if (param && this.reports.some((r) => r.code === param)) { + this.activeReport = param; + } + } + + showReport(code: GlReportCode): void { + this.activeReport = code; + // Keep the URL deep-linkable so the GL menu items land on the right report. + this.router.navigate(['/gl/reports', code], { replaceUrl: true }); + } + + get reportLabel(): string { + return this.reports.find((r) => r.code === this.activeReport)?.label ?? ''; + } + + // ── Filter resolution ────────────────────────────────────────────────────── + + /** Turns the filter controls into the concrete date range the reports use. */ + get range(): GlDateRange { + if (this.periodMode === 'day') { + return clampToLedger({ from: this.filterDay, to: this.filterDay }); + } + if (this.periodMode === 'month') { + const [ + y, + m + ] = this.filterMonth.split('-').map(Number); + const last = new Date(Date.UTC(y, m, 0)).getUTCDate(); + return clampToLedger({ from: `${this.filterMonth}-01`, to: `${this.filterMonth}-${`${last}`.padStart(2, '0')}` }); + } + return clampToLedger({ from: this.filterFrom, to: this.filterTo }); + } + + get rangeLabel(): string { + const r = this.range; + return r.from === r.to ? r.from : `${r.from} to ${r.to}`; + } + + get branchLabel(): string { + return this.branchCode ? glBranchName(this.branchCode) : 'All Branches (Consolidated)'; + } + + resetFilters(): void { + this.periodMode = 'month'; + this.filterMonth = '2026-07'; + this.filterDay = '2026-07-31'; + this.filterFrom = GL_PERIOD.fyStart; + this.filterTo = GL_PERIOD.fyEnd; + this.branchCode = ''; + } + + /** All seven reports for the current filter — memoised inside buildReports. */ + get data(): GlReportSet { + return buildReports(this.range, this.branchCode); + } + + // ── Trial Balance ────────────────────────────────────────────────────────── + get tbOpeningDebit(): number { + return sum(this.data.trialBalance, (r) => r.openingDebit); + } + get tbOpeningCredit(): number { + return sum(this.data.trialBalance, (r) => r.openingCredit); + } + get tbPeriodDebit(): number { + return sum(this.data.trialBalance, (r) => r.periodDebit); + } + get tbPeriodCredit(): number { + return sum(this.data.trialBalance, (r) => r.periodCredit); + } + get tbClosingDebit(): number { + return sum(this.data.trialBalance, (r) => r.closingDebit); + } + get tbClosingCredit(): number { + return sum(this.data.trialBalance, (r) => r.closingCredit); + } + /** A trial balance that does not tally is the single most important thing to surface. */ + get tbBalanced(): boolean { + return Math.round(this.tbClosingDebit) === Math.round(this.tbClosingCredit); + } + + // ── Profit and Loss ──────────────────────────────────────────────────────── + get pnlIncome(): PnlRow[] { + return this.data.pnl.income; + } + get pnlExpense(): PnlRow[] { + return this.data.pnl.expense; + } + get totalIncome(): number { + return sum(this.pnlIncome, (r) => r.amount); + } + get totalExpense(): number { + return sum(this.pnlExpense, (r) => r.amount); + } + get netProfit(): number { + return this.totalIncome - this.totalExpense; + } + get netProfitPct(): number { + return this.totalIncome ? (this.netProfit / this.totalIncome) * 100 : 0; + } + + // ── Balance Sheet ────────────────────────────────────────────────────────── + get bsAssets(): BalanceSheetRow[] { + return this.data.balanceSheet.assets; + } + get bsLiabilities(): BalanceSheetRow[] { + return this.data.balanceSheet.liabilities; + } + get bsSurplus(): number { + return this.data.balanceSheet.surplus; + } + get bsTotalAssets(): number { + return sum(this.bsAssets, (r) => r.amount); + } + get bsTotalLiabilities(): number { + return sum(this.bsLiabilities, (r) => r.amount) + this.bsSurplus; + } + get bsBalanced(): boolean { + return Math.round(this.bsTotalAssets) === Math.round(this.bsTotalLiabilities); + } + + // ── Comparative Balance Sheet ────────────────────────────────────────────── + get compAssets(): ComparativeRow[] { + return this.data.comparative.filter((r) => r.side === 'ASSET'); + } + get compLiabilities(): ComparativeRow[] { + return this.data.comparative.filter((r) => r.side === 'LIABILITY'); + } + + // ── Contingent Liabilities ───────────────────────────────────────────────── + get contingentTotal(): number { + return sum(this.data.contingent, (r) => r.amount); + } + + // ── GL Wise DR/CR ────────────────────────────────────────────────────────── + get glWiseDebit(): number { + return sum(this.data.glWise, (r) => r.debit); + } + get glWiseCredit(): number { + return sum(this.data.glWise, (r) => r.credit); + } + + // ── Misc Expense Ledger ──────────────────────────────────────────────────── + get miscRows(): MiscExpenseRow[] { + return this.data.miscExpense; + } + get miscTotal(): number { + return sum(this.miscRows, (r) => r.amount); + } + + branchName(code: string): string { + return glBranchName(code); + } + + // ── Excel export ─────────────────────────────────────────────────────────── + + /** Exports whichever report is on screen, carrying the active filter into the header. */ + async exportExcel(): Promise { + this.generating = true; + try { + const wb = new ExcelJS.Workbook(); + wb.creator = 'CBSNexora'; + wb.created = new Date(); + const sheet = wb.addWorksheet(this.reportLabel.slice(0, 31)); + + const { headers, rows, widths } = this.exportShape(); + sheet.columns = widths.map((w) => ({ width: w })); + + const title = sheet.addRow([this.reportLabel]); + sheet.mergeCells(title.number, 1, title.number, headers.length); + title.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } }; + title.alignment = { horizontal: 'center', vertical: 'middle' }; + title.height = 24; + title.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A237E' } }; + + const meta = sheet.addRow([`Period: ${this.rangeLabel} · Branch: ${this.branchLabel}`]); + sheet.mergeCells(meta.number, 1, meta.number, headers.length); + meta.font = { italic: true, size: 10 }; + sheet.addRow([]); + + const head = sheet.addRow(headers); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center' }; + head.eachCell((c) => { + c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } }; + c.border = { bottom: { style: 'thin' } }; + }); + + for (const r of rows) { + const row = sheet.addRow(r); + for (let c = 1; c <= headers.length; c++) { + if (typeof r[c - 1] === 'number') { + row.getCell(c).numFmt = '#,##0.00'; + } + } + } + sheet.views = [{ state: 'frozen', ySplit: 4 }]; + + const buffer = await wb.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${this.reportLabel.replace(/[^\w]+/g, '-')}-${this.range.from}-to-${this.range.to}.xlsx`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + this.toast(`${this.reportLabel} exported for ${this.rangeLabel}`); + } catch { + this.toast('Could not generate the workbook — please retry'); + } finally { + this.generating = false; + } + } + + /** Header row, data rows and column widths for the active report. */ + private exportShape(): { headers: string[]; rows: (string | number | null)[][]; widths: number[] } { + switch (this.activeReport) { + case 'trial-balance': + return { + headers: [ + 'GL Code', + 'Account', + 'Group', + 'Opening Dr', + 'Opening Cr', + 'Period Dr', + 'Period Cr', + 'Closing Dr', + 'Closing Cr' + ], + widths: [12, 42, 26, 16, 16, 16, 16, 16, 16], + rows: [ + ...this.data.trialBalance.map((r) => [ + r.code, + r.name, + r.group, + r.openingDebit, + r.openingCredit, + r.periodDebit, + r.periodCredit, + r.closingDebit, + r.closingCredit + ]), + [ + 'TOTAL', + '', + '', + this.tbOpeningDebit, + this.tbOpeningCredit, + this.tbPeriodDebit, + this.tbPeriodCredit, + this.tbClosingDebit, + this.tbClosingCredit + ] + ] + }; + case 'profit-and-loss': + return { + headers: [ + 'Side', + 'GL Code', + 'Account', + 'Group', + 'Amount' + ], + widths: [14, 12, 42, 26, 18], + rows: [ + ...this.pnlExpense.map((r) => ['Expenditure', r.code, r.name, r.group, r.amount]), + ['Total Expenditure', '', '', '', this.totalExpense], + ...this.pnlIncome.map((r) => ['Income', r.code, r.name, r.group, r.amount]), + ['Total Income', '', '', '', this.totalIncome], + ['Net Profit', '', '', '', this.netProfit] + ] + }; + case 'balance-sheet': + return { + headers: [ + 'Side', + 'GL Code', + 'Account', + 'Group', + 'Amount' + ], + widths: [14, 12, 42, 26, 18], + rows: [ + ...this.bsLiabilities.map((r) => ['Liability', r.code, r.name, r.group, r.amount]), + ['Liability', '', 'Surplus for the period', 'Profit & Loss', this.bsSurplus], + ['Total Liabilities', '', '', '', this.bsTotalLiabilities], + ...this.bsAssets.map((r) => ['Asset', r.code, r.name, r.group, r.amount]), + ['Total Assets', '', '', '', this.bsTotalAssets] + ] + }; + case 'comparative-balance-sheet': + return { + headers: [ + 'Side', + 'GL Code', + 'Account', + 'Current', + 'Previous', + 'Variance', + 'Variance %' + ], + widths: [12, 12, 42, 18, 18, 18, 14], + rows: this.data.comparative.map((r) => [ + r.side, + r.code, + r.name, + r.current, + r.previous, + r.variance, + r.variancePct === null ? '—' : Number(r.variancePct.toFixed(2)) + ]) + }; + case 'contingent-liabilities': + return { + headers: [ + 'GL Code', + 'Particulars', + 'Group', + 'Amount' + ], + widths: [12, 48, 26, 18], + rows: [ + ...this.data.contingent.map((r) => [r.code, r.name, r.group, r.amount]), + ['TOTAL', '', '', this.contingentTotal] + ] + }; + case 'gl-wise-dr-cr': + return { + headers: [ + 'GL Code', + 'Account', + 'Type', + 'Group', + 'Debit', + 'Credit', + 'Net (Dr-Cr)', + 'Vouchers' + ], + widths: [12, 42, 14, 26, 18, 18, 18, 12], + rows: [ + ...this.data.glWise.map((r) => [r.code, r.name, r.type, r.group, r.debit, r.credit, r.net, r.vouchers]), + ['TOTAL', '', '', '', this.glWiseDebit, this.glWiseCredit, this.glWiseDebit - this.glWiseCredit, null] + ] + }; + case 'misc-expense-ledger': + default: + return { + headers: [ + 'Date', + 'Voucher No', + 'Branch', + 'GL Code', + 'Account', + 'Narration', + 'Amount' + ], + widths: [14, 20, 18, 12, 36, 46, 18], + rows: [ + ...this.miscRows.map((r) => [ + r.date, + r.voucherNo, + this.branchName(r.branchCode), + r.code, + r.name, + r.narration, + r.amount + ]), + ['TOTAL', '', '', '', '', '', this.miscTotal] + ] + }; + } + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/gl-reports/gl-reports.compute.ts b/src/app/gl-reports/gl-reports.compute.ts new file mode 100644 index 000000000..705d7fcaf --- /dev/null +++ b/src/app/gl-reports/gl-reports.compute.ts @@ -0,0 +1,379 @@ +/** + * 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/. + */ + +/** + * Derives all seven General Ledger reports from the posting ledger. + * + * Each report is a pure function of (postings, date range, branch), so changing + * the day / month / branch filter genuinely recomputes the figures. Results are + * memoised per filter key because the ledger runs to roughly twenty thousand + * postings and Angular re-evaluates template getters on every change detection. + */ + +import { + GL_ACCOUNTS, + GL_PERIOD, + GlAccount, + GlPosting, + glAccount, + glPostings +} from './gl-reports-data'; + +export interface GlDateRange { + from: string; + to: string; +} + +export interface TrialBalanceRow { + code: string; + name: string; + group: string; + openingDebit: number; + openingCredit: number; + periodDebit: number; + periodCredit: number; + closingDebit: number; + closingCredit: number; +} + +export interface PnlRow { + code: string; + name: string; + group: string; + amount: number; +} + +export interface BalanceSheetRow { + code: string; + name: string; + group: string; + amount: number; +} + +export interface ComparativeRow { + code: string; + name: string; + group: string; + side: 'ASSET' | 'LIABILITY'; + current: number; + previous: number; + variance: number; + variancePct: number | null; +} + +export interface GlWiseRow { + code: string; + name: string; + group: string; + type: string; + debit: number; + credit: number; + net: number; + vouchers: number; +} + +export interface MiscExpenseRow { + date: string; + voucherNo: string; + branchCode: string; + code: string; + name: string; + narration: string; + amount: number; +} + +/** Natural side of an account — assets and expenses sit on the debit side. */ +function isDebitSide(account: GlAccount): boolean { + return account.type === 'ASSET' || account.type === 'EXPENSE' || account.type === 'CONTINGENT'; +} + +function matchesBranch(p: GlPosting, branchCode: string): boolean { + return !branchCode || p.branchCode === branchCode; +} + +/** Signed balance on the account's natural side, for postings up to and including `to`. */ +function cumulativeBalances(to: string, branchCode: string): Map { + const out = new Map(); + for (const p of glPostings()) { + if (p.date > to || !matchesBranch(p, branchCode)) { + continue; + } + const account = glAccount(p.accountCode); + if (!account) { + continue; + } + const delta = isDebitSide(account) ? p.debit - p.credit : p.credit - p.debit; + out.set(p.accountCode, (out.get(p.accountCode) ?? 0) + delta); + } + return out; +} + +/** Debit and credit turnover strictly inside the range. */ +function periodTurnover(range: GlDateRange, branchCode: string): Map }> { + const out = new Map }>(); + for (const p of glPostings()) { + if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) { + continue; + } + let entry = out.get(p.accountCode); + if (!entry) { + entry = { debit: 0, credit: 0, vouchers: new Set() }; + out.set(p.accountCode, entry); + } + entry.debit += p.debit; + entry.credit += p.credit; + entry.vouchers.add(p.voucherNo); + } + return out; +} + +/** The day before `date`, ISO formatted — used for opening balances. */ +function previousDay(date: string): string { + const d = new Date(`${date}T00:00:00Z`); + d.setUTCDate(d.getUTCDate() - 1); + return d.toISOString().slice(0, 10); +} + +// ── Reports ────────────────────────────────────────────────────────────────── + +export function trialBalance(range: GlDateRange, branchCode: string): TrialBalanceRow[] { + const opening = cumulativeBalances(previousDay(range.from), branchCode); + const turnover = periodTurnover(range, branchCode); + const rows: TrialBalanceRow[] = []; + + for (const account of GL_ACCOUNTS) { + // Contingent accounts are memoranda and never appear in the trial balance. + if (account.type === 'CONTINGENT') { + continue; + } + const open = opening.get(account.code) ?? 0; + const move = turnover.get(account.code); + const periodDebit = move?.debit ?? 0; + const periodCredit = move?.credit ?? 0; + if (!open && !periodDebit && !periodCredit) { + continue; + } + const debitSide = isDebitSide(account); + const closingNatural = open + (debitSide ? periodDebit - periodCredit : periodCredit - periodDebit); + const closingSigned = debitSide ? closingNatural : -closingNatural; + + rows.push({ + code: account.code, + name: account.name, + group: account.group, + openingDebit: debitSide && open > 0 ? open : !debitSide && open < 0 ? -open : 0, + openingCredit: !debitSide && open > 0 ? open : debitSide && open < 0 ? -open : 0, + periodDebit, + periodCredit, + closingDebit: closingSigned > 0 ? closingSigned : 0, + closingCredit: closingSigned < 0 ? -closingSigned : 0 + }); + } + return rows; +} + +export function profitAndLoss(range: GlDateRange, branchCode: string): { income: PnlRow[]; expense: PnlRow[] } { + const turnover = periodTurnover(range, branchCode); + const income: PnlRow[] = []; + const expense: PnlRow[] = []; + + for (const account of GL_ACCOUNTS) { + if (account.type !== 'INCOME' && account.type !== 'EXPENSE') { + continue; + } + const move = turnover.get(account.code); + if (!move) { + continue; + } + const amount = + account.type === 'INCOME' ? move.credit - move.debit : move.debit - move.credit; + if (!amount) { + continue; + } + const row: PnlRow = { code: account.code, name: account.name, group: account.group, amount }; + (account.type === 'INCOME' ? income : expense).push(row); + } + return { income, expense }; +} + +/** + * Balance sheet as at the period end. The current-period surplus is folded in as + * a liability line so the two sides tally the way the printed statement does. + */ +export function balanceSheet(range: GlDateRange, branchCode: string): { + assets: BalanceSheetRow[]; + liabilities: BalanceSheetRow[]; + surplus: number; +} { + const closing = cumulativeBalances(range.to, branchCode); + const assets: BalanceSheetRow[] = []; + const liabilities: BalanceSheetRow[] = []; + let income = 0; + let expense = 0; + + for (const account of GL_ACCOUNTS) { + const bal = closing.get(account.code) ?? 0; + if (account.type === 'INCOME') { + income += bal; + continue; + } + if (account.type === 'EXPENSE') { + expense += bal; + continue; + } + if (account.type === 'CONTINGENT' || !bal) { + continue; + } + const row: BalanceSheetRow = { code: account.code, name: account.name, group: account.group, amount: bal }; + (account.type === 'ASSET' ? assets : liabilities).push(row); + } + return { assets, liabilities, surplus: income - expense }; +} + +/** Current period against the equivalent span immediately before it. */ +export function comparativeBalanceSheet(range: GlDateRange, branchCode: string): ComparativeRow[] { + const priorTo = previousDay(range.from); + const current = cumulativeBalances(range.to, branchCode); + const previous = cumulativeBalances(priorTo, branchCode); + const rows: ComparativeRow[] = []; + + for (const account of GL_ACCOUNTS) { + if (account.type !== 'ASSET' && account.type !== 'LIABILITY') { + continue; + } + const cur = current.get(account.code) ?? 0; + const prev = previous.get(account.code) ?? 0; + if (!cur && !prev) { + continue; + } + const variance = cur - prev; + rows.push({ + code: account.code, + name: account.name, + group: account.group, + side: account.type, + current: cur, + previous: prev, + variance, + variancePct: prev ? (variance / Math.abs(prev)) * 100 : null + }); + } + return rows; +} + +export function contingentLiabilities(range: GlDateRange, branchCode: string): BalanceSheetRow[] { + const closing = cumulativeBalances(range.to, branchCode); + return GL_ACCOUNTS.filter((a) => a.type === 'CONTINGENT') + .map((a) => ({ code: a.code, name: a.name, group: a.group, amount: closing.get(a.code) ?? 0 })) + .filter((r) => r.amount !== 0); +} + +export function glWiseDrCr(range: GlDateRange, branchCode: string): GlWiseRow[] { + const turnover = periodTurnover(range, branchCode); + const rows: GlWiseRow[] = []; + for (const account of GL_ACCOUNTS) { + const move = turnover.get(account.code); + if (!move || (!move.debit && !move.credit)) { + continue; + } + rows.push({ + code: account.code, + name: account.name, + group: account.group, + type: account.type, + debit: move.debit, + credit: move.credit, + net: move.debit - move.credit, + vouchers: move.vouchers.size + }); + } + return rows; +} + +export function miscExpenseLedger(range: GlDateRange, branchCode: string): MiscExpenseRow[] { + const miscCodes = new Set(GL_ACCOUNTS.filter((a) => a.miscExpense).map((a) => a.code)); + const rows: MiscExpenseRow[] = []; + for (const p of glPostings()) { + if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) { + continue; + } + if (!miscCodes.has(p.accountCode) || !p.debit) { + continue; + } + rows.push({ + date: p.date, + voucherNo: p.voucherNo, + branchCode: p.branchCode, + code: p.accountCode, + name: glAccount(p.accountCode)?.name ?? p.accountCode, + narration: p.narration, + amount: p.debit + }); + } + return rows.sort((a, b) => (a.date === b.date ? a.voucherNo.localeCompare(b.voucherNo) : a.date.localeCompare(b.date))); +} + +// ── Memoisation ────────────────────────────────────────────────────────────── + +export interface GlReportSet { + trialBalance: TrialBalanceRow[]; + pnl: { income: PnlRow[]; expense: PnlRow[] }; + balanceSheet: { assets: BalanceSheetRow[]; liabilities: BalanceSheetRow[]; surplus: number }; + comparative: ComparativeRow[]; + contingent: BalanceSheetRow[]; + glWise: GlWiseRow[]; + miscExpense: MiscExpenseRow[]; +} + +let cacheKey = ''; +let cached: GlReportSet | null = null; + +/** All seven reports for one filter selection, computed once per selection. */ +export function buildReports(range: GlDateRange, branchCode: string): GlReportSet { + const key = `${range.from}|${range.to}|${branchCode}`; + if (cached && cacheKey === key) { + return cached; + } + cached = { + trialBalance: trialBalance(range, branchCode), + pnl: profitAndLoss(range, branchCode), + balanceSheet: balanceSheet(range, branchCode), + comparative: comparativeBalanceSheet(range, branchCode), + contingent: contingentLiabilities(range, branchCode), + glWise: glWiseDrCr(range, branchCode), + miscExpense: miscExpenseLedger(range, branchCode) + }; + cacheKey = key; + return cached; +} + +/** Clamp a range to the generated ledger so filters cannot query empty space. */ +export function clampToLedger(range: GlDateRange): GlDateRange { + return { + from: range.from < GL_PERIOD.fyStart ? GL_PERIOD.fyStart : range.from, + to: range.to > GL_PERIOD.fyEnd ? GL_PERIOD.fyEnd : range.to + }; +} + +export function sum(rows: T[], pick: (row: T) => number): number { + return rows.reduce((acc, r) => acc + pick(r), 0); +} + +/** Groups rows by their `group` field, preserving chart-of-accounts order. */ +export function groupBy(rows: T[]): { group: string; rows: T[]; total: number }[] { + const order: string[] = []; + const map = new Map(); + for (const r of rows) { + if (!map.has(r.group)) { + map.set(r.group, []); + order.push(r.group); + } + map.get(r.group)!.push(r); + } + return order.map((g) => ({ group: g, rows: map.get(g)!, total: 0 })); +} diff --git a/src/app/gl-reports/gl-reports.module.ts b/src/app/gl-reports/gl-reports.module.ts new file mode 100644 index 000000000..018c0acbd --- /dev/null +++ b/src/app/gl-reports/gl-reports.module.ts @@ -0,0 +1,20 @@ +/** + * 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 { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { GlReportsRoutingModule } from './gl-reports-routing.module'; + +@NgModule({ + imports: [ + SharedModule, + GlReportsRoutingModule + ] +}) +export class GlReportsModule {} diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.html b/src/app/rbi-circulars/aml-rules/aml-rules.component.html index 6726b0a4d..e9c58ede8 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.html +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.html @@ -45,6 +45,12 @@ + + + Income / Occupation Limit + {{ getIncomeLimitLabel(element) }} + + Rule Reason / Criteria diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.scss b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss index ca8f739c5..8234e9486 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.scss +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss @@ -114,6 +114,15 @@ max-width: 400px; } +.income-limit-cell { + color: #334155; + font-size: 13px; + font-weight: 500; + white-space: normal; + max-width: 200px; + line-height: 1.35; +} + /* Badges for Risk Category */ .risk-badge { display: inline-flex; diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.ts b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts index 913ca16c5..5d5843c5e 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.ts +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts @@ -42,6 +42,7 @@ export class AmlRulesComponent implements OnInit { 'orderPosition', 'ruleName', 'riskCategory', + 'incomeLimit', 'reason', 'amlCheckStatus', 'bannedEntityMatch' @@ -84,4 +85,39 @@ export class AmlRulesComponent implements OnInit { return ''; } } + + /** Formats whichever income/turnover/occupation criterion drives this rule's risk mark. */ + getIncomeLimitLabel(rule: any): string { + const inr = (n: number) => '₹' + Number(n).toLocaleString('en-IN'); + if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0) { + return rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 + ? `Monthly Income ${inr(rule.minMonthlyIncome)} - ${inr(rule.maxMonthlyIncome)}` + : `Monthly Income > ${inr(rule.minMonthlyIncome)}`; + } + if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0) { + return rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 + ? `Annual Income ${inr(rule.minAnnualIncome)} - ${inr(rule.maxAnnualIncome)}` + : `Annual Income > ${inr(rule.minAnnualIncome)}`; + } + if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0) { + return rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 + ? `Annual Turnover ${inr(rule.minAnnualTurnover)} - ${inr(rule.maxAnnualTurnover)}` + : `Annual Turnover > ${inr(rule.minAnnualTurnover)}`; + } + if (rule.occupationName) return `Occupation: ${rule.occupationName}`; + if (rule.occupationCategoryName) return `Occupation Category: ${rule.occupationCategoryName}`; + if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) { + const parts: string[] = []; + if (rule.txnThresholdAmountMediumRisk != null) parts.push(`${inr(rule.txnThresholdAmountMediumRisk)} (Medium Risk)`); + if (rule.txnThresholdAmountHighRisk != null) parts.push(`${inr(rule.txnThresholdAmountHighRisk)} (High Risk)`); + return `Txn Amount ≥ ${parts.join(' / ')}`; + } + if (rule.txnThresholdAmount != null) { + const amt = inr(rule.txnThresholdAmount); + if (rule.txnRuleType === 'THRESHOLD') return `Txn Amount ≥ ${amt}`; + if (rule.txnWindowDays != null) return `${amt} within ${rule.txnWindowDays} days`; + return `Txn Amount ≥ ${amt}`; + } + return 'Behavioral / Transaction Pattern'; + } } diff --git a/src/app/rbi-circulars/oss-return.model.ts b/src/app/rbi-circulars/oss-return.model.ts new file mode 100644 index 000000000..5f210af18 --- /dev/null +++ b/src/app/rbi-circulars/oss-return.model.ts @@ -0,0 +1,134 @@ +/** + * 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/. + */ + +/** + * Shapes common to every RBI CIMS off-site surveillance return (OSS 1..7). + * Kept free of any ExcelJS import so data files stay dependency-light. + */ + +/** One labelled row of a return. */ +export interface ReturnLine { + label: string; + /** 0 = numbered head, 1 = roman sub-item, 2 = a/b/c leaf. Drives indentation. */ + level: number; + /** null renders as an empty cell, never as 0 — RBI treats blank and zero differently. */ + value: number | null; + /** Derived/aggregate row — shaded and bold, like the blue cells in the workbook. */ + total?: boolean; + /** Row is a headcount (depositors / borrowers / employees), not a rupee amount. */ + count?: boolean; +} + +/** + * One row of a multi-column statement. Returns from OSS 3 onwards are matrices + * (risk category x asset class) rather than the single-value lists of OSS 1/2. + */ +export interface MatrixRow { + label: string; + /** 0 = numbered head, 1..n = progressively indented sub-items. */ + level: number; + /** One entry per data column; null renders blank, never 0. */ + values: (number | null)[]; + /** Derived/aggregate row — shaded and bold. */ + total?: boolean; + /** Caption-only row that spans the table with no figures of its own. */ + header?: boolean; + /** Render values as percentages rather than amounts. */ + percent?: boolean; +} + +/** A labelled statement made of a header band plus matrix rows. */ +export interface MatrixTable { + title: string; + subtitle?: string; + /** Column captions excluding the leading label column. */ + columns: string[]; + rows: MatrixRow[]; + notes?: string[]; + unit?: string; +} + +/** A signatory block as it appears on the Signatory sheet of every OSS return. */ +export interface ReturnSignatory { + heading: string; + signature: string; + name: string; + designation: string; + email: string; + telephoneOffice: string; + telephoneResidence: string; + place: string; + date: string; +} + +/** A `label -> value` row on the General Information sheet. */ +export interface ReturnGeneralInfoRow { + label: string; + value: string; +} + +/** Field order shared by the Signatory sheet of every OSS return. */ +export const SIGNATORY_FIELDS: { key: keyof ReturnSignatory; caption: string }[] = [ + { key: 'signature', caption: 'Signature' }, + { key: 'name', caption: 'Name' }, + { key: 'designation', caption: 'Designation' }, + { key: 'email', caption: 'e-mail ID' }, + { key: 'telephoneOffice', caption: 'Tele. No. (O)' }, + { key: 'telephoneResidence', caption: 'Tele. No. (R)' }, + { key: 'place', caption: 'Place' }, + { key: 'date', caption: 'Date' } +]; + +/** + * Identity of the reporting bank, shared by every OSS return so the General + * Information sheets can never drift apart between OSS 1 and OSS 2. + */ +export const OSS_BANK = { + name: 'CBSNEXORA BANK LTD., HEAD OFFICE', + bankCode: '91007', + roName: 'MUMBAI', + institutionType: 'Urban Cooperative Banks', + reportingFrequency: 'Quarterly', + quarterEndDate: '30-Jun-2026', + auditFlag: 'Un-Audited', + dateOfAudit: '', + returnVersion: 'V1.0', + startDate: '01-Apr-2026', + /** OSS 3 prints dates as DD-MM-YYYY rather than DD-Mon-YYYY. */ + startDateNumeric: '01-04-2026', + quarterEndDateNumeric: '30-06-2026' +}; + +/** + * Signatories for the demo bank. The `.example` domain is reserved for + * documentation, so these addresses can never reach a real inbox. + */ +export const OSS_SIGNATORIES: ReturnSignatory[] = [ + { + heading: 'Authorised Reporting Official', + signature: 'ROHAN DESHPANDE', + name: 'ROHAN DESHPANDE', + designation: 'MANAGER — REGULATORY REPORTING', + email: 'rohan.deshpande@cbsnexorabank.example', + telephoneOffice: '022-40015520', + telephoneResidence: '022-40015588', + place: 'MUMBAI', + date: '14/07/2026' + }, + { + heading: 'Countersigned By', + signature: 'ANJALI MENON', + name: 'ANJALI MENON', + designation: 'CHIEF EXECUTIVE OFFICER', + email: 'ceo@cbsnexorabank.example', + telephoneOffice: '022-40015500', + telephoneResidence: '022-40015501', + place: 'MUMBAI', + date: '14/07/2026' + } +]; diff --git a/src/app/rbi-circulars/oss-return.scss b/src/app/rbi-circulars/oss-return.scss new file mode 100644 index 000000000..fd59d774d --- /dev/null +++ b/src/app/rbi-circulars/oss-return.scss @@ -0,0 +1,417 @@ +/** + * 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/. + */ + +:host { + /* Mirrors the RBI CIMS workbook palette so the page and the export read alike. */ + --o-navy: #002f5f; + --o-blue: #1d4ed8; + --o-blue-l: #eff6ff; + --o-derived: #cfe2f8; + --o-green: #15803d; + --o-green-l: #dcfce7; + --o-amber-l: #ffc; + --o-amber-d: #92400e; + --o-red: #c0392b; + --o-red-l: #fdecea; + --o-g50: #f8fafc; + --o-g100: #f1f5f9; + --o-g200: #e2e8f0; + --o-g300: #cbd5e1; + --o-g500: #64748b; + --o-g700: #334155; + --o-g900: #0f172a; + + display: block; + font-family: + 'DM Sans', + system-ui, + -apple-system, + sans-serif; + background: var(--o-g50); + color: var(--o-g900); + font-size: 14px; + line-height: 1.5; +} + +.oss-root { + padding: 18px 22px; + max-width: 1500px; + margin: 0 auto; +} + +/* HEADER */ +.oss-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.oss-head-left { + display: flex; + align-items: center; + gap: 14px; +} + +.oss-head-ic { + font-size: 28px; + color: var(--o-navy); +} + +.oss-head-t { + margin: 0; + font-size: 18px; + font-weight: 700; +} + +.oss-head-s { + font-size: 12px; + color: var(--o-g500); +} + +.oss-head-right { + display: flex; + align-items: center; + gap: 12px; +} + +.oss-audit-pill { + padding: 4px 12px; + border-radius: 999px; + background: var(--o-amber-l); + color: var(--o-amber-d); + border: 1px solid #fcd34d; + font-size: 12px; + font-weight: 700; +} + +/* BUTTONS */ +.oss-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 16px; + border: 1px solid var(--o-g300); + background: #fff; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + color: var(--o-g700); + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.oss-btn-xls { + background: #16a34a; + border-color: #16a34a; + color: #fff; + + &:hover:not(:disabled) { + background: #15803d; + border-color: #15803d; + } +} + +/* BALANCE CHECK */ +.oss-balance { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 10px; + background: var(--o-green-l); + border: 1px solid #86efac; + color: #14532d; + font-size: 13px; + margin-bottom: 14px; + + &--bad { + background: var(--o-red-l); + border-color: #fca5a5; + color: #7f1d1d; + } +} + +/* TABS */ +.oss-tabs { + display: flex; + gap: 4px; + background: #fff; + border: 1px solid var(--o-g200); + border-radius: 10px; + padding: 4px; + margin-bottom: 16px; + overflow-x: auto; +} + +.oss-tab { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + border: none; + background: transparent; + font-size: 13px; + font-weight: 500; + color: var(--o-g500); + cursor: pointer; + border-radius: 7px; + white-space: nowrap; + transition: all 0.15s; + font-family: inherit; + + &:hover { + color: var(--o-g900); + } + + &.active { + background: var(--o-navy); + color: #fff; + box-shadow: 0 2px 6px rgb(0 47 95 / 25%); + } +} + +.oss-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +/* Full-width banner, matching the dark blue header band in the workbook. */ +.oss-sheet-banner { + background: var(--o-navy); + color: #fff; + padding: 10px 18px; + border-radius: 8px; + font-size: 14px; + font-weight: 700; + text-align: center; + letter-spacing: 0.02em; +} + +/* TABLES */ +.oss-tbl-wrap { + background: #fff; + border: 1px solid var(--o-g200); + border-radius: 10px; + overflow: auto; + box-shadow: 0 1px 3px rgb(0 0 0 / 4%); +} + +.oss-tbl-h { + background: var(--o-blue-l); + color: var(--o-navy); + padding: 10px 16px; + font-size: 12px; + font-weight: 700; + border-bottom: 1px solid var(--o-g200); +} + +.oss-tbl { + width: 100%; + border-collapse: collapse; + + th { + text-align: left; + padding: 9px 12px; + font-size: 11px; + font-weight: 700; + color: var(--o-g700); + background: var(--o-g100); + border-bottom: 1px solid var(--o-g200); + border-right: 1px solid var(--o-g200); + } + + td { + padding: 8px 12px; + border-bottom: 1px solid var(--o-g100); + border-right: 1px solid var(--o-g100); + font-size: 13px; + color: var(--o-g700); + } + + tr:last-child td { + border-bottom: none; + } + + tbody tr:hover td { + background: var(--o-g50); + } +} + +/* Key/value layout used by General Information and Signatory. */ +.oss-tbl--kv { + th { + width: 40%; + background: #d9d9d9; + color: var(--o-g900); + } + + td.filled { + background: var(--o-derived); + } + + td.audit { + background: var(--o-amber-l); + font-weight: 700; + } +} + +/* Derived/aggregate rows — the blue cells in the original sheet. */ +.total-row td { + font-weight: 700; + color: var(--o-g900); +} + +td.derived { + background: var(--o-derived); + font-weight: 700; + color: var(--o-navy); +} + +/* Indentation matching the return's numbered / roman / lettered hierarchy. */ +.lvl-0 { + font-weight: 600; +} + +.lvl-1 { + padding-left: 30px !important; +} + +.lvl-2 { + padding-left: 52px !important; +} + +.lvl-3 { + padding-left: 74px !important; +} + +/* Caption-only row spanning a statement (e.g. "PRIORITY SECTOR"). */ +.section-row td { + background: var(--o-g100); + font-weight: 700; + color: var(--o-g900); +} + +/* Wide matrices keep their label column readable while the figures scroll. */ +.oss-col-label { + min-width: 320px; +} + +.oss-tbl-unit { + float: right; + font-weight: 600; + color: var(--o-g500); +} + +.oss-tbl-sub { + padding: 8px 16px; + font-size: 11px; + font-weight: 600; + color: var(--o-g700); + background: var(--o-g100); + border-bottom: 1px solid var(--o-g200); +} + +.ta-c { + text-align: center; +} + +.ta-r { + text-align: right; +} + +.mono { + font-family: 'DM Mono', monospace; + font-size: 12px; +} + +/* NOTES */ +.oss-notes { + background: var(--o-g100); + border: 1px solid var(--o-g200); + border-radius: 8px; + padding: 10px 16px; +} + +.oss-note-line { + font-size: 12px; + font-weight: 600; + color: var(--o-g700); + padding: 2px 0; +} + +.oss-foot-note { + margin: -6px 2px 0; + font-size: 11px; + color: var(--o-g500); +} + +/* Tie-up status pills (cross-return reconciliation checks). */ +.report-pill { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + + &.report-ok { + background: var(--o-green-l); + color: var(--o-green); + } + + &.report-bad { + background: var(--o-red-l); + color: var(--o-red); + } +} + +.oss-sign-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +/* TOAST */ +.oss-toast { + position: fixed; + bottom: 32px; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: var(--o-navy); + color: #fff; + padding: 12px 22px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + box-shadow: 0 6px 20px rgb(0 0 0 / 25%); + opacity: 0; + pointer-events: none; + transition: all 0.25s; + z-index: 1500; + + &.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@media (width <= 1000px) { + .oss-sign-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/app/rbi-circulars/oss-workbook.ts b/src/app/rbi-circulars/oss-workbook.ts new file mode 100644 index 000000000..f355ac2c7 --- /dev/null +++ b/src/app/rbi-circulars/oss-workbook.ts @@ -0,0 +1,228 @@ +/** + * 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 * as ExcelJS from 'exceljs'; +import { + MatrixTable, + ReturnGeneralInfoRow, + ReturnLine, + ReturnSignatory, + SIGNATORY_FIELDS +} from './oss-return.model'; + +/** Palette lifted from the RBI CIMS workbooks so exports look like the originals. */ +export const XL = { + banner: 'FF002F5F', + bannerText: 'FFFFFFFF', + headerGrey: 'FFC0C0C0', + derivedBlue: 'FF9CC3F5', + auditYellow: 'FFFFFFCC', + keyCell: 'FFD9D9D9', + border: 'FF000000' +}; + +const AMOUNT_FMT = '#,##0.00'; +const COUNT_FMT = '#,##0'; + +export function solidFill(argb: string): ExcelJS.Fill { + return { type: 'pattern', pattern: 'solid', fgColor: { argb } }; +} + +/** Dark-blue full-width banner that heads every sheet in the RBI templates. */ +export function addBanner(sheet: ExcelJS.Worksheet, title: string, lastCol: string): void { + const row = sheet.addRow([title]); + sheet.mergeCells(`A${row.number}:${lastCol}${row.number}`); + row.height = 26; + const cell = row.getCell(1); + cell.fill = solidFill(XL.banner); + cell.font = { bold: true, size: 13, color: { argb: XL.bannerText } }; + cell.alignment = { vertical: 'middle', horizontal: 'center' }; + sheet.addRow([]); +} + +export function boxAllCells( + sheet: ExcelJS.Worksheet, + from: number, + to: number, + lastColIndex: number +): void { + for (let r = from; r <= to; r++) { + for (let c = 1; c <= lastColIndex; c++) { + sheet.getRow(r).getCell(c).border = { + top: { style: 'thin', color: { argb: XL.border } }, + left: { style: 'thin', color: { argb: XL.border } }, + bottom: { style: 'thin', color: { argb: XL.border } }, + right: { style: 'thin', color: { argb: XL.border } } + }; + } + } +} + +export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLabel?: string): void { + const row = sheet.addRow([label, unitLabel ?? '']); + row.font = { bold: true }; + row.eachCell((cell) => (cell.fill = solidFill(XL.headerGrey))); + row.getCell(2).alignment = { horizontal: 'right' }; +} + +/** Renders a labelled block; indents by level and shades derived totals. */ +export function addLineRows(sheet: ExcelJS.Worksheet, lines: ReturnLine[]): void { + for (const line of lines) { + const row = sheet.addRow([line.label, line.value]); + const labelCell = row.getCell(1); + const valueCell = row.getCell(2); + + labelCell.alignment = { wrapText: true, vertical: 'middle', indent: line.level }; + if (line.level === 0) { + labelCell.font = { bold: true }; + } + valueCell.numFmt = line.count ? COUNT_FMT : AMOUNT_FMT; + valueCell.alignment = { horizontal: 'right' }; + if (line.total) { + valueCell.fill = solidFill(XL.derivedBlue); + valueCell.font = { bold: true }; + } + } +} + +/** + * Writes a multi-column statement: title band, column header row, then the matrix + * rows. Returns the row number after the block so callers can keep stacking. + */ +export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): void { + const colCount = table.columns.length + 1; + const start = sheet.rowCount + 1; + + const titleRow = sheet.addRow([table.title, ...(table.unit ? [table.unit] : [])]); + if (!table.unit) { + sheet.mergeCells(titleRow.number, 1, titleRow.number, colCount); + } + titleRow.font = { bold: true }; + for (let c = 1; c <= colCount; c++) { + titleRow.getCell(c).fill = solidFill(XL.headerGrey); + } + + if (table.subtitle) { + const sub = sheet.addRow([table.subtitle]); + sheet.mergeCells(sub.number, 1, sub.number, colCount); + sub.font = { bold: true, size: 10 }; + sub.alignment = { wrapText: true }; + sub.getCell(1).fill = solidFill(XL.headerGrey); + } + + const head = sheet.addRow(['Particulars', ...table.columns]); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const row of table.rows) { + const excelRow = sheet.addRow([row.label, ...row.values]); + const labelCell = excelRow.getCell(1); + labelCell.alignment = { wrapText: true, vertical: 'middle', indent: row.level }; + if (row.level === 0 || row.header) { + labelCell.font = { bold: true }; + } + if (row.header) { + labelCell.fill = solidFill(XL.headerGrey); + continue; + } + for (let c = 2; c <= colCount; c++) { + const cell = excelRow.getCell(c); + cell.numFmt = row.percent ? '0.00"%"' : AMOUNT_FMT; + cell.alignment = { horizontal: 'right' }; + if (row.total) { + cell.fill = solidFill(XL.derivedBlue); + cell.font = { bold: true }; + } + } + } + boxAllCells(sheet, start, sheet.rowCount, colCount); + + for (const note of table.notes ?? []) { + const noteRow = sheet.addRow([note]); + sheet.mergeCells(noteRow.number, 1, noteRow.number, colCount); + noteRow.font = { bold: true, size: 9 }; + noteRow.alignment = { wrapText: true }; + noteRow.getCell(1).fill = solidFill(XL.headerGrey); + } + sheet.addRow([]); +} + +/** The General Information sheet is identical in shape across every OSS return. */ +export function buildGeneralInfoSheet(wb: ExcelJS.Workbook, rows: ReturnGeneralInfoRow[]): void { + const sheet = wb.addWorksheet('General Information'); + sheet.columns = [ + { width: 26 }, + { width: 58 } + ]; + addBanner(sheet, 'General Information', 'B'); + + const first = sheet.rowCount + 1; + for (const item of rows) { + const row = sheet.addRow([item.label, item.value]); + row.getCell(1).fill = solidFill(XL.keyCell); + // The workbook tints the audit flag yellow and leaves unfilled cells grey. + const fill = item.label === 'Audit Flag' ? XL.auditYellow : item.value ? XL.derivedBlue : XL.headerGrey; + row.getCell(2).fill = solidFill(fill); + } + boxAllCells(sheet, first, sheet.rowCount, 2); +} + +/** The Signatory sheet is likewise identical across returns. */ +export function buildSignatorySheet( + wb: ExcelJS.Workbook, + signatories: ReturnSignatory[], + sheetName = 'Signatory' +): void { + const sheet = wb.addWorksheet(sheetName); + sheet.columns = [ + { width: 44 }, + { width: 24 } + ]; + addBanner(sheet, sheetName, 'B'); + + for (const person of signatories) { + const start = sheet.rowCount + 1; + const head = sheet.addRow([person.heading, '']); + head.font = { bold: true }; + head.alignment = { horizontal: 'center' }; + head.getCell(1).fill = solidFill(XL.keyCell); + + for (const f of SIGNATORY_FIELDS) { + const row = sheet.addRow([person[f.key], f.caption]); + row.getCell(2).font = { bold: true }; + row.getCell(2).alignment = { horizontal: 'center' }; + row.getCell(2).fill = solidFill(XL.keyCell); + } + boxAllCells(sheet, start, sheet.rowCount, 2); + sheet.addRow([]); + } +} + +/** Serialises the workbook and triggers a browser download. */ +export async function downloadWorkbook(wb: ExcelJS.Workbook, filename: string): Promise { + const buffer = await wb.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +export function newWorkbook(): ExcelJS.Workbook { + const wb = new ExcelJS.Workbook(); + wb.creator = 'CBSNexora'; + wb.created = new Date(); + return wb; +} diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts b/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts new file mode 100644 index 000000000..fb9243c85 --- /dev/null +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts @@ -0,0 +1,431 @@ +/** + * 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/. + */ + +/** + * OSS 1 — Return on Assets and Liabilities (RAL), return code R070. + * + * Mirrors the RBI CIMS "Form RAL_CIMS Template1 OSS 1" workbook sheet-for-sheet: + * General Information / Assets & Liabilities / Annexure 1 / Annexure 2 / Signatory + * + * All monetary values are Rs in Thousands, exactly as the return prescribes. + * A `null` value means the source return leaves that cell blank — blank and zero + * are different to RBI, so they must not be collapsed into one another. + */ + +import { OSS_BANK, OSS_SIGNATORIES, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory } from '../oss-return.model'; + +/** One labelled row of the return. */ +export type RalLine = ReturnLine; + +export interface RalTopDeposit { + srNo: number; + institution: string; + amount: number | null; + rateOfInterest: number | null; + dateOfReceipt: string; + days: number | null; + months: number | null; + years: number | null; +} + +export interface RalAgeingRow { + period: string; + noOfDebitEntries: number | null; + amtOfDebitEntries: number | null; + noOfCreditEntries: number | null; + amtOfCreditEntries: number | null; +} + +export interface RalUnsecuredGuarantee { + srNo: number; + borrower: string; + beneficiary: string; + amountOutstanding: number | null; +} + +export interface RalSundryDebtor { + srNo: number; + dueFromName: string; + amountDue: number | null; + provisionMade: number | null; +} + +export type RalSignatory = ReturnSignatory; + +/** + * Sheet 1 — General Information. + * Return Name / Return Code are fixed by RBI; everything else is this bank's own + * identity and the quarter being reported. + */ +export const RAL_GENERAL_INFO: ReturnGeneralInfoRow[] = [ + { label: 'Return Name', value: 'Return on Assets and Liabilities (RAL)' }, + { label: 'Return Code', value: 'R070' }, + { label: 'Bank Name', value: OSS_BANK.name }, + { label: 'Bank Code', value: OSS_BANK.bankCode }, + { label: 'RO Name', value: OSS_BANK.roName }, + { label: 'Reporting Frequency', value: OSS_BANK.reportingFrequency }, + { label: 'Quarter End Date', value: OSS_BANK.quarterEndDate }, + { label: 'Audit Flag', value: OSS_BANK.auditFlag }, + { label: 'Date of Audit', value: OSS_BANK.dateOfAudit }, + { label: 'Return Version', value: OSS_BANK.returnVersion }, + { label: 'Start Date', value: OSS_BANK.startDate } +]; + +/** Sheet 2 — Assets & Liabilities :: Liabilities */ +export const RAL_LIABILITIES: RalLine[] = [ + { label: '1. Paid-up Capital', level: 0, value: 27278, total: true }, + { label: 'i. Individuals', level: 1, value: 27278 }, + { label: 'ii. State Government', level: 1, value: null }, + { label: 'iii. Others', level: 1, value: null }, + + { label: '2. Reserve Fund and other Reserves', level: 0, value: 60863, total: true }, + { label: 'i. Statutory Reserve Fund', level: 1, value: 31149 }, + { label: 'ii. Building Fund', level: 1, value: 13642 }, + { label: 'iii. Dividend Equalization Fund', level: 1, value: 36 }, + { label: 'iv. Revaluation Reserve', level: 1, value: null }, + { label: 'v. Provision for Standard Assets', level: 1, value: 3750 }, + { label: 'vi. General Provisions (other than provision for standard advances)', level: 1, value: 282 }, + { + label: 'vii. Other Funds and Reserves not in the nature of outside liability-(Refer Annexure I)', + level: 1, + value: 12004 + }, + + { label: '3. Profit (+) / Loss (-)', level: 0, value: 10397, total: true }, + { label: 'i. Surplus Unallocated and Carried over from Previous years', level: 1, value: 5592 }, + { label: 'ii. Accumulated Losses of Previous years', level: 1, value: null }, + { label: 'iii. Operating Surplus in Current year', level: 1, value: 4805 }, + { label: 'iv. Operating Deficit in Current year', level: 1, value: null }, + + { label: '4. Total Capital And Reserves', level: 0, value: 98538, total: true }, + { label: '5. Subordinated Debt (Tier II Capital) #', level: 0, value: null }, + + { label: '6. Deposits and other Accounts', level: 0, value: 634309, total: true }, + { label: 'Customer Deposits -', level: 1, value: 634309, total: true }, + { label: 'Total No of Depositors', level: 1, value: 8156, total: true, count: true }, + { label: 'A. Resident Deposits', level: 1, value: 634309, total: true }, + { + label: + 'i. Term Deposits (including recurring deposits, cash certificates, staff provident funds,security deposits and matured term deposits)', + level: 2, + value: 406601 + }, + { label: 'No. of Term Depositors', level: 2, value: 3462, count: true }, + { label: 'ii. Savings Bank Deposits', level: 2, value: 208116 }, + { label: 'No of Savings Depositors', level: 2, value: 4455, count: true }, + { label: 'iii. Current Deposits', level: 2, value: 19592 }, + { label: 'No of Current Depositors', level: 2, value: 239, count: true }, + { label: 'B. Non Resident Deposits', level: 1, value: 0, total: true }, + { label: '1. NRI Deposits (Other than deposits in NRO Accounts)', level: 2, value: 0, total: true }, + { label: 'i. FCNR Deposits', level: 2, value: null }, + { label: 'No of FCNR Depositors', level: 2, value: null, count: true }, + { label: 'ii. NRE Deposits', level: 2, value: null }, + { label: 'No of NRE Depositors', level: 2, value: null, count: true }, + { label: '2. NRO Deposits', level: 2, value: null, total: true }, + { label: 'No of NRO Depositors', level: 2, value: null, count: true }, + { label: 'C. Deposit of Banks', level: 1, value: 0, total: true }, + { label: 'i. Commercial Banks', level: 2, value: null }, + { label: 'ii. Scheduled Cooperative Banks', level: 2, value: null }, + { label: 'iii. Other Cooperative Banks', level: 2, value: null }, + + { label: '7. Borrowings from', level: 0, value: 0, total: true }, + { label: 'i. RBI / NABARD / SIDBI / NHB', level: 1, value: null }, + { label: 'ii. State/Central Coop. Bank of the State/ District', level: 1, value: 0, total: true }, + { label: 'a. Short-term loans', level: 2, value: null }, + { label: 'b. Medium-term loans', level: 2, value: null }, + { label: 'c. Cash credits and overdrafts', level: 2, value: null }, + { label: 'iii. Other banks (exclude borrowings from call money market)', level: 1, value: null }, + { label: 'iv. Call money market', level: 1, value: null }, + { label: 'v. Long Term Deposits', level: 1, value: null }, + { label: 'vi. Others', level: 1, value: null }, + + { label: '8. Bills for collection being bills receivable as per contra', level: 0, value: null }, + { label: '9. Branch Adjustments *', level: 0, value: null }, + { label: '10. Overdue Interest Reserve', level: 0, value: 32096 }, + + { label: '11. Interest Payable on', level: 0, value: 731, total: true }, + { label: 'i. Deposits', level: 1, value: 731 }, + { label: 'ii. Borrowings', level: 1, value: null }, + + { + label: + '12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)', + level: 0, + value: 39440, + total: true + }, + { label: 'i. For investments (Investment Depreciation Reserve)', level: 1, value: 3750 }, + { label: 'ii. BDDR, Spl BDDR', level: 1, value: 24341 }, + { label: 'iii. Depreciation on fixed assets', level: 1, value: null }, + { label: 'iv. For other impaired assets', level: 1, value: 11349 }, + { label: 'v. For contingent / non-funded exposures', level: 1, value: null }, + + { label: '13. Other Liabilities', level: 0, value: 5806, total: true }, + { label: 'i. Bills payable', level: 1, value: 291 }, + { label: 'ii. Unclaimed dividends', level: 1, value: null }, + { label: 'iii.Provident fund payable to Regional Provident Fund Commissioner', level: 1, value: null }, + { label: 'iv.Others', level: 1, value: 5515 }, + + { label: '14. Total Outside Liabilities', level: 0, value: 712382, total: true }, + { label: '15. Total Liabilities', level: 0, value: 810920, total: true }, + + { label: '16. Contingent Liabilities', level: 0, value: 2638, total: true }, + { label: 'i. Claims against the bank not acknowledged as debt', level: 1, value: null }, + { label: 'ii. Liabilities for partly paid investments', level: 1, value: null }, + { label: 'iii. Letter of Credit Documentary', level: 1, value: null }, + { label: 'iv. Guarantees - Financial', level: 1, value: null }, + { label: 'v. Guarantees- Others (Indemnity/ Tender/ Bid Bonds)', level: 1, value: null }, + { label: 'vi. Sale and Repurchase Agreement /Asset sales with recourse', level: 1, value: null }, + { label: 'vii. Liability on account of outstanding forward exchange contracts', level: 1, value: null }, + { label: 'viii. Acceptances, endorsements and other obligations', level: 1, value: null }, + { label: 'ix. Others', level: 1, value: 2638 } +]; + +/** Sheet 2 — Assets & Liabilities :: Assets */ +export const RAL_ASSETS: RalLine[] = [ + { label: '1. Cash in Hand', level: 0, value: 9996 }, + + { label: '2. I. Due from Banks', level: 0, value: 57600, total: true }, + { label: 'A. Balances with Banks', level: 1, value: 57600, total: true }, + { label: 'i. State/ Central Cooperative Bank of the State/District concerned in', level: 2, value: 32268, total: true }, + { label: 'a. Current', level: 2, value: 5980 }, + { label: 'b. Savings', level: 2, value: null }, + { label: 'c. Fixed', level: 2, value: 26288 }, + { label: 'ii. RBI :', level: 2, value: 0, total: true }, + { label: 'a. Current', level: 2, value: null }, + { label: 'b. Others', level: 2, value: null }, + { label: 'iii. SBI and other notified banks :', level: 2, value: 25332, total: true }, + { label: 'a. Current', level: 2, value: 13667 }, + { label: 'b. Savings', level: 2, value: null }, + { label: 'c. Fixed', level: 2, value: 11665 }, + { label: 'B. Loans and Advances to Banks (including bills)', level: 1, value: 0, total: true }, + { label: 'i. Call and Short Notice Money to Cooperative Banks', level: 2, value: null }, + { label: 'ii. Call and Short Notice Money to Other Banks', level: 2, value: null }, + { label: 'iii. Loans and Advances to Scheduled Cooperative Banks', level: 2, value: null }, + { label: 'iv.Loans and advances to Non scheduled Cooperative Banks', level: 2, value: null }, + { label: 'v. Loans and Advances to Other Banks', level: 2, value: null }, + + { + label: '2. II. Due from Other institutions (including post office / treasuries / others)', + level: 0, + value: 77353, + total: true + }, + { label: 'a. Current', level: 1, value: 9954 }, + { label: 'b. Fixed', level: 1, value: 67399 }, + { label: 'c. Others', level: 1, value: null }, + + { label: '3. Investments', level: 0, value: 234297, total: true }, + { + label: 'A. SLR Investments in Government and Other Approved Central and State Govt Securities', + level: 1, + value: 211734, + total: true + }, + { label: 'i. GOI T. Bills', level: 2, value: null }, + { label: 'ii. GOI (dated) Securities', level: 2, value: 63839 }, + { label: 'iii. State Government Securities', level: 2, value: 147895 }, + { + label: 'iv. Other Trustee/approved securities including Land Development Bank debentures', + level: 2, + value: null + }, + { label: 'B. Non - SLR Investments', level: 1, value: 22563, total: true }, + { label: 'B1. Other Debt Securities', level: 2, value: 0, total: true }, + { label: 'i. Other State Government Securities', level: 2, value: null }, + { label: 'ii. Bonds of PSUs / All India Financial Institutions', level: 2, value: null }, + { label: 'B2. Equities / Units of Mutual Funds', level: 2, value: 63, total: true }, + { label: 'i. Shares in Cooperative Institutions', level: 2, value: 63 }, + { label: 'ii. Shares of All India Financial Institutions', level: 2, value: null }, + { label: 'iii. Units of Mutual Funds (UTI,etc)', level: 2, value: null }, + { label: 'B3. Investment in Subsidiaries / Equity Participations', level: 2, value: null }, + { label: 'B4. Other Investments', level: 2, value: 22500 }, + + { label: '4. Loans and Advances', level: 0, value: 391288, total: true }, + { label: 'A. Gross Loans and Advances: (Other than banks)', level: 1, value: 391288, total: true }, + { label: 'Total No of Borrowers', level: 1, value: 363, total: true, count: true }, + { label: 'i. Short term loans', level: 2, value: 125082, total: true }, + { label: 'No of Borrowers', level: 2, value: 137, count: true }, + { label: 'a. Secured', level: 2, value: 125082 }, + { label: 'b. Unsecured', level: 2, value: null }, + { label: 'ii. Cash credits and overdrafts', level: 2, value: 166614, total: true }, + { label: 'No of Borrowers', level: 2, value: 140, count: true }, + { label: 'a. Secured', level: 2, value: 166614 }, + { label: 'b. Unsecured', level: 2, value: null }, + { label: 'iii. Medium - term loans', level: 2, value: 70236, total: true }, + { label: 'No of Borrowers', level: 2, value: 66, count: true }, + { label: 'a. Secured', level: 2, value: 70070 }, + { label: 'b. Unsecured', level: 2, value: 166 }, + { label: 'iv. Long - term loans', level: 2, value: 29356, total: true }, + { label: 'No of Borrowers', level: 2, value: 20, count: true }, + { label: 'a. Secured', level: 2, value: 29356 }, + { label: 'b. Unsecured', level: 2, value: null }, + { label: 'v. Bills/hundies purchased/discounted', level: 2, value: 0, total: true }, + { label: 'No of Borrowers', level: 2, value: null, count: true }, + { label: 'a. Documentary', level: 2, value: null }, + { label: 'b. Clean', level: 2, value: null }, + { label: 'B. Netting items on Loans and Advances', level: 1, value: 0, total: true }, + { label: 'i. Unrealised Interest in Suspense', level: 2, value: null }, + { label: 'ii. Credit Recoveries in Suspense', level: 2, value: null }, + { label: 'iii. Provisions for Credit Losses ( funded credits)', level: 2, value: null }, + { label: 'C. Loans and Advances (Net)', level: 1, value: 391288, total: true }, + + { label: '5. Interest Receivable on', level: 0, value: 33541, total: true }, + { label: 'i. Investments', level: 1, value: 1445 }, + { label: 'ii.Loans and Advances', level: 1, value: 32096 }, + { label: 'Of 5 (ii) above, overdues', level: 1, value: 32096 }, + + { label: '6. Bills Receivable being bills for collection as per contra', level: 0, value: null }, + { label: '7. Premises (less depreciation)', level: 0, value: 3185 }, + { label: '8. Furniture and Fixtures (less depreciation)', level: 0, value: 1763 }, + { label: '9. Branch Adjustments **', level: 0, value: null }, + + { label: '10. Other Assets', level: 0, value: 1897, total: true }, + { label: 'i. Advance Tax Paid and TDS (net)', level: 1, value: null }, + { label: 'ii. Amounts due from RBI', level: 1, value: null }, + { label: 'iii. Sundry Debtors', level: 1, value: null }, + { label: 'iv. Intangible Assets', level: 1, value: null }, + { label: 'v. All Other', level: 1, value: 1897 }, + + { label: '11. Non-banking Assets acquired in satisfaction of claims', level: 0, value: null }, + { label: '12. Total Assets (1 to 11)', level: 0, value: 810920, total: true } +]; + +/** Sheet 2 — Memo block */ +export const RAL_MEMO: RalLine[] = [ + { label: '1.Capital Infusion during Apr to Date', level: 0, value: 0, total: true }, + { label: '1.1 Tier 1 Capital Raised during April to Date', level: 1, value: null }, + { label: '1.2 Tier 2 Capital Raised during April to Date', level: 1, value: null }, + { label: '2. Capital Instruments due for redemption in next twelve months', level: 0, value: null } +]; + +export const RAL_BALANCE_SHEET_NOTES: string[] = [ + 'Note : * To be furnished as per the books of the bank.', + 'Note : ** Branch adjustments: Net position to be shown as an item of liability / assets, as the case may be.', + 'Note: # Capital raised by way of RCPS / RNCPS / PNCPS to be reported under this item' +]; + +/** Sheet 3 — Annexure 1 :: 1. Break-up of Other Funds and Reserves */ +export const RAL_OTHER_FUNDS: RalLine[] = [ + { label: 'i. Gratuity Fund', level: 1, value: null }, + { label: 'ii. Staff Welfare Fund', level: 1, value: 1310 }, + { label: 'iii. Silver Jubilee Fund', level: 1, value: null }, + { label: 'iv. Development Fund', level: 1, value: 36 }, + { label: 'v. Charity Fund', level: 1, value: 1718 }, + { label: 'vi. Any Other Fund/ Disclosed reserves', level: 1, value: 8940 }, + { + label: + 'vii. Undisclosed Reserves created out of PAT and included in B/S under heads like other liabilities and provisions', + level: 1, + value: null + }, + { label: 'Total', level: 0, value: 12004, total: true } +]; + +/** Sheet 3 — Annexure 1 :: 2. Top 20 deposits from other banks / institutions */ +export const RAL_TOP_20_DEPOSITS: RalTopDeposit[] = Array.from( + { length: 20 }, + (_, i): RalTopDeposit => ({ + srNo: i + 1, + institution: '', + amount: null, + rateOfInterest: null, + dateOfReceipt: '', + days: null, + months: null, + years: null + }) +); + +/** Column captions shared by the two age-wise analyses in Annexure 1. */ +export const RAL_AGEING_HEADERS: string[] = [ + 'Time period', + 'No. of debit entries', + 'Amt. of debit entries', + 'No. of credit entries', + 'Amt. of credit entries' +]; + +/** Sheet 3 — Annexure 1 :: 3. Age-wise Inter Branch Adjustment Account */ +export const RAL_INTER_BRANCH_AGEING: RalAgeingRow[] = [ + { period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 } +]; + +/** Sheet 3 — Annexure 1 :: 4. Age-wise Inter Bank Reconciliation Account */ +export const RAL_INTER_BANK_AGEING: RalAgeingRow[] = [ + { period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 } +]; + +/** Sheet 3 — Annexure 1 :: 5. Unsecured guarantees issued by the bank */ +export const RAL_UNSECURED_GUARANTEES: RalUnsecuredGuarantee[] = [ + { srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null } +]; +export const RAL_UNSECURED_GUARANTEES_TOTAL = 0; + +/** Sheet 4 — Annexure 2 :: Sundry Debtors ageing buckets */ +export const RAL_SUNDRY_DEBTORS: { title: string; subtitle: string; rows: RalSundryDebtor[] }[] = [ + { + title: 'Sundry Debtors Account -1', + subtitle: 'Long Outstanding Large Amounts - Outstanding for over 6 months but less than 1 year*', + rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }] + }, + { + title: 'Sundry Debtors Account -2', + subtitle: 'Long Outstanding Large Amounts - Outstanding for over 1 year but less than 3 years*', + rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }] + }, + { + title: 'Sundry Debtors Account -3', + subtitle: 'Long Outstanding Large Amounts - Outstanding for over 3 years*', + rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }] + } +]; + +export const RAL_SUNDRY_DEBTORS_NOTE = + '* Large Amounts: Rs. 2 lakhs for banks having total assets of Rs. 100 lakhs and over Rs. 50,000 for other banks'; + +/** Sheet 4 — Annexure 2 :: Details of Depositors & Borrowers */ +export const RAL_DEPOSITORS_BORROWERS: RalLine[] = [ + { label: '1.Total No of Depositors', level: 0, value: 8156, total: true, count: true }, + { label: 'Total Customer Deposits', level: 0, value: 634309, total: true }, + { label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh', level: 1, value: 6568, count: true }, + { label: 'ii) Of which total Deposit Amount', level: 1, value: 109992 }, + { label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 739, count: true }, + { label: 'ii) Of which total Deposit Amount', level: 1, value: 102475 }, + { label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 580, count: true }, + { label: 'ii) Of which total Deposit Amount', level: 1, value: 176820 }, + + { label: '2.Total No of Borrowers', level: 0, value: 363, total: true, count: true }, + { label: 'Total Loans and Advances (Other than Banks)', level: 0, value: 391288, total: true }, + { label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh', level: 1, value: 55, count: true }, + { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2061 }, + { label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 18, count: true }, + { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2837 }, + { label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 55, count: true }, + { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 21260 } +]; + +/** Sheet 4 — Annexure 2 :: Complaints Received from public */ +export const RAL_COMPLAINTS: RalLine[] = [ + { label: 'a) No. Of complaints outstanding at the beginning of the quarter', level: 1, value: 0, count: true }, + { label: 'b) No. Of complaints received during the quarter', level: 1, value: 0, count: true }, + { label: 'c) No. of complaints addressed during the quarter', level: 1, value: 0, count: true }, + { label: 'd) No. of complaints outstanding at the end of the quarter', level: 1, value: 0, count: true }, + { label: 'e) No. of complaints outstanding for over 2 quarters', level: 1, value: 0, count: true } +]; + +/** Sheet 5 — Signatory. Shared across returns so OSS 1 and OSS 2 always agree. */ +export const RAL_SIGNATORIES: RalSignatory[] = OSS_SIGNATORIES; diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html new file mode 100644 index 000000000..c59c196f1 --- /dev/null +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html @@ -0,0 +1,422 @@ + + +
+ +
+
+ +
+

OSS 1 — {{ returnName }}

+
+ {{ bankName }} · Bank Code {{ bankCode }} · Return Code + {{ returnCode }} · Quarter ending {{ quarterEndDate }} +
+
+
+
+ {{ auditFlag }} + +
+
+ + +
+ + + Total Liabilities ₹{{ totalLiabilities | number: '1.2-2' }} · Total Assets + ₹{{ totalAssets | number: '1.2-2' }} (Rs in Thousands) — + {{ isBalanced ? 'balance sheet tallies' : 'MISMATCH — resolve before filing with RBI' }} + +
+ + +
+ + + + + +
+ + + @if (activeTab === 'general') { +
+
General Information
+
+ + + @for (item of generalInfo; track item.label) { + + + + + } + +
{{ item.label }}{{ item.value }}
+
+
+ } + + + @if (activeTab === 'balance-sheet') { +
+
Assets & Liabilities
+ +
+ + + + + + + + + @for (line of liabilities; track $index) { + + + + + } + +
LiabilitiesRs in Thousands
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} +
+
+ +
+ + + + + + + + + @for (line of assets; track $index) { + + + + + } + +
AssetsRs in Thousands
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} +
+
+ +
+ @for (note of balanceSheetNotes; track note) { +
{{ note }}
+ } +
+ +
+ + + + + + + + + @for (line of memo; track $index) { + + + + + } + +
MemoRs in Thousands
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: '1.2-2') }} +
+
+
+ } + + + @if (activeTab === 'annexure1') { +
+
Annexure 1
+ +
+
1. Break-up of Other Funds and Reserves — Rs in Thousands
+ + + @for (line of otherFunds; track line.label) { + + + + + } + +
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: '1.2-2') }} +
+
+ +
+
+ 2. Particulars of top 20 deposits accepted from other banks / and institutions as on the date of review +
+ + + + + + + + + + + + + + + @for (d of topDeposits; track d.srNo) { + + + + + + + + + + + } + +
Sr. NoName of Bank / InstitutionAmount of depositRate of interest (%)Date of receiptDaysMonthsYears
{{ d.srNo }}{{ d.institution }}{{ d.amount === null ? '' : (d.amount | number: '1.2-2') }}{{ d.rateOfInterest }}{{ d.dateOfReceipt }}{{ d.days }}{{ d.months }}{{ d.years }}
+
+ +
+
+ 3. Age-wise analysis of Inter Branch Adjustment Account — Position as on: {{ quarterEndDate }} +
+ + + + @for (h of ageingHeaders; track h) { + + } + + + + @for (r of interBranchAgeing; track r.period) { + + + + + + + + } + +
{{ h }}
{{ r.period }}{{ r.noOfDebitEntries }} + {{ r.amtOfDebitEntries === null ? '' : (r.amtOfDebitEntries | number: '1.2-2') }} + {{ r.noOfCreditEntries }} + {{ r.amtOfCreditEntries === null ? '' : (r.amtOfCreditEntries | number: '1.2-2') }} +
+
+ +
+
+ 4. Age-wise analysis of Inter Bank Reconciliation Account — Position as on: {{ quarterEndDate }} +
+ + + + @for (h of ageingHeaders; track h) { + + } + + + + @for (r of interBankAgeing; track r.period) { + + + + + + + + } + +
{{ h }}
{{ r.period }}{{ r.noOfDebitEntries }} + {{ r.amtOfDebitEntries === null ? '' : (r.amtOfDebitEntries | number: '1.2-2') }} + {{ r.noOfCreditEntries }} + {{ r.amtOfCreditEntries === null ? '' : (r.amtOfCreditEntries | number: '1.2-2') }} +
+
+ +
+
5. Unsecured guarantees issued by the bank
+ + + + + + + + + + + @for (g of unsecuredGuarantees; track g.srNo) { + + + + + + + } + + + + + +
Sr. NoName of BorrowerName of BeneficiaryAmount Outstanding as on: {{ quarterEndDate }}
{{ g.srNo }}{{ g.borrower }}{{ g.beneficiary }} + {{ g.amountOutstanding === null ? '' : (g.amountOutstanding | number: '1.2-2') }} +
Total{{ unsecuredGuaranteesTotal | number: '1.2-2' }}
+
+

+ Note: * Give name if borrower/beneficiary is director/top management/related entity. Other guarantees may be + consolidated and reported against others. +

+
+ } + + + @if (activeTab === 'annexure2') { +
+
Annexure 2
+ + @for (block of sundryDebtors; track block.title) { +
+
{{ block.title }} — {{ block.subtitle }}
+ + + + + + + + + + + @for (r of block.rows; track r.srNo) { + + + + + + + } + +
Sr. NoDue from NameAmount dueProvision made
{{ r.srNo }}{{ r.dueFromName }}{{ r.amountDue === null ? '' : (r.amountDue | number: '1.2-2') }}{{ r.provisionMade === null ? '' : (r.provisionMade | number: '1.2-2') }}
+
+ } +

{{ sundryDebtorsNote }}

+ +
+
Details of Depositors & Borrowers — Rs in Thousands
+ + + @for (line of depositorsBorrowers; track $index) { + + + + + } + +
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} +
+
+ +
+
Complaints Received from public
+ + + @for (line of complaints; track line.label) { + + + + + } + +
{{ line.label }}{{ line.value }}
+
+
+ } + + + @if (activeTab === 'signatory') { +
+
Signatory
+
+ @for (p of signatories; track p.heading) { +
+
{{ p.heading }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ p.signature }}Signature
{{ p.name }}Name
{{ p.designation }}Designation
{{ p.email }}e-mail ID
{{ p.telephoneOffice }}Tele. No. (O)
{{ p.telephoneResidence }}Tele. No. (R)
{{ p.place }}Place
{{ p.date }}Date
+
+ } +
+
+ } + + +
{{ toastMsg }}
+
diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts new file mode 100644 index 000000000..5329297c4 --- /dev/null +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts @@ -0,0 +1,423 @@ +/** + * 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 { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import * as ExcelJS from 'exceljs'; +import { + addBanner, + addLineRows, + addSectionHeader, + boxAllCells, + buildGeneralInfoSheet, + buildSignatorySheet, + downloadWorkbook, + newWorkbook, + solidFill, + XL +} from '../oss-workbook'; +import { + RAL_AGEING_HEADERS, + RAL_ASSETS, + RAL_BALANCE_SHEET_NOTES, + RAL_COMPLAINTS, + RAL_DEPOSITORS_BORROWERS, + RAL_GENERAL_INFO, + RAL_INTER_BANK_AGEING, + RAL_INTER_BRANCH_AGEING, + RAL_LIABILITIES, + RAL_MEMO, + RAL_OTHER_FUNDS, + RAL_SIGNATORIES, + RAL_SUNDRY_DEBTORS, + RAL_SUNDRY_DEBTORS_NOTE, + RAL_TOP_20_DEPOSITS, + RAL_UNSECURED_GUARANTEES, + RAL_UNSECURED_GUARANTEES_TOTAL +} from './oss1-ral-data'; + +type Oss1Tab = 'general' | 'balance-sheet' | 'annexure1' | 'annexure2' | 'signatory'; + +@Component({ + selector: 'mifosx-oss1-ral', + templateUrl: './oss1-ral.component.html', + styleUrls: ['../oss-return.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + FormsModule + ] +}) +export class Oss1RalComponent { + activeTab: Oss1Tab = 'general'; + + generalInfo = RAL_GENERAL_INFO; + liabilities = RAL_LIABILITIES; + assets = RAL_ASSETS; + memo = RAL_MEMO; + balanceSheetNotes = RAL_BALANCE_SHEET_NOTES; + otherFunds = RAL_OTHER_FUNDS; + topDeposits = RAL_TOP_20_DEPOSITS; + interBranchAgeing = RAL_INTER_BRANCH_AGEING; + interBankAgeing = RAL_INTER_BANK_AGEING; + ageingHeaders = RAL_AGEING_HEADERS; + unsecuredGuarantees = RAL_UNSECURED_GUARANTEES; + unsecuredGuaranteesTotal = RAL_UNSECURED_GUARANTEES_TOTAL; + sundryDebtors = RAL_SUNDRY_DEBTORS; + sundryDebtorsNote = RAL_SUNDRY_DEBTORS_NOTE; + depositorsBorrowers = RAL_DEPOSITORS_BORROWERS; + complaints = RAL_COMPLAINTS; + signatories = RAL_SIGNATORIES; + + generating = false; + toastMsg = ''; + toastVisible = false; + private toastTimer: any; + + showTab(tab: Oss1Tab): void { + this.activeTab = tab; + } + + private info(label: string): string { + return this.generalInfo.find((g) => g.label === label)?.value ?? ''; + } + + get returnName(): string { + return this.info('Return Name'); + } + get returnCode(): string { + return this.info('Return Code'); + } + get bankName(): string { + return this.info('Bank Name'); + } + get bankCode(): string { + return this.info('Bank Code'); + } + get quarterEndDate(): string { + return this.info('Quarter End Date'); + } + get auditFlag(): string { + return this.info('Audit Flag'); + } + + /** Total Liabilities must equal Total Assets — surfaced so a mismatch can't be filed unnoticed. */ + get totalLiabilities(): number { + return this.liabilities.find((l) => l.label.startsWith('15.'))?.value ?? 0; + } + get totalAssets(): number { + return this.assets.find((a) => a.label.startsWith('12.'))?.value ?? 0; + } + get isBalanced(): boolean { + return this.totalLiabilities === this.totalAssets; + } + + // ══════════════ Excel generation ══════════════ + + /** Rebuilds the RBI workbook: one sheet per section, original layout and styling. */ + async generateExcel(): Promise { + this.generating = true; + try { + const wb = newWorkbook(); + + buildGeneralInfoSheet(wb, this.generalInfo); + this.buildBalanceSheet(wb); + this.buildAnnexure1(wb); + this.buildAnnexure2(wb); + buildSignatorySheet(wb, this.signatories); + + await downloadWorkbook(wb, `Form RAL_CIMS ${this.returnCode} OSS 1 - ${this.quarterEndDate}.xlsx`); + this.toast(`${this.returnCode} workbook generated for quarter ending ${this.quarterEndDate}`); + } catch { + this.toast('Could not generate the workbook — please retry'); + } finally { + this.generating = false; + } + } + + private buildBalanceSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Assets & Liabilities'); + sheet.columns = [ + { width: 78 }, + { width: 20 } + ]; + addBanner(sheet, 'Assets & Liabilities', 'B'); + + const first = sheet.rowCount + 1; + addSectionHeader(sheet, 'Liabilities', 'Rs in Thousands'); + addLineRows(sheet, this.liabilities); + + addSectionHeader(sheet, 'Assets', 'Rs in Thousands'); + addLineRows(sheet, this.assets); + boxAllCells(sheet, first, sheet.rowCount, 2); + + sheet.addRow([]); + const notesStart = sheet.rowCount + 1; + for (const note of this.balanceSheetNotes) { + const row = sheet.addRow([note]); + sheet.mergeCells(`A${row.number}:B${row.number}`); + row.font = { bold: true, size: 10 }; + row.getCell(1).fill = solidFill(XL.headerGrey); + } + boxAllCells(sheet, notesStart, sheet.rowCount, 2); + + sheet.addRow([]); + const memoStart = sheet.rowCount + 1; + addSectionHeader(sheet, 'Memo', 'Rs in Thousands'); + addLineRows(sheet, this.memo); + boxAllCells(sheet, memoStart, sheet.rowCount, 2); + + sheet.views = [{ state: 'frozen', ySplit: 3 }]; + } + + private buildAnnexure1(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Annexure 1'); + sheet.columns = [ + { width: 10 }, + { width: 46 }, + { width: 20 }, + { width: 18 }, + { width: 16 }, + { width: 12 }, + { width: 12 }, + { width: 12 } + ]; + addBanner(sheet, 'Annexure 1', 'H'); + + // 1. Break-up of Other Funds and Reserves + let start = sheet.rowCount + 1; + const fundsTitle = sheet.addRow(['1. Break-up of Other Funds and Reserves', '', 'Rs in Thousands']); + sheet.mergeCells(`A${fundsTitle.number}:B${fundsTitle.number}`); + fundsTitle.font = { bold: true }; + fundsTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + for (const line of this.otherFunds) { + const row = sheet.addRow([line.label, '', line.value]); + sheet.mergeCells(`A${row.number}:B${row.number}`); + row.getCell(1).alignment = { wrapText: true, vertical: 'middle' }; + row.getCell(3).numFmt = '#,##0.00'; + if (line.total) { + row.font = { bold: true }; + row.getCell(3).fill = solidFill(XL.derivedBlue); + row.getCell(1).alignment = { horizontal: 'center' }; + } + } + boxAllCells(sheet, start, sheet.rowCount, 3); + sheet.addRow([]); + + // 2. Top 20 deposits from other banks / institutions + start = sheet.rowCount + 1; + const depTitle = sheet.addRow([ + `2. Particulars of top 20 deposits accepted from other banks / and institutions as on the date of review` + ]); + sheet.mergeCells(`A${depTitle.number}:H${depTitle.number}`); + depTitle.font = { bold: true }; + depTitle.getCell(1).fill = solidFill(XL.headerGrey); + + const depHead = sheet.addRow([ + 'Sr. No', + 'Name of Bank / Institution', + 'Amount of deposit', + 'Rate of interest (%)', + 'Date of receipt', + 'Period in Days', + 'Period in Months', + 'Period in Years' + ]); + depHead.font = { bold: true }; + depHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + depHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + for (const d of this.topDeposits) { + const row = sheet.addRow([ + d.srNo, + d.institution, + d.amount, + d.rateOfInterest, + d.dateOfReceipt, + d.days, + d.months, + d.years + ]); + row.getCell(3).numFmt = '#,##0.00'; + } + boxAllCells(sheet, start, sheet.rowCount, 8); + sheet.addRow([]); + + // 3 & 4 — ageing analyses + start = sheet.rowCount + 1; + this.addAgeingBlock( + sheet, + `3. Age-wise analysis of Inter Branch Adjustment Account - Position as on:${this.quarterEndDate}`, + this.interBranchAgeing + ); + sheet.addRow([]); + this.addAgeingBlock( + sheet, + `4. Age-wise analysis of Inter Bank Reconciliation Account - Position as on:${this.quarterEndDate}`, + this.interBankAgeing + ); + sheet.addRow([]); + + // 5. Unsecured guarantees + start = sheet.rowCount + 1; + const gTitle = sheet.addRow(['5. Unsecured guarantees issued by the bank']); + sheet.mergeCells(`A${gTitle.number}:D${gTitle.number}`); + gTitle.font = { bold: true }; + gTitle.getCell(1).fill = solidFill(XL.headerGrey); + + const gHead = sheet.addRow([ + 'Sr. No', + 'Name of Borrower', + 'Name of Beneficiary', + `Amount Outstanding as on:${this.quarterEndDate}` + ]); + gHead.font = { bold: true }; + gHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + gHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + for (const g of this.unsecuredGuarantees) { + const row = sheet.addRow([g.srNo, g.borrower, g.beneficiary, g.amountOutstanding]); + row.getCell(4).numFmt = '#,##0.00'; + } + const gTotal = sheet.addRow(['Total', '', '', this.unsecuredGuaranteesTotal]); + gTotal.font = { bold: true }; + gTotal.getCell(4).numFmt = '#,##0.00'; + gTotal.getCell(4).fill = solidFill(XL.derivedBlue); + + const gNote = sheet.addRow([ + 'Note : * Give name if borrower/beneficiary is director/top management/related entity. Other guarantees may be consolidated and reported against others.' + ]); + sheet.mergeCells(`A${gNote.number}:D${gNote.number}`); + gNote.font = { bold: true, size: 10 }; + gNote.alignment = { wrapText: true }; + boxAllCells(sheet, start, sheet.rowCount, 4); + } + + private addAgeingBlock(sheet: ExcelJS.Worksheet, title: string, rows: typeof RAL_INTER_BRANCH_AGEING): void { + const start = sheet.rowCount + 1; + const titleRow = sheet.addRow([title]); + sheet.mergeCells(`A${titleRow.number}:E${titleRow.number}`); + titleRow.font = { bold: true }; + titleRow.getCell(1).fill = solidFill(XL.headerGrey); + + const head = sheet.addRow([...this.ageingHeaders]); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const r of rows) { + const row = sheet.addRow([ + r.period, + r.noOfDebitEntries, + r.amtOfDebitEntries, + r.noOfCreditEntries, + r.amtOfCreditEntries + ]); + row.getCell(3).numFmt = '#,##0.00'; + row.getCell(5).numFmt = '#,##0.00'; + if (r.period === 'Total') { + row.font = { bold: true }; + [ + 2, + 3, + 4, + 5 + ].forEach( + (c) => (row.getCell(c).fill = solidFill(XL.derivedBlue)) + ); + } + } + boxAllCells(sheet, start, sheet.rowCount, 5); + } + + private buildAnnexure2(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Annexure 2'); + sheet.columns = [ + { width: 10 }, + { width: 62 }, + { width: 20 }, + { width: 20 } + ]; + addBanner(sheet, 'Annexure 2', 'D'); + + for (const block of this.sundryDebtors) { + const start = sheet.rowCount + 1; + const t = sheet.addRow([block.title]); + sheet.mergeCells(`A${t.number}:D${t.number}`); + t.font = { bold: true }; + t.getCell(1).fill = solidFill(XL.headerGrey); + + const s = sheet.addRow([block.subtitle]); + sheet.mergeCells(`A${s.number}:D${s.number}`); + s.font = { bold: true, size: 10 }; + s.getCell(1).fill = solidFill(XL.headerGrey); + + const head = sheet.addRow(['Sr. No', 'Due from Name', 'Amount due', 'Provision made']); + head.font = { bold: true }; + head.alignment = { horizontal: 'center' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const r of block.rows) { + const row = sheet.addRow([r.srNo, r.dueFromName, r.amountDue, r.provisionMade]); + row.getCell(3).numFmt = '#,##0.00'; + row.getCell(4).numFmt = '#,##0.00'; + } + + const note = sheet.addRow([this.sundryDebtorsNote]); + sheet.mergeCells(`A${note.number}:D${note.number}`); + note.font = { bold: true, size: 10 }; + note.alignment = { wrapText: true }; + boxAllCells(sheet, start, sheet.rowCount, 4); + sheet.addRow([]); + } + + // Details of Depositors & Borrowers + let start = sheet.rowCount + 1; + const dbTitle = sheet.addRow(['Details of Depositors & Borrowers', '', 'Rs. In Thousands']); + sheet.mergeCells(`A${dbTitle.number}:B${dbTitle.number}`); + dbTitle.font = { bold: true }; + dbTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + dbTitle.getCell(3).alignment = { horizontal: 'right' }; + + for (const line of this.depositorsBorrowers) { + const row = sheet.addRow([line.label, '', line.value]); + sheet.mergeCells(`A${row.number}:B${row.number}`); + row.getCell(1).alignment = { wrapText: true, vertical: 'middle', indent: line.level }; + row.getCell(3).numFmt = line.count ? '#,##0' : '#,##0.00'; + if (line.total) { + row.font = { bold: true }; + row.getCell(3).fill = solidFill(XL.derivedBlue); + } + } + boxAllCells(sheet, start, sheet.rowCount, 3); + sheet.addRow([]); + + // Complaints + start = sheet.rowCount + 1; + const cTitle = sheet.addRow(['Complaints Received from public']); + sheet.mergeCells(`A${cTitle.number}:B${cTitle.number}`); + cTitle.font = { bold: true }; + cTitle.getCell(1).fill = solidFill(XL.headerGrey); + for (const line of this.complaints) { + const row = sheet.addRow([line.label, '', line.value]); + sheet.mergeCells(`A${row.number}:B${row.number}`); + row.getCell(1).alignment = { wrapText: true, vertical: 'middle' }; + row.getCell(3).numFmt = '#,##0'; + } + boxAllCells(sheet, start, sheet.rowCount, 3); + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/rbi-circulars/oss2-roe/oss2-roe-data.ts b/src/app/rbi-circulars/oss2-roe/oss2-roe-data.ts new file mode 100644 index 000000000..54eed9ddd --- /dev/null +++ b/src/app/rbi-circulars/oss2-roe/oss2-roe-data.ts @@ -0,0 +1,203 @@ +/** + * 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/. + */ + +/** + * OSS 2 — Return on Earnings (RoE), return code R071. + * + * Mirrors the RBI CIMS "Form OSS-2_CIMS Template1" workbook sheet-for-sheet: + * General Information / Earnings / Interest Receivable Not Recognized / + * Other Operating Expenses / Signatory Information + * + * All monetary values are Rs in Thousands. A `null` value means the source return + * leaves that cell blank — RBI treats blank and zero differently, so the two are + * never collapsed into one another. + */ + +import { OSS_BANK, OSS_SIGNATORIES, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory } from '../oss-return.model'; + +export type RoeLine = ReturnLine; +export type RoeSignatory = ReturnSignatory; + +/** A row on the Interest Receivable Not Recognized sheet. */ +export interface RoeInterestNotRecognizedRow { + label: string; + value: number | null; + total?: boolean; +} + +/** A line item on the Other Operating Expenses detail sheet. */ +export interface RoeOtherExpense { + srNo: number; + particulars: string; + amount: number | null; +} + +/** Sheet 1 — General Information */ +export const ROE_GENERAL_INFO: ReturnGeneralInfoRow[] = [ + { label: 'Return Name', value: 'Return on Earnings (RoE)' }, + { label: 'Return Code', value: 'R071' }, + { label: 'Name Of Bank', value: OSS_BANK.name }, + { label: 'Bank Code', value: OSS_BANK.bankCode }, + { label: 'RO Name', value: OSS_BANK.roName }, + { label: 'Quarter End Date', value: OSS_BANK.quarterEndDate }, + { label: 'Reporting Frequency', value: OSS_BANK.reportingFrequency }, + { label: 'Audit Flag', value: OSS_BANK.auditFlag }, + { label: 'Date of Audit', value: OSS_BANK.dateOfAudit }, + { label: 'Return Version', value: OSS_BANK.returnVersion }, + { label: 'Start Date', value: OSS_BANK.startDate } +]; + +/** Sheet 2 — Earnings (items 1 to 33) */ +export const ROE_EARNINGS: RoeLine[] = [ + { label: '1. Interest/discount received (2+3+4+5)', level: 0, value: 15069, total: true }, + { + label: '2. Interest/discount received on loans and advances (other than from banks)', + level: 0, + value: 12070, + total: true + }, + { label: 'i) Actually received', level: 1, value: 12070 }, + { label: 'ii) Accrued interest on Standard advances', level: 1, value: null }, + { label: '3. Interest on market lending ( to banks, if any)', level: 0, value: null }, + { label: '4. Interest on investments', level: 0, value: 2999 }, + { label: 'i) Interest received /accrued on Standard Investments', level: 1, value: null }, + { + label: '5. Interest on additional balance with RBI, inter-bank deposits, placements and credits', + level: 0, + value: null + }, + + { label: '6. Other Operating income', level: 0, value: 285, total: true }, + { label: 'i) Fee, Commission, exchange and brokerage', level: 1, value: 51 }, + { label: 'ii) Profit (+) / Loss (-) on forex operations', level: 1, value: null }, + { label: 'iii) Profit (+) / Loss (-) on trading and sale of securities', level: 1, value: null }, + { label: 'iv) Dividend Income', level: 1, value: null }, + { label: 'v) Miscellaneous Income', level: 1, value: 234 }, + + { label: '7. Total Operating Income [1+6]', level: 0, value: 15354, total: true }, + + { label: '8. Non Operating Income [Capital gains / losses (+)/ (-)]', level: 0, value: 0, total: true }, + { label: 'i) Profit / (Loss) on sale of fixed assets (+) / (-)', level: 1, value: null }, + { label: 'ii) Profit / (Loss) on sale of other assets (+) / (-)', level: 1, value: null }, + { label: 'iii) Other Non-operating Income / (Expenses) (+) / (-)', level: 1, value: null }, + + { label: '9. Total income[7+8]', level: 0, value: 15354, total: true }, + + { label: '10. Interest paid', level: 0, value: 5659, total: true }, + { label: 'i) On Customer deposits', level: 1, value: 5659 }, + { label: 'ii) On Inter-bank deposits', level: 1, value: null }, + { label: 'iii) On Inter-bank borrowings (including from RBI/FIs)', level: 1, value: null }, + { label: 'iv) Others', level: 1, value: null }, + + { label: '11. Operating Expenses', level: 0, value: 3352, total: true }, + { label: 'i) Staff expenses', level: 1, value: 3244 }, + { label: 'ii) Directors fees', level: 1, value: 45 }, + { label: "iii) Auditors' fees", level: 1, value: 63 }, + + { label: '12. Other Operating expenses', level: 0, value: 1038, total: true }, + { label: 'i) Rent, taxes and lighting', level: 1, value: null }, + { label: 'ii) Insurance', level: 1, value: 4 }, + { label: 'iii) Law charges', level: 1, value: 0 }, + { label: 'iv) Postage, telegrams and stamps', level: 1, value: 34 }, + { label: 'v) Stationery and printing', level: 1, value: 23 }, + { label: 'vi) Advertisement and publicity', level: 1, value: null }, + { label: "vii) Depreciation on bank's property", level: 1, value: null }, + { label: 'viii) Repairs and maintenance', level: 1, value: null }, + { label: 'ix) Others', level: 1, value: 977, total: true }, + + { label: '13. Total operating expenses [10 + 11+ 12]', level: 0, value: 10049, total: true }, + { label: '14. Operating Profit/Loss [7 - 13]', level: 0, value: 5305, total: true }, + + { + label: + '15. Value adjustment of Securities (marking to market of securities in AFS & HFT) [Only loss is to be reported]', + level: 0, + value: null + }, + + { label: '16. Write off', level: 0, value: 0, total: true }, + { label: 'i) Bad Debts written off', level: 1, value: null }, + { label: 'ii) Other assets written off', level: 1, value: null }, + { label: 'iii) Capitalised expenditure written off', level: 1, value: null }, + + { label: '17. Provision against risks/ contingencies', level: 0, value: 0, total: true }, + { label: 'i) Provision for loan losses (BDDR, Spl. BDDR, etc.) *', level: 1, value: null }, + { label: 'ii) Provision for depreciation in investments including AFS #', level: 1, value: null }, + { + label: + 'iii) Other risk provisions towards losses on account of frauds, misappropriations, robberies, claims against the bank, etc.', + level: 1, + value: null + }, + { label: 'iv) Other provisions', level: 1, value: null }, + + { label: '18. Net Operating Profit (+)/Loss (-) [14 - 15 - 16 - 17]', level: 0, value: 5305, total: true }, + { label: '19. Non Operating Income [Capital gains]', level: 0, value: 0, total: true }, + { label: '20. Non Operating Income [Capital losses]', level: 0, value: 0, total: true }, + { label: '21. Net Profit (+)/ Loss (-) before taxes - (EBPT) [18 + 19 - 20]', level: 0, value: 5305, total: true }, + { label: '22. Provisions for taxes (if any)', level: 0, value: 500 }, + { label: '23. Net Profit (+)/ Loss (-) after taxes [21 - 22]', level: 0, value: 4805, total: true }, + { label: '24. Balance of previous year’s profit (+) / Accumulated losses (-)', level: 0, value: 5592 }, + { label: '25. Net distributable profit (+) / Accumulated losses (-) [23 + 24]', level: 0, value: 10397, total: true }, + + { label: '26. Appropriation of profits', level: 0, value: 10397, total: true }, + { label: 'i) Appropriation towards Statutory Reserves', level: 1, value: 3638 }, + { label: 'ii) Appropriation towards Building Fund', level: 1, value: 519 }, + { label: 'iii) Appropriation towards Dividend Equalisation Fund', level: 1, value: null }, + { label: 'iv) Appropriation towards Other Reserves/ Provisions', level: 1, value: 4058 }, + { label: 'v) Dividend', level: 1, value: 2182 }, + + { label: '27. Net interest income', level: 0, value: 9410, total: true }, + { label: '28. Non-interest income', level: 0, value: 285, total: true }, + { label: '29. Non-interest expenditure', level: 0, value: 4390, total: true }, + { label: '30. Average Total Assets', level: 0, value: 796295 }, + { label: '31. Average Earning Assets', level: 0, value: 716407 }, + { label: '32. Working Fund', level: 0, value: 778829 }, + { label: '33. Number of Employee', level: 0, value: 11, count: true } +]; + +/** + * The two cross-return tie-ups printed at the foot of the Earnings sheet. Both + * reference line items on OSS 1, which is why the page checks them live. + */ +export const ROE_EARNINGS_NOTES: string[] = [ + 'Note: * The value of {17 (i) Provision for loan losses (BDDR, Spl. BDDR, etc.)} should tally with {12(ii) {BDDR, spl. BDDR} of liabilities in OSS-1.', + 'Note: # The value of {17 (ii) Provision for Depreciation in investments including AFS} should tally with {i. For investments (Investment Depreciation Reserve)} of liabilities in OSS-1 .' +]; + +/** Sheet 3 — Interest Receivable Not Recognized */ +export const ROE_INTEREST_NOT_RECOGNIZED: RoeInterestNotRecognizedRow[] = [ + { label: 'i. Interest receivable not recognized during the quarter', value: 2614 }, + { label: 'ii. Balance at the beginning of the quarter', value: 35101 }, + { + label: + 'iii. Interest received and reversed to income from (i) in the quarter on NPAs upgraded to Performing / accrual basis', + value: 5620 + }, + { label: 'Balance at the end of the quarter (i)+(ii)-(iii)', value: 32095, total: true } +]; + +/** Sheet 4 — Details of Other Operating Expenses (feeds Earnings line 12 ix) */ +export const ROE_OTHER_OPERATING_EXPENSES: RoeOtherExpense[] = [ + { srNo: 1, particulars: 'Miscelaneous expenses', amount: 55 }, + { srNo: 2, particulars: 'Entertainment Exps', amount: 24 }, + { srNo: 3, particulars: 'Commission On Mini Deposits', amount: 317 }, + { srNo: 4, particulars: 'AGM expenses', amount: 19 }, + { srNo: 5, particulars: 'Computer Exps', amount: 441 }, + { srNo: 6, particulars: 'Govt sec premium', amount: 0 }, + { srNo: 7, particulars: 'CIC Member Ship', amount: 0 }, + { srNo: 8, particulars: 'transaction expenses', amount: 7 }, + { srNo: 9, particulars: 'Digital Banking', amount: 11 }, + { srNo: 10, particulars: 'GST', amount: 26 }, + { srNo: 11, particulars: 'inventory write off', amount: 0 }, + { srNo: 12, particulars: 'Electricity Expenses', amount: 77 }, + { srNo: 13, particulars: 'Donation to Jila Sahakari', amount: 0 } +]; + +/** Sheet 5 — Signatory Information. Shared so OSS 1 and OSS 2 always agree. */ +export const ROE_SIGNATORIES: RoeSignatory[] = OSS_SIGNATORIES; diff --git a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html new file mode 100644 index 000000000..60328bdc9 --- /dev/null +++ b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html @@ -0,0 +1,278 @@ + + +
+ +
+
+ +
+

OSS 2 — {{ returnName }}

+
+ {{ bankName }} · Bank Code {{ bankCode }} · Return Code + {{ returnCode }} · Quarter ending {{ quarterEndDate }} +
+
+
+
+ {{ auditFlag }} + +
+
+ + +
+ + + Operating Profit ₹{{ operatingProfit | number: '1.2-2' }} · Net Profit after tax + ₹{{ netProfitAfterTax | number: '1.2-2' }} (Rs in Thousands) + +
+ + +
+ + + + + +
+ + + @if (activeTab === 'general') { +
+
General Information
+
+ + + @for (item of generalInfo; track item.label) { + + + + + } + +
{{ item.label }}{{ item.value }}
+
+
+ } + + + @if (activeTab === 'earnings') { +
+
Earnings
+ + +
+ + + {{ + allTieUpsMatch + ? 'Item 17 tallies with OSS-1 liabilities as the footnotes require.' + : 'Item 17 does not tally with OSS-1 liabilities — see the tie-up check below.' + }} + +
+ +
+
OSS-1 tie-up check
+ + + + + + + + + + + + @for (t of oss1TieUps; track t.label) { + + + + + + + + } + +
OSS-2 itemOSS-2 valueMust tally withOSS-1 valueStatus
{{ t.label }}{{ t.oss2Value === null ? '—' : (t.oss2Value | number: '1.2-2') }}{{ t.oss1Label }}{{ t.oss1Value === null ? '—' : (t.oss1Value | number: '1.2-2') }} + + {{ t.tallies ? 'Tallies' : 'Mismatch' }} + +
+
+ +
+ + + + + + + + + @for (line of earnings; track $index) { + + + + + } + +
ParticularsRs in Thousand
{{ line.label }} + {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} +
+
+ +
+ @for (note of earningsNotes; track note) { +
{{ note }}
+ } +
+
+ } + + + @if (activeTab === 'interest') { +
+
Interest Receivable Not Recognized
+
+ + + + + + + + + @for (r of interestNotRecognized; track r.label) { + + + + + } + +
Interest receivable on NPAs not recognized as income{{ quarterEndDate }} (Rs in Thousand)
{{ r.label }} + {{ r.value === null ? '' : (r.value | number: '1.2-2') }} +
+
+
+ } + + + @if (activeTab === 'other-expenses') { +
+
Other Operating Expenses
+ +
+ + + Detail total ₹{{ otherExpensesTotal | number: '1.2-2' }} vs Earnings line 12 ix) + Others + ₹{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }} + — {{ otherExpensesTally ? 'tallies' : 'MISMATCH — resolve before filing' }} + +
+ +
+
Details of Other Operating Expenses — Rs in Thousand
+ + + + + + + + + + @for (e of otherExpenses; track e.srNo) { + + + + + + } + + + + + + +
Sr NoParticularsAmount
{{ e.srNo }}{{ e.particulars }}{{ e.amount === null ? '' : (e.amount | number: '1.2-2') }}
Total{{ otherExpensesTotal | number: '1.2-2' }}
+
+

Feeds Earnings sheet line 12 ix) Others.

+
+ } + + + @if (activeTab === 'signatory') { +
+
Signatory Information
+
+ @for (p of signatories; track p.heading) { +
+
{{ p.heading }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ p.signature }}Signature
{{ p.name }}Name
{{ p.designation }}Designation
{{ p.email }}e-mail ID
{{ p.telephoneOffice }}Tel. No. (O)
{{ p.telephoneResidence }}Tel. No. (R)
{{ p.place }}Place
{{ p.date }}Date
+
+ } +
+
+ } + + +
{{ toastMsg }}
+
diff --git a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts new file mode 100644 index 000000000..9fb6d1483 --- /dev/null +++ b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts @@ -0,0 +1,283 @@ +/** + * 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 { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import * as ExcelJS from 'exceljs'; +import { + addBanner, + addLineRows, + addSectionHeader, + boxAllCells, + buildGeneralInfoSheet, + buildSignatorySheet, + downloadWorkbook, + newWorkbook, + solidFill, + XL +} from '../oss-workbook'; +import { RAL_LIABILITIES } from '../oss1-ral/oss1-ral-data'; +import { + ROE_EARNINGS, + ROE_EARNINGS_NOTES, + ROE_GENERAL_INFO, + ROE_INTEREST_NOT_RECOGNIZED, + ROE_OTHER_OPERATING_EXPENSES, + ROE_SIGNATORIES +} from './oss2-roe-data'; + +type Oss2Tab = 'general' | 'earnings' | 'interest' | 'other-expenses' | 'signatory'; + +/** One of the two OSS-1 tie-ups printed at the foot of the Earnings sheet. */ +interface TieUp { + label: string; + oss2Value: number | null; + oss1Label: string; + oss1Value: number | null; + tallies: boolean; +} + +@Component({ + selector: 'mifosx-oss2-roe', + templateUrl: './oss2-roe.component.html', + styleUrls: ['../oss-return.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + FormsModule + ] +}) +export class Oss2RoeComponent { + activeTab: Oss2Tab = 'general'; + + generalInfo = ROE_GENERAL_INFO; + earnings = ROE_EARNINGS; + earningsNotes = ROE_EARNINGS_NOTES; + interestNotRecognized = ROE_INTEREST_NOT_RECOGNIZED; + otherExpenses = ROE_OTHER_OPERATING_EXPENSES; + signatories = ROE_SIGNATORIES; + + generating = false; + toastMsg = ''; + toastVisible = false; + private toastTimer: any; + + showTab(tab: Oss2Tab): void { + this.activeTab = tab; + } + + private info(label: string): string { + return this.generalInfo.find((g) => g.label === label)?.value ?? ''; + } + + get returnName(): string { + return this.info('Return Name'); + } + get returnCode(): string { + return this.info('Return Code'); + } + get bankName(): string { + return this.info('Name Of Bank'); + } + get bankCode(): string { + return this.info('Bank Code'); + } + get quarterEndDate(): string { + return this.info('Quarter End Date'); + } + get auditFlag(): string { + return this.info('Audit Flag'); + } + + private earningsValue(prefix: string): number | null { + return this.earnings.find((l) => l.label.startsWith(prefix))?.value ?? null; + } + + private liabilityValue(prefix: string): number | null { + return RAL_LIABILITIES.find((l) => l.label.startsWith(prefix))?.value ?? null; + } + + get netProfitAfterTax(): number { + return this.earningsValue('23.') ?? 0; + } + get operatingProfit(): number { + return this.earningsValue('14.') ?? 0; + } + + /** Sum of the detail sheet — must equal Earnings line 12 ix) Others. */ + get otherExpensesTotal(): number { + return this.otherExpenses.reduce((sum, e) => sum + (e.amount ?? 0), 0); + } + get otherExpensesLineValue(): number | null { + return this.earningsValue('ix) Others'); + } + get otherExpensesTally(): boolean { + return this.otherExpensesTotal === (this.otherExpensesLineValue ?? 0); + } + + /** + * The Earnings sheet carries two footnotes requiring 17(i) and 17(ii) to tally + * with OSS-1 liabilities 12(ii) and 12(i). Checked live rather than left to a + * reader spotting it in the fine print. + */ + get oss1TieUps(): TieUp[] { + const rows: TieUp[] = [ + { + label: '17 (i) Provision for loan losses (BDDR, Spl. BDDR, etc.)', + oss2Value: this.earningsValue('i) Provision for loan losses'), + oss1Label: 'OSS-1 liabilities 12 (ii) BDDR, Spl BDDR', + oss1Value: this.liabilityValue('ii. BDDR, Spl BDDR'), + tallies: false + }, + { + label: '17 (ii) Provision for depreciation in investments including AFS', + oss2Value: this.earningsValue('ii) Provision for depreciation in investments'), + oss1Label: 'OSS-1 liabilities 12 (i) Investment Depreciation Reserve', + oss1Value: this.liabilityValue('i. For investments (Investment Depreciation Reserve)'), + tallies: false + } + ]; + return rows.map((r) => ({ ...r, tallies: (r.oss2Value ?? 0) === (r.oss1Value ?? 0) })); + } + + get allTieUpsMatch(): boolean { + return this.oss1TieUps.every((t) => t.tallies); + } + + // ══════════════ Excel generation ══════════════ + + /** Rebuilds the RBI workbook: one sheet per section, original layout and styling. */ + async generateExcel(): Promise { + this.generating = true; + try { + const wb = newWorkbook(); + + buildGeneralInfoSheet(wb, this.generalInfo); + this.buildEarningsSheet(wb); + this.buildInterestSheet(wb); + this.buildOtherExpensesSheet(wb); + buildSignatorySheet(wb, this.signatories, 'Signatory Information'); + + await downloadWorkbook(wb, `Form OSS-2_CIMS ${this.returnCode} - ${this.quarterEndDate}.xlsx`); + this.toast(`${this.returnCode} workbook generated for quarter ending ${this.quarterEndDate}`); + } catch { + this.toast('Could not generate the workbook — please retry'); + } finally { + this.generating = false; + } + } + + private buildEarningsSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Earnings'); + sheet.columns = [ + { width: 90 }, + { width: 20 } + ]; + addBanner(sheet, 'Earnings', 'B'); + + const first = sheet.rowCount + 1; + addSectionHeader(sheet, '', 'Rs in Thousand'); + addLineRows(sheet, this.earnings); + boxAllCells(sheet, first, sheet.rowCount, 2); + + sheet.addRow([]); + const notesStart = sheet.rowCount + 1; + for (const note of this.earningsNotes) { + const row = sheet.addRow([note]); + sheet.mergeCells(`A${row.number}:B${row.number}`); + row.font = { bold: true, italic: true, size: 10 }; + row.alignment = { wrapText: true }; + row.getCell(1).fill = solidFill(XL.headerGrey); + } + boxAllCells(sheet, notesStart, sheet.rowCount, 2); + + sheet.views = [{ state: 'frozen', ySplit: 3 }]; + } + + private buildInterestSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Interest Receivable Not Recog'); + sheet.columns = [ + { width: 72 }, + { width: 22 } + ]; + addBanner(sheet, 'Interest Receivable Not Recognized', 'B'); + + const start = sheet.rowCount + 1; + const unit = sheet.addRow(['', 'Rs in Thousand']); + unit.font = { bold: true }; + unit.alignment = { horizontal: 'center' }; + unit.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + const head = sheet.addRow(['Interest receivable on NPAs not recognized as income', this.quarterEndDate]); + head.font = { bold: true }; + head.getCell(2).alignment = { horizontal: 'center' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const r of this.interestNotRecognized) { + const row = sheet.addRow([r.label, r.value]); + row.getCell(1).alignment = { wrapText: true, vertical: 'middle' }; + row.getCell(1).fill = solidFill(XL.headerGrey); + row.getCell(2).numFmt = '#,##0.00'; + if (r.total) { + row.getCell(2).fill = solidFill(XL.derivedBlue); + row.getCell(2).font = { bold: true }; + } + } + boxAllCells(sheet, start, sheet.rowCount, 2); + } + + private buildOtherExpensesSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Other Operating Expenses'); + sheet.columns = [ + { width: 10 }, + { width: 56 }, + { width: 20 } + ]; + addBanner(sheet, 'Other Operating Expenses', 'C'); + + const start = sheet.rowCount + 1; + const title = sheet.addRow(['Details of Other Operating Expenses', '', 'Rs in Thousand']); + sheet.mergeCells(`A${title.number}:B${title.number}`); + title.font = { bold: true }; + title.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + title.getCell(3).alignment = { horizontal: 'center' }; + + const head = sheet.addRow(['Sr No', 'Particulars', 'Amount']); + head.font = { bold: true }; + head.alignment = { horizontal: 'center' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const e of this.otherExpenses) { + const row = sheet.addRow([e.srNo, e.particulars, e.amount]); + row.getCell(3).numFmt = '#,##0.00'; + } + + const total = sheet.addRow(['Total', '', this.otherExpensesTotal]); + sheet.mergeCells(`A${total.number}:B${total.number}`); + total.font = { bold: true }; + total.getCell(1).fill = solidFill(XL.headerGrey); + total.getCell(3).numFmt = '#,##0.00'; + total.getCell(3).fill = solidFill(XL.derivedBlue); + + boxAllCells(sheet, start, sheet.rowCount, 3); + + const ref = sheet.addRow(['', '', 'Refer Earnings Sheet — line 12 ix) Others']); + ref.getCell(3).font = { italic: true, size: 10 }; + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts b/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts new file mode 100644 index 000000000..907e9fd74 --- /dev/null +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts @@ -0,0 +1,1124 @@ +/** + * 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/. + */ + +/** + * OSS 3 — Return on Asset Quality (RAQ-UCB), return code R323. + * + * Mirrors the RBI iFILE "R32312 OSS 3" workbook statement-for-statement: + * Filing Information / PortfolioAnalysis(loans_advncs) / Classifcation_Loans_Adv / + * Movement_of_loans / StatementOtherRiskAssets / TopImpairedCredits / + * ExposureLimits / QualitySecurityPortoflio / OtherNonSLRSecurity / + * SectoralCredit / IndustryBreakup / SensitiveSector / Signatory + * + * All monetary values are Rs in Thousands. A `null` means the source return leaves + * the cell blank — RBI treats blank and zero differently, so the two are never merged. + */ + +import { + MatrixTable, + OSS_BANK, + OSS_SIGNATORIES, + ReturnGeneralInfoRow, + ReturnSignatory +} from '../oss-return.model'; + +/** A row of Table 5 on the Top Impaired Credits statement. */ +export interface Oss3ImpairedCredit { + pan: string; + name: string; + loansAdvances: number | null; + funded: number | null; + interestInArrear: number | null; + pctTotalAdvances: number | null; + pctCapitalFunds: number | null; + assetClassification: string; + lossProvisionHeld: number | null; +} + +/** A row of Table 2 on the Exposure Limits statement. */ +export interface Oss3LargeExposure { + pan: string; + name: string; + limitSanctioned: number | null; + pctTotalAdvances: number | null; + outstandingBalance: number | null; + pctTier1Capital: number | null; + assetClassification: string; +} + +/** Sheet 1 — Filing Information */ +export const RAQ_FILING_INFO: ReturnGeneralInfoRow[] = [ + { label: 'Return Name', value: 'Return on Asset Quality (RAQ-UCB)' }, + { label: 'Return Code', value: 'R323' }, + { label: 'Name of reporting institution', value: OSS_BANK.name }, + { label: 'Bank Code', value: OSS_BANK.bankCode }, + { label: 'RO Name', value: OSS_BANK.roName }, + { label: 'Institution Type', value: OSS_BANK.institutionType }, + { label: 'Reporting frequency', value: OSS_BANK.reportingFrequency }, + { label: 'Reporting start date', value: OSS_BANK.startDateNumeric }, + { label: 'Reporting end date', value: OSS_BANK.quarterEndDateNumeric }, + { label: 'Date of Report', value: '' }, + { label: 'Reporting currency', value: 'INR' }, + { label: 'Reporting scale', value: 'Thousands' }, + { label: 'Taxonomy version', value: '1.4.0' }, + { label: 'Tool name', value: 'RBI iFILE RAQ-DCBS' }, + { label: 'Tool version', value: '1.0.0' }, + { label: 'Report status', value: OSS_BANK.auditFlag }, + { label: 'Validation Status :', value: '' }, + { label: 'Date of Audit', value: OSS_BANK.dateOfAudit }, + { label: 'General remarks', value: '' } +]; + +/** Statement 1 — Portfolio Analysis (By delinquency in Interest Payment) */ +export const RAQ_PORTFOLIO_ANALYSIS: MatrixTable = { + title: 'Table 1: Portfolio Analysis (By delinquency in Interest Payment)', + unit: 'Rs in Thousands', + columns: [ + 'Loans and Advances', + 'Overdrafts & Cash Credits', + 'Bills Purchased and Discounted', + 'Gross Loan and Advances (2+3+4)', + 'Commercial Paper', + 'Notes & bonds of corporates #', + 'Inter- Bank Assets', + 'Lease receivables +', + 'SLR Investments', + 'Others', + 'Total OIBA (6+7+8+9+10+11)' + ], + rows: [ + { + label: 'A. PERFORMING [1+2]', + level: 0, + total: true, + values: [ + 247398, + 137190, + 0, + 384588, + 0, + 0, + 109359, + 0, + 211734, + 16734, + 337827 + ] + }, + { + label: '1) Current', + level: 1, + values: [ + 247398, + 137190, + null, + 384588, + null, + null, + 109359, + null, + 211734, + 16734, + 337827 + ] + }, + { + label: '2) Overdue [2.a + 2.b]', + level: 1, + total: true, + values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + label: '2.a) Loans and Advances where 90 day norms is applicable as per IRAC', + level: 2, + total: true, + values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + label: '2.a.1 Overdue less than 30 days (SMA -0)', + level: 3, + values: [ + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: '2.a.2 Overdue for 31 to 60 days (SMA -1)', + level: 3, + values: [ + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: '2.a.3 Overdue for 61 to 90 days (SMA -2)', + level: 3, + values: [ + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: '2.b) Loans and Advances where 90 day norms is NOT applicable as per IRAC', + level: 2, + values: [ + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: 'B. NON-PERFORMING @ [3+4+5]', + level: 0, + total: true, + values: [ + 2558, + 24058, + 0, + 26616, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + label: '3) Substandard', + level: 1, + values: [ + 594, + 9647, + null, + 10241, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: '4) Doubtful', + level: 1, + values: [ + 1964, + 14411, + null, + 16375, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: '5) Loss', + level: 1, + values: [ + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0 + ] + }, + { + label: 'TOTAL (A + B)', + level: 0, + total: true, + values: [ + 249956, + 161248, + 0, + 411204, + 0, + 0, + 109359, + 0, + 211734, + 16734, + 337827 + ] + } + ], + notes: [ + '(i.e. interest suspended / not recognized as income)', + '@ Non Performing Advances as defined in RBI (UBD) Circular issued from time to time and in force at time of review.', + '# - including those of PSUs (owned by central and state governments)', + '+ - if not included in Portfolio Analysis for loans and Advances' + ] +}; + +/** Memorandum on the Portfolio Analysis statement. */ +export const RAQ_PORTFOLIO_MEMO: MatrixTable = { + title: 'Memorandum Item', + columns: [ + 'Current quarter', + 'April to Date' + ], + rows: [ + { + label: 'Interest in arrears on Non Performing loans and advances', + level: 0, + values: [ + 281, + 33452 + ] + } + ] +}; + +/** Statement 2 — Classification of Risk Assets, Part A */ +export const RAQ_CLASSIFICATION_PART_A: MatrixTable = { + title: 'Part - A CLASSIFICATION OF LOANS AND ADVANCES (including Bill Credit)', + unit: 'Rs in Thousands', + columns: [ + 'Gross Loans and Advances Outstanding', + 'Provisions/Deductions', + 'Net Loans and Advances Outstanding' + ], + rows: [ + { label: '1. Standard#', level: 0, values: [384588, 3750, 380838] }, + { label: '1.1. Of which Restructured Standard', level: 1, values: [null, null, 0] }, + { label: '2. Sub-standard', level: 0, values: [10241, 1024, 9217] }, + { label: '2.1. Of which restructured Sub-standard', level: 1, values: [null, null, 0] }, + { label: '3. Doubtful', level: 0, total: true, values: [16375, 35225, -18850] }, + { label: '3.1. Doubtful upto 1 year', level: 1, values: [911, 182, 729] }, + { label: '3.2. Doubtful more than 1 year and upto 3 years', level: 1, values: [5174, 1552, 3622] }, + { label: '3.3. Doubtful more than 3 years', level: 1, values: [10290, 33491, -23201] }, + { label: '4. Loss', level: 0, values: [null, null, 0] }, + { label: 'TOTAL', level: 0, total: true, values: [411204, 39999, 371205] } + ] +}; + +/** Statement 2 — Part B: Netting Items for NPAs 1 */ +export const RAQ_NETTING_ITEMS_1: MatrixTable = { + title: 'Part-B: Netting Items for NPAs 1', + columns: ['Amount'], + rows: [ + { + label: + 'i. Balance in interest suspense account/OIR in respect of unrealized interest capitalized and included in loans and advances shown as outstanding', + level: 0, + values: [null] + }, + { label: 'ii. DICGC/ECGC claims received and pending adjustment', level: 0, values: [null] }, + { label: 'iii. Part payment of NPA amounts received and pending adjustment', level: 0, values: [null] }, + { label: 'iv. Total NPA provisions held (under BDDR, special BDDR,etc.)', level: 0, values: [36249] }, + { label: 'v. Other Netting Items', level: 0, values: [null] }, + { label: 'Total Netting Items for NPAs', level: 0, total: true, values: [36249] } + ] +}; + +/** Statement 2 — Part B: Netting Items for NPAs 2 (derived ratios) */ +export const RAQ_NETTING_ITEMS_2: MatrixTable = { + title: 'Part-B: Netting Items for NPAs 2', + columns: ['Values'], + rows: [ + { label: 'Gross Non Performing Advances', level: 0, total: true, values: [26616] }, + { label: 'Gross Non Performing Advances Ratio(%)', level: 0, total: true, percent: true, values: [6.47] }, + { label: 'Net Non Performing Advances', level: 0, total: true, values: [-9633] }, + { label: 'Net Non Performing Advances Ratio(%)', level: 0, total: true, percent: true, values: [-2.6] } + ] +}; + +/** Statement 2 — Memorandum */ +export const RAQ_CLASSIFICATION_MEMO: MatrixTable = { + title: 'Memorandum Item', + columns: ['Amount'], + rows: [ + { + label: + '# Include amount of loans & advances to Government or carrying Government guarantee which are non-performing', + level: 0, + values: [null] + }, + { + label: 'Amount of interest not recognized on the above govt and govt. guaranteed loans', + level: 0, + total: true, + values: [0] + }, + { label: 'a) Upto 1 year', level: 1, values: [null] }, + { label: 'b) More than 1 year and upto 3 years', level: 1, values: [null] }, + { label: 'c) More than 3 years', level: 1, values: [null] } + ] +}; + +/** Statement 3 — Part 1: Movement in Loans and Advances during the quarter */ +export const RAQ_MOVEMENT_PART_1: MatrixTable = { + title: 'Part-1: Movement in Loans and Advances during the quarter', + unit: 'Rs in Thousands', + columns: [ + 'Standard', + 'Sub-standard', + 'Doubtful', + 'Loss', + 'Total' + ], + rows: [ + { label: 'Migration of Advances', level: 0, header: true, values: [] }, + { label: 'From Standard', level: 1, values: [null, null, null, null, 0] }, + { label: 'From Sub-standard', level: 1, values: [null, null, null, null, 0] }, + { label: 'From Doubtful', level: 1, values: [null, null, null, null, 0] }, + { label: 'From Loss', level: 1, values: [null, null, null, null, 0] }, + { label: 'Total Upgrade/Downgrade', level: 1, total: true, values: [0, 0, 0, 0, 0] }, + { label: 'Movement in Loans and Advances', level: 0, header: true, values: [] }, + { + label: + 'A. Loans and advances at the beginning of the quarter (Should be equal to the figure at end of previous quarter)', + level: 1, + values: [388243, 6214, 16924, null, 411381] + }, + { label: 'B. Loans and Advances Disbursed during the quarter', level: 1, values: [23072, 4027, null, null, 27099] }, + { label: 'C. Actual Repayments / Recoveries during the quarter', level: 1, values: [26727, null, 549, null, 27276] }, + { + label: 'D. Write-offs / Technical Write offs / Compromise etc. during the quarter', + level: 1, + values: [null, null, null, null, 0] + }, + { label: 'E. Others (+ / -)', level: 1, values: [null, null, null, null, 0] }, + { + label: 'F. Outstanding Loans and Advances at end of period (A+B-C-D+E)', + level: 1, + total: true, + values: [384588, 10241, 16375, 0, 411204] + }, + { label: 'G. Provisions Required on Current Balance (Estimate)', level: 1, values: [970, 1024, 12024, null, 14018] }, + { label: 'H. Provisions Held', level: 1, values: [3750, 1024, 35225, null, 39999] }, + { + label: 'I. Excess/Deficit in Provisions Held (G-H)', + level: 1, + total: true, + values: [-2780, 0, -23201, 0, -25981] + } + ] +}; + +/** Statement 3 — Part 2: Change in NPAs Profile during the Period */ +export const RAQ_MOVEMENT_PART_2: MatrixTable = { + title: 'Part-2: Change in NPAs Profile during the Period (Current Quarter)', + unit: 'Rs in Thousands', + columns: [ + 'Sub-Standard', + 'Doubtful', + 'Loss', + 'Total' + ], + rows: [ + { label: 'A. NPAs at the beginning of the quarter', level: 0, values: [6214, 16924, null, 23138] }, + { label: 'B. New Addition to NPAs during the current quarter', level: 0, values: [4027, null, null, 4027] }, + { label: 'C. Reduction in NPAs during the current quarter', level: 0, total: true, values: [0, 549, 0, 549] }, + { label: 'C.1 Due to Upgradation', level: 1, values: [null, null, null, 0] }, + { label: 'C.2 Due to Actual Recoveries', level: 1, values: [null, 549, null, 549] }, + { label: 'C.3 Due to Write-offs', level: 1, values: [null, null, null, 0] }, + { + label: + 'C.4 Due to Regulatory Instructions, Amalgamation, Compliance to RBI Inspection Action Points, any other reason etc.', + level: 1, + values: [null, null, null, 0] + }, + { + label: 'D. Increase due to change in Asset Classification (excl. up-gradation) during the quarter', + level: 0, + values: [null, null, null, 0] + }, + { + label: 'E. Decrease due to change in Asset Classification (excl. up-gradation) during the quarter', + level: 0, + values: [null, null, null, 0] + }, + { label: 'F. NPAs at the end of the Quarter', level: 0, total: true, values: [10241, 16375, 0, 26616] } + ], + notes: [ + '1. Reporting of flow figures for New Accretion to NPAs, up-gradation, recoveries and write-offs should be on gross basis (i.e., even though the slippage and the related upgradation/recoveries is within the same reporting period). In other words, the data on up-gradation, recoveries and write-offs from new NPAs during the year should also be reported in respective categories.', + '2. Actual Recoveries should Include recoveries through compromise, sale of NPAs, etc.', + "3. 'A. NPAs at the beginning of the quarter' should tally with 'F. NPAs at the end of the Period' for the previous quarter." + ] +}; + +/** Statement 3 — Part 3: Movement in Provisions for NPAs */ +export const RAQ_MOVEMENT_PART_3: MatrixTable = { + title: 'Part 3: Movement in Provisions for NPAs in Loans/Advances (Current Quarter)', + unit: 'Rs in Thousands', + columns: ['Amount'], + rows: [ + { label: 'A. Amount of Provisions for NPAs at beginning of the FY', level: 0, values: [36249] }, + { label: 'B. New additions during the Period (April to Date)', level: 0, values: [null] }, + { label: 'C. Reductions during the Period (April to Date)', level: 0, total: true, values: [0] }, + { label: 'C1. On account of Write Offs (Prov. Utilised for write-off)', level: 1, values: [null] }, + { label: 'C2. Others', level: 1, values: [null] }, + { label: 'D. Amount of Provisions for NPAs at end of the FY/Period', level: 0, total: true, values: [36249] } + ], + notes: [ + "Note: 'A. Amount of Provisions for NPAs at beginning of the FY' should tally with 'D. Amount of Provisions for NPAs at end of the FY/Period for the previous financial year." + ] +}; + +/** Statement 4 — Other Risk Assets and Exposures, on balance sheet */ +export const RAQ_OTHER_RISK_ON_BS: MatrixTable = { + title: 'ON BALANCE SHEET', + unit: 'Rs in Thousands', + columns: [ + 'Standard', + 'Sub-Standard', + 'Doubtful', + 'Loss', + 'Total' + ], + rows: [ + { label: '1. Inter-bank assets', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [109359, null, null, null, 109359] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: '2. SLR Securities', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [211734, null, null, null, 211734] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: '3. Receivables', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Sub-total (Amount) (1+2+3)', level: 0, total: true, values: [321093, 0, 0, 0, 321093] }, + { label: 'Sub-total (Loss Provision) (1+2+3)', level: 0, total: true, values: [0, 0, 0, 0, 0] } + ] +}; + +/** Statement 4 — Other Risk Assets and Exposures, off balance sheet (invoked) */ +export const RAQ_OTHER_RISK_OFF_BS: MatrixTable = { + title: 'OFF BALANCE SHEET (Invoked)', + unit: 'Rs in Thousands', + columns: [ + 'Standard', + 'Sub-Standard', + 'Doubtful', + 'Loss', + 'Total' + ], + rows: [ + { label: '4. Guarantee outstanding', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: '5. Letters of credit outstanding', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: '6. Other contingent credit exposures', level: 0, header: true, values: [] }, + { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, + { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Sub-total (Amount) (4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, + { label: 'Sub-total (Loss provision) (4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, + { label: 'Grand-total (Amount) (1+2+3+4+5+6)', level: 0, total: true, values: [321093, 0, 0, 0, 321093] }, + { label: 'Grand-total (Loss provision) (1+2+3+4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] } + ] +}; + +/** Statement 5 — Table 1: Capital as reported in the "Return on Capital Position" */ +export const RAQ_CAPITAL_MEMO: MatrixTable = { + title: 'Table 1: Memo: Capital as reported in the "Return on Capital Position"', + unit: 'Rs in Thousands', + columns: ['Amount'], + rows: [ + { label: 'Tier 1 Capital', level: 0, values: [91944] }, + { label: 'Tier 2 Capital', level: 0, values: [5652] }, + { label: 'Total Capital', level: 0, total: true, values: [97596] } + ] +}; + +/** Statement 5 — Tables 2 to 4, all nil this quarter. */ +export const RAQ_RECOVERY_TABLES: MatrixTable[] = [ + { + title: 'Table 2: Extensions granted', + columns: [ + 'Current qtr — No. of accounts', + 'Current qtr — Amount involved', + 'April till date — No. of accounts', + 'April till date — Amount involved', + 'Still outstanding — No. of accounts', + 'Still outstanding — Amount involved' + ], + rows: [{ label: '—', level: 0, values: [null, null, null, null, null, null] }] + }, + { + title: 'Table 3: Waiver of Debts/Written offs (Other than Compromise Settlement)', + columns: [ + 'Current qtr — No. of accounts', + 'Current qtr — Amount involved', + 'April till date — No. of accounts', + 'April till date — Amount involved' + ], + rows: [{ label: '—', level: 0, values: [null, null, null, null] }] + }, + { + title: 'Table 4: Compromise Settlements', + columns: [ + 'Current qtr — No. of accounts', + 'Current qtr — Amount involved', + 'Current qtr — Amount recovered', + 'Current qtr — Amount outstanding', + 'Current qtr — Amount written off', + 'April till date — No. of accounts', + 'April till date — Amount involved', + 'April till date — Amount recovered', + 'April till date — Amount outstanding', + 'April till date — Amount written off' + ], + rows: [ + { + label: '—', + level: 0, + values: [null, null, null, null, null, null, null, null, null, null] + } + ] + } +]; + +/** Statement 5 — Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs */ +export const RAQ_IMPAIRED_CREDITS: Oss3ImpairedCredit[] = [ + { pan: 'ABKPP9779L', name: 'PAWAN TRADERS', loansAdvances: 7500, funded: 7535, interestInArrear: 329, pctTotalAdvances: 1.83, pctCapitalFunds: 7.72, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, + { pan: 'AUAPD8276F', name: 'M/S DEV ENTERPRISES', loansAdvances: 2500, funded: 2550, interestInArrear: 1346, pctTotalAdvances: 0.62, pctCapitalFunds: 2.61, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, + { pan: 'AHKPR8438E', name: 'SHRI RAM MOTERS', loansAdvances: 2000, funded: 1951, interestInArrear: 15358, pctTotalAdvances: 0.47, pctCapitalFunds: 2.0, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'BFXPD4028Q', name: 'HEMANT TRADERS', loansAdvances: 1900, funded: 1989, interestInArrear: 1265, pctTotalAdvances: 0.48, pctCapitalFunds: 2.04, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'CDXPD8520J', name: 'THAKUR TRADERS', loansAdvances: 1600, funded: 1788, interestInArrear: 1121, pctTotalAdvances: 0.43, pctCapitalFunds: 1.83, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'ALYPK3932E', name: 'KATARE KRISHI SEVA', loansAdvances: 1200, funded: 1224, interestInArrear: 431, pctTotalAdvances: 0.3, pctCapitalFunds: 1.25, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, + { pan: 'CDXPR0380J', name: 'SONU RAGHUWANSHI', loansAdvances: 1000, funded: 1040, interestInArrear: 2109, pctTotalAdvances: 0.25, pctCapitalFunds: 1.07, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'ALEPA6589P', name: 'BALAJI ENTERPRISES', loansAdvances: 950, funded: 999, interestInArrear: 80, pctTotalAdvances: 0.24, pctCapitalFunds: 1.02, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, + { pan: 'AEEPA0954P', name: 'M/S GIRRAJ TRAETOR', loansAdvances: 900, funded: 910, interestInArrear: 63, pctTotalAdvances: 0.22, pctCapitalFunds: 0.93, assetClassification: 'Doubtful 1', lossProvisionHeld: 0 }, + { pan: 'APNPJ9600B', name: 'SHREE TARAN CREATION', loansAdvances: 700, funded: 730, interestInArrear: 856, pctTotalAdvances: 0.18, pctCapitalFunds: 0.75, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'GYEPS4039B', name: 'M/S LAKSHYA ENTERPRISES', loansAdvances: 700, funded: 712, interestInArrear: 332, pctTotalAdvances: 0.17, pctCapitalFunds: 0.73, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, + { pan: 'AIKPJ8057E', name: 'M/S PRADEEP KUMAR', loansAdvances: 650, funded: 655, interestInArrear: 220, pctTotalAdvances: 0.16, pctCapitalFunds: 0.67, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, + { pan: 'AWQPA1615F', name: 'AGARAWAL SADI', loansAdvances: 600, funded: 605, interestInArrear: 21, pctTotalAdvances: 0.15, pctCapitalFunds: 0.62, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, + { pan: 'CWAPM2065H', name: 'SHANTANU MATHUR', loansAdvances: 600, funded: 593, interestInArrear: 53, pctTotalAdvances: 0.14, pctCapitalFunds: 0.61, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, + { pan: 'BJDPM9396J', name: 'SHRI NARAYAN JEWELLERS', loansAdvances: 500, funded: 506, interestInArrear: 28, pctTotalAdvances: 0.12, pctCapitalFunds: 0.52, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, + { pan: 'CEFPK8768G', name: 'KAILAS KIRANA STORE', loansAdvances: 400, funded: 414, interestInArrear: 356, pctTotalAdvances: 0.1, pctCapitalFunds: 0.42, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'CEFPK8768G', name: 'MISHRA BHANDU TRADERS', loansAdvances: 400, funded: 328, interestInArrear: 926, pctTotalAdvances: 0.08, pctCapitalFunds: 0.34, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'FKZPS2638E', name: 'CHANCHAL KANGAN STORES', loansAdvances: 400, funded: 322, interestInArrear: 215, pctTotalAdvances: 0.08, pctCapitalFunds: 0.33, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'RBIIR1099T', name: 'RAGUVEER SHING BAGHEL', loansAdvances: 400, funded: 420, interestInArrear: 2540, pctTotalAdvances: 0.1, pctCapitalFunds: 0.43, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'FQHPS2985A', name: 'SHRI KRISHNA AUTO PARTS', loansAdvances: 300, funded: 319, interestInArrear: 327, pctTotalAdvances: 0.08, pctCapitalFunds: 0.33, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'BAPPS2459K', name: 'SIGNA PUR JADI BUTI', loansAdvances: 250, funded: 26, interestInArrear: 3, pctTotalAdvances: 0.01, pctCapitalFunds: 0.03, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'AIYPY3947N', name: 'MUSKAN BEAUTI PARLAR &', loansAdvances: 200, funded: 208, interestInArrear: 173, pctTotalAdvances: 0.05, pctCapitalFunds: 0.21, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'DMYPS7044P', name: 'MANOJ SHARMA', loansAdvances: 194, funded: 229, interestInArrear: 267, pctTotalAdvances: 0.06, pctCapitalFunds: 0.23, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'BSSPP6047B', name: 'SHRI SHANTI CYCLE SARVICE', loansAdvances: 175, funded: 192, interestInArrear: 1299, pctTotalAdvances: 0.05, pctCapitalFunds: 0.2, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'RBIIM1528T', name: 'MOHAR SINGH RAJPUT', loansAdvances: 100, funded: 106, interestInArrear: 2581, pctTotalAdvances: 0.03, pctCapitalFunds: 0.11, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'RBIIH0522T', name: 'HARNAAM SINGH S/O', loansAdvances: 100, funded: 93, interestInArrear: 27, pctTotalAdvances: 0.02, pctCapitalFunds: 0.1, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'RBIIM1527T', name: 'MAHESHWARI AGENCIES', loansAdvances: 75, funded: 88, interestInArrear: 1105, pctTotalAdvances: 0.02, pctCapitalFunds: 0.09, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, + { pan: 'ACJPD2095F', name: 'MAHENDRA KUMAR', loansAdvances: 50, funded: 32, interestInArrear: 5, pctTotalAdvances: 0.01, pctCapitalFunds: 0.03, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, + { pan: 'RBIIA2002T', name: 'ANAND SHARMA', loansAdvances: 50, funded: 42, interestInArrear: 0, pctTotalAdvances: 0.01, pctCapitalFunds: 0.04, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 } +]; + +/** Statement 6 — Table 1: Capital Position as on Previous March */ +export const RAQ_CAPITAL_PREVIOUS_MARCH: MatrixTable = { + title: 'Table 1: Memo: Capital Position as on Previous March', + unit: 'Rs in Thousands', + columns: ['Amount'], + rows: [ + { label: 'Tier 1 Capital', level: 0, values: [89930] }, + { label: 'Tier 2 Capital', level: 0, values: [5983] }, + { label: 'Total Capital', level: 0, total: true, values: [95913] } + ] +}; + +/** Statement 6 — Table 2: Large exposure, single borrower */ +export const RAQ_LARGE_EXPOSURES: Oss3LargeExposure[] = [ + { pan: 'AAEFR4731M', name: 'RATAN CHAND VIMAL CHAND', limitSanctioned: 10000, pctTotalAdvances: 2.43, outstandingBalance: 10002, pctTier1Capital: 11.12, assetClassification: 'Standard' }, + { pan: 'ADMPJ6589N', name: 'DEEPAK TRADING COMPANY', limitSanctioned: 8000, pctTotalAdvances: 1.95, outstandingBalance: 7601, pctTier1Capital: 8.45, assetClassification: 'Standard' }, + { pan: 'ACRPT3542M', name: 'VISHWAMBHARA STORES', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7524, pctTier1Capital: 8.37, assetClassification: 'Standard' }, + { pan: 'ABKPP9779L', name: 'PAWAN TRADERS', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7627, pctTier1Capital: 8.48, assetClassification: 'Standard' }, + { pan: 'AAHPT8829R', name: 'VISHWAMBHARA AGENCIES', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7533, pctTier1Capital: 8.38, assetClassification: 'Standard' }, + { pan: 'ABKPP9778M', name: 'M/S TIRUMALA TRADERS', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7562, pctTier1Capital: 8.41, assetClassification: 'Standard' }, + { pan: 'ANGPG7237G', name: 'YASHI ENTERPRISE', limitSanctioned: 6500, pctTotalAdvances: 1.58, outstandingBalance: 6539, pctTier1Capital: 7.27, assetClassification: 'Standard' }, + { pan: 'ADDPS3571L', name: 'PRATEEK ENTERPRISES', limitSanctioned: 6400, pctTotalAdvances: 1.56, outstandingBalance: 6438, pctTier1Capital: 7.16, assetClassification: 'Standard' }, + { pan: 'ANYPS4198L', name: 'RAJA RAM RAMPRASAD SAHU', limitSanctioned: 6000, pctTotalAdvances: 1.46, outstandingBalance: 6059, pctTier1Capital: 6.74, assetClassification: 'Standard' }, + { pan: 'AOVPJ4417P', name: 'JAIN SHREE FLORA MIL', limitSanctioned: 6000, pctTotalAdvances: 1.46, outstandingBalance: 5305, pctTier1Capital: 5.9, assetClassification: 'Standard' }, + { pan: 'BFMPJ0127D', name: 'NEHA JAIN', limitSanctioned: 5800, pctTotalAdvances: 1.41, outstandingBalance: 5549, pctTier1Capital: 6.17, assetClassification: 'Standard' }, + { pan: 'AFWPP7649M', name: 'M/S SANJAY PALOD &CO. PRO. SANJAY PALOD', limitSanctioned: 5500, pctTotalAdvances: 1.34, outstandingBalance: 5475, pctTier1Capital: 6.09, assetClassification: 'Standard' }, + { pan: 'ADPPA9025K', name: 'RAJNI ENTERPRISES PRO. RAJNI DEVI AGARWAL', limitSanctioned: 5250, pctTotalAdvances: 1.28, outstandingBalance: 5605, pctTier1Capital: 6.23, assetClassification: 'Standard' }, + { pan: 'ADPPA6372F', name: 'LAXMINARAYANRAJESH KUMAR', limitSanctioned: 5010, pctTotalAdvances: 1.22, outstandingBalance: 4134, pctTier1Capital: 4.6, assetClassification: 'Standard' }, + { pan: 'CFSPR8020J', name: 'M/S KRIT TRADERS PRO.RANI RAGHUWANSHI', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 5004, pctTier1Capital: 5.56, assetClassification: 'Standard' }, + { pan: 'AATPA9975H', name: 'M/S SUDARSHAN ENTERPRISES PRO.BHARTI AGRAWAL', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 3357, pctTier1Capital: 3.73, assetClassification: 'Standard' }, + { pan: 'ATEPB5535A', name: 'ANSHLEKHA BHAWSAR', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 4909, pctTier1Capital: 5.46, assetClassification: 'Standard' }, + { pan: 'AXNPA7126R', name: 'IFTI KHAR AHMAD', limitSanctioned: 4700, pctTotalAdvances: 1.14, outstandingBalance: 3590, pctTier1Capital: 3.99, assetClassification: 'Standard' }, + { pan: 'ABBPP3524A', name: 'RAM NARAYAN PALOD', limitSanctioned: 4625, pctTotalAdvances: 1.12, outstandingBalance: 4667, pctTier1Capital: 5.19, assetClassification: 'Standard' } +]; + +export const RAQ_EXPOSURE_NOTES: string[] = [ + 'Table 2 threshold: in excess of 15% of the bank’s Tier 1 capital as on previous March, or a minimum of 20 large credits.', + 'Table 3 (Group Borrower) and Table 4 (Top Ten Credit Exposure to Banks) are nil for this quarter.', + '# Due from (other) banks · @ Confirmation of LCs opened by other banks' +]; + +/** Statement 7 — Quality of Investment Portfolio */ +export const RAQ_INVESTMENT_QUALITY: MatrixTable = { + title: 'Table 1: Quality of Investment Portfolio', + unit: 'Rs in Thousands', + columns: [ + 'Book Value — Held to Maturity', + 'Book Value — Available for Sale', + 'Book Value — Held for Trading', + 'Market Value — Available for Sale', + 'Market Value — Held for Trading' + ], + rows: [ + { label: 'A. SLR Investments - Total (i+ii+iii+iv)', level: 0, total: true, values: [151734, 60000, 0, 0, 0] }, + { label: '(i) GOI Treasury bills', level: 1, values: [null, null, null, null, null] }, + { label: '(ii) GOI Securities(Dated)', level: 1, values: [63838, null, null, null, null] }, + { label: '(iii) State Govt. Securities(Dated)', level: 1, values: [87896, 60000, null, null, null] }, + { label: '(iv) Other approved Securities', level: 1, values: [null, null, null, null, null] }, + { label: 'B. Debt Securities - Total (Ba + Bb)', level: 0, total: true, values: [131583, 0, 0, 0, 0] }, + { label: 'Ba. Debt Securities - Quoted - Total(i+ii+iii+iv)', level: 1, total: true, values: [24234, 0, 0, 0, 0] }, + { label: '(i) PSU bonds', level: 2, values: [null, null, null, null, null] }, + { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [24234, null, null, null, null] }, + { label: '(iii) Bank Certificate of Deposits', level: 2, values: [null, null, null, null, null] }, + { label: '(iv) All others excluding equities', level: 2, values: [null, null, null, null, null] }, + { label: 'Bb. Debt Securities - Unquoted-Total(i+ii+iii+iv)', level: 1, total: true, values: [107349, 0, 0, 0, 0] }, + { label: '(i) PSU bonds', level: 2, values: [null, null, null, null, null] }, + { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [null, null, null, null, null] }, + { label: '(iii) Bank Certificate of Deposits', level: 2, values: [107287, null, null, null, null] }, + { label: '(iv) All others excluding equities', level: 2, values: [62, null, null, null, null] }, + { label: 'C. Equities - Total (i+ii)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, + { label: '(i) Quoted', level: 1, values: [null, null, null, null, null] }, + { label: '(ii) Unquoted', level: 1, values: [null, null, null, null, null] }, + { + label: 'Out of C Equities -Total - investments in equity - oriented mutual funds', + level: 1, + values: [null, null, null, null, null] + }, + { label: 'Total Investments (A+B+C)', level: 0, total: true, values: [283317, 60000, 0, 0, 0] } + ] +}; + +/** Statement 8 — Other Details of Non-SLR Securities (all nil this quarter) */ +export const RAQ_NON_SLR_SECURITIES: MatrixTable = { + title: 'Table 1: Other Details of Non-SLR Securities', + unit: 'Rs in Thousands', + columns: [ + 'Total Holdings — Rated', + 'Total Holdings — Unrated', + 'Total Holdings — Borrowers', + 'Total Holdings — Non Borrowers', + 'Private Placement — Rated', + 'Private Placement — Unrated', + 'Total NPIs', + 'Total Provisions' + ], + rows: [ + { label: 'Total Quoted - I', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, + { label: '1. PSU Bonds', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: '2. Mutual funds (Including Units)', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: '3. Others', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: 'Total Unquoted - II', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, + { label: '1. Mutual funds (Including Units)', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: '2. Others', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: 'Equities of AIFIs: - III', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, + { label: '1. Quoted', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: '2. Unquoted', level: 1, values: [null, null, null, null, null, null, null, null] }, + { label: 'Grand Total (I+II+III)', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] } + ] +}; + +/** Statement 9 — Part A: Quality of Loan Assets, sectoral analysis */ +export const RAQ_SECTORAL_CREDIT: MatrixTable = { + title: 'Table 1: Part A: Quality of Loan Assets - Sectoral Analysis', + unit: 'Rs in Thousands', + columns: [ + 'Gross Loans and Advances', + 'Gross NPAs', + 'Provisions made' + ], + rows: [ + { label: 'I. Gross Advances (II + III)', level: 0, total: true, values: [411381, 23138, 39999] }, + { label: 'II. Food Credit', level: 0, values: [null, null, null] }, + { label: 'III. Non-Food Credit ( 1 to 5)', level: 0, total: true, values: [411381, 23138, 39999] }, + { label: '1. Agriculture and Allied Activities', level: 1, values: [4001, 227, 227] }, + { label: '2. Industry (2.1 to 2.4)', level: 1, total: true, values: [0, 0, 0] }, + { label: '2.1 Micro', level: 2, values: [null, null, null] }, + { label: '2.2 Small', level: 2, values: [null, null, null] }, + { label: '2.3 Medium', level: 2, values: [null, null, null] }, + { label: '2.4 Large', level: 2, values: [null, null, null] }, + { label: '3. Services (3.1 to 3.9 or 3.a to 3.d)', level: 1, total: true, values: [256126, 4417, 3163] }, + { label: '3.1 Transport Operators', level: 2, values: [null, null, null] }, + { label: '3.2 Post and Telecommunication Services', level: 2, values: [null, null, null] }, + { label: '3.3 Tourism, Hotel and Restaurants', level: 2, values: [null, null, null] }, + { label: '3.4 Trade', level: 2, total: true, values: [256126, 4417, 3163] }, + { label: '3.4.1 Wholesale Trade (other than Food Procurement)', level: 3, values: [222886, 4417, 3163] }, + { label: '3.4.2 Retail Trade', level: 3, values: [33240, null, null] }, + { label: '3.5 Financial Intermediation', level: 2, total: true, values: [0, 0, 0] }, + { label: '3.5.1 Banking & Finance other than NBFCs and DFIs', level: 3, total: true, values: [0, 0, 0] }, + { label: '3.5.1.1 Scheduled Commercial Banks', level: 3, values: [null, null, null] }, + { label: '3.5.1.2 Co-operative Banks', level: 3, total: true, values: [0, 0, 0] }, + { label: '3.5.1.2.1 Urban Cooperative Banks', level: 3, values: [null, null, null] }, + { label: '3.5.1.2.2 District Central Cooperative Banks', level: 3, values: [null, null, null] }, + { label: '3.5.1.2.3 Other Cooperative Banks', level: 3, values: [null, null, null] }, + { label: '3.5.2 MFIs, HFCs and NBFCs other than DFIs', level: 3, total: true, values: [0, 0, 0] }, + { label: '3.5.2.1 Micro Financial Institutions (MFIs)', level: 3, values: [null, null, null] }, + { label: '3.5.2.2 Housing Finance Companies (HFCs)', level: 3, values: [null, null, null] }, + { label: '3.5.2.3 Other NBFCs', level: 3, values: [null, null, null] }, + { label: '3.5.3 Other Financial Intermediation (incl DFIs)', level: 3, values: [null, null, null] }, + { label: '3.6 Real Estate Activities', level: 2, total: true, values: [0, 0, 0] }, + { label: '3.6.1 Commercial Real Estate (CRE)', level: 3, values: [null, null, null] }, + { + label: '3.6.2 Other Real Estate Activites, other than Housing Loans (Reported in Sec 4.1)', + level: 3, + values: [null, null, null] + }, + { label: '3.7 Computer and Related Activities', level: 2, values: [null, null, null] }, + { label: '3.8 Professional Services', level: 2, values: [null, null, null] }, + { label: '3.9 Other Services', level: 2, values: [null, null, null] }, + { label: '3.a Micro', level: 2, values: [222886, 4417, 3163] }, + { label: '3.b Small', level: 2, values: [33240, null, null] }, + { label: '3.c Medium', level: 2, values: [null, null, null] }, + { label: '3.d Large', level: 2, values: [null, null, null] }, + { label: '4. Retail Loans (4.1 to 4.8 )', level: 1, total: true, values: [151254, 18494, 36609] }, + { label: '4.1 Housing Loans (incl. priority sector Housing)', level: 2, total: true, values: [10241, 1559, 1559] }, + { label: '4.1.1 Loans > 30 Lakhs', level: 3, values: [null, null, null] }, + { label: '4.1.2 Loans <= 30 Lakhs', level: 3, values: [10241, 1559, 1559] }, + { label: '4.2 Consumer Durables', level: 2, values: [null, null, null] }, + { label: '4.3 Credit Card Receivables', level: 2, values: [null, null, null] }, + { label: '4.4 Vehicle/Auto Loans', level: 2, values: [null, null, null] }, + { label: '4.5 Education Loans', level: 2, values: [2403, null, null] }, + { label: '4.6 Advances against Fixed Deposits (incl. FCNR(B), etc.)', level: 2, values: [13201, null, null] }, + { label: '4.7 Advances to Individuals against Shares, Bonds, etc.', level: 2, values: [null, null, null] }, + { label: '4.8 Other Retail Loans', level: 2, values: [125409, 16935, 35050] }, + { label: '5. Other Non-food Credit, if any, please specify', level: 1, total: true, values: [0, 0, 0] } + ] +}; + +/** Statement 9 — Table 2: Memo */ +export const RAQ_SECTORAL_MEMO: MatrixTable = { + title: 'Table 2: Memo', + unit: 'Rs in Thousands', + columns: [ + 'Gross Loans and Advances', + 'Gross NPAs', + 'Provisions made' + ], + rows: [ + { label: 'Total advances against pledge of gold/ silver ornamnets', level: 0, values: [null, null, null] }, + { label: 'Gold loans under bullet repayment scheme', level: 0, values: [null, null, null] }, + { label: 'SME Loans', level: 0, total: true, values: [256126, 4417, 3163] } + ] +}; + +/** Statement 9 — Table 3: Part B, segment-wise analysis of advances */ +export const RAQ_SEGMENT_ANALYSIS: MatrixTable = { + title: 'Table 3: Part B - Segment-wise analysis of Advances', + unit: 'Rs in Thousands', + columns: [ + 'Outstanding — No. of A/cs', + 'Outstanding — Amount', + 'Gross NPAs — Amount', + 'Gross NPAs — Provision' + ], + rows: [ + { label: 'PRIORITY SECTOR', level: 0, header: true, values: [] }, + { label: 'i. Retail Trade', level: 1, values: [163, 222886, 4417, 3163] }, + { label: 'ii. Small Business', level: 1, values: [5, 15421, null, null] }, + { label: 'iii. Small scale industries', level: 1, values: [5, 17819, null, null] }, + { + label: + 'iv. Housing Loans (Purchase / Construction of dwelling units and / or repairs to the damaged dwelling units)', + level: 1, + values: [31, 10241, 1559, 1559] + }, + { label: 'v. Agricultural & Allied activities', level: 1, values: [8, 4001, 227, 227] }, + { label: 'vi. Small Road Transport Operators', level: 1, values: [null, null, null, null] }, + { label: 'vii. Professional and Self Employed artisans and craftsmen', level: 1, values: [null, null, null, null] }, + { label: 'viii. Education', level: 1, values: [32, 2403, null, null] }, + { label: 'ix. Other Priority Sectors', level: 1, values: [2, 40, 40, 40] }, + { label: 'TOTAL PRIORITY SECTOR', level: 0, total: true, values: [246, 272811, 6243, 4989] }, + { label: 'NON PRIORITY SECTOR', level: 0, header: true, values: [] }, + { label: 'i. Medium and Large Industries', level: 1, values: [null, null, null, null] }, + { label: 'ii. Export Trade', level: 1, values: [null, null, null, null] }, + { label: 'iii. Banks', level: 1, values: [null, null, null, null] }, + { label: 'iv. Non-Bank Financial Institution', level: 1, values: [null, null, null, null] }, + { label: 'v. Governments (Central / States)', level: 1, values: [null, null, null, null] }, + { label: 'vi. Food Credit (FCI consortium)', level: 1, values: [null, null, null, null] }, + { label: 'vii. Real Estate', level: 1, values: [null, null, null, null] }, + { label: 'viii. Other PSUs', level: 1, values: [null, null, null, null] }, + { label: 'ix. All Other', level: 1, values: [126, 138570, 16895, 35010] }, + { label: 'TOTAL NON-PRIORITY SECTOR', level: 0, total: true, values: [126, 138570, 16895, 35010] }, + { label: 'TOTAL (Priority Sector + Non-Priority Sector)', level: 0, total: true, values: [372, 411381, 23138, 39999] } + ], + notes: [ + '1. In case of overlapping in classification of accounts in various Sectors/Industries, the classification may be done from top to bottom.' + ] +}; + +/** Statement 10 — Part C: Industry Breakup (nil across the board this quarter) */ +export const RAQ_INDUSTRY_BREAKUP: MatrixTable = { + title: 'Table 1: Part C: Industry Breakup', + unit: 'Rs in Thousands', + columns: [ + 'Gross Loans and Advances', + 'Gross NPAs — Sub Standard', + 'Gross NPAs — Doubtful', + 'Gross NPAs — Loss', + 'Gross NPAs — Total', + 'Provisions — Sub Standard', + 'Provisions — Doubtful', + 'Provisions — Loss', + 'Provisions — Total' + ], + rows: [ + { label: 'A. Mining and Quarrying', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'B. Food Processing', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { label: 'B.1 Sugar', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'B.2 Edible Oils and Vanaspati', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'B.3 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { + label: 'C. Beverages (excluding Tea & Coffee) and Tobacco', + level: 0, + total: true, + values: [0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + { label: 'C.1 Tobacco and tobacco products', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'C.2 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'D. Textiles', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { label: 'D.1 Cotton', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'D.2 Jute', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'D.3 Man-made', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'D.4 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'E. Leather and Leather products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'F. Wood and Wood Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'G. Paper and Paper Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { + label: 'H. Petroleum (non-infra), Coal Products (non-mining) and Nuclear Fuels', + level: 0, + values: [null, null, null, null, 0, null, null, null, 0] + }, + { + label: 'I. Chemicals and Chemical Products (Dyes, Paints, etc.)', + level: 0, + values: [null, null, null, null, 0, null, null, null, 0] + }, + { label: 'J. Rubber, Plastic and their Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { + label: 'K. Glass, Glassware and other non-metallic mineral products (Except Cement and Cement products)', + level: 0, + values: [null, null, null, null, 0, null, null, null, 0] + }, + { label: 'L. Cement and Cement Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'M. Basic Metal and Metal Products', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { label: 'M.1 Iron and Steel', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'M.2 Other Metal and Metal Products', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'N. All Engineering', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { + label: 'O. Vehicles, Vehicle Parts and Transport Equipments', + level: 0, + values: [null, null, null, null, 0, null, null, null, 0] + }, + { label: 'P. Gems and Jewellery', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'Q. Construction', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'R. Infrastructure', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'S. Other Industries, pl. specify', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { label: 'All Industries (A to S)', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] } + ], + notes: [ + '1. In case of overlapping in classification of accounts in various Sectors/Industries, the classification may be done from top to bottom.' + ] +}; + +/** Statement 11 — Part A: Exposures to Capital Market */ +export const RAQ_CAPITAL_MARKET: MatrixTable = { + title: 'Table 1: Part A: Exposures to Capital Market', + unit: 'Rs in Thousands', + columns: [ + 'Gross Amount Outstanding', + 'Gross NPAs' + ], + rows: [ + { label: 'A1. Advances to Capital Market (Fund based)', level: 0, header: true, values: [] }, + { label: '1. Individuals', level: 1, values: [null, null] }, + { label: '2. Market Makers', level: 1, values: [null, null] }, + { label: '3. All other borrowers against Security of Shares', level: 1, values: [null, null] }, + { label: '4. Any other (Pl. specify)', level: 1, total: true, values: [0, 0] }, + { label: 'Total Advances to Capital Market (1 to 4)', level: 0, total: true, values: [0, 0] }, + { label: 'A2. Advances to Capital Market (Non-fund based)', level: 0, header: true, values: [] }, + { + label: '1. Financial Guarantees issued to Stock Exchanges on behalf of Stock Brokers', + level: 1, + values: [null, null] + }, + { label: '2. Other Financial Guarantees', level: 1, values: [null, null] }, + { label: '3. Any other (Pl. specify)', level: 1, total: true, values: [0, null] }, + { label: 'Total Non-fund based', level: 0, total: true, values: [0, null] }, + { label: 'A3. Investments in Capital Market', level: 0, header: true, values: [] }, + { label: '1. Convertible Bonds and Debentures', level: 1, values: [null, null] }, + { label: '2. Equities (of AIFIs)', level: 1, values: [null, null] }, + { label: '3. Equity Oriented Mutual Funds', level: 1, values: [null, null] }, + { label: '4. Any other (to be specified)', level: 1, total: true, values: [24234, 0] }, + { label: 'NON SLR INVESTMENT', level: 2, values: [24234, null] }, + { label: 'Total Investments in Capital Market', level: 0, total: true, values: [24234, 0] }, + { label: 'Total Capital Market Exposure (A1 + A2 + A3)', level: 0, total: true, values: [24234, 0] } + ], + notes: [ + 'Note: Extant circular on exposure norms may be referred. Non-funded exposure should include derivative contracts, if any.' + ] +}; + +/** Statement 11 — Part B: Exposure to Real Estate Sector */ +export const RAQ_REAL_ESTATE: MatrixTable = { + title: 'Table 2: Part B: Exposure to Real Estate Sector', + unit: 'Rs in Thousands', + columns: [ + 'Amount Outstanding - Funded', + 'Gross NPAs' + ], + rows: [ + { label: 'I. Exposure to Real Estate:', level: 0, header: true, values: [] }, + { label: 'A. Residential Mortgage *', level: 1, total: true, values: [17229, 2148] }, + { + label: + 'a. Loans to individuals / cooperative / group housing societies, housing boards undertaking housing projects or schemes for Economically Weaker Sections, Low Income Group and Middle Income Group and to Owners of houses / flats for extension up-gradation major repairs for eligible housing schemes*', + level: 2, + values: [17229, 2148] + }, + { + label: 'b. Other loans under finance for housing schemes not covered under (a) above *', + level: 2, + values: [null, null] + }, + { + label: 'c. All other loans for purchase of residential property (including for land)', + level: 2, + values: [null, null] + }, + { + label: + 'd. Amount of Refinance obtained from National Housing Bank / funds obtained (for housing loans) from higher financing agencies.', + level: 2, + values: [null, null] + }, + { label: 'B. Commercial Real Estate', level: 1, total: true, values: [14445, 0] }, + { label: 'a. Advances for acquiring / developing commercial Real Estate', level: 2, values: [14445, null] }, + { label: 'b. Non Fund Based Limits for above purposes', level: 2, values: [null, null] }, + { label: 'C. Any Other', level: 1, total: true, values: [0, 0] }, + { + label: + 'i) Exposures in bonds of National Housing Bank / HUDCO only for financing of housing and included under Priority Sector Lending', + level: 2, + values: [null, null] + }, + { + label: 'ii) Other Fund Based and Non Fund Based Exposures on NHB / HUDCO and Housing Finance Companies, if any', + level: 2, + values: [null, null] + }, + { + label: 'iii) Investments in Mortgage Backed Securities (MBS) and other securitized exposures', + level: 2, + total: true, + values: [0, 0] + }, + { label: 'a. Residential', level: 3, values: [null, null] }, + { label: 'b. Commercial Real Estate.', level: 3, values: [null, null] }, + { + label: 'iv) Advances to builders / contractors (other than for commercial Real Estate).', + level: 2, + values: [null, null] + }, + { label: 'v) Others', level: 2, values: [null, null] }, + { label: 'D. TOTAL (A+B+C)', level: 0, total: true, values: [31674, 2148] }, + { + label: 'Out of above, the total exposure taken after obtaining pari passu (2nd mortgage)', + level: 1, + values: [null, null] + }, + { label: 'II. Other Exposures to Real Estate', level: 0, header: true, values: [] }, + { label: 'Fund Based', level: 1, values: [null, null] }, + { label: 'Of which loan given after obtaining pari passu (2nd mortgage)', level: 2, values: [null, null] }, + { label: 'Non Fund Based', level: 1, values: [null, null] }, + { label: 'Of which facility given after obtaining pari passu (2nd mortgage)', level: 2, values: [null, null] }, + { label: 'III. Housing Finance under Priority Sector', level: 0, header: true, values: [] }, + { + label: '(i) Loan upto Rs.25 lakhs for purchase / construction of houses by Individuals', + level: 1, + values: [10241, 1559] + }, + { + label: + '(ii) Other Housing Loans to individuals for repairs / additions / alterations etc classified under Priority Sector Lending)', + level: 1, + values: [null, null] + }, + { label: '(iii) All other housing related exposures under Priority Sector', level: 1, values: [null, null] }, + { label: 'Total Housing Finance Under Priority Sector', level: 0, total: true, values: [10241, 1559] } + ] +}; + +/** Sheet 13 — Signatory. Shared so every OSS return always agrees. */ +export const RAQ_SIGNATORIES: ReturnSignatory[] = OSS_SIGNATORIES; diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html new file mode 100644 index 000000000..da583820f --- /dev/null +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html @@ -0,0 +1,372 @@ + + +
+ +
+
+ +
+

OSS 3 — {{ returnName }}

+
+ {{ bankName }} · Bank Code {{ bankCode }} · Return Code + {{ returnCode }} · Quarter ending {{ reportingEndDate }} +
+
+
+
+ {{ reportStatus }} + +
+
+ + +
+ + + Gross NPA ₹{{ grossNpa | number: '1.2-2' }} ({{ grossNpaRatio | number: '1.2-2' }}%) + · Net NPA ₹{{ netNpa | number: '1.2-2' }} ({{ netNpaRatio | number: '1.2-2' }}%) + · + {{ + totalsAgree + ? 'Portfolio Analysis and Classification totals agree' + : 'MISMATCH between Portfolio Analysis and Classification totals — resolve before filing' + }} + +
+ + +
+ @for (t of tabs; track t.id) { + + } +
+ + + @if (activeTab === 'filing') { +
+
Filing Information
+
+ + + @for (item of filingInfo; track item.label) { + + + + + } + +
{{ item.label }}{{ item.value }}
+
+
+ } + + + @if (activeTab === 'portfolio') { +
+
PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment)
+ + +
+ } + + + @if (activeTab === 'classification') { +
+
Classifcation_Loans_Adv — Classification of Risk Assets
+ + + + +
+ } + + + @if (activeTab === 'movement') { +
+
Movement_of_loans — Change in Asset Quality Profile and Recoveries of NPAs
+ + + +
+ } + + + @if (activeTab === 'other-risk') { +
+
StatementOtherRiskAssets — Other Risk Assets and Exposures
+ + +
+ } + + + @if (activeTab === 'impaired') { +
+
TopImpairedCredits — Top Impaired Credits
+ + +
+
Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs — Rs in Thousands
+ + + + + + + + + + + + + + + + @for (r of impairedCredits; track $index) { + + + + + + + + + + + + } + + + + + + + + +
PAN of BorrowerName of BorrowerLoans & AdvancesFunded ExposureInterest in arrear% to Total Advances% to Capital FundsAsset ClassificationLoss Provision held
{{ r.pan }}{{ r.name }}{{ r.loansAdvances | number: '1.2-2' }}{{ r.funded | number: '1.2-2' }}{{ r.interestInArrear | number: '1.2-2' }}{{ r.pctTotalAdvances | number: '1.2-2' }}%{{ r.pctCapitalFunds | number: '1.2-2' }}% + + {{ r.assetClassification }} + + {{ r.lossProvisionHeld | number: '1.2-2' }}
Total ({{ impairedCredits.length }} borrowers){{ impairedFundedTotal | number: '1.2-2' }}{{ impairedInterestTotal | number: '1.2-2' }}{{ impairedProvisionTotal | number: '1.2-2' }}
+
+ + @for (t of recoveryTables; track t.title) { + + } +
+ } + + + @if (activeTab === 'exposure') { +
+
ExposureLimits — Single and Group Borrower Exposure Limit
+ + +
+
+ Table 2: Statement on Large Exposure — Single Borrower (in excess of 15% of Tier 1 capital as on + previous March, or a minimum of 20 large credits) +
+ + + + + + + + + + + + + + @for (r of largeExposures; track $index) { + + + + + + + + + + } + +
PAN of BorrowerName of BorrowerLimit sanctioned% to Total AdvancesOutstanding Balance% to Tier 1 capitalAsset Classification
{{ r.pan }}{{ r.name }}{{ r.limitSanctioned | number: '1.2-2' }}{{ r.pctTotalAdvances | number: '1.2-2' }}%{{ r.outstandingBalance | number: '1.2-2' }}{{ r.pctTier1Capital | number: '1.2-2' }}% + {{ r.assetClassification }} +
+
+ +
+ @for (note of exposureNotes; track note) { +
{{ note }}
+ } +
+
+ } + + + @if (activeTab === 'securities') { +
+
QualitySecurityPortoflio — Quality of Security Portfolio
+ +
OtherNonSLRSecurity — Other Details of Non-SLR Securities
+ +
+ } + + + @if (activeTab === 'sectoral') { +
+
SectoralCredit — Sectoral Credit
+ +
+ + + @if (sectoralAgreesWithClosing) { + Sectoral totals agree with the Classification statement. + } @else { + Sectoral gross advances ₹{{ sectoralGross | number: '1.2-2' }} and NPAs + ₹{{ sectoralNpa | number: '1.2-2' }} do not match the closing position reported by + Classification (₹{{ classificationTotal | number: '1.2-2' }} / + ₹{{ grossNpa | number: '1.2-2' }}). + @if (sectoralMatchesOpening) { + They match the opening balances in Movement Parts 1 and 2 instead — this statement appears to + have been prepared on the previous quarter's book. + } + } + +
+ + + + +
+ } + + + @if (activeTab === 'industry') { +
+
IndustryBreakup — Industry Breakup
+ +
+ } + + + @if (activeTab === 'sensitive') { +
+
SensitiveSector — Sensitive Sector
+ + +
+ } + + + @if (activeTab === 'signatory') { +
+
Signatory — Authorised Signatory
+
+ @for (p of signatories; track p.heading) { +
+
{{ p.heading }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ p.signature }}Signature
{{ p.name }}Name
{{ p.designation }}Designation
{{ p.email }}e-mail ID
{{ p.telephoneOffice }}Tele. No. (O)
{{ p.telephoneResidence }}Tele. No. (R)
{{ p.place }}Place
{{ p.date }}Date
+
+ } +
+
+ } + + + +
+
+ {{ t.title }}@if (t.unit) {{{ t.unit }}} +
+ @if (t.subtitle) { +
{{ t.subtitle }}
+ } + + + + + @for (c of t.columns; track $index) { + + } + + + + @for (row of t.rows; track $index) { + + + @if (row.header) { + + } @else { + @for (v of row.values; track $index) { + + } + } + + } + +
Particulars{{ c }}
{{ row.label }} + {{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }} +
+
+ @if (t.notes?.length) { +
+ @for (note of t.notes; track note) { +
{{ note }}
+ } +
+ } +
+ + +
{{ toastMsg }}
+
diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts new file mode 100644 index 000000000..173ae15c1 --- /dev/null +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts @@ -0,0 +1,470 @@ +/** + * 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 { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import * as ExcelJS from 'exceljs'; +import { MatrixTable } from '../oss-return.model'; +import { + addBanner, + addMatrixTable, + boxAllCells, + buildGeneralInfoSheet, + buildSignatorySheet, + downloadWorkbook, + newWorkbook, + solidFill, + XL +} from '../oss-workbook'; +import { + RAQ_CAPITAL_MARKET, + RAQ_CAPITAL_MEMO, + RAQ_CAPITAL_PREVIOUS_MARCH, + RAQ_CLASSIFICATION_MEMO, + RAQ_CLASSIFICATION_PART_A, + RAQ_EXPOSURE_NOTES, + RAQ_FILING_INFO, + RAQ_IMPAIRED_CREDITS, + RAQ_INDUSTRY_BREAKUP, + RAQ_INVESTMENT_QUALITY, + RAQ_LARGE_EXPOSURES, + RAQ_MOVEMENT_PART_1, + RAQ_MOVEMENT_PART_2, + RAQ_MOVEMENT_PART_3, + RAQ_NETTING_ITEMS_1, + RAQ_NETTING_ITEMS_2, + RAQ_NON_SLR_SECURITIES, + RAQ_OTHER_RISK_OFF_BS, + RAQ_OTHER_RISK_ON_BS, + RAQ_PORTFOLIO_ANALYSIS, + RAQ_PORTFOLIO_MEMO, + RAQ_REAL_ESTATE, + RAQ_RECOVERY_TABLES, + RAQ_SECTORAL_CREDIT, + RAQ_SECTORAL_MEMO, + RAQ_SEGMENT_ANALYSIS, + RAQ_SIGNATORIES +} from './oss3-raq-data'; + +type Oss3Tab = + | 'filing' + | 'portfolio' + | 'classification' + | 'movement' + | 'other-risk' + | 'impaired' + | 'exposure' + | 'securities' + | 'sectoral' + | 'industry' + | 'sensitive' + | 'signatory'; + +@Component({ + selector: 'mifosx-oss3-raq', + templateUrl: './oss3-raq.component.html', + styleUrls: ['../oss-return.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + FormsModule + ] +}) +export class Oss3RaqComponent { + activeTab: Oss3Tab = 'filing'; + + /** Tab strip definition — keeps the template free of a dozen near-identical buttons. */ + readonly tabs: { id: Oss3Tab; label: string; icon: string }[] = [ + { id: 'filing', label: 'Filing Information', icon: 'info-circle' }, + { id: 'portfolio', label: 'Portfolio Analysis', icon: 'chart-pie' }, + { id: 'classification', label: 'Classification of Risk Assets', icon: 'university' }, + { id: 'movement', label: 'Movement of Loans', icon: 'exchange-alt' }, + { id: 'other-risk', label: 'Other Risk Assets', icon: 'exclamation-triangle' }, + { id: 'impaired', label: 'Top Impaired Credits', icon: 'file-alt' }, + { id: 'exposure', label: 'Exposure Limits', icon: 'users' }, + { id: 'securities', label: 'Security Portfolio', icon: 'coins' }, + { id: 'sectoral', label: 'Sectoral Credit', icon: 'chart-pie' }, + { id: 'industry', label: 'Industry Breakup', icon: 'file-alt' }, + { id: 'sensitive', label: 'Sensitive Sector', icon: 'shield-alt' }, + { id: 'signatory', label: 'Signatory', icon: 'check-circle' } + ]; + + filingInfo = RAQ_FILING_INFO; + portfolioAnalysis = RAQ_PORTFOLIO_ANALYSIS; + portfolioMemo = RAQ_PORTFOLIO_MEMO; + classificationPartA = RAQ_CLASSIFICATION_PART_A; + nettingItems1 = RAQ_NETTING_ITEMS_1; + nettingItems2 = RAQ_NETTING_ITEMS_2; + classificationMemo = RAQ_CLASSIFICATION_MEMO; + movementPart1 = RAQ_MOVEMENT_PART_1; + movementPart2 = RAQ_MOVEMENT_PART_2; + movementPart3 = RAQ_MOVEMENT_PART_3; + otherRiskOnBs = RAQ_OTHER_RISK_ON_BS; + otherRiskOffBs = RAQ_OTHER_RISK_OFF_BS; + capitalMemo = RAQ_CAPITAL_MEMO; + recoveryTables = RAQ_RECOVERY_TABLES; + impairedCredits = RAQ_IMPAIRED_CREDITS; + capitalPreviousMarch = RAQ_CAPITAL_PREVIOUS_MARCH; + largeExposures = RAQ_LARGE_EXPOSURES; + exposureNotes = RAQ_EXPOSURE_NOTES; + investmentQuality = RAQ_INVESTMENT_QUALITY; + nonSlrSecurities = RAQ_NON_SLR_SECURITIES; + sectoralCredit = RAQ_SECTORAL_CREDIT; + sectoralMemo = RAQ_SECTORAL_MEMO; + segmentAnalysis = RAQ_SEGMENT_ANALYSIS; + industryBreakup = RAQ_INDUSTRY_BREAKUP; + capitalMarket = RAQ_CAPITAL_MARKET; + realEstate = RAQ_REAL_ESTATE; + signatories = RAQ_SIGNATORIES; + + generating = false; + toastMsg = ''; + toastVisible = false; + private toastTimer: any; + + showTab(tab: Oss3Tab): void { + this.activeTab = tab; + } + + private info(label: string): string { + return this.filingInfo.find((g) => g.label === label)?.value ?? ''; + } + + get returnName(): string { + return this.info('Return Name'); + } + get returnCode(): string { + return this.info('Return Code'); + } + get bankName(): string { + return this.info('Name of reporting institution'); + } + get bankCode(): string { + return this.info('Bank Code'); + } + get reportingEndDate(): string { + return this.info('Reporting end date'); + } + get reportStatus(): string { + return this.info('Report status'); + } + + private matrixValue(table: MatrixTable, prefix: string, col: number): number | null { + return table.rows.find((r) => r.label.startsWith(prefix))?.values[col] ?? null; + } + + get grossNpa(): number { + return this.matrixValue(this.nettingItems2, 'Gross Non Performing Advances', 0) ?? 0; + } + get grossNpaRatio(): number { + return this.matrixValue(this.nettingItems2, 'Gross Non Performing Advances Ratio', 0) ?? 0; + } + get netNpa(): number { + return this.matrixValue(this.nettingItems2, 'Net Non Performing Advances', 0) ?? 0; + } + get netNpaRatio(): number { + return this.matrixValue(this.nettingItems2, 'Net Non Performing Advances Ratio', 0) ?? 0; + } + + /** + * Portfolio Analysis TOTAL must equal Classification TOTAL — the two statements + * describe the same book from different angles and RBI validates them against + * each other, so a divergence is surfaced rather than left to be spotted. + */ + get portfolioTotal(): number { + return this.matrixValue(this.portfolioAnalysis, 'TOTAL (A + B)', 3) ?? 0; + } + get classificationTotal(): number { + return this.matrixValue(this.classificationPartA, 'TOTAL', 0) ?? 0; + } + get totalsAgree(): boolean { + return this.portfolioTotal === this.classificationTotal; + } + + /** + * Sectoral Credit reports its own gross advances and NPA figures, which should + * describe the same closing book as the Classification statement. In this filing + * they instead match the *opening* balances from Movement Part 1/2, so the + * divergence is shown rather than buried. + */ + get sectoralGross(): number { + return this.matrixValue(this.sectoralCredit, 'I. Gross Advances', 0) ?? 0; + } + get sectoralNpa(): number { + return this.matrixValue(this.sectoralCredit, 'I. Gross Advances', 1) ?? 0; + } + get movementOpeningGross(): number { + return this.matrixValue(this.movementPart1, 'A. Loans and advances at the beginning', 4) ?? 0; + } + get movementOpeningNpa(): number { + return this.matrixValue(this.movementPart2, 'A. NPAs at the beginning', 3) ?? 0; + } + get sectoralAgreesWithClosing(): boolean { + return this.sectoralGross === this.classificationTotal && this.sectoralNpa === this.grossNpa; + } + get sectoralMatchesOpening(): boolean { + return this.sectoralGross === this.movementOpeningGross && this.sectoralNpa === this.movementOpeningNpa; + } + + /** Table 5 footer totals, summed rather than hardcoded so they cannot drift. */ + get impairedFundedTotal(): number { + return this.impairedCredits.reduce((s, r) => s + (r.funded ?? 0), 0); + } + get impairedInterestTotal(): number { + return this.impairedCredits.reduce((s, r) => s + (r.interestInArrear ?? 0), 0); + } + get impairedProvisionTotal(): number { + return this.impairedCredits.reduce((s, r) => s + (r.lossProvisionHeld ?? 0), 0); + } + + // ══════════════ Excel generation ══════════════ + + /** Rebuilds the RBI iFILE workbook: one sheet per statement, original layout. */ + async generateExcel(): Promise { + this.generating = true; + try { + const wb = newWorkbook(); + + buildGeneralInfoSheet(wb, this.filingInfo); + this.sheetOf(wb, 'PortfolioAnalysis', [ + this.portfolioAnalysis, + this.portfolioMemo + ]); + this.sheetOf(wb, 'Classifcation_Loans_Adv', [ + this.classificationPartA, + this.nettingItems1, + this.nettingItems2, + this.classificationMemo + ]); + this.sheetOf(wb, 'Movement_of_loans', [ + this.movementPart1, + this.movementPart2, + this.movementPart3 + ]); + this.sheetOf(wb, 'StatementOtherRiskAssets', [ + this.otherRiskOnBs, + this.otherRiskOffBs + ]); + this.buildImpairedCreditsSheet(wb); + this.buildExposureLimitsSheet(wb); + this.sheetOf(wb, 'QualitySecurityPortoflio', [this.investmentQuality]); + this.sheetOf(wb, 'OtherNonSLRSecurity', [this.nonSlrSecurities]); + this.sheetOf(wb, 'SectoralCredit', [ + this.sectoralCredit, + this.sectoralMemo, + this.segmentAnalysis + ]); + this.sheetOf(wb, 'IndustryBreakup', [this.industryBreakup]); + this.sheetOf(wb, 'SensitiveSector', [ + this.capitalMarket, + this.realEstate + ]); + buildSignatorySheet(wb, this.signatories, 'Signatory'); + + await downloadWorkbook(wb, `R32312 OSS 3 ${this.returnCode} - ${this.reportingEndDate}.xlsx`); + this.toast(`${this.returnCode} workbook generated for quarter ending ${this.reportingEndDate}`); + } catch { + this.toast('Could not generate the workbook — please retry'); + } finally { + this.generating = false; + } + } + + /** One worksheet holding a stack of matrix statements. */ + private sheetOf(wb: ExcelJS.Workbook, name: string, tables: MatrixTable[]): void { + const sheet = wb.addWorksheet(name); + const widest = Math.max(...tables.map((t) => t.columns.length)); + sheet.columns = [ + { width: 62 }, + ...Array.from({ length: widest }, () => ({ width: 18 })) + ]; + addBanner(sheet, name, this.colLetter(widest + 1)); + for (const table of tables) { + addMatrixTable(sheet, table); + } + } + + private colLetter(index: number): string { + let n = index; + let s = ''; + while (n > 0) { + const rem = (n - 1) % 26; + s = String.fromCharCode(65 + rem) + s; + n = Math.floor((n - 1) / 26); + } + return s || 'A'; + } + + private buildImpairedCreditsSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('TopImpairedCredits'); + sheet.columns = [ + { width: 16 }, + { width: 40 }, + { width: 16 }, + { width: 16 }, + { width: 16 }, + { width: 16 }, + { width: 18 }, + { width: 18 }, + { width: 18 } + ]; + addBanner(sheet, 'TopImpairedCredits - Top Impaired Credits', 'I'); + addMatrixTable(sheet, this.capitalMemo); + for (const t of this.recoveryTables) { + addMatrixTable(sheet, t); + } + + const start = sheet.rowCount + 1; + const title = sheet.addRow(['Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs']); + sheet.mergeCells(title.number, 1, title.number, 9); + title.font = { bold: true }; + title.getCell(1).fill = solidFill(XL.headerGrey); + + const head = sheet.addRow([ + 'PAN of Borrower', + 'Name of Borrower', + 'Loans & Advances', + 'Funded Exposure', + 'Interest in arrear', + '% to Total Advances', + '% to Capital Funds of the Bank', + 'Asset Classification', + 'Loss Provision held' + ]); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const r of this.impairedCredits) { + const row = sheet.addRow([ + r.pan, + r.name, + r.loansAdvances, + r.funded, + r.interestInArrear, + r.pctTotalAdvances, + r.pctCapitalFunds, + r.assetClassification, + r.lossProvisionHeld + ]); + [ + 3, + 4, + 5, + 9 + ].forEach((c) => (row.getCell(c).numFmt = '#,##0.00')); + [ + 6, + 7 + ].forEach((c) => (row.getCell(c).numFmt = '0.00"%"')); + } + + const total = sheet.addRow([ + 'Total', + '', + null, + this.impairedFundedTotal, + this.impairedInterestTotal, + null, + null, + '', + this.impairedProvisionTotal + ]); + total.font = { bold: true }; + [ + 4, + 5, + 9 + ].forEach((c) => { + total.getCell(c).numFmt = '#,##0.00'; + total.getCell(c).fill = solidFill(XL.derivedBlue); + }); + boxAllCells(sheet, start, sheet.rowCount, 9); + } + + private buildExposureLimitsSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('ExposureLimits'); + sheet.columns = [ + { width: 16 }, + { width: 46 }, + { width: 18 }, + { width: 18 }, + { width: 20 }, + { width: 22 }, + { width: 20 } + ]; + addBanner(sheet, 'ExposureLimits - Single and Group Borrower Exposure Limit', 'G'); + addMatrixTable(sheet, this.capitalPreviousMarch); + + const start = sheet.rowCount + 1; + const title = sheet.addRow(['Table 2: STATEMENT ON LARGE EXPOSURE - SINGLE BORROWER']); + sheet.mergeCells(title.number, 1, title.number, 7); + title.font = { bold: true }; + title.getCell(1).fill = solidFill(XL.headerGrey); + + const sub = sheet.addRow([ + 'In excess of 15% of banks Tier 1 capital as on previous March or a minimum of 20 large credits' + ]); + sheet.mergeCells(sub.number, 1, sub.number, 7); + sub.font = { bold: true, size: 10 }; + sub.getCell(1).fill = solidFill(XL.headerGrey); + + const head = sheet.addRow([ + 'PAN of Borrower', + 'Name of Borrower', + 'Limit sanctioned', + '% to Total Advances', + 'Outstanding Balance', + '% to Tier 1 capital of the Bank', + 'Asset Classification' + ]); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; + head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); + + for (const r of this.largeExposures) { + const row = sheet.addRow([ + r.pan, + r.name, + r.limitSanctioned, + r.pctTotalAdvances, + r.outstandingBalance, + r.pctTier1Capital, + r.assetClassification + ]); + [ + 3, + 5 + ].forEach((c) => (row.getCell(c).numFmt = '#,##0.00')); + [ + 4, + 6 + ].forEach((c) => (row.getCell(c).numFmt = '0.00"%"')); + } + boxAllCells(sheet, start, sheet.rowCount, 7); + + for (const note of this.exposureNotes) { + const n = sheet.addRow([note]); + sheet.mergeCells(n.number, 1, n.number, 7); + n.font = { bold: true, size: 9 }; + n.alignment = { wrapText: true }; + n.getCell(1).fill = solidFill(XL.headerGrey); + } + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/rbi-circulars/rbi-circulars-routing.module.ts b/src/app/rbi-circulars/rbi-circulars-routing.module.ts index 3cb6f2329..d0feae670 100644 --- a/src/app/rbi-circulars/rbi-circulars-routing.module.ts +++ b/src/app/rbi-circulars/rbi-circulars-routing.module.ts @@ -24,6 +24,22 @@ const routes: Routes = [ path: 'rbi/aml', component: AmlRulesComponent, data: { title: 'AML Rules Master', breadcrumb: 'AML Rules Master' } + }, + { + path: 'rbi/oss1', + // Lazy — the ExcelJS workbook builder should not weigh down the eager bundle. + loadComponent: () => import('./oss1-ral/oss1-ral.component').then((m) => m.Oss1RalComponent), + data: { title: 'OSS 1 — Return on Assets and Liabilities', breadcrumb: 'OSS 1 (RAL)' } + }, + { + path: 'rbi/oss2', + loadComponent: () => import('./oss2-roe/oss2-roe.component').then((m) => m.Oss2RoeComponent), + data: { title: 'OSS 2 — Return on Earnings', breadcrumb: 'OSS 2 (RoE)' } + }, + { + path: 'rbi/oss3', + loadComponent: () => import('./oss3-raq/oss3-raq.component').then((m) => m.Oss3RaqComponent), + data: { title: 'OSS 3 — Return on Asset Quality', breadcrumb: 'OSS 3 (RAQ-UCB)' } } ]) ]; diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 52cc1da1e..b66d7426a 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260731', - 'hash': 'fd28788d' + 'version': '260801', + 'hash': 'b9b37397' }, 'allow_switching_backend_instance': true };