Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ Subpaths follow the source layout: `stdnum/<country>/<number>` (e.g.
`stdnum/it/codicefiscale`), and `stdnum/<country>` for a whole country's
validators.

Tree-shaking the root `stdnum` entry works because `package.json` declares
`"sideEffects": false`: anything you import from it that you don't use, such as
the `stdnum` object or the validator tables below, can be dropped by the
bundler.

## Validating by country code

When the country is only known at runtime, these functions pick the
validators for an ISO 3166-1 alpha-2 country code (case-insensitive):

import { validatePerson, validateEntity, validateVat } from 'stdnum';

validatePerson('BR', '390.533.447-05'); // personal ID numbers
validateEntity('US', '12-3456789'); // business and tax numbers
validateVat('DE', 'DE136695976'); // VAT numbers

Each returns `{ checked: true, isValid, matchedValidators }` when it knows the
country, and `{ checked: false }` when it does not.

`validateVat` covers the countries in the `euVat` table: the EU member states
plus some non-EU countries (such as AD and CH). It only pulls in those VAT
validators, so it adds far less to a bundle than `validateEntity`, which
references every country's business validators.

## Supported Countries and Numbers

How you can help! This library currently support about half the countries in the world. It would be great if we can get to 100%. Submit an issue with any reference documentation and I'll do my best to integrate it, bonus points if you can get detailed descriptions of checksums or other validation criteria.
Expand Down
46 changes: 46 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,49 @@ describe('index exports', () => {
expect(lib.stdnum.AI.tin.validate('12345-67890').isValid).toBe(true);
});
});

describe('validateVat', () => {
// Valid and invalid values are taken from the country specs.
it.each([
['DE', 'DE136695976', lib.DE.vat], // src/de/vat.spec.ts
['FR', '40303265045', lib.FR.tva], // src/fr/tva.spec.ts
['CH', 'CHE-107.787.577 IVA', lib.CH.vat], // src/ch/vat.spec.ts, non-EU
])('accepts a valid %s VAT number', (country, value, validator) => {
expect(lib.validateVat(country, value)).toEqual({
checked: true,
isValid: true,
matchedValidators: [validator],
});
});

it('rejects an invalid VAT number', () => {
// Bad check digit, per src/fr/tva.spec.ts.
expect(lib.validateVat('FR', '84 323 140 391')).toEqual({
checked: true,
isValid: false,
matchedValidators: [],
});
});

it('accepts a lowercase country code', () => {
expect(lib.validateVat('de', 'DE136695976').isValid).toBe(true);
expect(lib.validateVat('fr', '84 323 140 391')).toEqual({
checked: true,
isValid: false,
matchedValidators: [],
});
});

it('does not check an unknown country', () => {
expect(lib.validateVat('XX', '40303265045')).toEqual({ checked: false });
expect(lib.validateVat('XX', '40303265045')).toEqual(
lib.validateEntity('XX', '40303265045'),
);
});

it('does not check a country that has entity validators but no VAT entry', () => {
// Valid US EIN, per src/us/ein.spec.ts.
expect(lib.validateEntity('US', '123-45-6789').isValid).toBe(true);
expect(lib.validateVat('US', '123-45-6789')).toEqual({ checked: false });
});
});
26 changes: 26 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,32 @@ export function validateEntity(
return { checked: true, isValid: match.length > 0, matchedValidators: match };
}

/**
* Apply the VAT validator for a given country to validate a VAT number.
*
* Covers the countries in `euVat`, which besides the EU member states
* includes some non-EU countries (such as AD and CH). A country that is not
* in `euVat` returns `{ checked: false }`, the same result `validateEntity`
* gives for a country it does not know.
*
* This only reads `euVat`, so a bundler can leave out the much larger
* `stdnum`, `personValidators` and `entityValidators` tables.
*/
export function validateVat(
country: string,
value: string,
): { checked: boolean; isValid?: boolean; matchedValidators?: Validator[] } {
const vset = euVat[country.toUpperCase()];

if (!vset || vset.length === 0) {
return { checked: false };
}

const match = vset.filter(grp => grp.validate(value).isValid);

return { checked: true, isValid: match.length > 0, matchedValidators: match };
}

/**
* Validate a passport number for a given country.
* Like validatePerson, returns `{ checked: false }` when the country has no
Expand Down
Loading