1import { ValidationResult } from '../types/index.js';
2import {
3 normalizeIFSC,
4 normalizeMICR,
5 normalizePIN,
6 isValidIFSCFormat,
7 isValidMICRFormat,
8 isValidPINFormat,
9 isValidSWIFTFormat,
10 extractBankCodeFromIFSC,
11} from '../utils/helpers.js';
12import logger from '../utils/logger.js';
13
14export class IFSCValidator {
15 private static readonly VALID_BANK_CODES = new Set([
16 'SBIN', 'ICIC', 'HDFC', 'KKBK', 'AXIS', 'PNB', 'BOB', 'UNI', 'IDIB',
17 'CBIN', 'CNRB', 'SYNB', 'ANDB', 'VIJB', 'KARB', 'INDB', 'YESB', 'FDRL',
18 'BDBL', 'JAKA', 'RATN', 'ESAF', 'SIBL', 'CITI', 'SCBL', 'HSBC', 'DEUT',
19 'BARB', 'UBIN', 'ALLA', 'BKID', 'MAHB', 'PSIB', 'CORP', 'ORBC', 'CANA',
20 'SBBJ', 'SBHY', 'SBMY', 'SBT', 'UCBA', 'WBSC', 'ABHY', 'ACBX',
21 'ADBX', 'AGVX', 'AIRP', 'APBL', 'APGV', 'ASBL', 'AUBL', 'BACB', 'BAGB',
22 'BAJD', 'BARC', 'BCEY', 'BCBM', 'BDBS', 'BDNF', 'BDRN', 'BEHL',
23 'BFSB', 'BGCB', 'BGSB', 'BHB', 'BHOJ', 'BHSB', 'BKDN', 'BKSX',
24 'BMCB', 'BMBL', 'BMCX', 'BNPA', 'BNSB', 'BOFA', 'BOMR', 'BOTM', 'BPCX',
25 'BRCX', 'BRDX', 'BRGB', 'BSBX', 'BUCX', 'BUGS', 'BUNX', 'BUSX', 'BWMX',
26 'CCBL', 'CCCX', 'CGBX', 'CHAS', 'CHBX', 'CIUB', 'CJAX',
27 'CLBL', 'CMCB', 'CNSX', 'COAS', 'COSB', 'CPDX', 'CRLY', 'CRSX',
28 'CSBX', 'CTBA', 'CTCB', 'CTOX', 'CUBX', 'CUSX', 'DBSS', 'DCBL', 'DCSX',
29 'DEGX', 'DENS', 'DHBX', 'DICG', 'DICX', 'DLSC', 'DNSB', 'DOHB',
30 'DSAX', 'DSCB', 'DSSX', 'ECSX', 'EDSX', 'EIBI', 'ESFB', 'ETCX',
31 'EUCX', 'EWBX', 'FCBX', 'FDFX', 'FEKX', 'FGCB', 'FINO', 'FIRN',
32 'FSFB', 'GADX', 'GBCB', 'GCBX', 'GCUL', 'GDCB', 'GGSX', 'GMBX', 'GNCX',
33 'GNSX', 'GODX', 'GOSX', 'GPCX', 'GRAX', 'GSCB', 'GSSX', 'GTCX', 'GUBX',
34 'HAMX', 'HARC', 'HCBL', 'HCBX', 'HDFC', 'HDBX', 'HPSC', 'HSBC',
35 'HSSX', 'HUTX', 'IBBK', 'IBKL', 'IBKO', 'ICBK', 'ICIC', 'ICLL', 'IDFB',
36 'IDIB', 'IDUK', 'ILCB', 'IMCX', 'IMPX', 'INDX', 'IOBA', 'IPCX', 'IPOS',
37 'IPPB', 'ISBX', 'ITBL', 'ITCX', 'JAKA', 'JAMX', 'JASB', 'JASX', 'JCBX',
38 'JDCX', 'JDLX', 'JGBX', 'JIBX', 'JIMX', 'JIOX', 'JJSB', 'JKAX', 'JKBX',
39 'JMSX', 'JPCX', 'JRKX', 'JSAX', 'JSBL', 'JSBP', 'JSFB', 'JSKX', 'JSSX',
40 'JSVX', 'JTIX', 'JUCX', 'JUSX', 'KACE', 'KACX', 'KAIJ', 'KALX', 'KAMX',
41 'KANG', 'KANT', 'KARB', 'KARX', 'KASX', 'KATX', 'KAYX', 'KBCX', 'KBNX',
42 'KBSX', 'KCBL', 'KCCB', 'KCCX', 'KDCB', 'KDCX', 'KDIX', 'KDKN', 'KDMX',
43 'KEJX', 'KESX', 'KGSX', 'KHAX', 'KHDX', 'KHNX', 'KIBL', 'KICX',
44 'KJSB', 'KJSX', 'KKBL', 'KKBK', 'KLGB', 'KMBL', 'KMSX', 'KNCX', 'KNSB',
45 'KNSX', 'KOBX', 'KOLH', 'KOSX', 'KOTX', 'KPCX', 'KRCX', 'KRDX', 'KRIX',
46 'KRKX', 'KRMX', 'KRSX', 'KSBX', 'KSCB', 'KSMX', 'KSWX', 'KTCX', 'KTDX',
47 'KUBX', 'KUCB', 'KUCX', 'KUKX', 'KULX', 'KUMX', 'KUNX', 'KURX', 'KVBL',
48 'KVCX', 'KVGB', 'LAVB', 'LBCX', 'LCBX', 'LCNX', 'LDCX', 'LDPX', 'LICB',
49 'LICX', 'LKBL', 'LKMX', 'LNSX', 'LOKX', 'LONX', 'LUCX', 'MAHB', 'MAHX',
50 'MAJX', 'MAKX', 'MALX', 'MAMX', 'MAOX', 'MASH', 'MASX', 'MAUX', 'MAVX',
51 'MAWX', 'MAYX', 'MBGX', 'MCCX', 'MCIX', 'MCNB', 'MDCB', 'MDCX', 'MDEX',
52 'MDIX', 'MDMX', 'MDPX', 'MEDX', 'MERX', 'MFCX', 'MFGX', 'MGCX', 'MHCX',
53 'MHMX', 'MHNX', 'MHSX', 'MICX', 'MIDX', 'MIRX', 'MIZX', 'MKSX', 'MLCG',
54 'MMCX', 'MNCX', 'MNSX', 'MOGX', 'MOSB', 'MOSX', 'MPDX', 'MPRX', 'MRBX',
55 'MRTX', 'MSAX', 'MSBL', 'MSCI', 'MSHQ', 'MSLX', 'MSNU', 'MSOX', 'MSRX',
56 'MSSX', 'MUBL', 'MUCX', 'MUNX', 'MURX', 'MUSX', 'MVIX', 'MYAX', 'MYSX',
57 'MZRX', 'NABX', 'NACX', 'NAGX', 'NAIX', 'NANX', 'NASX', 'NATX', 'NAUX',
58 'NBCX', 'NCBL', 'NCCX', 'NCOX', 'NCUB', 'NDCX', 'NESF', 'NEYX', 'NGBX',
59 'NGSB', 'NICB', 'NIRX', 'NKGS', 'NLBX', 'NMCB', 'NMCX', 'NMGB', 'NNSB',
60 'NOBX', 'NOSC', 'NPCX', 'NPKL', 'NRDX', 'NSBB', 'NSBX', 'NSCX', 'NSGX',
61 'NSIX', 'NSMB', 'NSPB', 'NSPX', 'NTBL', 'NUCB', 'NUVX', 'NVNM', 'NVSB',
62 'OIBA', 'OICX', 'OMCX', 'ONSX', 'ORBC', 'OSMX', 'PABX', 'PACX', 'PALX',
63 'PANX', 'PARX', 'PASX', 'PATX', 'PAYX', 'PBCX', 'PCBX', 'PCCX', 'PCMX',
64 'PDBX', 'PDNX', 'PDSX', 'PDUX', 'PETX', 'PFCX', 'PGBX', 'PGCX', 'PHIX',
65 'PICX', 'PIRX', 'PKBX', 'PMCB', 'PMEC', 'PMNX', 'PNBX', 'PNCX', 'PNSX',
66 'PPBX', 'PRCX', 'PRDX', 'PRGX', 'PRHX', 'PRIY', 'PRKX', 'PRLX', 'PRMX',
67 'PROX', 'PRPX', 'PRSX', 'PRTX', 'PSBX', 'PSCX', 'PSIB', 'PSRX', 'PSSX',
68 'PTCX', 'PTIX', 'PUBX', 'PUDX', 'PUNB', 'PUNX', 'PURX', 'PUCX', 'PVAX',
69 'PVBX', 'PVCX', 'PVVX', 'PWNX', 'PYTM', 'QNBA', 'RABO', 'RACX', 'RAEX',
70 'RAJX', 'RAMX', 'RANX', 'RATN', 'RBBX', 'RBCX', 'RCBX', 'RCCX', 'RCDX',
71 'RDCX', 'RGSB', 'RHMX', 'RICX', 'RJCX', 'RJDX', 'RJIX', 'RJSB', 'RKCX',
72 'RLCX', 'RMCX', 'RNSB', 'ROCX', 'ROHX', 'RPDX', 'RSCB', 'RSSX', 'RSUX',
73 'RSVX', 'RUCX', 'RUSX', 'RVSX', 'SABR', 'SACB', 'SACX', 'SADX', 'SAGX',
74 'SAHE', 'SAHX', 'SAIX', 'SALX', 'SAMX', 'SANT', 'SANX', 'SAPX', 'SARX',
75 'SASX', 'SATX', 'SAUX', 'SAVX', 'SBCX', 'SBHY', 'SBIN', 'SBIS', 'SBJX',
76 'SBKX', 'SBLS', 'SBMX', 'SBNX', 'SBPX', 'SBRD', 'SBSX', 'SBTR', 'SBUJ',
77 'SCBL', 'SCCX', 'SCMX', 'SCNX', 'SCOX', 'SCSX', 'SCUB', 'SDCB', 'SDCX',
78 'SDHX', 'SDSX', 'SEBX', 'SEMX', 'SENX', 'SEPX', 'SFCX', 'SGBX', 'SGDX',
79 'SGSX', 'SHAX', 'SHBK', 'SHCB', 'SHCX', 'SHEX', 'SHGX', 'SHIX', 'SHKX',
80 'SHMX', 'SHNX', 'SHSX', 'SIBL', 'SICX', 'SIDC', 'SIDX', 'SIGX', 'SIHX',
81 'SIKX', 'SINX', 'SIRX', 'SJSX', 'SKCX', 'SKKX', 'SKSB', 'SLAX', 'SMBX',
82 'SMCB', 'SMCX', 'SMEX', 'SMNB', 'SMNX', 'SMPX', 'SMRX', 'SMSX', 'SNCX',
83 'SNKX', 'SNLX', 'SNPX', 'SNSV', 'SOGE', 'SOLX', 'SONX', 'SPBX', 'SPCB',
84 'SPCX', 'SPDX', 'SPKX', 'SPMX', 'SPNX', 'SPSX', 'SRCB', 'SRDX', 'SRHX',
85 'SRKX', 'SRLX', 'SRMX', 'SRSX', 'SSBX', 'SSDX', 'SSHX', 'SSKX', 'SSMX',
86 'SSNX', 'SSOX', 'SSRX', 'STBP', 'STCB', 'STCX', 'STDX', 'STLX', 'STRX',
87 'SUBX', 'SUCX', 'SUDX', 'SUEX', 'SURX', 'SURY', 'SUSB', 'SUSX', 'SUTB',
88 'SVAX', 'SVBL', 'SVCX', 'SVGX', 'SVMC', 'SVNX', 'SVSB', 'SWDX', 'SXCX',
89 'SXKX', 'SYNB', 'TACX', 'TADX', 'TAMX', 'TASX', 'TBCX', 'TBDX', 'TBHX',
90 'TBMX', 'TBPX', 'TBSX', 'TBTX', 'TCBX', 'TCCX', 'TDCB', 'TDCX', 'TDIX',
91 'TDMX', 'TDSX', 'TECX', 'TEMX', 'TFCX', 'TGBX', 'TGDX', 'TGMB', 'TGSB',
92 'THCX', 'THMX', 'THRX', 'TIBX', 'TIDX', 'TIRX', 'TJCX', 'TKAX', 'TKCX',
93 'TKDX', 'TKMX', 'TKSX', 'TMBL', 'TMCX', 'TMNX', 'TMPX', 'TMSX', 'TNCX',
94 'TNDC', 'TNDX', 'TNGX', 'TNIX', 'TNPX', 'TNSC', 'TOCX', 'TPCX', 'TPDX',
95 'TPSX', 'TRAX', 'TRDX', 'TRSX', 'TSAB', 'TSAX', 'TSBX', 'TSCX', 'TSDX',
96 'TSIX', 'TSKX', 'TSMX', 'TSNX', 'TSRX', 'TSSX', 'TSUX', 'TTBX', 'TTCB',
97 'TTCX', 'TTDX', 'TTEX', 'TUBX', 'TUCX', 'TUDX', 'TUMX', 'TUNX', 'TVDX',
98 'TVPX', 'UBIN', 'UBSW', 'UCBA', 'UCDX', 'UCOX', 'UDGX', 'UJSX',
99 'UJVN', 'UKGX', 'UMBX', 'UMCX', 'UMSX', 'UNIX', 'UNMX', 'UOEX', 'UPCB',
100 'URBN', 'USFB', 'UTBI', 'UTBX', 'UTGX', 'UTIB', 'UTKX', 'UTMX', 'UTSX',
101 'UUCB', 'UUCX', 'VADX', 'VAIX', 'VANX', 'VARA', 'VASJ', 'VASX', 'VAVX',
102 'VCBX', 'VCNX', 'VCOB', 'VDCX', 'VDYX', 'VGBX', 'VHDX', 'VICX', 'VIDX',
103 'VJAX', 'VJSX', 'VKCX', 'VKSX', 'VMMX', 'VNSX', 'VOCX', 'VRDX',
104 'VSBL', 'VSBX', 'VSCX', 'VSSX', 'VSVX', 'VUCX', 'VVSB', 'WACX',
105 'WARX', 'WBSC', 'WCBX', 'WDCX', 'WKGX', 'WNBX', 'WRCX', 'WUCX', 'WZUX',
106 'XJKG', 'YADX', 'YANX', 'YAVX', 'YBSX', 'YCBX', 'YDCX', 'YESB', 'YLDX',
107 'YMNX', 'ZBBX', 'ZBSX', 'ZCBL', 'ZIBX', 'ZILX', 'ZIMX', 'ZISX', 'ZKBX',
108 'ZLLX', 'ZMMX', 'ZRNX', 'ZSBX', 'ZSGX', 'ZSIX', 'ZSLX', 'ZSMX', 'ZSNX',
109 'ZSPX', 'ZSRX', 'ZSSX', 'ZSUX', 'ZSVX', 'ZUCX', 'ZSAX',
110 ]);
111
112 public static validateIFSC(ifsc: string): ValidationResult {
113 const normalized = normalizeIFSC(ifsc);
114 const errors: string[] = [];
115
116 if (!normalized) {
117 errors.push('IFSC code is required');
118 return { valid: false, field: 'ifsc', value: ifsc, errors };
119 }
120
121 if (normalized.length !== 11) {
122 errors.push(`IFSC must be exactly 11 characters, got ${normalized.length}`);
123 }
124
125 if (!isValidIFSCFormat(normalized)) {
126 errors.push('Invalid IFSC format. Expected: AAAA0NNNNNN (4 letters + 0 + 6 alphanumeric)');
127 }
128
129 const bankCode = extractBankCodeFromIFSC(normalized);
130 if (!this.VALID_BANK_CODES.has(bankCode)) {
131 errors.push(`Unknown bank code: ${bankCode}`);
132 }
133
134 const valid = errors.length === 0;
135 logger.debug({ ifsc: normalized, valid, errors }, 'IFSC validation');
136
137 return {
138 valid,
139 field: 'ifsc',
140 value: ifsc,
141 errors,
142 normalizedValue: valid ? normalized : undefined,
143 };
144 }
145
146 public static validateMICR(micr: string): ValidationResult {
147 const normalized = normalizeMICR(micr);
148 const errors: string[] = [];
149
150 if (!normalized) {
151 errors.push('MICR code is required');
152 return { valid: false, field: 'micr', value: micr, errors };
153 }
154
155 if (normalized.length !== 9) {
156 errors.push(`MICR must be exactly 9 digits, got ${normalized.length}`);
157 }
158
159 if (!isValidMICRFormat(normalized)) {
160 errors.push('Invalid MICR format. Expected: 9 digits');
161 }
162
163 const valid = errors.length === 0;
164 return {
165 valid,
166 field: 'micr',
167 value: micr,
168 errors,
169 normalizedValue: valid ? normalized : undefined,
170 };
171 }
172
173 public static validateBankCode(bankCode: string): ValidationResult {
174 const normalized = bankCode.toUpperCase().trim();
175 const errors: string[] = [];
176
177 if (!normalized) {
178 errors.push('Bank code is required');
179 return { valid: false, field: 'bankCode', value: bankCode, errors };
180 }
181
182 if (normalized.length !== 4) {
183 errors.push(`Bank code must be exactly 4 characters, got ${normalized.length}`);
184 }
185
186 if (!/^[A-Z]{4}$/.test(normalized)) {
187 errors.push('Invalid bank code format. Expected: 4 letters');
188 }
189
190 const valid = errors.length === 0;
191 return {
192 valid,
193 field: 'bankCode',
194 value: bankCode,
195 errors,
196 normalizedValue: valid ? normalized : undefined,
197 };
198 }
199
200 public static validatePIN(pincode: string): ValidationResult {
201 const normalized = normalizePIN(pincode);
202 const errors: string[] = [];
203
204 if (!normalized) {
205 errors.push('PIN code is required');
206 return { valid: false, field: 'pincode', value: pincode, errors };
207 }
208
209 if (normalized.length !== 6) {
210 errors.push(`PIN code must be exactly 6 digits, got ${normalized.length}`);
211 }
212
213 if (!isValidPINFormat(normalized)) {
214 errors.push('Invalid PIN format. Expected: 6 digits');
215 }
216
217 const valid = errors.length === 0;
218 return {
219 valid,
220 field: 'pincode',
221 value: pincode,
222 errors,
223 normalizedValue: valid ? normalized : undefined,
224 };
225 }
226
227 public static validateSWIFT(swift: string): ValidationResult {
228 const normalized = swift.toUpperCase().trim().replace(/\s/g, '');
229 const errors: string[] = [];
230
231 if (!normalized) {
232 errors.push('SWIFT code is required');
233 return { valid: false, field: 'swift', value: swift, errors };
234 }
235
236 if (normalized.length !== 8 && normalized.length !== 11) {
237 errors.push(`SWIFT must be 8 or 11 characters, got ${normalized.length}`);
238 }
239
240 if (!isValidSWIFTFormat(normalized)) {
241 errors.push('Invalid SWIFT format. Expected: AAAA BB CC (DDD)');
242 }
243
244 const valid = errors.length === 0;
245 return {
246 valid,
247 field: 'swift',
248 value: swift,
249 errors,
250 normalizedValue: valid ? normalized : undefined,
251 };
252 }
253
254 public static validateBranch(branch: string): ValidationResult {
255 const normalized = branch.trim();
256 const errors: string[] = [];
257
258 if (!normalized) {
259 errors.push('Branch name is required');
260 return { valid: false, field: 'branch', value: branch, errors };
261 }
262
263 if (normalized.length < 2) {
264 errors.push('Branch name must be at least 2 characters');
265 }
266
267 if (normalized.length > 100) {
268 errors.push('Branch name must not exceed 100 characters');
269 }
270
271 const valid = errors.length === 0;
272 return {
273 valid,
274 field: 'branch',
275 value: branch,
276 errors,
277 normalizedValue: valid ? normalized : undefined,
278 };
279 }
280
281 public static validateAddress(address: string): ValidationResult {
282 const normalized = address.trim();
283 const errors: string[] = [];
284
285 if (!normalized) {
286 errors.push('Address is required');
287 return { valid: false, field: 'address', value: address, errors };
288 }
289
290 if (normalized.length < 5) {
291 errors.push('Address must be at least 5 characters');
292 }
293
294 if (normalized.length > 500) {
295 errors.push('Address must not exceed 500 characters');
296 }
297
298 const valid = errors.length === 0;
299 return {
300 valid,
301 field: 'address',
302 value: address,
303 errors,
304 normalizedValue: valid ? normalized : undefined,
305 };
306 }
307
308 public static validateDistrict(district: string): ValidationResult {
309 const normalized = district.trim();
310 const errors: string[] = [];
311
312 if (!normalized) {
313 errors.push('District is required');
314 return { valid: false, field: 'district', value: district, errors };
315 }
316
317 if (normalized.length < 2) {
318 errors.push('District must be at least 2 characters');
319 }
320
321 const valid = errors.length === 0;
322 return {
323 valid,
324 field: 'district',
325 value: district,
326 errors,
327 normalizedValue: valid ? normalized : undefined,
328 };
329 }
330
331 public static validateState(state: string): ValidationResult {
332 const normalized = state.trim();
333 const errors: string[] = [];
334
335 if (!normalized) {
336 errors.push('State is required');
337 return { valid: false, field: 'state', value: state, errors };
338 }
339
340 if (normalized.length < 2) {
341 errors.push('State must be at least 2 characters');
342 }
343
344 const valid = errors.length === 0;
345 return {
346 valid,
347 field: 'state',
348 value: state,
349 errors,
350 normalizedValue: valid ? normalized : undefined,
351 };
352 }
353
354 public static validateCity(city: string): ValidationResult {
355 const normalized = city.trim();
356 const errors: string[] = [];
357
358 if (!normalized) {
359 errors.push('City is required');
360 return { valid: false, field: 'city', value: city, errors };
361 }
362
363 if (normalized.length < 2) {
364 errors.push('City must be at least 2 characters');
365 }
366
367 const valid = errors.length === 0;
368 return {
369 valid,
370 field: 'city',
371 value: city,
372 errors,
373 normalizedValue: valid ? normalized : undefined,
374 };
375 }
376
377 public static validateUPI(upiEnabled: boolean): ValidationResult {
378 return {
379 valid: true,
380 field: 'upi',
381 value: String(upiEnabled),
382 errors: [],
383 };
384 }
385
386 public static validateNEFT(neftEnabled: boolean): ValidationResult {
387 return {
388 valid: true,
389 field: 'neft',
390 value: String(neftEnabled),
391 errors: [],
392 };
393 }
394
395 public static validateRTGS(rtgsEnabled: boolean): ValidationResult {
396 return {
397 valid: true,
398 field: 'rtgs',
399 value: String(rtgsEnabled),
400 errors: [],
401 };
402 }
403
404 public static validateIMPS(impsEnabled: boolean): ValidationResult {
405 return {
406 valid: true,
407 field: 'imps',
408 value: String(impsEnabled),
409 errors: [],
410 };
411 }
412}
413
414export default IFSCValidator;