From 3fec5ad02d00df08b1310248929c1a95aebc3d26 Mon Sep 17 00:00:00 2001 From: Andrew Chi <17609957+gccbb02498@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:40:24 +0800 Subject: [PATCH] fix(tw/ubn): accept 2023 relaxed check-digit rule (divisible by 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Effective 2023-04-01, Taiwan's Ministry of Finance changed the Unified Business Number (統一編號) check-digit logic so the weighted digit sum only needs to be divisible by 5 (previously 10), expanding the number space while keeping the same 8-digit format. Every old, divisible-by-10 number is still divisible by 5, so existing numbers remain valid. - Relax the checksum from `sum % 10 === 0` to `sum % 5 === 0`. - Keep the 7th-digit-is-7 special case (7*4=28 sums to 10), now checked against the mod-5 remainder. - Add tests: an old-rule number (04595257), a new divisible-by-5 number (00501508), and a new 7th-digit special case (00000074). Ref: https://www.fia.gov.tw/singlehtml/3?cntId=c4d9cff38c8642ef8872774ee9987283 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tw/ubn.spec.ts | 21 +++++++++++++++++++++ src/tw/ubn.ts | 11 ++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/tw/ubn.spec.ts b/src/tw/ubn.spec.ts index 83691c5c..91e03241 100644 --- a/src/tw/ubn.spec.ts +++ b/src/tw/ubn.spec.ts @@ -14,6 +14,27 @@ describe('tw/ubn', () => { expect(result.isValid && result.compact).toEqual('00501503'); }); + // Old-rule numbers (weighted digit sum divisible by 10) stay valid. + it('validate:04595257 (old rule, divisible by 10)', () => { + const result = validate('04595257'); + + expect(result.isValid && result.compact).toEqual('04595257'); + }); + + // Since 2023-04-01 the weighted digit sum only needs to be divisible by 5. + it('validate:00501508 (divisible by 5, new rule)', () => { + const result = validate('00501508'); + + expect(result.isValid && result.compact).toEqual('00501508'); + }); + + // 7th digit is 7: product 7*4=28 sums to 10, so a remainder of 4 also passes. + it('validate:00000074 (7th-digit special case, new rule)', () => { + const result = validate('00000074'); + + expect(result.isValid && result.compact).toEqual('00000074'); + }); + it('validate:12345', () => { const result = validate('12345'); diff --git a/src/tw/ubn.ts b/src/tw/ubn.ts index 5ed84474..7ba98923 100644 --- a/src/tw/ubn.ts +++ b/src/tw/ubn.ts @@ -5,9 +5,15 @@ * within Taiwan for tax (VAT) purposes. The number consists of 8 digits, the * last being a check digit. * + * Since 2023-04-01 the Ministry of Finance relaxed the check-digit logic from + * "the weighted digit sum is divisible by 10" to "divisible by 5", so that the + * number space can be expanded while staying compatible with existing numbers + * (every old, divisible-by-10 number is also divisible by 5). + * * Source * https://zh.wikipedia.org/wiki/統一編號 * https://findbiz.nat.gov.tw/fts/query/QueryBar/queryInit.do?request_locale=en + * https://www.fia.gov.tw/singlehtml/3?cntId=c4d9cff38c8642ef8872774ee9987283 * * ENTITY */ @@ -63,7 +69,10 @@ const impl: Validator = { .split('') .reduce((acc, d) => (acc + parseInt(d, 10)) % 10, 0); - if (!(sum === 0 || (sum === 9 && value[6] === '7'))) { + // The weighted digit sum must be divisible by 5 (was 10 before 2023-04-01). + // Special case: when the 7th digit is 7 its product 7*4=28 sums to 10, which + // may be counted as either 1 or 0, so a remainder of 4 is also accepted. + if (!(sum % 5 === 0 || (value[6] === '7' && (sum + 1) % 5 === 0))) { return { isValid: false, error: new exceptions.InvalidChecksum() }; }