UNPKG

rntrc

Version:

Library for decoding and generating Ukrainian taxpayer identification numbers

203 lines (201 loc) 7.78 kB
/** * Represents an RNTRC (Ukrainian personal tax identifier with embedded information about birthdate and gender). * * @see {@link https://uk.wikipedia.org/wiki/Реєстраційний_номер_облікової_картки_платника_податків|RNTRC on Ukrainian Wikipedia} * @see {@link https://zakon.rada.gov.ua/laws/show/z1306-17#n230|RNTRC number structure} */ class Rntrc { /** * Creates a new RNTRC instance * * @param {string | number} rntrc - The RNTRC number * @param {boolean} [ignoreInvalid = false] - Whether to skip validation of the RNTRC number * * @throws {Error} If the RNTRC is invalid and ignoreInvalid is false, or RNTRC isn't 10 digits. * * @see {@link https://uk.wikipedia.org/wiki/Реєстраційний_номер_облікової_картки_платника_податків|RNTRC on Ukrainian Wikipedia} * @see {@link https://zakon.rada.gov.ua/laws/show/z1306-17#n230|RNTRC number structure} */ constructor(rntrc, ignoreInvalid) { this.value = String(rntrc); this.ignoreInvalid = ignoreInvalid !== null && ignoreInvalid !== void 0 ? ignoreInvalid : false; if ((!this.ignoreInvalid && !this.valid()) || this.value.length !== 10 || !/^\d{10}$/.test(this.value)) { throw new Error('Invalid RNTRC'); } } /** * Gets the RNTRC number string. * * @returns {string} RNTRC number string * */ toString() { return this.value; } [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); } /** * Defines how the object should be converted to a primitive value * Called automatically during type coercion operations like string * concatenation, mathematical operations, and equality comparisons * * @returns {string} The primitive string representation of the TIN * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive} */ [Symbol.toPrimitive]() { return this.toString(); } /** * Gets the birthdate encoded in the RNTRC * * @returns {Date} The birthdate */ birthdate() { const days = parseInt(this.value.slice(0, 5), 10); return new Date(Rntrc.BASE_DATE_MS + days * Rntrc.MILLIS_PER_DAY); } age() { const birthdate = this.birthdate(); const now = new Date(); let years = now.getFullYear() - birthdate.getFullYear(); let months = now.getMonth() - birthdate.getMonth(); let days = now.getDate() - birthdate.getDate(); if (days < 0) { months -= 1; const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0); days += prevMonth.getDate(); } if (months < 0) { years -= 1; months += 12; } return { years, months, days }; } /** * Gets the gender encoded in the RNTRC * * @returns {Gender} The gender ('male' or 'female') */ gender() { return Number(this.value[8]) % 2 !== 0 ? 'male' : 'female'; } /** * Checks if the RNTRC belongs to a male person * * @returns {boolean} True if male, false otherwise */ male() { return this.gender() === 'male'; } /** * Checks if the RNTRC belongs to a female person * * @returns {boolean} True if female, false otherwise */ female() { return this.gender() === 'female'; } /** * Gets sequential number of the Облікова картка № 1ДР * * @returns {string} The sequential number from RNTRC */ accountingCardNumber() { return this.value.slice(5, 9); } static checksum(rntrc) { const digits = String(rntrc) .split('') .map((d) => parseInt(d, 10)); const len = Math.min(digits.length, Rntrc.WEIGHTS.length); let sum = 0; for (let i = 0; i < len; i++) { sum += digits[i] * Rntrc.WEIGHTS[i]; } let checksum = (sum % 11) % 10; if (checksum === 10) checksum = 0; return checksum.toString(); } /** * Validates the RNTRC number * * @returns {boolean} True if the RNTRC is valid, false otherwise */ valid() { if (this.value[8] === '0') return false; return this.value[9] === Rntrc.checksum(this.value); } /** * Generate a random RNTRC instance. * * Generates a valid RNTRC based on the provided parameters. If parameters are not provided, * random valid values will be used. The generated RNTRC will include a birthdate between * 1900-01-01 and 2173-10-14, a random record number and a gender digit based on the * specified or random gender. * * @param {GenerateParams} params - Configuration object for RNTRC generation * @param {string | number} [params.day] - Day of birth * @param {string | number} [params.month] - Month of birth * @param {string | number} [params.year] - Year of birth * @param {Gender} [params.gender] - Gender of the person * * @returns {Rntrc} A new valid RNTRC instance * * @throws {Error} If date is invalid or outside allowed range * @throws {Error} If gender value is invalid */ static generate({ day, month, year, gender } = {}) { const today = new Date(); const random = (min, max) => { return Math.floor(Math.random() * (max - min + 1)) + min; }; year = isNaN(Number(year)) ? random(1900, today.getFullYear()) : Number(year); const maxMonth = year === today.getFullYear() ? today.getMonth() + 1 : new Date(year, 11, 31).getMonth() + 1; month = isNaN(Number(month)) ? random(1, maxMonth) : Number(month); const maxDay = year === today.getFullYear() && month === today.getMonth() + 1 ? today.getDate() : new Date(year, month, 0).getDate(); day = isNaN(Number(day)) ? random(1, maxDay) : Number(day); const testDate = new Date(Date.UTC(year, month - 1, day)); if (testDate.getDate() !== day || testDate.getMonth() !== month - 1 || testDate.getFullYear() !== year) { throw new Error(`Invalid date: ${year}-${month}-${day}`); } const birthdate = new Date(Date.UTC(year, month - 1, day)).getTime(); if (birthdate < Rntrc.MIN_DATE.getTime() || birthdate > Rntrc.MAX_DATE.getTime()) throw new Error(`Birthdate must be between ${Rntrc.MIN_DATE.toLocaleDateString()} and ${Rntrc.MAX_DATE.toLocaleDateString()}`); const days = String(Math.floor((birthdate - Rntrc.BASE_DATE_MS) / Rntrc.MILLIS_PER_DAY)).padStart(5, '0'); const recordNumeric = String(random(0, 999)).padStart(3, '0'); let genderNumeric; if (!gender) { genderNumeric = random(1, 8); } else if (gender === 'female') { genderNumeric = random(0, 3) * 2 + 2; } else if (gender === 'male') { genderNumeric = random(0, 4) * 2 + 1; } else { throw new Error('Gender must be either "male" or "female"'); } const rntrc = days + recordNumeric + genderNumeric; return new Rntrc(rntrc + Rntrc.checksum(rntrc)); } } Rntrc.WEIGHTS = [-1, 5, 7, 9, 4, 6, 10, 5, 7]; Rntrc.BASE_DATE_MS = Date.UTC(1899, 11, 31); Rntrc.MIN_DATE = new Date(Date.UTC(1900, 0, 1)); Rntrc.MAX_DATE = new Date(Date.UTC(2173, 9, 14)); Rntrc.MILLIS_PER_DAY = 86400000; export { Rntrc as default };