Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/openapi.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -15,6 +15,7 @@ const result = {
const examples: Record<string, string> = {
tckn: '10000000146',
vkn: '1234567890',
'tax-id': '1234567890',
iban: 'TR330006100519786457841326',
phone: '05321234567',
plate: '34ABC123',
Expand Down
5 changes: 3 additions & 2 deletions src/validators/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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';

export const VALIDATORS = {
tckn: validateTckn,
vkn: validateVkn,
'tax-id': validateTaxId,
iban: validateIban,
phone: validatePhone,
plate: validatePlate,
Expand All @@ -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';
Expand Down
15 changes: 15 additions & 0 deletions src/validators/vkn.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 } };
}
19 changes: 18 additions & 1 deletion test/validators.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand Down
Loading