Skip to content

Commit

Permalink
feature: added Taiwan individual tax numbers
Browse files Browse the repository at this point in the history
  • Loading branch information
koblas committed Jun 30, 2023
1 parent 26596a1 commit 1533790
Show file tree
Hide file tree
Showing 11 changed files with 513 additions and 3 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,18 @@ All country validators are in the "namespace" of the ISO country code.
| Romania | RO | CNP | Person | Cod Numeric Personal, Romanian Numerical Personal Code) |
| Romania | RO | CUI | Tax | Codul Unic de Înregistrare, Romanian company identifier |
| Romania | RO | ONRC | Company | Ordine din Registrul Comerţului, Romanian Trade Register identifier |
| San Marcos | SM | COE | Company | Codice operatore economico, San Marino national tax number |
| San Marino | SM | COE | Company | Codice operatore economico, San Marino national tax number |
| Serbia | RS | PIB | Vat | Poreski identifikacioni broj Tax identification number |
| Serbia | RS | JMBG | Person | Unique Master Citizen Number (Jedinstveni matični broj građana) |
| Sweden | SE | ORGNR | Company | Organisationsnummer, Swedish company number |
| Sweden | SE | PERSONNUMMER | Person | Personnummer (Swedish personal identity number) |
| Sweden | SE | VAT | Vat | VAT (Moms, Mervärdesskatt, Swedish VAT number) |
| Singapore | SG | UEN | Company | Singapore's Unique Entity Number |
| Thailand | TH | IDNR | Person | Thai National ID (บัตรประจำตัวประชาชนไทย) |
| Taiwan | TW | NATID | Person | National ID Card Number |
| Taiwan | TW | TAX_CODE | Person | Tax Code |
| Taiwan | TW | UBN | Company | Unified Business Number, 統一編號, Taiwanese tax number |
| Taiwan | TW | UI | Person | UI Number |
| Turkey | TR | TCKIMLIK | Person | Türkiye Cumhuriyeti Kimlik Numarası (Personal ID) |
| Turkey | TR | VKN | Tax | Vergi Kimlik Numarası, Turkish tax identification number |
| Slovenia | SI | DDV | Vat | ID za DDV (Davčna številka, Slovenian VAT number) |
Expand Down
28 changes: 28 additions & 0 deletions src/tw/ban.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { validate, format } from './ban';
import { InvalidLength, InvalidFormat } from '../exceptions';

describe('tw/ban', () => {
it('format:0050150 3', () => {
const result = format('0050150 3');

expect(result).toEqual('00501503');
});

it('validate:00501503', () => {
const result = validate('00501503');

expect(result.isValid && result.compact).toEqual('00501503');
});

it('validate:12345', () => {
const result = validate('12345');

expect(result.error).toBeInstanceOf(InvalidLength);
});

it('validate:12345AAA', () => {
const result = validate('12345AAA');

expect(result.error).toBeInstanceOf(InvalidFormat);
});
});
66 changes: 66 additions & 0 deletions src/tw/ban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* BAN
*
* The BAN of a business entity is shown on the company registration certificate or
* business registration certificate. For non-profit organizations, the BAN is shown
* on the BAN-issuance notification.
*
* Sources:
* https://www.mof.gov.tw/Eng/download/16968
*
* ENTITY
*/

import * as exceptions from '../exceptions';
import { strings } from '../util';

function clean(input: string) {
return strings.cleanUnicode(input, ' -');
}

const impl = {
abbreviation: 'BAN',
name: 'Taiwanese Unified Business Number',
localName: '統一編號',

compact(input: string) {
const [value, err] = clean(input);

if (err) {
throw err;
}

return value;
},

format(input: string) {
const [value] = clean(input);

return value;
},

validate(input: string) {
const [value, error] = clean(input);

if (error) {
return { isValid: false, error };
}
if (value.length !== 8) {
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: true,
};
},
};

export const { name, localName, abbreviation, validate, format, compact } =
impl;
export default impl;
4 changes: 4 additions & 0 deletions src/tw/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export * as ubn from './ubn';
export * as ban from './ban';
export * as natid from './natid';
export * as ui from './ui';
export * as tax_code from './tax_code';
28 changes: 28 additions & 0 deletions src/tw/natid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { validate, format } from './natid';
import { InvalidLength, InvalidComponent } from '../exceptions';

describe('tw/natid', () => {
it('format:0050150 3', () => {
const result = format('005-0150 3');

expect(result).toEqual('00501503');
});

it('validate:A200501503', () => {
const result = validate('A200501503');

expect(result.isValid && result.compact).toEqual('A200501503');
});

it('validate:12345', () => {
const result = validate('12345');

expect(result.error).toBeInstanceOf(InvalidLength);
});

it('validate:12345AAAA', () => {
const result = validate('12345AAA1A');

expect(result.error).toBeInstanceOf(InvalidComponent);
});
});
104 changes: 104 additions & 0 deletions src/tw/natid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* National ID Card Number
*
* The National ID Card Number is shown on the ID card of the R.O.C., the
* household certificate, and the passport.
*
* A ten-digit code with the first an alphabetic letter followed by a
* nine-digit numeric string. The alphabetic letter is the area code
* of the municipality/county/city in which the individual applies
* for household registration. The leading number represents gender:
* “1” for males and “2” for females. The last number is a check
* digit.
*
* Note: NATID and UI only differ by gender coding
*
* Sources:
* https://www.mof.gov.tw/Eng/download/16968
* https://en.wikipedia.org/wiki/National_identification_card_(Taiwan)
*
* PERSON
*/

import * as exceptions from '../exceptions';
import { strings, weightedSum } from '../util';

function clean(input: string) {
return strings.cleanUnicode(input, ' -');
}

// ID Encoding alphabet
//
// A: 10 B: 11 C: 12 D: 13 E: 14 F: 15 G: 16 H: 17 I: 34
// J: 18 K: 19 L: 20 M: 21 N: 22 O: 35 P: 23 Q: 24 R: 25
// S: 26 T: 27 U: 28 V: 29 W: 32 X: 30 Y: 31 Z: 33
//
export const ALPHABET = '0123456789ABCDEFGHJKLMNPQRSTUVXYWZIO';

const impl = {
localName: '中華民國國民身分證',
abbreviation: 'NATID',
name: 'National ID Number',

compact(input: string) {
const [value, err] = clean(input);

if (err) {
throw err;
}

return value;
},

format(input: string) {
const [value] = clean(input);

return value;
},

validate(input: string) {
const [value, error] = clean(input);

if (error) {
return { isValid: false, error };
}
if (value.length !== 10) {
return { isValid: false, error: new exceptions.InvalidLength() };
}

const [issuer, gender, code, check] = strings.splitAt(value, 1, 2, 9);

if (!strings.isalpha(issuer)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
if (!['1', '2'].includes(gender)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
if (!strings.isdigits(code)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
if (!strings.isdigits(check)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}

const sum = weightedSum(value, {
weights: [1, 9, 8, 7, 6, 5, 4, 3, 2, 1],
alphabet: ALPHABET,
modulus: 10,
});
if (sum % 10 !== 0) {
return { isValid: false, error: new exceptions.InvalidChecksum() };
}

return {
isValid: true,
compact: value,
isIndividual: true,
isCompany: false,
};
},
};

export const { name, localName, abbreviation, validate, format, compact } =
impl;
export default impl;
36 changes: 36 additions & 0 deletions src/tw/tax_code.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { validate, format } from './tax_code';
import { InvalidLength, InvalidFormat } from '../exceptions';

describe('tw/tax_code', () => {
it('format:0050150 3', () => {
const result = format('0050150 3');

expect(result).toEqual('00501503');
});

test.each(['9000503', '2000 05 03 AA'])('validate:%s', value => {
const result = validate(value);

console.log(value, result);

expect(result.isValid).toEqual(true);
});

it('validate:9 00 05 15', () => {
const result = validate('9 00 05 15');

expect(result.isValid && result.compact).toEqual('9000515');
});

it('validate:12345', () => {
const result = validate('12345');

expect(result.error).toBeInstanceOf(InvalidLength);
});

it('validate:12345AA', () => {
const result = validate('12345AA');

expect(result.error).toBeInstanceOf(InvalidFormat);
});
});
97 changes: 97 additions & 0 deletions src/tw/tax_code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Tax Code
*
* Individual taxpayers who have neither National ID Card Number nor UI Number
* (usually because they were not physically present or only stayed for a short
* period of time in the R.O.C.) may produce Tax Codes themselves as their TINs
* by reference to the coding princip
*
* Tax Code
* (1) For individuals of Mainland China area: a seven-digit code that begins with 9
* followed by the last two numbers of the individuals’ birth year and the four
* numbers of the individuals’ birth month and day (mmdd).
* (2) For individuals other than the ones indicated in (1): a ten-digit code that
* begins with the individuals’ date of birth (yyyymmdd), followed by the first
* two alphabetic letters of the individuals’ name on the passport.
*
* Sources:
* https://www.mof.gov.tw/Eng/download/16968
*
* PERSON
*/

import * as exceptions from '../exceptions';
import {
isValidDateCompactYYMMDD,
isValidDateCompactYYYYMMDD,
strings,
} from '../util';

function clean(input: string) {
return strings.cleanUnicode(input, ' -');
}

const impl = {
abbreviation: '',
localName: '',
name: 'Tax Code',

compact(input: string) {
const [value, err] = clean(input);

if (err) {
throw err;
}

return value;
},

format(input: string) {
const [value] = clean(input);

return value;
},

validate(input: string) {
const [value, error] = clean(input);

if (error) {
return { isValid: false, error };
}

if (value.length !== 10 && value.length !== 7) {
return { isValid: false, error: new exceptions.InvalidLength() };
}
if (value.length === 7) {
const [lead, yymmdd] = strings.splitAt(value, 1);
if (lead !== '9') {
return { isValid: false, error: new exceptions.InvalidFormat() };
}
if (!strings.isdigits(yymmdd)) {
return { isValid: false, error: new exceptions.InvalidFormat() };
}
if (!isValidDateCompactYYMMDD(yymmdd)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
} else {
const [yyyymmdd, name] = strings.splitAt(value, 8);
if (!isValidDateCompactYYYYMMDD(yyyymmdd)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
if (!/^[A-Z]+$/i.test(name)) {
return { isValid: false, error: new exceptions.InvalidComponent() };
}
}

return {
isValid: true,
compact: value,
isIndividual: true,
isCompany: false,
};
},
};

export const { name, localName, abbreviation, validate, format, compact } =
impl;
export default impl;
Loading

0 comments on commit 1533790

Please sign in to comment.