@hy_ong/zod-kit 0.0.4 → 0.0.6
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/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Taiwan National ID (身分證/居留證) validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides validation for Taiwan National ID cards (身分證) and
|
|
5
|
+
* Resident Certificates (居留證) with support for both old and new formats.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for national ID validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface NationalIdMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when national ID format or checksum is invalid
|
|
21
|
+
*/
|
|
22
|
+
export type NationalIdMessages = {
|
|
23
|
+
required?: string
|
|
24
|
+
invalid?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Types of Taiwan national identification documents
|
|
29
|
+
*
|
|
30
|
+
* @typedef {"citizen" | "resident" | "both"} NationalIdType
|
|
31
|
+
*
|
|
32
|
+
* Available types:
|
|
33
|
+
* - citizen: National ID card (身分證字號) for Taiwan citizens
|
|
34
|
+
* - resident: Resident certificate (居留證號) for foreign residents
|
|
35
|
+
* - both: Accept both citizen and resident IDs
|
|
36
|
+
*/
|
|
37
|
+
export type NationalIdType =
|
|
38
|
+
| "citizen" // National ID card (身分證字號)
|
|
39
|
+
| "resident" // Resident certificate (居留證號)
|
|
40
|
+
| "both" // Both citizen and resident IDs accepted
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration options for Taiwan national ID validation
|
|
44
|
+
*
|
|
45
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
46
|
+
*
|
|
47
|
+
* @interface NationalIdOptions
|
|
48
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
49
|
+
* @property {NationalIdType} [type="both"] - Type of ID to accept
|
|
50
|
+
* @property {boolean} [allowOldResident=true] - Whether to accept old-style resident certificates
|
|
51
|
+
* @property {Function} [transform] - Custom transformation function for ID
|
|
52
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
53
|
+
* @property {Record<Locale, NationalIdMessages>} [i18n] - Custom error messages for different locales
|
|
54
|
+
*/
|
|
55
|
+
export type NationalIdOptions<IsRequired extends boolean = true> = {
|
|
56
|
+
required?: IsRequired
|
|
57
|
+
type?: NationalIdType
|
|
58
|
+
allowOldResident?: boolean
|
|
59
|
+
transform?: (value: string) => string
|
|
60
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
61
|
+
i18n?: Record<Locale, NationalIdMessages>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Type alias for national ID validation schema based on required flag
|
|
66
|
+
*
|
|
67
|
+
* @template IsRequired - Whether the field is required
|
|
68
|
+
* @typedef NationalIdSchema
|
|
69
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
70
|
+
*/
|
|
71
|
+
export type NationalIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mapping of Taiwan city/county codes to their numeric values
|
|
75
|
+
*
|
|
76
|
+
* @constant {Record<string, number>} CITY_CODES
|
|
77
|
+
* @description Maps the first letter of Taiwan ID to corresponding numeric code
|
|
78
|
+
*/
|
|
79
|
+
const CITY_CODES: Record<string, number> = {
|
|
80
|
+
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17,
|
|
81
|
+
'I': 34, 'J': 18, 'K': 19, 'L': 20, 'M': 21, 'N': 22, 'O': 35, 'P': 23,
|
|
82
|
+
'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29, 'W': 32, 'X': 30,
|
|
83
|
+
'Y': 31, 'Z': 33
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates Taiwan citizen national ID card (身分證字號)
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value - The citizen ID to validate
|
|
90
|
+
* @returns {boolean} True if the citizen ID is valid
|
|
91
|
+
*
|
|
92
|
+
* @description
|
|
93
|
+
* Validates Taiwan citizen ID format: 1 letter + 1 gender digit (1-2) + 8 digits
|
|
94
|
+
* Uses checksum algorithm with city code conversion and weighted sum.
|
|
95
|
+
*
|
|
96
|
+
* Format: [A-Z][1-2]XXXXXXXX
|
|
97
|
+
* - First letter: City/county code
|
|
98
|
+
* - Second digit: Gender (1=male, 2=female)
|
|
99
|
+
* - Last 8 digits: Serial number + checksum
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* validateCitizenId("A123456789") // true/false based on checksum
|
|
104
|
+
* validateCitizenId("A323456789") // false (invalid gender digit)
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
const validateCitizenId = (value: string): boolean => {
|
|
108
|
+
// 格式檢查:1個英文字母 + 9個數字
|
|
109
|
+
if (!/^[A-Z][1-2]\d{8}$/.test(value)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const letter = value[0]
|
|
114
|
+
const digits = value.slice(1).split('').map(Number)
|
|
115
|
+
|
|
116
|
+
// 獲取縣市代碼
|
|
117
|
+
const cityCode = CITY_CODES[letter]
|
|
118
|
+
if (!cityCode) return false
|
|
119
|
+
|
|
120
|
+
// 計算校驗碼
|
|
121
|
+
const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
|
|
122
|
+
const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
|
123
|
+
|
|
124
|
+
let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1]
|
|
125
|
+
for (let i = 0; i < 8; i++) {
|
|
126
|
+
sum += digits[i] * coefficients[i + 2]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const checksum = (10 - (sum % 10)) % 10
|
|
130
|
+
return checksum === digits[8]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validates old-style Taiwan resident certificate (舊式居留證號)
|
|
135
|
+
*
|
|
136
|
+
* @param {string} value - The old-style resident ID to validate
|
|
137
|
+
* @returns {boolean} True if the old-style resident ID is valid
|
|
138
|
+
*
|
|
139
|
+
* @description
|
|
140
|
+
* Validates old-style resident ID format: 1 letter + 1 gender letter + 8 digits
|
|
141
|
+
* Uses checksum algorithm with city code and gender code conversion.
|
|
142
|
+
*
|
|
143
|
+
* Format: [A-Z][ABCD]XXXXXXXX
|
|
144
|
+
* - First letter: City/county code
|
|
145
|
+
* - Second letter: Gender code (A/C=male, B/D=female)
|
|
146
|
+
* - Last 8 digits: Serial number + checksum
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* validateOldResidentId("AA12345678") // true/false based on checksum
|
|
151
|
+
* validateOldResidentId("AE12345678") // false (invalid gender letter)
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
const validateOldResidentId = (value: string): boolean => {
|
|
155
|
+
// 格式檢查:1個英文字母 + [AB或CD] + 8個數字
|
|
156
|
+
if (!/^[A-Z][ABCD]\d{8}$/.test(value)) {
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const letter = value[0]
|
|
161
|
+
const genderCode = value[1]
|
|
162
|
+
const digits = value.slice(2).split('').map(Number)
|
|
163
|
+
|
|
164
|
+
// 獲取縣市代碼
|
|
165
|
+
const cityCode = CITY_CODES[letter]
|
|
166
|
+
if (!cityCode) return false
|
|
167
|
+
|
|
168
|
+
// 性別代碼轉換
|
|
169
|
+
const genderValue = genderCode === 'A' || genderCode === 'C' ? 1 : 0
|
|
170
|
+
|
|
171
|
+
// 計算校驗碼
|
|
172
|
+
const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
|
|
173
|
+
const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
|
174
|
+
|
|
175
|
+
let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1] + genderValue * coefficients[2]
|
|
176
|
+
for (let i = 0; i < 7; i++) {
|
|
177
|
+
sum += digits[i] * coefficients[i + 3]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const checksum = (10 - (sum % 10)) % 10
|
|
181
|
+
return checksum === digits[7]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Validates new-style Taiwan resident certificate (新式居留證號)
|
|
186
|
+
*
|
|
187
|
+
* @param {string} value - The new-style resident ID to validate
|
|
188
|
+
* @returns {boolean} True if the new-style resident ID is valid
|
|
189
|
+
*
|
|
190
|
+
* @description
|
|
191
|
+
* Validates new-style resident ID format: 1 letter + 1 type digit + 8 digits
|
|
192
|
+
* Uses the same checksum algorithm as citizen IDs.
|
|
193
|
+
*
|
|
194
|
+
* Format: [A-Z][89]XXXXXXXX
|
|
195
|
+
* - First letter: City/county code
|
|
196
|
+
* - Second digit: Type indicator (8 or 9)
|
|
197
|
+
* - Last 8 digits: Serial number + checksum
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* validateNewResidentId("A812345678") // true/false based on checksum
|
|
202
|
+
* validateNewResidentId("A712345678") // false (invalid type digit)
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
const validateNewResidentId = (value: string): boolean => {
|
|
206
|
+
// 格式檢查:1個英文字母 + [89] + 1個數字[0-9] + 7個數字
|
|
207
|
+
if (!/^[A-Z][89]\d{8}$/.test(value)) {
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const letter = value[0]
|
|
212
|
+
const digits = value.slice(1).split('').map(Number)
|
|
213
|
+
|
|
214
|
+
// 獲取縣市代碼
|
|
215
|
+
const cityCode = CITY_CODES[letter]
|
|
216
|
+
if (!cityCode) return false
|
|
217
|
+
|
|
218
|
+
// 計算校驗碼 (與身分證字號相同邏輯)
|
|
219
|
+
const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
|
|
220
|
+
const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
|
221
|
+
|
|
222
|
+
let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1]
|
|
223
|
+
for (let i = 0; i < 8; i++) {
|
|
224
|
+
sum += digits[i] * coefficients[i + 2]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const checksum = (10 - (sum % 10)) % 10
|
|
228
|
+
return checksum === digits[8]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Main validation function for Taiwan national IDs
|
|
233
|
+
*
|
|
234
|
+
* @param {string} value - The national ID to validate
|
|
235
|
+
* @param {NationalIdType} [type="both"] - Type of ID to accept
|
|
236
|
+
* @param {boolean} [allowOldResident=true] - Whether to accept old-style resident certificates
|
|
237
|
+
* @returns {boolean} True if the national ID is valid
|
|
238
|
+
*
|
|
239
|
+
* @description
|
|
240
|
+
* Validates Taiwan national IDs based on the specified type and options.
|
|
241
|
+
* Supports citizen IDs, resident certificates (both old and new styles).
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* validateTaiwanNationalId("A123456789", "citizen") // Citizen ID only
|
|
246
|
+
* validateTaiwanNationalId("A812345678", "resident") // Resident ID only
|
|
247
|
+
* validateTaiwanNationalId("A123456789", "both") // Accept any valid format
|
|
248
|
+
* validateTaiwanNationalId("AA12345678", "both", false) // Reject old resident format
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
const validateTaiwanNationalId = (value: string, type: NationalIdType = "both", allowOldResident: boolean = true): boolean => {
|
|
252
|
+
if (!/^[A-Z].{9}$/.test(value)) {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
switch (type) {
|
|
257
|
+
case "citizen":
|
|
258
|
+
return validateCitizenId(value)
|
|
259
|
+
case "resident":
|
|
260
|
+
return (allowOldResident ? validateOldResidentId(value) : false) || validateNewResidentId(value)
|
|
261
|
+
case "both":
|
|
262
|
+
return validateCitizenId(value) || (allowOldResident ? validateOldResidentId(value) : false) || validateNewResidentId(value)
|
|
263
|
+
default:
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Creates a Zod schema for Taiwan National ID validation
|
|
270
|
+
*
|
|
271
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
272
|
+
* @param {NationalIdOptions<IsRequired>} [options] - Configuration options for national ID validation
|
|
273
|
+
* @returns {NationalIdSchema<IsRequired>} Zod schema for national ID validation
|
|
274
|
+
*
|
|
275
|
+
* @description
|
|
276
|
+
* Creates a comprehensive Taiwan National ID validator that supports both
|
|
277
|
+
* citizen IDs and resident certificates with configurable validation rules.
|
|
278
|
+
*
|
|
279
|
+
* Features:
|
|
280
|
+
* - Citizen ID validation (身分證字號)
|
|
281
|
+
* - Resident certificate validation (居留證號, old and new formats)
|
|
282
|
+
* - Configurable ID type acceptance
|
|
283
|
+
* - Automatic case conversion to uppercase
|
|
284
|
+
* - Checksum verification
|
|
285
|
+
* - Custom transformation functions
|
|
286
|
+
* - Comprehensive internationalization
|
|
287
|
+
* - Optional field support
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // Accept any valid Taiwan ID
|
|
292
|
+
* const anyIdSchema = nationalId()
|
|
293
|
+
* anyIdSchema.parse("A123456789") // ✓ Valid citizen ID
|
|
294
|
+
* anyIdSchema.parse("A812345678") // ✓ Valid new resident ID
|
|
295
|
+
* anyIdSchema.parse("AA12345678") // ✓ Valid old resident ID
|
|
296
|
+
*
|
|
297
|
+
* // Citizen IDs only
|
|
298
|
+
* const citizenSchema = nationalId({ type: "citizen" })
|
|
299
|
+
* citizenSchema.parse("A123456789") // ✓ Valid
|
|
300
|
+
* citizenSchema.parse("A812345678") // ✗ Invalid (resident ID)
|
|
301
|
+
*
|
|
302
|
+
* // Resident IDs only (new format only)
|
|
303
|
+
* const residentSchema = nationalId({
|
|
304
|
+
* type: "resident",
|
|
305
|
+
* allowOldResident: false
|
|
306
|
+
* })
|
|
307
|
+
* residentSchema.parse("A812345678") // ✓ Valid
|
|
308
|
+
* residentSchema.parse("AA12345678") // ✗ Invalid (old format)
|
|
309
|
+
*
|
|
310
|
+
* // Optional with custom transformation
|
|
311
|
+
* const optionalSchema = nationalId({
|
|
312
|
+
* required: false,
|
|
313
|
+
* transform: (value) => value.replace(/[^A-Z0-9]/g, '') // Remove special chars
|
|
314
|
+
* })
|
|
315
|
+
*
|
|
316
|
+
* // With custom error messages
|
|
317
|
+
* const customSchema = nationalId({
|
|
318
|
+
* i18n: {
|
|
319
|
+
* en: { invalid: "Please enter a valid Taiwan National ID" },
|
|
320
|
+
* 'zh-TW': { invalid: "請輸入有效的身分證或居留證號碼" }
|
|
321
|
+
* }
|
|
322
|
+
* })
|
|
323
|
+
* ```
|
|
324
|
+
*
|
|
325
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
326
|
+
* @see {@link NationalIdOptions} for all available configuration options
|
|
327
|
+
* @see {@link NationalIdType} for supported ID types
|
|
328
|
+
* @see {@link validateTaiwanNationalId} for validation logic details
|
|
329
|
+
*/
|
|
330
|
+
export function nationalId<IsRequired extends boolean = true>(options?: NationalIdOptions<IsRequired>): NationalIdSchema<IsRequired> {
|
|
331
|
+
const {
|
|
332
|
+
required = true,
|
|
333
|
+
type = "both",
|
|
334
|
+
allowOldResident = true,
|
|
335
|
+
transform,
|
|
336
|
+
defaultValue,
|
|
337
|
+
i18n
|
|
338
|
+
} = options ?? {}
|
|
339
|
+
|
|
340
|
+
// Set appropriate default value based on required flag
|
|
341
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
342
|
+
|
|
343
|
+
// Helper function to get custom message or fallback to default i18n
|
|
344
|
+
const getMessage = (key: keyof NationalIdMessages, params?: Record<string, any>) => {
|
|
345
|
+
if (i18n) {
|
|
346
|
+
const currentLocale = getLocale()
|
|
347
|
+
const customMessages = i18n[currentLocale]
|
|
348
|
+
if (customMessages && customMessages[key]) {
|
|
349
|
+
const template = customMessages[key]!
|
|
350
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return t(`taiwan.nationalId.${key}`, params)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Preprocessing function
|
|
357
|
+
const preprocessFn = (val: unknown) => {
|
|
358
|
+
if (val === "" || val === null || val === undefined) {
|
|
359
|
+
return actualDefaultValue
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let processed = String(val).trim().toUpperCase()
|
|
363
|
+
|
|
364
|
+
// If after trimming we have an empty string and the field is optional, return null
|
|
365
|
+
if (processed === "" && !required) {
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (transform) {
|
|
370
|
+
processed = transform(processed)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return processed
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
377
|
+
|
|
378
|
+
const schema = baseSchema.refine((val) => {
|
|
379
|
+
if (val === null) return true
|
|
380
|
+
|
|
381
|
+
// Required check
|
|
382
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
383
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (val === null) return true
|
|
387
|
+
if (!required && val === "") return true
|
|
388
|
+
|
|
389
|
+
// Taiwan National ID validation
|
|
390
|
+
if (!validateTaiwanNationalId(val, type, allowOldResident)) {
|
|
391
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return true
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
return schema as unknown as NationalIdSchema<IsRequired>
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Utility functions exported for external use
|
|
402
|
+
*
|
|
403
|
+
* @description
|
|
404
|
+
* These validation functions can be used independently for national ID validation
|
|
405
|
+
* without creating a full Zod schema. Useful for custom validation logic.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* import {
|
|
410
|
+
* validateTaiwanNationalId,
|
|
411
|
+
* validateCitizenId,
|
|
412
|
+
* validateOldResidentId,
|
|
413
|
+
* validateNewResidentId
|
|
414
|
+
* } from './national-id'
|
|
415
|
+
*
|
|
416
|
+
* // General validation
|
|
417
|
+
* const isValid = validateTaiwanNationalId("A123456789", "both") // boolean
|
|
418
|
+
*
|
|
419
|
+
* // Specific format validation
|
|
420
|
+
* const isCitizen = validateCitizenId("A123456789") // boolean
|
|
421
|
+
* const isOldResident = validateOldResidentId("AA12345678") // boolean
|
|
422
|
+
* const isNewResident = validateNewResidentId("A812345678") // boolean
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export { validateTaiwanNationalId, validateCitizenId, validateOldResidentId, validateNewResidentId }
|