From e8fab0989dfbb65b1f2b8940f7c87a23604b5897 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Tue, 18 Aug 2026 13:47:12 +0900 Subject: [PATCH] fix(cz/dic): correct special 9-digit DIC check to match python-stdnum cz/dic checkSpecial (the special 9-digit DIC case, numbers starting with 6) diverged from the python-stdnum reference (cz/dic.py::calc_check_digit_special) two ways: (1) it included the leading 6 in the weighted sum (splitAt(value,-1) -> 8-digit front, weights [8,7,6,5,4,3,2,1]); the reference sums digits 2..8 only (number[1:8], weights [8,7,6,5,4,3,2]), excluding the leading 6 and the check digit. (2) the final reduction (8 - ((10 - sum) % 11)) % 10 relied on Python's non-negative modulo, but JS % keeps the dividend's sign, so a negative inner value yielded e.g. '-1' and never matched. As a result every special-case Czech VAT was falsely rejected via validate() -> InvalidChecksum. Drop the leading digit from the weighted sum, use weights [8,7,6,5,4,3,2], and compute the final digit with the existing pymod helper. Fixes #164 --- src/cz/dic.spec.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ src/cz/dic.ts | 19 ++++++++++++------- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/cz/dic.spec.ts b/src/cz/dic.spec.ts index 7ac31f21..b40f3e51 100644 --- a/src/cz/dic.spec.ts +++ b/src/cz/dic.spec.ts @@ -25,4 +25,46 @@ describe('cz/dic', () => { expect(result.error).toBeInstanceOf(InvalidChecksum); }); + + // Special 9-digit case (DIČ starting with 6), see issue #164. + // These are valid per the official EU VIES service but were falsely + // rejected because checkSpecial diverged from python-stdnum. + it('validate:CZ687836437', () => { + const result = validate('CZ687836437'); + + expect(result.isValid).toBe(true); + }); + + it('validate:640903926', () => { + const result = validate('640903926'); + + expect(result.isValid).toBe(true); + }); + + it('validate:681208919', () => { + const result = validate('681208919'); + + expect(result.isValid).toBe(true); + }); + + // VAT-group ID (format 699nnnnnk). + it('validate:699005400', () => { + const result = validate('699005400'); + + expect(result.isValid).toBe(true); + }); + + // Wrong check digit on the special case must still be rejected. + it('validate:687836438', () => { + const result = validate('687836438'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); + + // Normal (non-special) 9-digit DIČ still delegates to rc and validates. + it('validate:CZ991231123', () => { + const result = validate('CZ991231123'); + + expect(result.isValid && result.compact).toEqual('991231123'); + }); }); diff --git a/src/cz/dic.ts b/src/cz/dic.ts index 7a7f6a4a..740aa33d 100644 --- a/src/cz/dic.ts +++ b/src/cz/dic.ts @@ -12,6 +12,7 @@ import * as exceptions from '../exceptions'; import { strings, weightedSum } from '../util'; +import { pymod } from '../util/pymod'; import { Validator, ValidateReturn } from '../types'; import { validate as rcValidate } from './rc'; @@ -41,17 +42,21 @@ function checkLegal(value: string): boolean { } function checkSpecial(value: string): boolean { - // check = sum((8 - i) * int(n) for i, n in enumerate(number)) % 11 - - // return str((8 - (10 - check) % 11) % 10) - - const [front, check] = strings.splitAt(value, -1); + // Reference: python-stdnum cz/dic.py::calc_check_digit_special + // check = sum((8 - i) * int(n) for i, n in enumerate(number[1:8])) % 11 + // return str((8 - (10 - check) % 11) % 10) + // + // The weighted sum runs over digits 2..8 only: the leading 6 and the check + // digit are excluded (weights [8, 7, 6, 5, 4, 3, 2]). The final reduction + // relies on Python's always-non-negative modulo, so pymod is used to match + // it -- JS '%' keeps the dividend's sign and would yield a negative digit. + const [, front, check] = strings.splitAt(value, 1, -1); const sum = weightedSum(front, { modulus: 11, - weights: [8, 7, 6, 5, 4, 3, 2, 1], + weights: [8, 7, 6, 5, 4, 3, 2], }); - const digit = String((8 - ((10 - sum) % 11)) % 10); + const digit = String(pymod(8 - ((10 - sum) % 11), 10)); return digit === check; }