diff --git a/README.md b/README.md index 4bc1e52..09056df 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,30 @@ Subpaths follow the source layout: `stdnum//` (e.g. `stdnum/it/codicefiscale`), and `stdnum/` 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. diff --git a/src/index.spec.ts b/src/index.spec.ts index 433c31f..06099e0 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -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 }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 7c591ea..cda5c86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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