Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Validator | Description
**isSlug(str)** | check if the string is of type slug.
**isStrongPassword(str [, options])** | check if the string can be considered a strong password or not. Allows for custom requirements or scoring rules. If `returnScore` is true, then the function returns an integer score for the password rather than a boolean.<br/>Default options: <br/>`{ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, returnScore: false, pointsPerUnique: 1, pointsPerRepeat: 0.5, pointsForContainingLower: 10, pointsForContainingUpper: 10, pointsForContainingNumber: 10, pointsForContainingSymbol: 10 }`
**isTime(str [, options])** | check if the string is a valid time e.g. [`23:01:59`, new Date().toLocaleTimeString()].<br/><br/> `options` is an object which can contain the keys `hourFormat` or `mode`.<br/><br/>`hourFormat` is a key and defaults to `'hour24'`.<br/><br/>`mode` is a key and defaults to `'default'`. <br/><br/>`hourFomat` can contain the values `'hour12'` or `'hour24'`, `'hour24'` will validate hours in 24 format and `'hour12'` will validate hours in 12 format. <br/><br/>`mode` can contain the values `'default'` or `'withSeconds'`, `'default'` will validate `HH:MM` format, `'withSeconds'` will validate the `HH:MM:SS` format.
**isTaxID(str, locale)** | check if the string is a valid Tax Identification Number. Default locale is `en-US`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`.<br/><br/>Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-CA', 'en-GB', 'en-IE', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-CA', 'fr-FR', 'fr-LU', 'hr-HR', 'hu-HU', 'it-IT', 'lb-LU', 'lt-LT', 'lv-LV', 'mt-MT', 'nl-BE', 'nl-NL', 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE' ]`.
**isTaxID(str, locale [, options])** | check if the string is a valid Tax Identification Number. Default locale is `en-US`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`.<br/><br/>`options` is an object that has no default.<br/><br/>`options` contains `localeOption` as key and can be used if the locale has multiple Tax Identification Number `(e.g.{localeOption:'GSTIN'})`.<br/><br/>`localeOption` is of type `string`.<br/><br/>Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-CA', 'en-GB', 'en-IE', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-CA', 'fr-FR', 'fr-LU', 'hr-HR', 'hu-HU', 'it-IT', 'lb-LU', 'lt-LT', 'lv-LV', 'mt-MT', 'nl-BE', 'nl-NL', 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE' ]`.<br/><br/>Supported localeOption: `['GSTIN','PAN']` for `en-IN` locale.
**isURL(str [, options])** | check if the string is a URL.<br/><br/>`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_port: false, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, allow_fragments: true, allow_query_components: true, disallow_auth: false, validate_length: true }`.<br/><br/>`require_protocol` - if set to true isURL will return false if protocol is not present in the URL.<br/>`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.<br/>`protocols` - valid protocols can be modified with this option.<br/>`require_host` - if set to false isURL will not check if host is present in the URL.<br/>`require_port` - if set to true isURL will check if port is present in the URL.<br/>`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.<br/>`allow_fragments` - if set to false isURL will return false if fragments are present.<br/>`allow_query_components` - if set to false isURL will return false if query components are present.<br/>`validate_length` - if set to false isURL will skip string length validation (2083 characters is IE max URL length).
**isUUID(str [, version])** | check if the string is a UUID (version 1, 2, 3, 4 or 5).
**isVariableWidth(str)** | check if the string contains a mixture of full and half-width chars.
Expand Down Expand Up @@ -302,4 +302,4 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[MIME Type]: https://en.wikipedia.org/wiki/Media_type
[mongoid]: http://docs.mongodb.org/manual/reference/object-id/
[RFC 3339]: https://tools.ietf.org/html/rfc3339
[VAT Number]: https://en.wikipedia.org/wiki/VAT_identification_number
[VAT Number]: https://en.wikipedia.org/wiki/VAT_identification_number
66 changes: 51 additions & 15 deletions src/lib/isTaxID.js
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,10 @@ const taxIdFormat = {
'en-CA': /^\d{9}$/,
'en-GB': /^\d{10}$|^(?!GB|NK|TN|ZZ)(?![DFIQUV])[A-Z](?![DFIQUVO])[A-Z]\d{6}[ABCD ]$/i,
'en-IE': /^\d{7}[A-W][A-IW]{0,1}$/i,
'en-IN': {
GSTIN: /^((?!39)(?!00)[0-3][0-9]|97)([A-Z]{3}[ABCFGHLJPT][A-Z](?!0000)[0-9]{4}[A-Z])[1-9A-Z]Z[0-9 A-Z]$/,
PAN: /^[A-Z]{3}[ABCFGHLJPT][A-Z](?!0000)[0-9]{4}[A-Z]$/,
},
'en-US': /^\d{2}[- ]{0,1}\d{7}$/,
'es-ES': /^(\d{0,8}|[XYZKLM]\d{7})[A-HJ-NP-TV-Z]$/i,
'et-EE': /^[1-6]\d{6}(00[1-9]|0[1-9][0-9]|[1-6][0-9]{2}|70[0-9]|710)\d$/,
Expand Down Expand Up @@ -1211,30 +1215,62 @@ const sanitizeRegexes = {
// sanitizeRegexes locale aliases
sanitizeRegexes['nl-BE'] = sanitizeRegexes['fr-BE'];

const multipleTinLocale = {
'en-IN': {
GSTIN: true,
PAN: true,
},
};

const availableOptions = {
localeOption: true,
};

// Validates args of isTaxID function
function validateArgs(str, locale, options) {
const localeOption = options?.localeOption;

try { assertString(str); } catch (err) { throw new Error(`${err.message} for str`); }
try { assertString(locale); } catch (err) { throw new Error(`${err.message} for locale`); }

for (const option in options) {
if (!(option in availableOptions)) throw new Error(`'${option}' is not available`);
}

if (!(locale in taxIdFormat)) throw new Error(`Invalid locale '${locale}'`);


if (locale in multipleTinLocale) {
try { assertString(localeOption); } catch (err) { throw new Error(`${err.message} for localeOption`); }
if (!(localeOption in multipleTinLocale[locale])) throw new Error(`Invalid localeOption '${localeOption}'`);
} else if (localeOption || localeOption === '') {
throw new Error(`Invalid localeOption for locale '${locale}'`);
}
}

Copy link
Contributor

@braaar braaar Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you get inspiration for this from some other validator using an options object or did you come up with this yourself? To me this seems a bit heavy handed. Are we generally interested in throwing errors if the user doesn't provide valid options? I haven't seen this much argument validation in other validators. Does the multiple locale possibility in en-IN warrant this much code here?

#2086 is somewhat similar to this PR in that it uses a local option in the options object. Maybe cut things down a bit, taking inspiration from there?

Copy link
Author

@Santhosh-Kumar-99 Santhosh-Kumar-99 Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @braaar thanks for your feedback,
I came up with this approach of validating args, but took inspiration of options object from #2157 and #2075.
The current function also validates args and throws error if user specifies a wrong locale or str, so I thought it might be useful if we throw error for wrong options too. And yeah since localeOption is dependent on locale it seems a bit heavy handed, even I had the same thought too. But it is just checking if the options provided are correct, if the locale has multiple TIN and also their types.
Wouldn't it be misleading if the validation returns false when incorrect options are passed ?

Yeah #2086 is a bit similar but according to #1874 but locale will not be a part of options object as it is mandatory. #1874 (comment).

For now, en-IN is the only locale which has multiple TIN, but when I researched through the internet there are several other countries who have multiple TIN, may be this approach would help us to include other countries soon.

Example : US TIN
US has 5 different Identification numbers.

Copy link
Contributor

@braaar braaar Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This complexity is starting to make me feel like maintaining a validator for multiple tax ids for multiple countries is an enourmous undertaking. I suppose it's the only natural way forward, unless we decide to scrap the validator altogether, of course.

How many app developers are making international systems that care about tax ids and are using open source validation code? In my eyes the use case for such an all-encompassing validator is quite rare. When I work with things like social security numbers and business IDs I use a country-specific package that contains what I need and is much easier to maintain on its own, should I spot an error or want something added.

What are your thoughts on this, @WikiRik? I suppose this enters into a territory where the maintainers should chime in. They are in control of the main direction of the project and also feel the burden of maintaining code the most.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, input from the maintainers is indeed something we'll need. But the fact is that we already have isTaxID with multiple countries so this is a logical addition in my view.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@profnandaa can you please help us here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I should open a separate issue about scrapping this validator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah and can we just add PAN regex alone for en-IN locale ,for now ?

/*
* Validator function
* Return true if the passed string is a valid tax identification number
* for the specified locale.
* Throw an error exception if the locale is not supported.
*/
export default function isTaxID(str, locale = 'en-US') {
assertString(str);
export default function isTaxID(str, locale = 'en-US', options) {
validateArgs(str, locale, options);
const localeOption = options?.localeOption;

// Copy TIN to avoid replacement if sanitized
let strcopy = str.slice(0);
if (locale in sanitizeRegexes) {
strcopy = strcopy.replace(sanitizeRegexes[locale], '');
}

if (locale in taxIdFormat) {
if (locale in sanitizeRegexes) {
strcopy = strcopy.replace(sanitizeRegexes[locale], '');
}
if (!taxIdFormat[locale].test(strcopy)) {
return false;
}

if (locale in taxIdCheck) {
return taxIdCheck[locale](strcopy);
}
// Fallthrough; not all locales have algorithmic checks
if (locale in multipleTinLocale) {
if (!taxIdFormat[locale][localeOption].test(strcopy)) return false;
return true;
}
throw new Error(`Invalid locale '${locale}'`);
if (!taxIdFormat[locale].test(strcopy)) return false;
if (locale in taxIdCheck) return taxIdCheck[locale](strcopy);

return true;
}

70 changes: 70 additions & 0 deletions test/validators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12139,6 +12139,40 @@ describe('Validators', () => {
'1234577A',
'1234577JA'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { localeOption: 'GSTIN' }],
valid: [
'27AAPFU0939F1ZV',
'27AASCS2460H1Z0',
'29AAGCB7383J1Z4'],
invalid: [
'',
'00AAGCB0000J1Z4',
'29AAGCB7383J114',
'88AAPFU093921ZV',
'**AAGCB7689J1Z4',
'S AAGCB7893J1Z4'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { localeOption: 'PAN' }],
valid: [
'MFWAA0001A',
'ABPPA2020K',
'KLSPK0909Z',
'ACSLY4499S'],
invalid: [
'123PA0001A',
'ACSZA2020K',
'ACSP19090K',
'ACSPA0000K',
'ACSYA44999',
'',
'ACS A4499A',
'*#$PA4499J',
'ACSLY4499SK'],
});
test({
validator: 'isTaxID',
args: ['en-US'],
Expand Down Expand Up @@ -12475,6 +12509,42 @@ describe('Validators', () => {
valid: [
'01-1234567'],
});
test({
validator: 'isTaxID',
args: [123, { localeOption: 'GSTIN' }],
error: [
'27AAPFU0939F1ZV'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { localeOption: 123 }],
error: [
'27AAPFU0939F1ZV'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { localeOption: 'is-NOT' }],
error: [
'27AAPFU0939F1ZV'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { isNOT: 'is-NOT' }],
error: [
'27AAPFU0939F1ZV'],
});
test({
validator: 'isTaxID',
args: ['en-US', { localeOption: 'GSTIN' }],
error: [
'27AAPFU0939F1ZV'],
});
test({
validator: 'isTaxID',
args: ['en-IN', { localeOption: 'GSTIN' }],
error: [
12345566],
});
test({
validator: 'isTaxID',
args: ['is-NOT'],
Expand Down