diff --git a/README.md b/README.md index ca7c4be..d58982e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ built-in type stripping. | --- | --- | --- | | `tckn` | T.C. Kimlik No checksum (11 digits) | — | | `vkn` | Vergi Kimlik No checksum (10 digits, GİB algorithm) | — | +| `tax-id` | Invoice "VKN / TCKN" field: picks the right check by length | `kind` (`vkn` / `tckn`), `holder` (`company` / `individual`) | | `iban` | TR format, reserve digit, ISO 13616 mod-97 | bank code, bank name, grouped format | | `phone` | `+90`, `0090`, `0…` and free-form input | E.164, national format, mobile / landline / 444 / 850 | | `plate` | province 01–81, letter/digit rules | formatted plate, province | @@ -32,7 +33,7 @@ built-in type stripping. git clone https://github.com/bahriy-dev/tr-validator-api.git cd tr-validator-api npm start # http://localhost:3000 -npm test # node:test, 43 tests +npm test # node:test, 47 tests ``` Or with Docker: diff --git a/src/openapi.ts b/src/openapi.ts index f8f525a..687bf0c 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -1,4 +1,4 @@ -const types = ['tckn', 'vkn', 'iban', 'phone', 'plate', 'postal-code']; +const types = ['tckn', 'vkn', 'tax-id', 'iban', 'phone', 'plate', 'postal-code']; const result = { type: 'object', @@ -15,6 +15,7 @@ const result = { const examples: Record = { tckn: '10000000146', vkn: '1234567890', + 'tax-id': '1234567890', iban: 'TR330006100519786457841326', phone: '05321234567', plate: '34ABC123', diff --git a/src/validators/index.ts b/src/validators/index.ts index 953cb75..6c6b47a 100644 --- a/src/validators/index.ts +++ b/src/validators/index.ts @@ -1,6 +1,6 @@ import type { ValidationResult } from '../types.ts'; import { validateTckn } from './tckn.ts'; -import { validateVkn } from './vkn.ts'; +import { validateVkn, validateTaxId } from './vkn.ts'; import { validateIban } from './iban.ts'; import { validatePhone } from './phone.ts'; import { validatePlate, validatePostalCode } from './address.ts'; @@ -8,6 +8,7 @@ import { validatePlate, validatePostalCode } from './address.ts'; export const VALIDATORS = { tckn: validateTckn, vkn: validateVkn, + 'tax-id': validateTaxId, iban: validateIban, phone: validatePhone, plate: validatePlate, @@ -21,7 +22,7 @@ export function isValidatorType(t: unknown): t is ValidatorType { } export { validateTckn, completeTckn } from './tckn.ts'; -export { validateVkn, vknCheckDigit } from './vkn.ts'; +export { validateVkn, vknCheckDigit, validateTaxId, detectTaxIdKind } from './vkn.ts'; export { validateIban, ibanCheckDigits, TR_BANKS } from './iban.ts'; export { validatePhone } from './phone.ts'; export { validatePlate, validatePostalCode } from './address.ts'; diff --git a/src/validators/vkn.ts b/src/validators/vkn.ts index bae3fc0..8e992cf 100644 --- a/src/validators/vkn.ts +++ b/src/validators/vkn.ts @@ -1,4 +1,5 @@ import type { ValidationResult } from '../types.ts'; +import { validateTckn } from './tckn.ts'; /** * Vergi Kimlik Numarası (Turkish tax ID, 10 digits) checksum validation, @@ -39,3 +40,17 @@ export function detectTaxIdKind(value: string): 'tckn' | 'vkn' | null { if (/^\d{10}$/.test(v)) return 'vkn'; return null; } + +/** + * Validates the "VKN / TCKN" field of an invoice: 10 digits means a company tax number, + * 11 digits means an individual's national ID (sole proprietors and e-Arşiv invoices use it). + */ +export function validateTaxId(input: string): ValidationResult { + const value = String(input ?? '').replace(/\s+/g, ''); + const kind = detectTaxIdKind(value); + if (!kind) return { valid: false, value, reason: 'Tax ID must be a 10-digit VKN or an 11-digit TCKN' }; + const result = kind === 'vkn' ? validateVkn(value) : validateTckn(value); + return result.valid + ? { ...result, details: { kind, holder: kind === 'vkn' ? 'company' : 'individual' } } + : { ...result, details: { kind } }; +} diff --git a/test/validators.test.ts b/test/validators.test.ts index 38f14b8..22cff2e 100644 --- a/test/validators.test.ts +++ b/test/validators.test.ts @@ -1,7 +1,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { - validateTckn, completeTckn, validateVkn, vknCheckDigit, validateIban, ibanCheckDigits, + validateTckn, completeTckn, validateVkn, vknCheckDigit, validateTaxId, validateIban, ibanCheckDigits, validatePhone, validatePlate, validatePostalCode, listProvinces, provinceByCode, } from '../src/validators/index.ts'; @@ -31,6 +31,23 @@ describe('VKN', () => { }); }); +describe('Tax ID (VKN or TCKN)', () => { + it('accepts a VKN as a company', () => { + const r = validateTaxId('1234567890'); + assert.equal(r.valid, true); + assert.deepEqual(r.details, { kind: 'vkn', holder: 'company' }); + }); + it('accepts a TCKN as an individual', () => { + assert.deepEqual(validateTaxId('10000000146').details, { kind: 'tckn', holder: 'individual' }); + }); + it('reports which check failed', () => { + const r = validateTaxId('10000000147'); + assert.equal(r.valid, false); + assert.equal(r.details?.kind, 'tckn'); + }); + it('rejects other lengths', () => assert.match(validateTaxId('123').reason!, /10-digit VKN or an 11-digit TCKN/)); +}); + describe('IBAN', () => { it('accepts a valid TR IBAN with spaces', () => { const r = validateIban('TR33 0006 1005 1978 6457 8413 26');