/** * 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/. */ /** Shape the KYC document-type picker works with. */ export interface KycDocumentType { name: string; code: string; mandatory: boolean; category?: string; } export const ENTITY_KYC_CATEGORY = 'Entity KYC documents'; /** * The KYC documents every legal entity must produce, whatever its constitution — RBI Master * Direction on KYC, Annexure I. Codes match PROOF_META in kyc-proof-types.ts, which is what * groups them under "Entity KYC documents" and supplies each chip's banner text. * * This is the single source for the entity list so a non-individual client sees the same * documents wherever the picker is rendered — the create stepper, the edit stepper, before a * constitution has been chosen and after, and once the backend KYC template has loaded. */ export const ENTITY_KYC_DOCUMENT_TYPES: { name: string; code: string }[] = [ { name: 'Certificate of Incorporation / Registration Certificate', code: 'CERT_INCORPORATION' }, { name: 'PAN Card of Entity', code: 'ENTITY_PAN' }, { name: 'Memorandum & Articles of Association / Partnership Deed / Trust Deed', code: 'CONSTITUTIVE_DOC' }, { name: 'Board Resolution / Authorization Letter', code: 'BOARD_RESOLUTION' }, { name: 'KYC of Authorized Signatory / Power of Attorney Holder', code: 'AUTH_SIGNATORY_KYC' }, { name: 'GST Registration Certificate', code: 'GST_CERTIFICATE' } ]; /** * Codes the backend KYC template uses for documents the entity list already covers * (ClientKycApiResource.getRequiredDocuments). Folding them onto the entity codes keeps one * chip per document instead of "PAN Card of Company" sitting next to "PAN Card of Entity". */ const ENTITY_DOC_CODE_ALIASES: Record = { CERT_INCORPORATION: 'CERT_INCORPORATION', COI: 'CERT_INCORPORATION', REG_CERT: 'CERT_INCORPORATION', ENTITY_PAN: 'ENTITY_PAN', PAN: 'ENTITY_PAN', CONSTITUTIVE_DOC: 'CONSTITUTIVE_DOC', MOA: 'CONSTITUTIVE_DOC', AOA: 'CONSTITUTIVE_DOC', PARTNERSHIP_DEED: 'CONSTITUTIVE_DOC', TRUST_DEED: 'CONSTITUTIVE_DOC', BOARD_RESOLUTION: 'BOARD_RESOLUTION', AUTH_SIGNATORY_KYC: 'AUTH_SIGNATORY_KYC', SIGNATORY_KYC: 'AUTH_SIGNATORY_KYC', GST_CERTIFICATE: 'GST_CERTIFICATE', GST_CERT: 'GST_CERTIFICATE' }; /** * The per-constitution lists carry no codes of their own (code === name), so those entries are * matched on the label instead. Patterns are anchored deliberately: a document that merely * mentions another one ("Proof of Business Existence (GST Certificate / Shop License)") is a * distinct requirement and has to survive as its own chip rather than vanish into GST. */ const ENTITY_DOC_NAME_PATTERNS: { pattern: RegExp; code: string }[] = [ { pattern: /^certificate of incorporation\b/, code: 'CERT_INCORPORATION' }, { pattern: /^registration certificate\b/, code: 'CERT_INCORPORATION' }, { pattern: /^(memorandum|articles of association|partnership deed|trust deed|llp agreement|by-?laws)\b/, code: 'CONSTITUTIVE_DOC' }, { pattern: /^pan\b/, code: 'ENTITY_PAN' }, { pattern: /^(board resolution|designated partners resolution|resolution authorizing)\b/, code: 'BOARD_RESOLUTION' }, { pattern: /^(kyc of|authorized signatory kyc|proprietor'?s kyc)\b/, code: 'AUTH_SIGNATORY_KYC' }, { pattern: /^gst (registration )?certificate\b/, code: 'GST_CERTIFICATE' } ]; /** The entity document this one duplicates, or null when it is a requirement of its own. */ function resolveEntityDocCode(doc: { name?: string; code?: string }): string | null { const code = (doc.code || '').toUpperCase().trim(); // A constitution list stores the label in `code`, so only treat it as a code when it is one. if (code && !code.includes(' ') && ENTITY_DOC_CODE_ALIASES[code]) { return ENTITY_DOC_CODE_ALIASES[code]; } const name = (doc.name || '').toLowerCase().trim(); return ENTITY_DOC_NAME_PATTERNS.find((p) => p.pattern.test(name))?.code || null; } /** * Puts the RBI entity documents in front of whatever list a caller has (per-constitution docs, * or the backend KYC template) instead of letting that list replace them. * * Without this the officer sees a different set of documents on every screen for the same * client: the entity list until a constitution is picked, the constitution's own list after, * and the backend template's list once the client exists and is reopened for edit. Anything in * `extra` that isn't already one of the six stays, grouped under `extraCategory` — those are * genuine constitution-specific requirements (Karta declaration, Udyam certificate, government * notification) and dropping them would lose real requirements. * * A mandatory flag on a duplicate carries over to the entity chip it folds into, so the * "at least one mandatory document" validator still sees it. */ export function withEntityKycDocuments( extra: { name: string; code?: string; mandatory?: boolean }[] = [], extraCategory?: string | null ): KycDocumentType[] { const mandatoryCodes = new Set(); const leftovers: KycDocumentType[] = []; extra.forEach((doc) => { const entityCode = resolveEntityDocCode(doc); if (entityCode) { if (doc.mandatory) { mandatoryCodes.add(entityCode); } return; } leftovers.push({ name: doc.name, code: doc.code || doc.name, mandatory: !!doc.mandatory, category: extraCategory ? `${extraCategory} documents` : 'Additional documents' }); }); const base: KycDocumentType[] = ENTITY_KYC_DOCUMENT_TYPES.map((d) => ({ name: d.name, code: d.code, mandatory: mandatoryCodes.has(d.code), category: ENTITY_KYC_CATEGORY })); return [ ...base, ...leftovers ]; }