Skip to content

Commit 75e790c

Browse files
authored
1 parent a954601 commit 75e790c

File tree

4 files changed

+261
-7
lines changed

4 files changed

+261
-7
lines changed

stdnum/sa/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
# provide aliases
2424
from stdnum.sa import vat_number as vat # noqa: F401
2525
from stdnum.sa import tin_number as business_tin # noqa: F401
26-
from stdnum.sa import tin_number as personal_tin # noqa: F401
26+
from stdnum.sa import nid as personal_tin # noqa: F401

stdnum/sa/nid.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# nid.py - functions for handling Saudi Arabian National ID numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2024
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19+
# 02110-1301 USA
20+
21+
"""National ID (Saudi Arabian National ID or Iqama number).
22+
23+
The Saudi Arabian National ID (for Saudi citizens, رقم الهوية الوطنية) or Iqama
24+
(Residence ID for expatriates, الإقامة) serve as the tax ID for individuals when applicable.
25+
26+
The number consists of 10 digits:
27+
- For Saudi citizens, it typically starts with 1 or 2
28+
- For resident expatriates (Iqama), it typically starts with 2
29+
- The number includes an internal check digit for validation
30+
31+
For tax purposes, sometimes a 12-digit format is used:
32+
- First 2 digits: region code
33+
- Remaining 10 digits: the unique ID
34+
35+
More information:
36+
* https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/
37+
38+
>>> validate('1012345678')
39+
'1012345678'
40+
>>> validate('abcdefghij') # doctest: +IGNORE_EXCEPTION_DETAIL
41+
Traceback (most recent call last):
42+
...
43+
InvalidComponent: ...
44+
>>> validate('10123456') # doctest: +IGNORE_EXCEPTION_DETAIL
45+
Traceback (most recent call last):
46+
...
47+
InvalidLength: ...
48+
>>> format('101-234-5678')
49+
'1012345678'
50+
>>> format('1312345678901')
51+
'2345678901'
52+
>>> is_valid('1012345678')
53+
True
54+
>>> is_valid('131012345678')
55+
True
56+
"""
57+
58+
from stdnum.exceptions import InvalidLength, InvalidComponent, ValidationError
59+
from stdnum.util import clean, isdigits
60+
61+
62+
def compact(number):
63+
"""Convert the number to the minimal representation.
64+
65+
This strips the number of any valid separators and removes surrounding
66+
whitespace.
67+
"""
68+
return clean(number, ' -/').strip()
69+
70+
71+
def validate(number):
72+
"""Check if the number is a valid Saudi Arabian National ID or Iqama number.
73+
74+
This checks the length and that all characters are digits.
75+
The ID can be either 10 digits (standard format) or 12 digits (with region code).
76+
For Saudi citizens, the ID typically starts with 1 or 2.
77+
For resident expatriates (Iqama), the ID typically starts with 2.
78+
"""
79+
number = compact(number)
80+
81+
if len(number) == 12:
82+
# If 12 digits (with region code), consider only the last 10 digits
83+
number = number[2:]
84+
85+
if len(number) != 10:
86+
raise InvalidLength()
87+
88+
if not isdigits(number):
89+
raise InvalidComponent()
90+
91+
first_digit = number[0]
92+
if first_digit not in ('1', '2'):
93+
raise InvalidComponent('National ID should start with 1 or 2')
94+
95+
# Note: The specific check digit algorithm is not publicly documented,
96+
# so we cannot implement a full validation here.
97+
98+
return number
99+
100+
101+
def is_valid(number):
102+
"""Check if the number is a valid Saudi Arabian National ID or Iqama number."""
103+
try:
104+
return bool(validate(number))
105+
except ValidationError:
106+
return False
107+
108+
109+
def format(number):
110+
"""Format the number to the standard representation.
111+
112+
If the number is 12 digits (with region code), return only the last 10 digits.
113+
If the number is 13 digits, return only the last 10 digits.
114+
Otherwise, return the compacted 10-digit number.
115+
"""
116+
number = compact(number)
117+
if len(number) == 12:
118+
return number[2:]
119+
elif len(number) == 13:
120+
return number[3:]
121+
return number

stdnum/sa/tin_number.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,77 @@
1818
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
1919
# 02110-1301 USA
2020

21-
from stdnum.exceptions import InvalidLength, ValidationError
21+
"""TIN (Saudi Arabian Tax Identification Number).
22+
23+
The Saudi Arabian Tax Identification Number (TIN), often referred to as the "Unique Number"
24+
(الرقم المميز) is issued by ZATCA (Zakat, Tax and Customs Authority) to
25+
taxpayer entities for tax and zakat purposes.
26+
27+
The number consists of 10 digits:
28+
- First digit is 3 (indicating KSA's GCC country code)
29+
- Next 8 digits are a serial identifier
30+
- Last digit is a check digit
31+
32+
More information:
33+
* https://web-archive.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Saudi-Arabia-TIN.pdf
34+
* https://zatca.gov.sa/
35+
36+
>>> validate('3002707692')
37+
'3002707692'
38+
>>> validate('1234567890') # doctest: +IGNORE_EXCEPTION_DETAIL
39+
Traceback (most recent call last):
40+
...
41+
InvalidComponent: ...
42+
>>> validate('300270769') # doctest: +IGNORE_EXCEPTION_DETAIL
43+
Traceback (most recent call last):
44+
...
45+
InvalidLength: ...
46+
>>> format('300-270-7692')
47+
'3002707692'
48+
>>> is_valid('3002707692')
49+
True
50+
"""
51+
52+
from stdnum.exceptions import InvalidLength, InvalidComponent, ValidationError
53+
from stdnum.util import clean, isdigits
54+
55+
56+
def compact(number):
57+
"""Convert the number to the minimal representation.
58+
59+
This strips the number of any valid separators and removes surrounding
60+
whitespace.
61+
"""
62+
return clean(number, ' -/').strip()
2263

2364

2465
def validate(number):
25-
"""Check if the number is valid. This checks the length."""
66+
"""Check if the number is a valid Saudi Arabian TIN number.
67+
68+
This checks the length, first digit (must be 3 for KSA), and that
69+
all characters are digits.
70+
"""
71+
number = compact(number)
2672
if len(number) != 10:
2773
raise InvalidLength()
74+
if not isdigits(number):
75+
raise InvalidComponent()
76+
if number[0] != '3':
77+
raise InvalidComponent()
78+
# Note: We would implement a check digit validation here if the specific
79+
# algorithm was known. Currently, it's known that the last digit is a check
80+
# digit, but the exact calculation method is not documented publicly.
2881
return number
2982

3083

3184
def is_valid(number):
32-
"""Check if the number is a valid."""
85+
"""Check if the number is a valid Saudi Arabian TIN number."""
3386
try:
3487
return bool(validate(number))
3588
except ValidationError:
3689
return False
90+
91+
92+
def format(number):
93+
"""Format the number to the standard representation."""
94+
return compact(number)

stdnum/sa/vat_number.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,94 @@
1818
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
1919
# 02110-1301 USA
2020

21-
from stdnum.exceptions import InvalidLength, ValidationError
21+
"""VAT (Kingdom of Saudi Arabia Value Added Tax number).
22+
23+
The Saudi Arabian VAT number is a 15-digit number used for Value Added Tax
24+
purposes in Saudi Arabia. It incorporates the 10-digit Tax Identification
25+
Number (TIN) and adds 5 more digits with specific meanings.
26+
27+
The structure is as follows:
28+
- First digit: GCC member state code (3 for Saudi Arabia)
29+
- Next 8 digits: Base serial number
30+
- Next 1 digit: Check digit (completes the 10-digit TIN)
31+
- Next 3 digits: Branch/establishment identifier (000 for headquarters,
32+
001, 002, etc. for branches)
33+
- Last 2 digits: Tax type identifier (01 for VAT)
34+
35+
More information:
36+
* https://zatca.gov.sa/en/eServices/Pages/TaxpayerLookup.aspx
37+
38+
>>> validate('300270769210001')
39+
'300270769210001'
40+
>>> validate('3002707692100') # too short
41+
Traceback (most recent call last):
42+
...
43+
stdnum.exceptions.InvalidLength: The number has an invalid length.
44+
>>> validate('300270769X10001') # invalid character
45+
Traceback (most recent call last):
46+
...
47+
stdnum.exceptions.InvalidFormat: The number has an invalid format.
48+
>>> is_valid('300270769210001')
49+
True
50+
>>> is_valid('400270769210001') # should start with 3 for Saudi Arabia
51+
False
52+
>>> format('300270769210001')
53+
'3 00270769 2 100 01'
54+
"""
55+
56+
from stdnum.exceptions import InvalidLength, InvalidFormat, ValidationError
57+
from stdnum.util import clean, isdigits
58+
59+
60+
def compact(number):
61+
"""Convert the number to the minimal representation.
62+
63+
This strips the number of any valid separators and removes
64+
surrounding whitespace.
65+
"""
66+
return clean(number, ' -').strip()
2267

2368

2469
def validate(number):
25-
"""Check if the number is valid. This checks the length."""
70+
"""Check if the number is a valid Saudi Arabian VAT number.
71+
72+
This checks the length, format, and basic country code.
73+
"""
74+
number = compact(number)
2675
if len(number) != 15:
2776
raise InvalidLength()
77+
if not isdigits(number):
78+
raise InvalidFormat()
79+
# First digit should be 3 for Saudi Arabia
80+
if number[0] != '3':
81+
raise InvalidFormat('First digit should be 3 for Saudi Arabia')
82+
# Last two digits should be 01 for VAT
83+
if number[-2:] != '01':
84+
raise InvalidFormat('Last two digits should be 01 for VAT')
2885
return number
2986

3087

3188
def is_valid(number):
32-
"""Check if the number is a valid."""
89+
"""Check if the number is a valid Saudi Arabian VAT number."""
3390
try:
3491
return bool(validate(number))
3592
except ValidationError:
3693
return False
94+
95+
96+
def format(number):
97+
"""Format the number in the standard format.
98+
99+
For a VAT number this would be: 3 00270769 2 100 01
100+
(Country code, serial, check digit, branch, tax type)
101+
"""
102+
number = compact(number)
103+
if len(number) != 15:
104+
raise InvalidLength()
105+
return ' '.join([
106+
number[0], # Country code (3)
107+
number[1:9], # Base serial
108+
number[9:10], # Check digit
109+
number[10:13], # Branch identifier
110+
number[13:15], # Tax type
111+
])

0 commit comments

Comments
 (0)