taiwan-validator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +214 -0
- package/README.md +214 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +128 -0
- package/src/index.ts +26 -0
- package/src/types.ts +22 -0
- package/src/validators/business-number.test.ts +63 -0
- package/src/validators/business-number.ts +59 -0
- package/src/validators/citizen-certificate.test.ts +73 -0
- package/src/validators/citizen-certificate.ts +41 -0
- package/src/validators/einvoice-donation-code.test.ts +78 -0
- package/src/validators/einvoice-donation-code.ts +42 -0
- package/src/validators/einvoice-mobile-barcode.test.ts +67 -0
- package/src/validators/einvoice-mobile-barcode.ts +45 -0
- package/src/validators/mobile-phone.test.ts +67 -0
- package/src/validators/mobile-phone.ts +41 -0
- package/src/validators/national-id.test.ts +99 -0
- package/src/validators/national-id.ts +181 -0
- package/src/validators/resident-certificate.test.ts +129 -0
- package/src/validators/resident-certificate.ts +181 -0
package/package.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taiwan-validator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A comprehensive validator for Taiwan identification numbers and codes (身分證字號、統一編號、居留證號、手機號碼、自然人憑證、電子發票條碼)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Gary Lai <garylai1990@gmail.com>",
|
|
7
|
+
"homepage": "https://github.com/imgarylai/taiwan-validator#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/imgarylai/taiwan-validator.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/imgarylai/taiwan-validator/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.mjs",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.mjs",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"clean": "rimraf dist coverage",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"predocs": "rimraf docs",
|
|
32
|
+
"docs": "typedoc",
|
|
33
|
+
"docs:watch": "typedoc --watch",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"prepare": "husky",
|
|
37
|
+
"test": "jest",
|
|
38
|
+
"test:coverage": "jest --coverage",
|
|
39
|
+
"type-check": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"config": {
|
|
42
|
+
"commitizen": {
|
|
43
|
+
"path": "./node_modules/cz-conventional-changelog"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^9.0.0",
|
|
50
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
51
|
+
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
52
|
+
"@semantic-release/git": "^10.0.0",
|
|
53
|
+
"@semantic-release/npm": "^13.0.0",
|
|
54
|
+
"@semantic-release/release-notes-generator": "^14.0.0",
|
|
55
|
+
"@types/jest": "^30.0.0",
|
|
56
|
+
"@types/node": "^24.0.0",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
59
|
+
"cz-conventional-changelog": "3.3.0",
|
|
60
|
+
"eslint": "^9.0.0",
|
|
61
|
+
"eslint-config-prettier": "^10.0.0",
|
|
62
|
+
"eslint-plugin-jest": "29.12.1",
|
|
63
|
+
"husky": "9.1.7",
|
|
64
|
+
"jest": "^30.0.0",
|
|
65
|
+
"lint-staged": "16.2.7",
|
|
66
|
+
"prettier": "^3.2.5",
|
|
67
|
+
"prettier-package-json": "^2.4.11",
|
|
68
|
+
"rimraf": "^6.0.0",
|
|
69
|
+
"semantic-release": "^25.0.0",
|
|
70
|
+
"sort-package-json": "3.6.0",
|
|
71
|
+
"ts-jest": "^29.1.2",
|
|
72
|
+
"tsup": "^8.0.2",
|
|
73
|
+
"typedoc": "^0.28.2",
|
|
74
|
+
"typescript": "5.9.3"
|
|
75
|
+
},
|
|
76
|
+
"keywords": [
|
|
77
|
+
"business-number",
|
|
78
|
+
"citizen-certificate",
|
|
79
|
+
"einvoice",
|
|
80
|
+
"id",
|
|
81
|
+
"identification",
|
|
82
|
+
"mobile-phone",
|
|
83
|
+
"national-id",
|
|
84
|
+
"resident-certificate",
|
|
85
|
+
"taiwan",
|
|
86
|
+
"validation",
|
|
87
|
+
"validator",
|
|
88
|
+
"台灣",
|
|
89
|
+
"居留證",
|
|
90
|
+
"手機",
|
|
91
|
+
"統一編號",
|
|
92
|
+
"自然人憑證",
|
|
93
|
+
"身分證",
|
|
94
|
+
"電子發票",
|
|
95
|
+
"驗證"
|
|
96
|
+
],
|
|
97
|
+
"engines": {
|
|
98
|
+
"node": ">=22.0.0",
|
|
99
|
+
"npm": ">=10.0.0"
|
|
100
|
+
},
|
|
101
|
+
"lint-staged": {
|
|
102
|
+
"*.{js,jsx,ts,tsx}": [
|
|
103
|
+
"eslint --fix",
|
|
104
|
+
"prettier --write"
|
|
105
|
+
],
|
|
106
|
+
"src/**/*.{js,jsx,ts,tsx}": [
|
|
107
|
+
"jest --bail --findRelatedTests"
|
|
108
|
+
],
|
|
109
|
+
"*.{json,md}": [
|
|
110
|
+
"prettier --write"
|
|
111
|
+
],
|
|
112
|
+
"package.json": [
|
|
113
|
+
"prettier-package-json --write"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"release": {
|
|
117
|
+
"branches": [
|
|
118
|
+
"main"
|
|
119
|
+
],
|
|
120
|
+
"plugins": [
|
|
121
|
+
"@semantic-release/commit-analyzer",
|
|
122
|
+
"@semantic-release/release-notes-generator",
|
|
123
|
+
"@semantic-release/changelog",
|
|
124
|
+
"@semantic-release/npm",
|
|
125
|
+
"@semantic-release/git"
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taiwan Validator - 台灣驗證器
|
|
3
|
+
* 提供完整的台灣身分證件和代碼驗證功能,包括:
|
|
4
|
+
* - 身分證字號
|
|
5
|
+
* - 營利事業統一編號
|
|
6
|
+
* - 居留證號
|
|
7
|
+
* - 手機號碼
|
|
8
|
+
* - 自然人憑證
|
|
9
|
+
* - 電子發票手機條碼
|
|
10
|
+
* - 電子發票捐贈碼
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { validateNationalId } from "./validators/national-id";
|
|
14
|
+
export { validateBusinessNumber } from "./validators/business-number";
|
|
15
|
+
export { validateResidentCertificate } from "./validators/resident-certificate";
|
|
16
|
+
export { validateMobilePhone } from "./validators/mobile-phone";
|
|
17
|
+
export { validateCitizenCertificate } from "./validators/citizen-certificate";
|
|
18
|
+
export { validateEInvoiceMobileBarcode } from "./validators/einvoice-mobile-barcode";
|
|
19
|
+
export { validateEInvoiceDonationCode } from "./validators/einvoice-donation-code";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
ValidationResult,
|
|
23
|
+
Gender,
|
|
24
|
+
NationalIdType,
|
|
25
|
+
ResidentCertificateType,
|
|
26
|
+
} from "./types";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 驗證結果介面
|
|
3
|
+
*/
|
|
4
|
+
export interface ValidationResult {
|
|
5
|
+
isValid: boolean;
|
|
6
|
+
message?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 身分證字號性別類型
|
|
11
|
+
*/
|
|
12
|
+
export type Gender = "male" | "female";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 居留證類型
|
|
16
|
+
*/
|
|
17
|
+
export type ResidentCertificateType = "old" | "new";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 身分證字號類型
|
|
21
|
+
*/
|
|
22
|
+
export type NationalIdType = "old" | "new";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { validateBusinessNumber } from "./business-number";
|
|
2
|
+
|
|
3
|
+
describe("validateBusinessNumber", () => {
|
|
4
|
+
describe("Valid Business Numbers", () => {
|
|
5
|
+
test("should validate correct business numbers", () => {
|
|
6
|
+
expect(validateBusinessNumber("12345676").isValid).toBe(true);
|
|
7
|
+
expect(validateBusinessNumber("53212539").isValid).toBe(true);
|
|
8
|
+
expect(validateBusinessNumber("04595257").isValid).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("should validate business numbers with 7th digit = 7 (special case)", () => {
|
|
12
|
+
// When 7th digit is 7, sum % 10 can be 0 or 1
|
|
13
|
+
expect(validateBusinessNumber("12345676").isValid).toBe(true);
|
|
14
|
+
expect(validateBusinessNumber("12345677").isValid).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("Invalid Business Numbers", () => {
|
|
19
|
+
test("should reject invalid business numbers", () => {
|
|
20
|
+
expect(validateBusinessNumber("12345670").isValid).toBe(false); // Wrong checksum
|
|
21
|
+
expect(validateBusinessNumber("12345671").isValid).toBe(false); // Wrong checksum
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("should reject incorrect length", () => {
|
|
25
|
+
expect(validateBusinessNumber("1234567").isValid).toBe(false); // Too short
|
|
26
|
+
expect(validateBusinessNumber("123456789").isValid).toBe(false); // Too long
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("should reject non-numeric input", () => {
|
|
30
|
+
expect(validateBusinessNumber("1234567A").isValid).toBe(false);
|
|
31
|
+
expect(validateBusinessNumber("ABCDEFGH").isValid).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("Edge Cases", () => {
|
|
36
|
+
test("should handle whitespace", () => {
|
|
37
|
+
expect(validateBusinessNumber(" 12345676 ").isValid).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("should reject empty or invalid input", () => {
|
|
41
|
+
expect(validateBusinessNumber("").isValid).toBe(false);
|
|
42
|
+
expect(validateBusinessNumber(" ").isValid).toBe(false);
|
|
43
|
+
// @ts-expect-error Testing invalid input type
|
|
44
|
+
expect(validateBusinessNumber(null).isValid).toBe(false);
|
|
45
|
+
// @ts-expect-error Testing invalid input type
|
|
46
|
+
expect(validateBusinessNumber(undefined).isValid).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should provide meaningful error messages", () => {
|
|
50
|
+
const result1 = validateBusinessNumber("1234567");
|
|
51
|
+
expect(result1.isValid).toBe(false);
|
|
52
|
+
expect(result1.message).toBe("統一編號必須為8位數字");
|
|
53
|
+
|
|
54
|
+
const result2 = validateBusinessNumber("12345670");
|
|
55
|
+
expect(result2.isValid).toBe(false);
|
|
56
|
+
expect(result2.message).toBe("統一編號檢查碼錯誤");
|
|
57
|
+
|
|
58
|
+
const result3 = validateBusinessNumber("");
|
|
59
|
+
expect(result3.isValid).toBe(false);
|
|
60
|
+
expect(result3.message).toBe("統一編號必須為非空字串");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣營利事業統一編號
|
|
5
|
+
* 格式:8位數字
|
|
6
|
+
* 使用加權檢查碼演算法
|
|
7
|
+
*
|
|
8
|
+
* @param number - 要驗證的統一編號
|
|
9
|
+
* @returns 驗證結果
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* validateBusinessNumber('12345678');
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function validateBusinessNumber(number: string): ValidationResult {
|
|
17
|
+
if (!number || typeof number !== "string") {
|
|
18
|
+
return {
|
|
19
|
+
isValid: false,
|
|
20
|
+
message: "統一編號必須為非空字串",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalized = number.trim();
|
|
25
|
+
|
|
26
|
+
// 檢查是否為8位數字
|
|
27
|
+
const pattern = /^\d{8}$/;
|
|
28
|
+
if (!pattern.test(normalized)) {
|
|
29
|
+
return {
|
|
30
|
+
isValid: false,
|
|
31
|
+
message: "統一編號必須為8位數字",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const digits = normalized.split("").map(Number);
|
|
36
|
+
const weights = [1, 2, 1, 2, 1, 2, 4, 1];
|
|
37
|
+
|
|
38
|
+
// 計算加權總和
|
|
39
|
+
let sum = 0;
|
|
40
|
+
for (let i = 0; i < 8; i++) {
|
|
41
|
+
let product = digits[i]! * weights[i]!;
|
|
42
|
+
|
|
43
|
+
// 如果乘積為兩位數,將十位數和個位數相加
|
|
44
|
+
if (product >= 10) {
|
|
45
|
+
product = Math.floor(product / 10) + (product % 10);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sum += product;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 特殊情況:第7位數字為7時
|
|
52
|
+
// 如果第7位數字為7且總和除以10的餘數為1,也視為有效
|
|
53
|
+
const isValid = sum % 10 === 0 || (digits[6]! === 7 && sum % 10 === 1);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
isValid,
|
|
57
|
+
message: isValid ? undefined : "統一編號檢查碼錯誤",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validateCitizenCertificate } from "./citizen-certificate";
|
|
2
|
+
|
|
3
|
+
describe("validateCitizenCertificate", () => {
|
|
4
|
+
describe("Valid Citizen Certificates", () => {
|
|
5
|
+
test("should validate correct citizen certificate numbers", () => {
|
|
6
|
+
expect(validateCitizenCertificate("AB12345678901234").isValid).toBe(true);
|
|
7
|
+
expect(validateCitizenCertificate("CD98765432109876").isValid).toBe(true);
|
|
8
|
+
expect(validateCitizenCertificate("XY11111111111111").isValid).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("should handle case insensitive input", () => {
|
|
12
|
+
expect(validateCitizenCertificate("ab12345678901234").isValid).toBe(true);
|
|
13
|
+
expect(validateCitizenCertificate("Ab12345678901234").isValid).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("should handle whitespace", () => {
|
|
17
|
+
expect(validateCitizenCertificate(" AB12345678901234 ").isValid).toBe(
|
|
18
|
+
true,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Invalid Citizen Certificates", () => {
|
|
24
|
+
test("should reject incorrect length", () => {
|
|
25
|
+
expect(validateCitizenCertificate("AB1234567890123").isValid).toBe(false); // Too short
|
|
26
|
+
expect(validateCitizenCertificate("AB123456789012345").isValid).toBe(
|
|
27
|
+
false,
|
|
28
|
+
); // Too long
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should reject incorrect format", () => {
|
|
32
|
+
expect(validateCitizenCertificate("A12345678901234").isValid).toBe(false); // Only 1 letter
|
|
33
|
+
expect(validateCitizenCertificate("ABC1234567890123").isValid).toBe(
|
|
34
|
+
false,
|
|
35
|
+
); // 3 letters
|
|
36
|
+
expect(validateCitizenCertificate("1212345678901234").isValid).toBe(
|
|
37
|
+
false,
|
|
38
|
+
); // Starts with digits
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("should reject non-alphanumeric characters", () => {
|
|
42
|
+
expect(validateCitizenCertificate("AB-1234567890123").isValid).toBe(
|
|
43
|
+
false,
|
|
44
|
+
);
|
|
45
|
+
expect(validateCitizenCertificate("AB 1234567890123").isValid).toBe(
|
|
46
|
+
false,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("Edge Cases", () => {
|
|
52
|
+
test("should reject empty or invalid input", () => {
|
|
53
|
+
expect(validateCitizenCertificate("").isValid).toBe(false);
|
|
54
|
+
expect(validateCitizenCertificate(" ").isValid).toBe(false);
|
|
55
|
+
// @ts-expect-error Testing invalid input type
|
|
56
|
+
expect(validateCitizenCertificate(null).isValid).toBe(false);
|
|
57
|
+
// @ts-expect-error Testing invalid input type
|
|
58
|
+
expect(validateCitizenCertificate(undefined).isValid).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("should provide meaningful error messages", () => {
|
|
62
|
+
const result1 = validateCitizenCertificate("AB123");
|
|
63
|
+
expect(result1.isValid).toBe(false);
|
|
64
|
+
expect(result1.message).toBe(
|
|
65
|
+
"自然人憑證編號必須為2個英文字母加上14位數字",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const result2 = validateCitizenCertificate("");
|
|
69
|
+
expect(result2.isValid).toBe(false);
|
|
70
|
+
expect(result2.message).toBe("自然人憑證編號必須為非空字串");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣自然人憑證編號
|
|
5
|
+
* 格式:2個大寫英文字母 + 14位數字
|
|
6
|
+
* 範例:AB12345678901234
|
|
7
|
+
*
|
|
8
|
+
* @param certNumber - 要驗證的自然人憑證編號
|
|
9
|
+
* @returns 驗證結果
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* validateCitizenCertificate('AB12345678901234');
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function validateCitizenCertificate(
|
|
17
|
+
certNumber: string,
|
|
18
|
+
): ValidationResult {
|
|
19
|
+
if (!certNumber || typeof certNumber !== "string") {
|
|
20
|
+
return {
|
|
21
|
+
isValid: false,
|
|
22
|
+
message: "自然人憑證編號必須為非空字串",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized = certNumber.trim().toUpperCase();
|
|
27
|
+
|
|
28
|
+
// 檢查格式:2個字母 + 14位數字
|
|
29
|
+
const pattern = /^[A-Z]{2}\d{14}$/;
|
|
30
|
+
|
|
31
|
+
if (!pattern.test(normalized)) {
|
|
32
|
+
return {
|
|
33
|
+
isValid: false,
|
|
34
|
+
message: "自然人憑證編號必須為2個英文字母加上14位數字",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isValid: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { validateEInvoiceDonationCode } from "./einvoice-donation-code";
|
|
2
|
+
|
|
3
|
+
describe("validateEInvoiceDonationCode", () => {
|
|
4
|
+
describe("Valid Donation Codes", () => {
|
|
5
|
+
test("should validate 3-digit donation codes", () => {
|
|
6
|
+
expect(validateEInvoiceDonationCode("123").isValid).toBe(true);
|
|
7
|
+
expect(validateEInvoiceDonationCode("000").isValid).toBe(true);
|
|
8
|
+
expect(validateEInvoiceDonationCode("999").isValid).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("should validate 4-digit donation codes", () => {
|
|
12
|
+
expect(validateEInvoiceDonationCode("1234").isValid).toBe(true);
|
|
13
|
+
expect(validateEInvoiceDonationCode("0000").isValid).toBe(true);
|
|
14
|
+
expect(validateEInvoiceDonationCode("9999").isValid).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("should validate 5-digit donation codes", () => {
|
|
18
|
+
expect(validateEInvoiceDonationCode("12345").isValid).toBe(true);
|
|
19
|
+
expect(validateEInvoiceDonationCode("00000").isValid).toBe(true);
|
|
20
|
+
expect(validateEInvoiceDonationCode("99999").isValid).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("should validate 6-digit donation codes", () => {
|
|
24
|
+
expect(validateEInvoiceDonationCode("123456").isValid).toBe(true);
|
|
25
|
+
expect(validateEInvoiceDonationCode("000000").isValid).toBe(true);
|
|
26
|
+
expect(validateEInvoiceDonationCode("999999").isValid).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("should validate 7-digit donation codes", () => {
|
|
30
|
+
expect(validateEInvoiceDonationCode("1234567").isValid).toBe(true);
|
|
31
|
+
expect(validateEInvoiceDonationCode("0000000").isValid).toBe(true);
|
|
32
|
+
expect(validateEInvoiceDonationCode("9999999").isValid).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("should handle whitespace", () => {
|
|
36
|
+
expect(validateEInvoiceDonationCode(" 12345 ").isValid).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("Invalid Donation Codes", () => {
|
|
41
|
+
test("should reject codes shorter than 3 digits", () => {
|
|
42
|
+
expect(validateEInvoiceDonationCode("12").isValid).toBe(false);
|
|
43
|
+
expect(validateEInvoiceDonationCode("1").isValid).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should reject codes longer than 7 digits", () => {
|
|
47
|
+
expect(validateEInvoiceDonationCode("12345678").isValid).toBe(false);
|
|
48
|
+
expect(validateEInvoiceDonationCode("123456789").isValid).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("should reject non-numeric codes", () => {
|
|
52
|
+
expect(validateEInvoiceDonationCode("12A").isValid).toBe(false);
|
|
53
|
+
expect(validateEInvoiceDonationCode("ABC").isValid).toBe(false);
|
|
54
|
+
expect(validateEInvoiceDonationCode("12-34").isValid).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("Edge Cases", () => {
|
|
59
|
+
test("should reject empty or invalid input", () => {
|
|
60
|
+
expect(validateEInvoiceDonationCode("").isValid).toBe(false);
|
|
61
|
+
expect(validateEInvoiceDonationCode(" ").isValid).toBe(false);
|
|
62
|
+
// @ts-expect-error Testing invalid input type
|
|
63
|
+
expect(validateEInvoiceDonationCode(null).isValid).toBe(false);
|
|
64
|
+
// @ts-expect-error Testing invalid input type
|
|
65
|
+
expect(validateEInvoiceDonationCode(undefined).isValid).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should provide meaningful error messages", () => {
|
|
69
|
+
const result1 = validateEInvoiceDonationCode("12");
|
|
70
|
+
expect(result1.isValid).toBe(false);
|
|
71
|
+
expect(result1.message).toBe("捐贈碼必須為3至7位數字");
|
|
72
|
+
|
|
73
|
+
const result2 = validateEInvoiceDonationCode("");
|
|
74
|
+
expect(result2.isValid).toBe(false);
|
|
75
|
+
expect(result2.message).toBe("捐贈碼必須為非空字串");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣電子發票捐贈碼
|
|
5
|
+
* 格式:3-7位數字
|
|
6
|
+
* 範例:123、12345、1234567
|
|
7
|
+
*
|
|
8
|
+
* 捐贈碼用於將電子發票捐贈給已註冊的慈善機構
|
|
9
|
+
*
|
|
10
|
+
* @param code - 要驗證的捐贈碼
|
|
11
|
+
* @returns 驗證結果
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* validateEInvoiceDonationCode('123');
|
|
16
|
+
* validateEInvoiceDonationCode('12345');
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function validateEInvoiceDonationCode(code: string): ValidationResult {
|
|
20
|
+
if (!code || typeof code !== "string") {
|
|
21
|
+
return {
|
|
22
|
+
isValid: false,
|
|
23
|
+
message: "捐贈碼必須為非空字串",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalized = code.trim();
|
|
28
|
+
|
|
29
|
+
// 檢查格式:3-7位數字
|
|
30
|
+
const pattern = /^\d{3,7}$/;
|
|
31
|
+
|
|
32
|
+
if (!pattern.test(normalized)) {
|
|
33
|
+
return {
|
|
34
|
+
isValid: false,
|
|
35
|
+
message: "捐贈碼必須為3至7位數字",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isValid: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { validateEInvoiceMobileBarcode } from "./einvoice-mobile-barcode";
|
|
2
|
+
|
|
3
|
+
describe("validateEInvoiceMobileBarcode", () => {
|
|
4
|
+
describe("Valid Mobile Barcodes", () => {
|
|
5
|
+
test("should validate correct mobile barcodes", () => {
|
|
6
|
+
expect(validateEInvoiceMobileBarcode("/ABCD123").isValid).toBe(true);
|
|
7
|
+
expect(validateEInvoiceMobileBarcode("/1234567").isValid).toBe(true);
|
|
8
|
+
expect(validateEInvoiceMobileBarcode("/ABC+123").isValid).toBe(true);
|
|
9
|
+
expect(validateEInvoiceMobileBarcode("/ABC-123").isValid).toBe(true);
|
|
10
|
+
expect(validateEInvoiceMobileBarcode("/ABC.123").isValid).toBe(true);
|
|
11
|
+
expect(validateEInvoiceMobileBarcode("/+++++++").isValid).toBe(true);
|
|
12
|
+
expect(validateEInvoiceMobileBarcode("/-------").isValid).toBe(true);
|
|
13
|
+
expect(validateEInvoiceMobileBarcode("/.......").isValid).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("should handle case insensitive input", () => {
|
|
17
|
+
expect(validateEInvoiceMobileBarcode("/abcd123").isValid).toBe(true);
|
|
18
|
+
expect(validateEInvoiceMobileBarcode("/AbCd123").isValid).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("should handle whitespace", () => {
|
|
22
|
+
expect(validateEInvoiceMobileBarcode(" /ABCD123 ").isValid).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("Invalid Mobile Barcodes", () => {
|
|
27
|
+
test("should reject barcodes not starting with /", () => {
|
|
28
|
+
expect(validateEInvoiceMobileBarcode("ABCD123").isValid).toBe(false);
|
|
29
|
+
expect(validateEInvoiceMobileBarcode("\\ABCD123").isValid).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should reject incorrect length", () => {
|
|
33
|
+
expect(validateEInvoiceMobileBarcode("/ABCD12").isValid).toBe(false); // Too short
|
|
34
|
+
expect(validateEInvoiceMobileBarcode("/ABCD1234").isValid).toBe(false); // Too long
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("should reject invalid characters", () => {
|
|
38
|
+
expect(validateEInvoiceMobileBarcode("/ABCD@23").isValid).toBe(false);
|
|
39
|
+
expect(validateEInvoiceMobileBarcode("/ABCD#23").isValid).toBe(false);
|
|
40
|
+
expect(validateEInvoiceMobileBarcode("/ABCD$23").isValid).toBe(false);
|
|
41
|
+
expect(validateEInvoiceMobileBarcode("/ABCD*23").isValid).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("Edge Cases", () => {
|
|
46
|
+
test("should reject empty or invalid input", () => {
|
|
47
|
+
expect(validateEInvoiceMobileBarcode("").isValid).toBe(false);
|
|
48
|
+
expect(validateEInvoiceMobileBarcode(" ").isValid).toBe(false);
|
|
49
|
+
// @ts-expect-error Testing invalid input type
|
|
50
|
+
expect(validateEInvoiceMobileBarcode(null).isValid).toBe(false);
|
|
51
|
+
// @ts-expect-error Testing invalid input type
|
|
52
|
+
expect(validateEInvoiceMobileBarcode(undefined).isValid).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should provide meaningful error messages", () => {
|
|
56
|
+
const result1 = validateEInvoiceMobileBarcode("ABCD123");
|
|
57
|
+
expect(result1.isValid).toBe(false);
|
|
58
|
+
expect(result1.message).toBe(
|
|
59
|
+
"手機條碼必須以 / 開頭,後接7個有效字元(A-Z、0-9、+、-、.)",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const result2 = validateEInvoiceMobileBarcode("");
|
|
63
|
+
expect(result2.isValid).toBe(false);
|
|
64
|
+
expect(result2.message).toBe("手機條碼必須為非空字串");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣電子發票手機條碼
|
|
5
|
+
* 格式:/ + 7個字元(大寫英文字母、數字、+、-、.)
|
|
6
|
+
* 範例:/ABCD123
|
|
7
|
+
*
|
|
8
|
+
* 手機條碼用於將電子發票儲存在手機載具中
|
|
9
|
+
*
|
|
10
|
+
* @param barcode - 要驗證的手機條碼
|
|
11
|
+
* @returns 驗證結果
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* validateEInvoiceMobileBarcode('/ABCD123');
|
|
16
|
+
* validateEInvoiceMobileBarcode('/1234567');
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function validateEInvoiceMobileBarcode(
|
|
20
|
+
barcode: string,
|
|
21
|
+
): ValidationResult {
|
|
22
|
+
if (!barcode || typeof barcode !== "string") {
|
|
23
|
+
return {
|
|
24
|
+
isValid: false,
|
|
25
|
+
message: "手機條碼必須為非空字串",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized = barcode.trim().toUpperCase();
|
|
30
|
+
|
|
31
|
+
// 檢查格式:以 / 開頭,後接7個字元
|
|
32
|
+
// 有效字元:A-Z、0-9、+、-、.
|
|
33
|
+
const pattern = /^\/[A-Z0-9+.-]{7}$/;
|
|
34
|
+
|
|
35
|
+
if (!pattern.test(normalized)) {
|
|
36
|
+
return {
|
|
37
|
+
isValid: false,
|
|
38
|
+
message: "手機條碼必須以 / 開頭,後接7個有效字元(A-Z、0-9、+、-、.)",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isValid: true,
|
|
44
|
+
};
|
|
45
|
+
}
|