Repository: typestack/class-validator
Pull request #1822 (2022-12-03 09:31:35)

feat: add `@IsTaxID` decorator
## Description
<!-- A clear and concise description what these changes does. -->

This PR adds `@IsTaxId` decorator.

## Checklist
<!-- Replace  the [ ] with [x] to check the boxes. -->
- [x] the pull request title describes what this PR does (not a vague title like `Update index.md`)
- [x] the pull request targets the *default* branch of the repository (`develop`)
- [x] the code follows the established code style of the repository
  - `npm run prettier:check` passes
  - `npm run lint:check` passes
- [x] tests are added for the changes I made (if any source code was modified)
- [x] documentation added or updated
- [x] I have run the project locally and verified that there are no errors

### Fixes
<!-- If there is no issue being resolved, open one before creating this pull request. -->
<!-- If the PR doesn't fully resolve the issue, replace 'fixes' with 'references'. -->
references https://github.com/typestack/class-validator/issues/1823



Patch:
diff --git a/README.md b/README.md
index 85ef7b2..c2434b0 100644
--- a/README.md
+++ b/README.md
@@ -883,6 +883,7 @@ isBoolean(value);
 | `@IsMultibyte()`                                | Checks if the string contains one or more multibyte chars. |
 | `@IsNumberString(options?: IsNumericOptions)`   | Checks if the string is numeric. |
 | `@IsSurrogatePair()`                            | Checks if the string contains any surrogate pairs chars. |
+| `@IsTaxId()`                                    | Checks if the string is a valid tax ID. Default locale is `en-US`.
 | `@IsUrl(options?: IsURLOptions)`                | Checks if the string is a URL. |
 | `@IsMagnetURI()`                                | Checks if the string is a [magnet uri format](https://en.wikipedia.org/wiki/Magnet_URI_scheme). |
 | `@IsUUID(version?: "3"\|"4"\|"5"\|"all")`       | Checks if the string is a UUID (version 3, 4, 5 or all ). |
diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts
index 542c9cb..1ec8c45 100644
--- a/src/decorator/decorators.ts
+++ b/src/decorator/decorators.ts
@@ -114,6 +114,7 @@ export * from './string/IsSemVer';
 export * from './string/IsStrongPassword';
 export * from './string/IsTimeZone';
 export * from './string/IsBase58';
+export * from './string/is-tax-id';
 
 // -------------------------------------------------------------------------
 // Type checkers
diff --git a/src/decorator/string/is-tax-id.ts b/src/decorator/string/is-tax-id.ts
new file mode 100644
index 0000000..950852f
--- /dev/null
+++ b/src/decorator/string/is-tax-id.ts
@@ -0,0 +1,42 @@
+import { ValidationOptions } from '../ValidationOptions';
+import { buildMessage, ValidateBy } from '../common/ValidateBy';
+import isTaxIDValidator from 'validator/lib/isTaxID';
+
+export const IS_TAX_ID = 'isTaxId';
+
+/**
+ * Checks if the string is a valid tax ID. Default locale is `en-US`.
+ * If given value is not a string, then it returns false.
+ *
+ * Supported locales: bg-BG, cs-CZ, de-AT, de-DE, dk-DK, el-CY, el-GR, en-CA,
+ * en-IE, en-US, es-ES, et-EE, fi-FI, fr-BE, fr-FR, fr-LU, hr-HR, hu-HU, it-IT,
+ * lv-LV, mt-MT, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, sk-SK, sl-SI, sv-SE.
+ */
+export function isTaxId(value: unknown, locale?: string): boolean {
+  return typeof value === 'string' && isTaxIDValidator(value, locale || 'en-US');
+}
+
+/**
+ * Checks if the string is a valid tax ID. Default locale is `en-US`.
+ * If given value is not a string, then it returns false.
+ *
+ * Supported locales: bg-BG, cs-CZ, de-AT, de-DE, dk-DK, el-CY, el-GR, en-CA,
+ * en-IE, en-US, es-ES, et-EE, fi-FI, fr-BE, fr-FR, fr-LU, hr-HR, hu-HU, it-IT,
+ * lv-LV, mt-MT, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, sk-SK, sl-SI, sv-SE.
+ */
+export function IsTaxId(locale?: string, validationOptions?: ValidationOptions): PropertyDecorator {
+  return ValidateBy(
+    {
+      name: IS_TAX_ID,
+      constraints: [locale],
+      validator: {
+        validate: (value, args): boolean => isTaxId(value, args?.constraints[0]),
+        defaultMessage: buildMessage(
+          eachPrefix => eachPrefix + '$property must be a Tax Identification Number',
+          validationOptions
+        ),
+      },
+    },
+    validationOptions
+  );
+}


Test patch:
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index c2826dd..a43cfd8 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -190,6 +190,8 @@ import {
   IsTimeZone,
   IsBase58,
   isBase58,
+  isTaxId,
+  IsTaxId,
 } from '../../src/decorator/decorators';
 import { Validator } from '../../src/validation/Validator';
 import { ValidatorOptions } from '../../src/validation/ValidatorOptions';
@@ -2304,6 +2306,39 @@ describe('IsByteLength', () => {
   });
 });
 
+describe('IsTaxId', () => {
+  const constraint = 'bg-BG';
+  const validValues = ['7501010010', '0101010012', '0111010010', '7521010014', '7541010019'];
+  const invalidValues = [null, undefined, '750101001', '75010100101', '75-01010/01 0', '7521320010', '7501010019'];
+
+  class MyClass {
+    @IsTaxId(constraint)
+    someProperty: string;
+  }
+
+  it('should not fail if validator.validate said that its valid', () => {
+    return checkValidValues(new MyClass(), validValues);
+  });
+
+  it('should fail if validator.validate said that its invalid', () => {
+    return checkInvalidValues(new MyClass(), invalidValues);
+  });
+
+  it('should not fail if method in validator said that its valid', () => {
+    validValues.forEach(value => expect(isTaxId(value, constraint)).toBeTruthy());
+  });
+
+  it('should fail if method in validator said that its invalid', () => {
+    invalidValues.forEach(value => expect(isTaxId(value, constraint)).toBeFalsy());
+  });
+
+  it('should return error object with proper data', () => {
+    const validationType = 'isTaxId';
+    const message = 'someProperty must be a Tax Identification Number';
+    checkReturnedError(new MyClass(), invalidValues, validationType, message);
+  });
+});
+
 describe('IsCreditCard', () => {
   const validValues = [
     '375556917985515',