1
|
|
|
<?php
|
2
|
|
|
namespace SKien\Sepa\CntryValidation;
|
3
|
|
|
|
4
|
|
|
use SKien\Sepa\Sepa;
|
5
|
|
|
|
6
|
|
|
/**
|
7
|
|
|
* Base class for country specific validation for IBAN, BIC and CI.
|
8
|
|
|
*
|
9
|
|
|
* Needed methods to calculate checksum and check the format of
|
10
|
|
|
* the values.
|
11
|
|
|
* Format check is made with regular expressions that can be set to
|
12
|
|
|
* the country specific rules.
|
13
|
|
|
*
|
14
|
|
|
* Create for each country to support a class extending this class.
|
15
|
|
|
* For most of the participating countries, it is sufficient to
|
16
|
|
|
* specify in the constructor the respective length, the formatting
|
17
|
|
|
* rule (RegEx) and the information whether alphanumeric characters
|
18
|
|
|
* are allowed.
|
19
|
|
|
*
|
20
|
|
|
* If more complicated rules apply in a country, the respective
|
21
|
|
|
* method for validation can be redefined in the extended class in
|
22
|
|
|
* order to map this rule.
|
23
|
|
|
* @see SepaCntryValidationBE class
|
24
|
|
|
*
|
25
|
|
|
* @package Sepa
|
26
|
|
|
* @author Stefanius <[email protected]>
|
27
|
|
|
* @copyright MIT License - see the LICENSE file for details
|
28
|
|
|
*/
|
29
|
|
|
class SepaCntryValidationBase implements SepaCntryValidation
|
30
|
|
|
{
|
31
|
|
|
/** @var string 2 digit ISO country code (ISO 3166-1) */
|
32
|
|
|
protected string $strCntry = '';
|
33
|
|
|
/** @var int length of the IBAN */
|
34
|
|
|
protected int $iLenIBAN = -1;
|
35
|
|
|
/** @var string regular expression to validate the format of the IBAN */
|
36
|
|
|
protected string $strRegExIBAN = '';
|
37
|
|
|
/** @var bool if true, alphanum values in the IBAN allowed */
|
38
|
|
|
protected bool $bAlphaNumIBAN = false;
|
39
|
|
|
/** @var int length of the IC */
|
40
|
|
|
protected int $iLenCI = -1;
|
41
|
|
|
/** @var string regular expression to validate the format of the CI */
|
42
|
|
|
protected string $strRegExCI = '';
|
43
|
|
|
/** @var bool if true, alphanum values in the CI allowed */
|
44
|
|
|
protected bool $bAlphaNumCI = false;
|
45
|
|
|
/** @var string last calculated checksum */
|
46
|
|
|
protected string $strLastCheckSum = '';
|
47
|
|
|
|
48
|
|
|
/**
|
49
|
|
|
* Create instance of validation.
|
50
|
|
|
* This constructor checks, if the country code from the parameter equals to the
|
51
|
|
|
* code defined in the extension class. This prevents the wrong class from being used
|
52
|
|
|
* for validation for a given country.
|
53
|
|
|
* @param string $strCntry 2 sign country code
|
54
|
|
|
*/
|
55
|
|
|
public function __construct(string $strCntry)
|
56
|
|
|
{
|
57
|
|
|
if (strtoupper($strCntry) != $this->strCntry) {
|
58
|
|
|
trigger_error('instanciation with invalid country ' . $strCntry . ' (expected ' . $this->strCntry . ')', E_USER_ERROR);
|
59
|
|
|
}
|
60
|
|
|
}
|
61
|
|
|
|
62
|
|
|
/**
|
63
|
|
|
* Validates given IBAN.
|
64
|
|
|
*
|
65
|
|
|
* @link https://www.ecbs.org/iban.htm
|
66
|
|
|
* @link http://www.pruefziffernberechnung.de/I/IBAN.shtml
|
67
|
|
|
*
|
68
|
|
|
* @param string $strIBAN IBAN to validate
|
69
|
|
|
* @return int Sepa::OK or errorcode <ul>
|
70
|
|
|
* <li><b> Sepa::ERR_IBAN_INVALID_CNTRY </b> invalid country code </li>
|
71
|
|
|
* <li><b> Sepa::ERR_IBAN_INVALID_LENGTH </b> invalid length </li>
|
72
|
|
|
* <li><b> Sepa::ERR_IBAN_INVALID_SIGN </b> IBAN contains invalid sign(s) </li>
|
73
|
|
|
* <li><b> Sepa::ERR_IBAN_CHECKSUM </b> wrong checksum </li></ul>
|
74
|
|
|
*/
|
75
|
|
|
public function validateIBAN(string $strIBAN) : int
|
76
|
|
|
{
|
77
|
|
|
// toupper, trim and remove containing blanks
|
78
|
|
|
$strIBAN = str_replace(' ', '', trim(strtoupper($strIBAN)));
|
79
|
|
|
if (strlen($strIBAN) != $this->iLenIBAN) {
|
80
|
|
|
return Sepa::ERR_IBAN_INVALID_LENGTH;
|
81
|
|
|
}
|
82
|
|
|
if (substr($strIBAN, 0, 2) != $this->strCntry) {
|
83
|
|
|
return Sepa::ERR_IBAN_INVALID_CNTRY;
|
84
|
|
|
}
|
85
|
|
|
if (!preg_match($this->strRegExIBAN, $strIBAN)) {
|
86
|
|
|
return Sepa::ERR_IBAN_INVALID_SIGN;
|
87
|
|
|
}
|
88
|
|
|
|
89
|
|
|
$strCS = substr($strIBAN, 2, 2);
|
90
|
|
|
$strBBAN = substr($strIBAN, 4);
|
91
|
|
|
|
92
|
|
|
// alphanumeric account number allowed (except the country code...)?
|
93
|
|
|
if ($this->bAlphaNumIBAN) {
|
94
|
|
|
$strBBAN = $this->replaceAlpha($strBBAN);
|
95
|
|
|
}
|
96
|
|
|
if ($this->getCheckSum($strBBAN) != $strCS) {
|
97
|
|
|
return Sepa::ERR_IBAN_CHECKSUM;
|
98
|
|
|
}
|
99
|
|
|
return Sepa::OK;
|
100
|
|
|
}
|
101
|
|
|
|
102
|
|
|
/**
|
103
|
|
|
* Validates given BIC.
|
104
|
|
|
* Follows <b> ISO 9362 </b><br>
|
105
|
|
|
* as far as I have determined, the format of the BIC is uniform within
|
106
|
|
|
* the participating countries for SEPA.
|
107
|
|
|
* @param string $strBIC BIC to validate
|
108
|
|
|
* @return int Sepa::OK or errorcode<ul>
|
109
|
|
|
* <li><b> Sepa::ERR_BIC_INVALID </b> invalid BIC </li>
|
110
|
|
|
* <li><b> Sepa::ERR_BIC_INVALID_CNTRY </b> invalid country code </li></ul>
|
111
|
|
|
*/
|
112
|
|
|
public function validateBIC(string $strBIC) : int
|
113
|
|
|
{
|
114
|
|
|
if (substr($strBIC, 4, 2) != $this->strCntry) {
|
115
|
|
|
return Sepa::ERR_BIC_INVALID_CNTRY;
|
116
|
|
|
}
|
117
|
|
|
$iErr = Sepa::ERR_BIC_INVALID;
|
118
|
|
|
if (preg_match('/^([A-Z]){4}([A-Z]){2}([0-9A-Z]){2}([0-9A-Z]{3})?$/', $strBIC)) {
|
119
|
|
|
$iErr = Sepa::OK;
|
120
|
|
|
}
|
121
|
|
|
return $iErr;
|
122
|
|
|
}
|
123
|
|
|
|
124
|
|
|
/**
|
125
|
|
|
* Validates given CI (Creditor Scheme Identification).
|
126
|
|
|
*
|
127
|
|
|
* The general structure for the CI is the following:
|
128
|
|
|
* - Position 1-2 filled with the ISO country code
|
129
|
|
|
* - Position 3-4 filled with the check digit according to ISO 7064 Mod 97-10
|
130
|
|
|
* - Position 5-7 filled with the Creditor Business Code, if not used then filled with ZZZ
|
131
|
|
|
* - Position 8 onwards filled with the country specific part of the identifier being
|
132
|
|
|
* a national identifier of the Creditor as defined by the concerned national community.
|
133
|
|
|
*
|
134
|
|
|
* NOTE: the CBC is not taken into account when calculating the checksum!
|
135
|
|
|
*
|
136
|
|
|
* @link https://www.sepaforcorporates.com/sepa-direct-debits/sepa-creditor-identifier/
|
137
|
|
|
* @link https://www.europeanpaymentscouncil.eu/sites/default/files/kb/file/2019-09/EPC262-08%20v7.0%20Creditor%20Identifier%20Overview_0.pdf
|
138
|
|
|
* @link https://www.europeanpaymentscouncil.eu/sites/default/files/KB/files/EPC114-06%20SDD%20Core%20Interbank%20IG%20V9.0%20Approved.pdf#page=10
|
139
|
|
|
*
|
140
|
|
|
* online CI Validator
|
141
|
|
|
* @link http://www.maric.info/fin/SEPA/ddchkden.htm
|
142
|
|
|
*
|
143
|
|
|
* @param string $strCI CI to validate
|
144
|
|
|
* @return int Sepa::OK or errorcode<ul>
|
145
|
|
|
* <li><b> Sepa::ERR_CI_INVALID_CNTRY </b> invalid country code </li>
|
146
|
|
|
* <li><b> Sepa::ERR_CI_INVALID_LENGTH </b> invalid length </li>
|
147
|
|
|
* <li><b> Sepa::ERR_CI_INVALID_SIGN </b> CI contains invalid sign(s) </li>
|
148
|
|
|
* <li><b> Sepa::ERR_CI_CHECKSUM </b> wrong checksum </li></ul>
|
149
|
|
|
*/
|
150
|
|
|
public function validateCI(string $strCI) : int
|
151
|
|
|
{
|
152
|
|
|
// toupper, trim and remove containing blanks
|
153
|
|
|
$strCheck = str_replace(' ', '', trim(strtoupper($strCI)));
|
154
|
|
|
if (strlen($strCheck) != $this->iLenCI) {
|
155
|
|
|
return Sepa::ERR_CI_INVALID_LENGTH;
|
156
|
|
|
}
|
157
|
|
|
if (substr($strCheck, 0, 2) != $this->strCntry) {
|
158
|
|
|
return Sepa::ERR_CI_INVALID_CNTRY;
|
159
|
|
|
}
|
160
|
|
|
if (!preg_match($this->strRegExCI, $strCheck)) {
|
161
|
|
|
return Sepa::ERR_CI_INVALID_SIGN;
|
162
|
|
|
}
|
163
|
|
|
|
164
|
|
|
$strCS = substr($strCheck, 2, 2);
|
165
|
|
|
// NOTE: the CBC is not taken into account when calculating the checksum!
|
166
|
|
|
$strCheck = substr($strCheck, 7);
|
167
|
|
|
if ($this->bAlphaNumCI) {
|
168
|
|
|
$strCheck = $this->replaceAlpha($strCheck);
|
169
|
|
|
}
|
170
|
|
|
if ($this->getCheckSum($strCheck) != $strCS) {
|
171
|
|
|
return Sepa::ERR_CI_CHECKSUM;
|
172
|
|
|
}
|
173
|
|
|
return Sepa::OK;
|
174
|
|
|
}
|
175
|
|
|
|
176
|
|
|
/**
|
177
|
|
|
* Return last calculated checksum.
|
178
|
|
|
* This method is only for test purposes.
|
179
|
|
|
* @return string
|
180
|
|
|
* @internal
|
181
|
|
|
*/
|
182
|
|
|
public function getLastCheckSum() : string
|
183
|
|
|
{
|
184
|
|
|
// @codeCoverageIgnoreStart
|
185
|
|
|
/*
|
186
|
|
|
* Outside of coverge, since this method only can be used to retrieve
|
187
|
|
|
* any calculated checksum for testpurposes and shouldn't be used in production code!
|
188
|
|
|
*/
|
189
|
|
|
return $this->strLastCheckSum;
|
190
|
|
|
// @codeCoverageIgnoreEnd
|
191
|
|
|
}
|
192
|
|
|
|
193
|
|
|
/**
|
194
|
|
|
* Calculate modulo 97 checksum for bankcode and accountnumber
|
195
|
|
|
* MOD 97-10 (see ISO 7064)
|
196
|
|
|
* @param string $strCheck
|
197
|
|
|
* @return string
|
198
|
|
|
*/
|
199
|
|
|
protected function getCheckSum(string $strCheck) : string
|
200
|
|
|
{
|
201
|
|
|
// calculate checksum
|
202
|
|
|
// 1. move 6 digit feft and add numerical countrycode
|
203
|
|
|
$strCS1 = $this->adjustFP(bcadd(bcmul($strCheck, '1000000'), $this->getAlpha2CntryCode() . '00'));
|
204
|
|
|
// 2. modulo 97 value
|
205
|
|
|
$strCS2 = $this->adjustFP(bcmod($strCS1, '97'));
|
206
|
|
|
// 3. subtract value from 98
|
207
|
|
|
$strCS = $this->adjustFP(bcsub('98', $strCS2));
|
208
|
|
|
// 4. always 2 digits...
|
209
|
|
|
if (strlen($strCS) < 2) {
|
210
|
|
|
$strCS = '0' . $strCS;
|
211
|
|
|
}
|
212
|
|
|
$this->strLastCheckSum = $strCS;
|
213
|
|
|
return $strCS;
|
214
|
|
|
}
|
215
|
|
|
|
216
|
|
|
/**
|
217
|
|
|
* In some cases there appears unwanted decimals (floatingpoint drift from bc - operations)
|
218
|
|
|
* ... just cut them off
|
219
|
|
|
*
|
220
|
|
|
* @param string $str
|
221
|
|
|
* @return string
|
222
|
|
|
*/
|
223
|
|
|
protected function adjustFP(string $str) : string
|
224
|
|
|
{
|
225
|
|
|
// @codeCoverageIgnoreStart
|
226
|
|
|
/*
|
227
|
|
|
* Note:
|
228
|
|
|
* This problem actually occurred in practice, unfortunately at the time I wrote
|
229
|
|
|
* the tests, I no longer have any concrete sample data that cause this error.
|
230
|
|
|
* This means that I am currently not in a position to intentionally induce the
|
231
|
|
|
* error in order to test its correct handling.
|
232
|
|
|
* That's why I take the function out of the coverage so that I don't have to look
|
233
|
|
|
* over and over again with every run to see what is not covered ...
|
234
|
|
|
*/
|
235
|
|
|
if (strpos('.', $str) !== false) {
|
236
|
|
|
$str = substr($str, 0, strpos('.', $str));
|
237
|
|
|
}
|
238
|
|
|
return $str;
|
239
|
|
|
// @codeCoverageIgnoreEnd
|
240
|
|
|
}
|
241
|
|
|
|
242
|
|
|
/**
|
243
|
|
|
* Get the ALPHA-2 country code.
|
244
|
|
|
* To calc the checksum, the first four digits of the IBAN (country code and check digit)
|
245
|
|
|
* have to be placed at the end of the IBAN. Check digit is represented by 00 for calculation.
|
246
|
|
|
* @return string
|
247
|
|
|
*/
|
248
|
|
|
protected function getAlpha2CntryCode() : string
|
249
|
|
|
{
|
250
|
|
|
$strNumCode = $this->convCharToNum($this->strCntry[0]) . $this->convCharToNum($this->strCntry[1]);
|
251
|
|
|
return $strNumCode;
|
252
|
|
|
}
|
253
|
|
|
|
254
|
|
|
/**
|
255
|
|
|
* Replace all alpha chars with it's numeric substitution.
|
256
|
|
|
* @param string $strCheck
|
257
|
|
|
* @return string
|
258
|
|
|
*/
|
259
|
|
|
protected function replaceAlpha(string $strCheck) : string
|
260
|
|
|
{
|
261
|
|
|
// account number may contains characters
|
262
|
|
|
foreach (range('A', 'Z') as $ch) {
|
263
|
|
|
$strCheck = str_replace((string)$ch, $this->convCharToNum((string)$ch), $strCheck);
|
264
|
|
|
}
|
265
|
|
|
return $strCheck;
|
266
|
|
|
}
|
267
|
|
|
|
268
|
|
|
/**
|
269
|
|
|
* Existing non-numeric characters must be converted to a numeric value for the calculation.
|
270
|
|
|
*
|
271
|
|
|
* A = 10 F = 15 K = 20 P = 25 U = 30 Z = 35
|
272
|
|
|
* B = 11 G = 16 L = 21 Q = 26 V = 31
|
273
|
|
|
* C = 12 H = 17 M = 22 R = 27 W = 32
|
274
|
|
|
* D = 13 I = 18 N = 23 S = 28 X = 33
|
275
|
|
|
* E = 14 J = 19 O = 24 T = 29 Y = 34
|
276
|
|
|
*
|
277
|
|
|
* @param string $ch
|
278
|
|
|
* @return string
|
279
|
|
|
*/
|
280
|
|
|
protected function convCharToNum(string $ch) : string
|
281
|
|
|
{
|
282
|
|
|
$iValue = ord($ch) - ord('A') + 10;
|
283
|
|
|
return strval($iValue);
|
284
|
|
|
}
|
285
|
|
|
} |