diff --git a/src/ro/onrc.spec.ts b/src/ro/onrc.spec.ts index 238f9e28..c0e1d68b 100644 --- a/src/ro/onrc.spec.ts +++ b/src/ro/onrc.spec.ts @@ -1,5 +1,10 @@ import { validate, format } from './onrc'; -import { InvalidComponent } from '../exceptions'; +import { + InvalidChecksum, + InvalidComponent, + InvalidFormat, + InvalidLength, +} from '../exceptions'; describe('ro/onrc', () => { it('format:J52/750/2012', () => { @@ -25,4 +30,104 @@ describe('ro/onrc', () => { expect(result.error).toBeInstanceOf(InvalidComponent); }); + + it('format:F5/1055/2013', () => { + const result = format('F5/1055/2013'); + + expect(result).toEqual('F05/1055/2013'); + }); + + it.each([ + ['F5/1055/2013', 'F05/1055/2013'], + ['J5/1243/2016', 'J05/1243/2016'], + ['J/2/750/2012', 'J02/750/2012'], + ['J1/384/13.05.2015', 'J01/384/2015'], + ])('validate:%s', (input: string, compact: string) => { + const result = validate(input); + + expect(result.isValid && result.compact).toEqual(compact); + }); + + it.each([ + ['J/52/750/2012', 'J52/750/2012'], + ['J 52 / 750 / 2012', 'J52/750/2012'], + [' J52/750/2012 ', 'J52/750/2012'], + ['F 26 1132/2007', 'F26/1132/2007'], + ['J22-1515-2007', 'J22/1515/2007'], + ])('validate:%s', (input: string, compact: string) => { + const result = validate(input); + + expect(result.isValid && result.compact).toEqual(compact); + }); + + it('validate:J22/1515/2007/123', () => { + const result = validate('J22/1515/2007/123'); + + expect(result.error).toBeInstanceOf(InvalidFormat); + }); + + it.each(['J52/100000/2012', 'J 52 / 750 / 200'])( + 'validate:%s', + (input: string) => { + const result = validate(input); + + expect(result.error).toBeInstanceOf(InvalidLength); + }, + ); + + it.each([ + 'J00/750/2012', + 'J41/750/2012', + 'J49/750/2012', + 'J002 /123 /2015', + 'J/52/750/1989', + 'J/52/750/2112', + ])('validate:%s', (input: string) => { + const result = validate(input); + + expect(result.error).toBeInstanceOf(InvalidComponent); + }); + + it('format:J2025007112004', () => { + const result = format('J2025007112004'); + + expect(result).toEqual('J2025007112004'); + }); + + it.each([ + 'J2025007112004', + 'J2012000750528', + 'C2005000007045', + 'J2016012984401', + 'J2024012585407', + 'J2024035703000', + ])('validate:%s', (input: string) => { + const result = validate(input); + + expect(result.isValid && result.compact).toEqual(input); + }); + + it('validate:J2012000750529', () => { + const result = validate('J2012000750529'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); + + it('validate:J201601298440', () => { + const result = validate('J201601298440'); + + expect(result.error).toBeInstanceOf(InvalidLength); + }); + + it.each([ + 'J1822012984405', // year too old + 'J2112012984408', // year in the future + 'J2016012984007', // county 00 before 2024 + 'J2024012984994', // invalid county in 2024 + 'J2025012984401', // county other than 00 after 2024 + ])('validate:%s', (input: string) => { + const result = validate(input); + + expect(result.error).toBeInstanceOf(InvalidComponent); + }); }); diff --git a/src/ro/onrc.ts b/src/ro/onrc.ts index 74e0f71c..64867eae 100644 --- a/src/ro/onrc.ts +++ b/src/ro/onrc.ts @@ -7,19 +7,28 @@ * registration year. This number can change when registration information * changes. * - * Source + * On 2024-07-26 a new 14 character format without slashes was introduced: + * the register letter, the registration year, a 6 digit serial number, the + * county code (00 for registrations after that date) and a check digit. Both + * formats are in use. + * + * Sources + * https://www.onrc.ro/index.php/ro/comunicate-de-presa-2024 + * https://github.com/arthurdejong/python-stdnum/blob/master/stdnum/ro/onrc.py * * ENTITY */ import * as exceptions from '../exceptions'; import { strings } from '../util'; +import { weightedSum } from '../util/checksum'; import { Validator, ValidateReturn } from '../types'; -const checkRe = /[A-Z]\d+\/\d{1,5}\/\d+/; +// old YJJ/XXXX/AAAA format +const oldFormatRe = /^[A-Z]\d+\/\d+\/\d+$/; function* genit() { - for (let i = 0; i <= 41; i += 1) { + for (let i = 1; i <= 40; i += 1) { yield i; } @@ -37,18 +46,98 @@ function clean(input: string): ReturnType { } const better = value + .trim() // Convert delimiters to '/' .replace(/[ /\\-]+/g, '/') // remove optional slash between first letter and county digits .replace(/^([A-Z])\//, '$1') - // normalize country to 2 digits - .replace(/([A-Z])(\d)\//, '$10$2') + // normalize county to 2 digits + .replace(/^([A-Z])(\d)\//, '$10$2/') // clean full dates .replace(/\/\d{2}[.]\d{2}[.](\d{4})$/, '/$1'); return [better, null]; } +function validateOldFormat(value: string): ValidateReturn { + if (!oldFormatRe.test(value)) { + return { isValid: false, error: new exceptions.InvalidFormat() }; + } + + const [county, serial, year] = value.substr(1).split('/'); + + if (serial.length > 5) { + return { isValid: false, error: new exceptions.InvalidLength() }; + } + if (county.length > 2 || !counties.has(parseInt(county, 10))) { + return { isValid: false, error: new exceptions.InvalidComponent() }; + } + if (year.length !== 4) { + return { isValid: false, error: new exceptions.InvalidLength() }; + } + // old format numbers are not issued after 2024 + const y = parseInt(year, 10); + if (y < 1990 || y > 2024) { + return { isValid: false, error: new exceptions.InvalidComponent() }; + } + + return { + isValid: true, + compact: value, + isIndividual: false, + isCompany: true, + }; +} + +function isValidCounty(year: number, county: number): boolean { + // numbers registered after 2024-07-26 have 00 as county code + if (year < 2024) { + return counties.has(county); + } + if (year === 2024) { + return county === 0 || counties.has(county); + } + + return county === 0; +} + +function validateNewFormat(value: string): ValidateReturn { + // new YAAAAXXXXXXJJC format + if (!strings.isdigits(value.substr(1))) { + return { isValid: false, error: new exceptions.InvalidFormat() }; + } + if (value.length !== 14) { + return { isValid: false, error: new exceptions.InvalidLength() }; + } + + const [, year, , county, check] = strings.splitAt(value, 1, 5, 11, 13); + const y = parseInt(year, 10); + + if (y < 1990 || y > new Date().getFullYear()) { + return { isValid: false, error: new exceptions.InvalidComponent() }; + } + if (!isValidCounty(y, parseInt(county, 10))) { + return { isValid: false, error: new exceptions.InvalidComponent() }; + } + + // the letter counts as its character code modulo 10 + const sum = weightedSum( + String(value.charCodeAt(0) % 10) + value.substr(1, 12), + { modulus: 10 }, + ); + + if (check !== String(sum)) { + return { isValid: false, error: new exceptions.InvalidChecksum() }; + } + + return { + isValid: true, + compact: value, + isIndividual: false, + isCompany: true, + }; +} + const impl: Validator = { name: 'Romanian Trade Register Identifier', localName: 'Ordine din Registrul Comerţului', @@ -76,32 +165,15 @@ const impl: Validator = { return { isValid: false, error }; } - if (!checkRe.test(value)) { - return { isValid: false, error: new exceptions.InvalidFormat() }; - } - + // J: legal entities, F: sole proprietorships and individual or family + // businesses, C: cooperative societies if (!'JFC'.includes(value[0])) { return { isValid: false, error: new exceptions.InvalidComponent() }; } - const [county, serial, year] = value.substr(1).split('/'); - - if (!counties.has(parseInt(county, 10))) { - return { isValid: false, error: new exceptions.InvalidComponent() }; - } - if (serial.length > 5) { - return { isValid: false, error: new exceptions.InvalidComponent() }; - } - if (year.length !== 4) { - return { isValid: false, error: new exceptions.InvalidComponent() }; - } - - return { - isValid: true, - compact: value, - isIndividual: false, - isCompany: true, - }; + return value.includes('/') + ? validateOldFormat(value) + : validateNewFormat(value); }, };