/** * 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; }