import { RuleError } from './errors'; const DIGITS_ONLY = /^\d{1,8}$/; /** * RULES.md section 2, modulo 11 basis 2: * right to left, multiply each digit by 2,3,4,5,6,7,8,9 then cycle back to 2, * sum, r = sum % 11, dv = r > 1 ? 11 - r : 0. * * TODO-TAX-VERIFY: confirm against at least five real published RUCs. The seed and the * fixtures generate their RUCs with this same function, so the product stays internally * consistent whatever the verification concludes. Pinned by ruc.test.ts. */ export function computeRucDv(base: string): number { assertRucBase(base); let factor = 2; let sum = 0; for (let i = base.length - 1; i >= 0; i--) { sum += Number(base[i]) * factor; factor = factor === 9 ? 2 : factor + 1; } const remainder = sum % 11; return remainder > 1 ? 11 - remainder : 0; } /** True when `dv` is the check digit for `base`. Never throws: invalid input is invalid. */ export function validateRuc(base: string, dv: number): boolean { if (!DIGITS_ONLY.test(base)) return false; if (!Number.isInteger(dv) || dv < 0 || dv > 9) return false; return computeRucDv(base) === dv; } /** * The digit that decides the calendario perpetuo day: the last digit of the base, NOT the * check digit (RULES.md section 2). */ export function deadlineDigit(base: string): number { assertRucBase(base); return Number(base[base.length - 1]); } /** `4123456` and `4` become `4123456-4`. */ export function formatRuc(base: string, dv: number): string { return `${base}-${dv}`; } /** Splits `4123456-4` into its parts. Returns null for anything malformed. */ export function parseRuc(value: string): { base: string; dv: number } | null { const match = /^(\d{1,8})-(\d)$/.exec(value.trim()); if (!match) return null; return { base: match[1] as string, dv: Number(match[2]) }; } function assertRucBase(base: string): void { if (!DIGITS_ONLY.test(base)) { throw new RuleError('invalid_ruc', `RUC base must be 1 to 8 digits, got: ${base}`, { base }); } }