diff --git a/README.md b/README.md index 5c70b4c..db9914b 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ All country validators are in the "namespace" of the ISO country code. | Estonia | EE | IK | Person | Isikukood (Estonian Personcal ID number). | | Estonia | EE | KMKR | Company | KMKR (Käibemaksukohuslase, Estonian VAT number) | | Estonia | EE | Registrikood | Company | Registrikood (Estonian organisation registration code) | +| Egypt | EG | TN | Company | Tax Registration Number (الرقم الضريبي, Egypt tax number) | | Ecuador | EC | RUC | Tax/Vat | Ecuadorian company tax number (Registro Único de Contribuyentes) | | El Salvador | SV | NIT | Tax | Tax Identifier (Número de Identificación Tributaria) | | Finland | FI | ALV | Company | ALV nro (Arvonlisäveronumero, Finnish VAT number) | diff --git a/bin/create-validator.py b/bin/create-validator.py new file mode 100755 index 0000000..b92ef34 --- /dev/null +++ b/bin/create-validator.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import os +import re +import os.path +import sys + +basedir = os.path.dirname(__file__) + +RE = r'{{\s*([a-zA-Z_]+[a-zA-Z0-9_])\s*}}' + +def fatal(msg): + print(msg) + sys.exit(1) + +def replace(tmpl, params): + return re.sub(RE, lambda match: params.get(match.group(1),''), tmpl) + +def main(kind): + with open(os.path.join(basedir, 'tmpl', 'tin.ts')) as fd: + tinTmpl = fd.read() + with open(os.path.join(basedir, 'tmpl', 'tin.spec.ts')) as fd: + specTmpl = fd.read() + + parts = os.path.split(kind) + if len(parts) != 2: + fatal("expected path should be COUNTRY/TIN") + if '.' in parts[1]: + fatal("found . in path") + + root = os.path.join(basedir, '..', 'src', parts[0]) + + if not os.path.isdir(root): + os.mkdir(root) + + tinFile = os.path.join(root, "{}.ts".format(parts[1])) + specFile = os.path.join(root, "{}.spec.ts".format(parts[1])) + + if os.path.isfile(tinFile): + fatal("TIN file exists {}".format(os.path.normpath(tinFile))) + if os.path.isfile(specFile): + fatal("SPEC file exists {}".format(os.path.normpath(specFile))) + + params = { + 'country': parts[0], + 'tincode': parts[1], + 'tincode_upper': parts[1].upper(), + } + + with open(tinFile, 'w') as fd: + fd.write(replace(tinTmpl, params)) + with open(specFile, 'w') as fd: + fd.write(replace(specTmpl, params)) + with open(os.path.join(root, 'index.ts'), 'a') as fd: + fd.write("export * as {tincode} from './{tincode}';\n".format(**params)) + + +main(sys.argv[1]) diff --git a/bin/tmpl/tin.spec.ts b/bin/tmpl/tin.spec.ts new file mode 100644 index 0000000..a965b38 --- /dev/null +++ b/bin/tmpl/tin.spec.ts @@ -0,0 +1,34 @@ +import { validate, format } from './{{tincode}}'; +import { InvalidLength, InvalidChecksum } from '../exceptions'; + +describe('{{country}}/{{tincode}}', () => { + it('format:VALUE', () => { + const result = format('VALUE'); + + expect(result).toEqual('VALUE'); + }); + + it('fvalidate:VALUE', () => { + const result = validate('VALUE'); + + expect(result.isValid && result.compact).toEqual('VALUE'); + }); + + test.each(['VALUE1', 'VALUE2'])('validate:%s', value => { + const result = validate(value); + + expect(result.isValid).toEqual(true); + }); + + it('validate:VALUE', () => { + const result = validate('VALUE'); + + expect(result.error).toBeInstanceOf(InvalidLength); + }); + + it('validate:VALUE', () => { + const result = validate('VALUE'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); +}); diff --git a/bin/tmpl/tin.ts b/bin/tmpl/tin.ts new file mode 100644 index 0000000..65dc7c6 --- /dev/null +++ b/bin/tmpl/tin.ts @@ -0,0 +1,81 @@ +/** + * XYZZY (description). + * + * DESCRIPTION + * + * Source + * HERE + * + * ENTITY/PERSON + */ + +import * as exceptions from '../exceptions'; +import { strings } from '../util'; +import { Validator, ValidateReturn } from '../types'; +import { weightedSum } from '../util/checksum'; + +function clean(input: string): ReturnType { + return strings.cleanUnicode(input, ' -'); +} + +// const validRe = /^[PCGQV]{1}00[A-Z0-9]{8}$/; + +// const ALPHABET = '0123456789X'; + +const impl: Validator = { + name: 'NAME', + localName: 'NAME', + abbreviation: '{{tincode_upper}}', + + compact(input: string): string { + const [value, err] = clean(input); + + if (err) { + throw err; + } + + return value; + }, + + format(input: string): string { + const [value] = clean(input); + + return value; + }, + + validate(input: string): ValidateReturn { + const [value, error] = clean(input); + + if (error) { + return { isValid: false, error }; + } + if (value.length !== 11) { + return { isValid: false, error: new exceptions.InvalidLength() }; + } + + // if (!validRe.test(value)) { + // return { isValid: false, error: new exceptions.InvalidFormat() }; + // } + + const [, front, check] = strings.splitAt(value, 1, 10); + + const sum = weightedSum(front, { + weights: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + modulus: 11, + }); + + // if (ALPHABET[sum] !== check) { + // return { isValid: false, error: new exceptions.InvalidChecksum() }; + // } + + return { + isValid: true, + compact: value, + isIndividual: value[0] === 'P', + isCompany: value[0] !== 'P', + }; + }, +}; + +export const { name, localName, abbreviation, validate, format, compact } = + impl; diff --git a/src/eg/index.ts b/src/eg/index.ts new file mode 100644 index 0000000..53f0a8e --- /dev/null +++ b/src/eg/index.ts @@ -0,0 +1 @@ +export * as tn from './tn'; diff --git a/src/eg/tn.spec.ts b/src/eg/tn.spec.ts new file mode 100644 index 0000000..d64ef63 --- /dev/null +++ b/src/eg/tn.spec.ts @@ -0,0 +1,44 @@ +import { validate, format } from './tn'; +import { InvalidLength, InvalidFormat } from '../exceptions'; + +describe('eg/tn', () => { + it('format:100531385', () => { + const result = format('100531385'); + + expect(result).toEqual('100-531-385'); + }); + + it('fvalidate:100-531-385', () => { + const result = validate('100-531-385'); + + expect(result.isValid && result.compact).toEqual('100531385'); + }); + + test.each([ + '100-531-385', + '٣٣١-١٠٥-٢٦٨', + '421 – 159 – 723', + '431-134-189', + '432-600-132', + '455-466-138', + '455273677', + '٩٤٦-١٤٩-٢٠٠', + '۹٤۹-۸۹۱-۲۰٤', + ])('validate:%s', value => { + const result = validate(value); + + expect(result.isValid).toEqual(true); + }); + + it('validate:VV3456789', () => { + const result = validate('VV3456789'); + + expect(result.error).toBeInstanceOf(InvalidFormat); + }); + + it('validate:12345', () => { + const result = validate('12345'); + + expect(result.error).toBeInstanceOf(InvalidLength); + }); +}); diff --git a/src/eg/tn.ts b/src/eg/tn.ts new file mode 100644 index 0000000..81685e5 --- /dev/null +++ b/src/eg/tn.ts @@ -0,0 +1,96 @@ +/** + * Tax Registration Number (الرقم الضريبي, Egypt tax number). + * + * This number consists of 9 digits, usually separated into three groups + * using hyphens to make it easier to read, like XXX-XXX-XXX. + * + * Source + * https://emsp.mts.gov.eg:8181/EMDB-web/faces/authoritiesandcompanies/authority/website/SearchAuthority.xhtml?lang=en + * + * ENTITY + */ + +import * as exceptions from '../exceptions'; +import { strings } from '../util'; +import { Validator, ValidateReturn } from '../types'; + +const ARABIC_NUMBERS_MAP: Record = { + // Arabic-indic digits. + '٠': '0', + '١': '1', + '٢': '2', + '٣': '3', + '٤': '4', + '٥': '5', + '٦': '6', + '٧': '7', + '٨': '8', + '٩': '9', + // Extended arabic-indic digits. + '۰': '0', + '۱': '1', + '۲': '2', + '۳': '3', + '۴': '4', + '۵': '5', + '۶': '6', + '۷': '7', + '۸': '8', + '۹': '9', +}; + +function clean(input: string): ReturnType { + // Normalize the arabic characters to ascii digits + const norm = input + .split('') + .map(c => ARABIC_NUMBERS_MAP[c] ?? c) + .join(''); + return strings.cleanUnicode(norm, ' -/'); +} + +const impl: Validator = { + name: 'NAME', + localName: 'NAME', + abbreviation: 'TN', + + compact(input: string): string { + const [value, err] = clean(input); + + if (err) { + throw err; + } + + return value; + }, + + format(input: string): string { + const [value] = clean(input); + + return strings.splitAt(value, 3, 6).join('-'); + }, + + validate(input: string): ValidateReturn { + const [value, error] = clean(input); + + if (error) { + return { isValid: false, error }; + } + if (value.length !== 9) { + return { isValid: false, error: new exceptions.InvalidLength() }; + } + + if (!strings.isdigits(value)) { + return { isValid: false, error: new exceptions.InvalidFormat() }; + } + + return { + isValid: true, + compact: value, + isIndividual: false, + isCompany: false, + }; + }, +}; + +export const { name, localName, abbreviation, validate, format, compact } = + impl; diff --git a/src/index.ts b/src/index.ts index 3066558..e1603a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ import * as DK from './dk'; import * as DO from './do'; import * as EC from './ec'; import * as EE from './ee'; +import * as EG from './eg'; import * as ES from './es'; import * as FI from './fi'; import * as FR from './fr'; @@ -114,6 +115,7 @@ export const stdnum: Record> = { DK, EC, EE, + EG, ES, FI, FR,