Skip to content

Commit fe5136f

Browse files
drupolalphp
andauthored
Refactor spain country handler (#42)
* Refactor Spain country handler to support more Spanish TIN * Join PATTERN_2 and PATTERN_3 * Improves readability * Fix PATTERN_2 * Add reference of Spanish TINs * Added more cases for testing * Added reference of Junta de Andalucía * static method getChecksum * Added TOC on documentation * Documentation: remove DNI and NIE link notes * getChacksum as private method * Refactor Spain country handler to support more Spanish TIN * Join PATTERN_2 and PATTERN_3 --------- Co-authored-by: Fernando Herrero <[email protected]>
1 parent d3a6a1e commit fe5136f

File tree

4 files changed

+256
-47
lines changed

4 files changed

+256
-47
lines changed

docs/TIN-Country_Sheet_ES.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# TAX IDENTIFICATION NUMBERS (TINs) - Country Sheet: Spain (ES)
2+
3+
**References:**
4+
- [Spain-TIN.pdf on OECD](https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Spain-TIN.pdf)
5+
- [BOE-A-2008-3580](https://www.boe.es/eli/es/o/2008/02/20/eha451/con#a3)
6+
- [Calculation of the DNI/NIE check digit](https://www.interior.gob.es/opencms/es/servicios-al-ciudadano/tramites-y-gestiones/dni/calculo-del-digito-de-control-del-nif-nie/)
7+
8+
# Table of contents
9+
- [Section I – TIN Description](#section-i--tin-description)
10+
- [Section II – TIN Structure](#section-ii--tin-structure)
11+
- [Natural Persons with DNI¹ or NIE²](#natural-persons-with-dni-or-nie)
12+
- [Natural Persons without DNI¹ or NIE²](#natural-persons-without-dni-or-nie)
13+
- [Non-Natural Persons](#non-natural-persons)
14+
- [Section III – Calculation of the TIN check digit](#section-iii--calculation-of-the-tin-check-digit)
15+
- [Natural Persons with DNI or NIE](#natural-persons-with-dni-or-nie-1)
16+
- [Natural Persons without DNI or NIE and Non-Natural Persons](#natural-persons-without-dni-or-nie-and-non-natural-persons)
17+
18+
## Section I – TIN Description
19+
20+
Spain issues TINs, which are reported on official documents of identification.
21+
22+
TIN in Spain is **unique** for tax and customs purposes and contains **nine characters, the last of them is a letter for control (Natural persons) or a control character (Non - natural persons)**.
23+
24+
**Natural persons of Spanish nationality:** Generally, the TIN is the number on your National Identity Card, issued by the Ministry of Internal Affairs (General Directorate of Police). The Tax Administration will provide Spanish natural persons who are not obliged to possess a National Identity Card (DNI) with a Tax Identification Number (TIN) starting with an L (non-resident Spaniards) or a K (resident Spaniards under the age of 14 years), upon request.
25+
26+
**Natural persons without Spanish nationality:** Generally, their Tax Identification Number (TIN) is the Foreigners’ Identification Number (NIE), likewise issued by the Ministry of Internal Affairs. Natural persons without Spanish nationality who do not possess a Foreigners’ Identification Number (NIE) but need a Tax Identification Number (TIN) because they are going to engage in transactions involving Spanish taxation can obtain a Tax Identification Number starting with the letter M, that will have a
27+
transitory nature, until they obtain a Foreigners’ Identification Number (NIE), where appropriate, also issued by the Tax Administration.
28+
29+
Concerning the **entities**, they are obliged to obtain a TIN, which is issued by the Tax Administration
30+
31+
[TOC](#table-of-contents)
32+
33+
## Section II – TIN Structure
34+
35+
### Natural Persons with DNI¹ or NIE²
36+
1. **DNI** = Documento Nacional de Identidad (National Identity Card)
37+
2. **NIE** = Número de Identificación de Extranjero (Foreigners’ Identification Number)
38+
39+
| Format | Explanation | Comments |
40+
|---------- |----------------------------------------|------------------------------|
41+
| 99999999C | 8 digits + 1 Control letter | Spanish Natural Persons: DNI |
42+
| X9999999C | Letter X + 7 digits + 1 Control letter | Foreigners with NIE |
43+
| Y9999999C | Letter Y + 7 digits + 1 Control letter | Foreigners with NIE |
44+
| Z9999999C | Letter Z + 7 digits + 1 Control letter | Foreigners with NIE |
45+
46+
[TOC](#table-of-contents)
47+
48+
### Natural Persons without DNI¹ or NIE²
49+
1. **DNI** = Documento Nacional de Identidad (National Identity Card)
50+
2. **NIE** = Número de Identificación de Extranjero (Foreigners’ Identification Number)
51+
52+
| Format | Explanation | Comments |
53+
|---------- |----------------------------------------|-----------------------------------------|
54+
| K9999999C | Letter K + 7 digits + 1 Control letter | Resident Spaniards under 14 without DNI |
55+
| L9999999C | Letter L + 7 digits + 1 Control letter | Non-resident Spaniards without DNI |
56+
| M9999999C | Letter M + 7 digits + 1 Control letter | Foreigners without NIE |
57+
58+
[TOC](#table-of-contents)
59+
60+
### Non-Natural Persons
61+
62+
| Format | Explanation | Comments |
63+
|-----------|-------------|----------|
64+
| L9999999C | Initial Letter + 7 digits + 1 Control character | The first letter reports on legal form ([BOE-A-2008-3580](https://www.boe.es/eli/es/o/2008/02/20/eha451/con#a3)) |
65+
66+
For entities, the tax identification number will begin with a letter, which will include information about its legal form according to the following keys:
67+
68+
| Letter | Spanish | English |
69+
|---|-|-|
70+
| A | Sociedades anónimas | Public limited companies |
71+
| B | Sociedades de responsabilidad limitada | Limited liability companies |
72+
| C | Sociedades colectivas | Collective societies |
73+
| D | Sociedades comanditarias | Limited partnerships |
74+
| E | Comunidades de bienes, herencias yacentes y demás entidades carentes de personalidad jurídica no incluidas expresamente en otras claves | Communities of property, existing inheritances and other entities lacking legal personality not expressly included in other keys |
75+
| F | Sociedades cooperativas | Cooperative societies |
76+
| G | Asociaciones | Associations |
77+
| H | Comunidades de propietarios en régimen de propiedad horizontal | Communities of owners under horizontal property regime |
78+
| J | Sociedades civiles | Civil societies |
79+
| P | Corporaciones Locales | Local Corporations |
80+
| Q | Organismos públicos | Public organizations |
81+
| R | Congregaciones e instituciones religiosas | Congregations and religious institutions |
82+
| S | Órganos de la Administración del Estado y de las Comunidades Autónomas | Bodies of the Administration of the State and the Autonomous Communities |
83+
| U | Uniones Temporales de Empresas | Temporary Business Unions |
84+
| V | Otros tipos no definidos en el resto de claves | Other types not defined in the rest of the keys |
85+
| N | Entidad extranjera | Foreign entity |
86+
| W | Establecimiento permanente de entidad no residente en territorio español | Permanent establishment of a non-resident in Spain |
87+
88+
[TOC](#table-of-contents)
89+
90+
## Section III – Calculation of the TIN check digit
91+
92+
### Natural Persons with DNI or NIE
93+
[Reference](https://www.interior.gob.es/opencms/es/servicios-al-ciudadano/tramites-y-gestiones/dni/calculo-del-digito-de-control-del-nif-nie/)
94+
95+
**Cases:**
96+
97+
| Format | Explanation | Type | Standardization |
98+
|---------- |----------------------------------------|------|-----------------|
99+
| 99999999C | 8 digits + 1 Control letter | DNI | 99999999 + C |
100+
| X9999999C | Letter X + 7 digits + 1 Control letter | NIE | 09999999 + C |
101+
| Y9999999C | Letter Y + 7 digits + 1 Control letter | NIE | 19999999 + C |
102+
| Z9999999C | Letter Z + 7 digits + 1 Control letter | NIE | 29999999 + C |
103+
104+
**For NIEs the first letter is replaced as follow:**
105+
```
106+
X => 0
107+
Y => 1
108+
Z => 2
109+
```
110+
111+
**Examples of standardization of NIEs:**
112+
```
113+
X1234567L => 01234567L
114+
Y1234567X => 11234567X
115+
Z1234567R => 21234567R
116+
```
117+
118+
The number is divided by 23 and the remainder is replaced by a letter that is determined by inspection using the following table:
119+
120+
| Remainder | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
121+
|-----------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|:--:|
122+
| **Letter**| T | R | W | A | G | M | Y | F | P | D | X | B |
123+
124+
| Remainder | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
125+
|-----------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
126+
| **Letter**| N | J | Z | S | Q | V | H | L | C | K | E |
127+
128+
**Example of calculus:**
129+
1. TIN = **X1234567L**
130+
2. Standardization of NIE: TIN = **01234567L**
131+
3. TinNumber = **01234567**, TinChecksum = **L**
132+
4. TinNumber MODULUS 23 = **01234567 % 23** = **19**
133+
5. **19** is the **L** letter
134+
135+
[TOC](#table-of-contents)
136+
137+
### Natural Persons without DNI or NIE and Non-Natural Persons
138+
[Reference](https://www.juntadeandalucia.es/servicios/madeja/sites/default/files/historico/1.4.0/contenido-libro-pautas-196.html#Validacion_de_NIF_con_tipo_distinto_a_DNI)
139+
140+
**Cases:**
141+
| Format | Explanation |
142+
|---------- |-------------------------------------------|
143+
| K9999999C | Letter K + 7 digits + 1 Control character |
144+
| L9999999C | Letter L + 7 digits + 1 Control character |
145+
| M9999999C | Letter M + 7 digits + 1 Control character |
146+
| L9999999C | 1 Letter + 7 digits + 1 Control character |
147+
148+
**Method:**
149+
In the case of NIF that are not obtained from the DNI or NIE, the control code is obtained using the 7-digit number, excluding the initial letter and the final letter or digit.
150+
1. The even positions of the 7 central digits are added, that is, the initial letter or the control code are not taken into account. (Sum = A)
151+
2. For each of the digits in the odd positions, the digit is multiplied by 2 and the figures in the result are added, but if the result has a single digit, this figure is simply added. (e.g. if the digit is 6, the result would be 6 x 2 = 12 -> 1 + 2 = 3, but if the digit is 2, the result would be 2 x 2 = 4). (Sum = B)
152+
3. Add the result of the 2 previous steps. (A + B = C)
153+
4. We subtract the last digit of the previous sum (C) from 10, the result of which would be the control code (e.g. if C = 14, the last digit is 4, so we would have 10 - 4 = 6). If the last digit of the sum from the previous step is 0 (e.g. C = 30), no subtraction is performed and 0 is taken as the control code.
154+
155+
If the control code is a number, this would be the result of the last operation. If it is a letter, the following relationship would be used:
156+
157+
| Result | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
158+
|---------|---|---|---|---|---|---|---|---|---|---|
159+
| Control | A | B | C | D | E | F | G | H | I | J |
160+
161+
**Example of calculus:**
162+
TIN = **M2812345C** => Digits: **2812345**
163+
1. Even positions digits (2**8**1**2**3**4**5): 8 + 2 + 4 = **14**
164+
2. Odd positions digits (**2**8**1**2**3**4**5**):
165+
**2** * 2 = **4**
166+
**1** * 2 = **2**
167+
**3** * 2 = **6**
168+
**5** * 2 = 10 => 1 + 0 = **1**
169+
**4** + **2** + **6** + **1** = **13**
170+
3. 14 + 13 = 2**7**
171+
4. 10 - **7** = **3**
172+
173+
Result = 3 => Letter **C**
174+
175+
[TOC](#table-of-contents)

spec/loophp/Tin/CountryHandler/SpainSpec.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
class SpainSpec extends AbstractAlgorithmSpec
1515
{
16-
public const INVALID_NUMBER_CHECK = 'X1234567Z';
16+
public const INVALID_NUMBER_CHECK = ['X1234567Z', 'P2009300B', 'K0867756J'];
1717

1818
public const INVALID_NUMBER_LENGTH = '542372254545445A';
1919

20-
public const INVALID_NUMBER_PATTERN = 'wwwwwwwww';
20+
public const INVALID_NUMBER_PATTERN = ['wwwwwwwww', 'K0867756N'];
2121

22-
public const VALID_NUMBER = ['54237A', 'X1234567L', 'Z1234567R', 'M2812345C'];
22+
public const VALID_NUMBER = ['54237A', 'X1234567L', 'Y1234567X', 'Z1234567R', 'M2812345C', 'B05327986', 'P2009300A', 'K0867756I'];
2323
}

src/CountryHandler/CountryHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ protected function matchLength(string $tin, int $length): bool
146146

147147
protected function matchPattern(string $subject, string $pattern): bool
148148
{
149-
return 1 === preg_match(sprintf('/%s/', $pattern), $subject);
149+
return 1 === preg_match(sprintf('/%s/i', $pattern), $subject);
150150
}
151151

152152
protected function normalizeTin(string $tin): string

src/CountryHandler/Spain.php

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,28 @@
99

1010
namespace loophp\Tin\CountryHandler;
1111

12-
use function strlen;
13-
1412
use const STR_PAD_LEFT;
1513

1614
/**
1715
* Spain.
1816
*/
1917
final class Spain extends CountryHandler
2018
{
19+
/**
20+
* @var string
21+
*/
22+
public const CHECKSUM_LETTER = 'KLMNPQRSW';
23+
24+
/**
25+
* @var string
26+
*/
27+
public const CONTROL_1 = 'TRWAGMYFPDXBNJZSQVHLCKE';
28+
29+
/**
30+
* @var string
31+
*/
32+
public const CONTROL_2 = 'JABCDEFGHI';
33+
2134
/**
2235
* @var string
2336
*/
@@ -29,25 +42,27 @@ final class Spain extends CountryHandler
2942
public const LENGTH = 9;
3043

3144
/**
32-
* @var string
45+
* @var array<string>
3346
*/
34-
public const PATTERN_1 = '\\d{8}[a-zA-Z]';
47+
public const NIE = ['X', 'Y', 'Z'];
3548

3649
/**
50+
* Spanish Natural Persons: DNI
51+
* Foreigners with NIE.
52+
*
3753
* @var string
3854
*/
39-
public const PATTERN_2 = '[XYZKLMxyzklm]\\d{7}[a-zA-Z]';
55+
public const PATTERN_1 = '(^[XYZ\d]\d{7})([' . self::CONTROL_1 . ']$)';
4056

4157
/**
42-
* @var array<int, string>
58+
* Non-resident Spaniards without DNI
59+
* Resident Spaniards under 14 without DNI
60+
* Foreigners without NIE
61+
* Legal entities (companies, organizations, public entities, ...).
62+
*
63+
* @var string
4364
*/
44-
private static $tabConvertToChar = [
45-
'T', 'R', 'W', 'A', 'G',
46-
'M', 'Y', 'F', 'P', 'D',
47-
'X', 'B', 'N', 'J', 'Z',
48-
'S', 'Q', 'V', 'H', 'L',
49-
'C', 'K', 'E',
50-
];
65+
public const PATTERN_2 = '(^[ABCDEFGHJKLMNPQRSUVW])(\d{7})([' . self::CONTROL_2 . '\d]$)';
5166

5267
public function getTIN(): string
5368
{
@@ -61,33 +76,48 @@ protected function hasValidPattern(string $tin): bool
6176

6277
protected function hasValidRule(string $tin): bool
6378
{
64-
return ($this->isFollowPattern1($tin) && $this->isFollowRule1($tin))
65-
|| ($this->isFollowPattern2($tin) && $this->isFollowRule2($tin));
79+
return $this->isFollowRule1($tin) || $this->isFollowRule2($tin);
6680
}
6781

68-
private function getCharFromNumber(int $sum): string
82+
/**
83+
* Return checksum char for Spanish TIN.
84+
*
85+
* @param string $tin
86+
* The TIN without Country indicative ('ES')
87+
* @param null|bool $digit
88+
* Optional: for Non-Natural Persons TIN forces return checksum char as digit 0-9
89+
*
90+
* @return null|string
91+
* Return checksum char or null on failure
92+
*/
93+
private function getChecksum(string $tin, ?bool $digit = null): ? string
6994
{
70-
return self::$tabConvertToChar[$sum - 1];
71-
}
95+
// Natural Persons with DNI or NIE
96+
if (1 === preg_match('~' . self::PATTERN_1 . '?~', strtoupper($tin), $tinParts)) {
97+
$tinNumber = (int) str_replace(self::NIE, array_keys(self::NIE), $tinParts[1]);
7298

73-
private function getNumberFromChar(string $m): int
74-
{
75-
switch ($m) {
76-
case 'K':
77-
case 'L':
78-
case 'M':
79-
case 'X':
80-
return 0;
99+
return substr(self::CONTROL_1, $tinNumber % 23, 1);
100+
}
101+
102+
// Natural Persons without DNI or NIE and Non-Natural Persons
103+
if (1 === preg_match('~' . self::PATTERN_2 . '?~', strtoupper($tin), $tinParts)) {
104+
$checksum = 0;
81105

82-
case 'Y':
83-
return 1;
106+
foreach (str_split($tinParts[2]) as $pos => $val) {
107+
$checksum += array_sum(str_split((string) ((int) $val * (2 - ($pos % 2)))));
108+
}
84109

85-
case 'Z':
86-
return 2;
110+
$checksum1 = (string) ((10 - ($checksum % 10)) % 10);
111+
$checksum2 = substr(self::CONTROL_2, (int) $checksum1, 1);
87112

88-
default:
89-
return -1;
113+
if (null === $digit) {
114+
$digit = false === strpos(self::CHECKSUM_LETTER, $tinParts[1]);
115+
}
116+
117+
return $digit ? $checksum1 : $checksum2;
90118
}
119+
120+
return null;
91121
}
92122

93123
private function isFollowPattern1(string $tin): bool
@@ -102,22 +132,26 @@ private function isFollowPattern2(string $tin): bool
102132

103133
private function isFollowRule1(string $tin): bool
104134
{
105-
$number = (int) (substr($tin, 0, strlen($tin) - 1));
106-
$checkDigit = $tin[strlen($tin) - 1];
107-
$remainderBy23 = $number % 23;
108-
$sum = $remainderBy23 + 1;
135+
if (1 !== preg_match('~' . self::PATTERN_1 . '~', strtoupper($tin), $tinParts)) {
136+
return false;
137+
}
138+
139+
[, $tinNumber, $tinChecksum] = $tinParts;
109140

110-
return $this->getCharFromNumber($sum) === $checkDigit;
141+
return $this->getChecksum($tinNumber) === $tinChecksum;
111142
}
112143

113144
private function isFollowRule2(string $tin): bool
114145
{
115-
$c1 = (string) $this->getNumberFromChar($tin[0]);
116-
$number = (int) ($c1 . substr($tin, 1, strlen($tin)));
117-
$checkDigit = $tin[strlen($tin) - 1];
118-
$remainderBy23 = $number % 23;
119-
$sum = $remainderBy23 + 1;
146+
if (1 !== preg_match('~' . self::PATTERN_2 . '~', strtoupper($tin), $tinParts)) {
147+
return false;
148+
}
149+
150+
[,$tinFirstLetter , $tinNumber, $tinChecksum] = $tinParts;
151+
152+
$tinNumber = $tinFirstLetter . $tinNumber;
153+
$digit = (false === strpos(self::CONTROL_2, $tinChecksum));
120154

121-
return $this->getCharFromNumber($sum) === $checkDigit;
155+
return $this->getChecksum($tinNumber, $digit) === $tinChecksum;
122156
}
123157
}

0 commit comments

Comments
 (0)