From 5e0241ae1269968b28d5cbb4477e2ded2926304c Mon Sep 17 00:00:00 2001 From: Ryan Duguid Date: Thu, 1 Oct 2026 08:47:20 +1000 Subject: [PATCH] Add validation for New Zealand Business Numbers --- stdnum/nz/nzbn.py | 74 +++++++++++++++++ tests/test_nz_nzbn.doctest | 158 +++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 stdnum/nz/nzbn.py create mode 100644 tests/test_nz_nzbn.doctest diff --git a/stdnum/nz/nzbn.py b/stdnum/nz/nzbn.py new file mode 100644 index 00000000..2c7c9903 --- /dev/null +++ b/stdnum/nz/nzbn.py @@ -0,0 +1,74 @@ +# nzbn.py - functions for handling New Zealand Business Numbers +# +# Copyright (C) 2026 Ryan Duguid +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see . + +"""NZBN (New Zealand Business Number). + +The New Zealand Business Number (NZBN) identifies a business and links to +its primary business data. It is a 13-digit Global Location Number (GLN) +supplied by GS1 New Zealand, beginning with 942 and ending in a check digit. + +This module checks the format, length, prefix and check digit. It does not +check whether a number has been issued as an NZBN. + +More information: + +* https://www.nzbn.govt.nz/whats-an-nzbn/about/ +* https://portal.api.business.govt.nz/api/nzbn +* https://www.companiesoffice.govt.nz/all-registers/insolvency-practitioners/obligations/report-a-serious-problem/ + +>>> compact(' 9429 0001-06078 ') +'9429000106078' +>>> validate('9429000106078') +'9429000106078' +>>> is_valid('9429000106079') +False +""" + +from __future__ import annotations + +from stdnum import ean +from stdnum.exceptions import * +from stdnum.util import isdigits + + +def compact(number: str) -> str: + """Convert the number to its minimal representation, removing valid + separators and surrounding whitespace.""" + return ean.compact(number) + + +def validate(number: str) -> str: + """Check the number's format, length, prefix and check digit. + + This does not check whether the number has been issued as an NZBN. + """ + number = compact(number) + if not isdigits(number): + raise InvalidFormat() + if len(number) != 13: + raise InvalidLength() + if not number.startswith('942'): + raise InvalidComponent() + return ean.validate(number) + + +def is_valid(number: str) -> bool: + """Check the number's format, length, prefix and check digit.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/tests/test_nz_nzbn.doctest b/tests/test_nz_nzbn.doctest new file mode 100644 index 00000000..f3620146 --- /dev/null +++ b/tests/test_nz_nzbn.doctest @@ -0,0 +1,158 @@ +test_nz_nzbn.doctest - more detailed doctests for the stdnum.nz.nzbn module + +Copyright (C) 2026 Ryan Duguid + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see . + + +>>> from stdnum import ean +>>> from stdnum.nz import nzbn +>>> from stdnum.util import get_cc_module + + +The official API documentation uses this NZBN as an example: +https://portal.api.business.govt.nz/api/nzbn + +>>> nzbn.validate(' 9429 0001-06078 ') +'9429000106078' +>>> nzbn.is_valid('9429000106078') +True + + +These public corporate NZBNs were retrieved from the Peppol Directory on +30 September 2026, selecting NZ businesses with Limited or Ltd names: +https://directory.peppol.eu/search/1.0/json?country=NZ&name=Limited&rpc=20&rpi=0 +The NZ Peppol Authority describes 0088 participant identifiers as NZBNs: +https://www.einvoicing.govt.nz/peppol +Only the numbers are retained. These samples demonstrate public use, not +current registration status. The tests do not require online lookups. + +>>> numbers = ''' +... 9429033583235 9429036313396 9429036508273 9429038040771 +... 9429042199526 9429046598677 9429047226265 9429047352476 +... 9429047631373 9429047646063 9429050822034 9429051109004 +... 9429051279066 9429051383916 9429051806613 9429051826130 +... 9429051919207 9429052018237 9429052239762 9429052925023 +... '''.split() +>>> all(nzbn.validate(number) == number for number in numbers) +True +>>> all(nzbn.is_valid(number) for number in numbers) +True + + +Compaction uses the shared cleaner, including recognised Unicode digits, +spaces and dashes. It does not validate the number. + +>>> nzbn.compact(' ABC-12 ') +'ABC12' +>>> nzbn.validate('9429000106078') +'9429000106078' +>>> nzbn.validate(' 9429\u00a00001\u221206078 ') +'9429000106078' + + +Malformed inputs raise InvalidFormat before checking length or checksum. +Unsupported Unicode digits and internal tabs are not stripped or coerced. + +>>> nzbn.validate('X') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> nzbn.validate(' - ') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> nzbn.validate('942900010607A') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> nzbn.validate('9429.000106078') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> nzbn.validate('9429\t000106078') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> nzbn.validate('942900010607\u1b53') +Traceback (most recent call last): + ... +InvalidFormat: ... + + +The length gate rejects EAN-valid 8-, 12- and 14-digit numbers, and checks +length before the checksum even when a 12-digit checksum is wrong. + +>>> all(ean.is_valid(number) for number in ( +... '73513537', '036000291452', '98412345678908')) +True +>>> nzbn.validate('73513537') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> nzbn.validate('036000291452') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> nzbn.validate('98412345678908') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> nzbn.validate('036000291453') +Traceback (most recent call last): + ... +InvalidLength: ... + + +The following numbers are constructed test cases, with no NZBN assignment +asserted. The documented prefix is 942, rather than a narrower 9429 rule. +A different prefix raises InvalidComponent before checking its checksum. + +>>> nzbn.validate('9420000000007') +'9420000000007' +>>> ean.validate('1234567890128') +'1234567890128' +>>> nzbn.validate('1234567890128') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> nzbn.validate('1234567890129') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> nzbn.validate('0000000000000') +Traceback (most recent call last): + ... +InvalidComponent: ... + + +A changed check digit fails once format, length and prefix are valid. +The boolean API catches every validation failure, including cleaner errors. + +>>> nzbn.validate('9429000106079') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> invalid_numbers = ( +... '', 'X', '73513537', '1234567890128', '9429000106079', None, 9429000106078) +>>> any(nzbn.is_valid(number) for number in invalid_numbers) +False + + +Country-module discovery includes NZBN without changing the NZ VAT alias. + +>>> get_cc_module('NZ', 'nzbn') is nzbn +True +>>> get_cc_module('nz', 'vat').__name__ +'stdnum.nz.ird'