1
|
|
|
<?php
|
2
|
|
|
namespace SKien\Sepa;
|
3
|
|
|
|
4
|
|
|
/**
|
5
|
|
|
* Helper trait containing some methods used by multiple classes in package
|
6
|
|
|
*
|
7
|
|
|
* @package Sepa
|
8
|
|
|
* @author Stefanius <[email protected]>
|
9
|
|
|
* @copyright MIT License - see the LICENSE file for details
|
10
|
|
|
* @internal
|
11
|
|
|
*/
|
12
|
|
|
trait SepaHelper
|
13
|
|
|
{
|
14
|
|
|
/** @var array<int,array<string>> array containing target2-dates per year */
|
15
|
|
|
static array $aTarget2 = [];
|
16
|
|
|
|
17
|
|
|
/**
|
18
|
|
|
* Check for valid type and trigger error in case of invalid type.
|
19
|
|
|
* @param string $type
|
20
|
|
|
* @return bool
|
21
|
|
|
*/
|
22
|
|
|
protected function isValidType(string $type) : bool
|
23
|
|
|
{
|
24
|
|
|
if ($type != Sepa::CCT && $type != Sepa::CDD) {
|
25
|
|
|
trigger_error('invalid type for ' . get_class($this), E_USER_ERROR);
|
26
|
|
|
}
|
27
|
|
|
return true;
|
28
|
|
|
}
|
29
|
|
|
|
30
|
|
|
/**
|
31
|
|
|
* Create unique ID.
|
32
|
|
|
* Format: 99999999-9999-9999-999999999999
|
33
|
|
|
* @return string
|
34
|
|
|
*/
|
35
|
|
|
public static function createUID() : string
|
36
|
|
|
{
|
37
|
|
|
mt_srand((int)microtime(true) * 10000);
|
38
|
|
|
$charid = strtoupper(md5(uniqid((string)rand(), true)));
|
39
|
|
|
$uuid = substr($charid, 0, 8) . chr(45) .
|
40
|
|
|
substr($charid, 8, 4) . chr(45) .
|
41
|
|
|
substr($charid, 12, 4) . chr(45) .
|
42
|
|
|
substr($charid, 16, 12);
|
43
|
|
|
|
44
|
|
|
return $uuid;
|
45
|
|
|
}
|
46
|
|
|
|
47
|
|
|
/**
|
48
|
|
|
* Convert input to valid SEPA string.
|
49
|
|
|
* <ol>
|
50
|
|
|
* <li>replacement of special chars</li>
|
51
|
|
|
* <li>limitation to supported chars dependend on validation type</li>
|
52
|
|
|
* <li>restriction to max length dependend on validation type</li>
|
53
|
|
|
* </ol>
|
54
|
|
|
*
|
55
|
|
|
* Validation Types: <ul>
|
56
|
|
|
* <li>SepaHelper::MAX35:</li>
|
57
|
|
|
* <li>SepaHelper::MAX70:</li>
|
58
|
|
|
* <li>SepaHelper::MAX140:</li>
|
59
|
|
|
* <li>SepaHelper::MAX1025:
|
60
|
|
|
* <ul>
|
61
|
|
|
* <li>max length = MAX[xxx]</li>
|
62
|
|
|
* <li>supported chars: A...Z, a...z, 0...9, blank, dot, comma, plus, minus, slash, questionmark, colon, open/closing bracket</li>
|
63
|
|
|
* </ul></li>
|
64
|
|
|
* <li>SepaHelper::ID1:
|
65
|
|
|
* <ul>
|
66
|
|
|
* <li>max length = 35</li>
|
67
|
|
|
* <li>supported chars: A...Z, a...z, 0...9, blank, dot, comma, plus, minus, slash</li>
|
68
|
|
|
* </ul></li>
|
69
|
|
|
* <li>SepaHelper::ID2:
|
70
|
|
|
* <ul>
|
71
|
|
|
* <li>max length = 35</li>
|
72
|
|
|
* <li>supported chars: ID1 without blank</li>
|
73
|
|
|
* </ul></li>
|
74
|
|
|
* </ul>
|
75
|
|
|
*
|
76
|
|
|
* @param string $str string to validate
|
77
|
|
|
* @param int $iType type of validation: one of SepaHelper::MAX35, SepaHelper::MAX70, SepaHelper::MAX140, SepaHelper::MAX1025, SepaHelper::ID1, SepaHelper::ID2
|
78
|
|
|
* @return string
|
79
|
|
|
*/
|
80
|
|
|
public static function validString(string $str, int $iType) : string
|
81
|
|
|
{
|
82
|
|
|
// replace specialchars...
|
83
|
|
|
$strValid = self::replaceSpecialChars($str);
|
84
|
|
|
|
85
|
|
|
// regular expresion for 'standard' types MAXxxx
|
86
|
|
|
$strRegEx = '/[^A-Za-z0-9 \.,\-\/\+():?]/'; // A...Z, a...z, 0...9, blank, dot, comma plus, minus, slash, questionmark, colon, open/closing bracket
|
87
|
|
|
$strReplace = ' ';
|
88
|
|
|
$iMaxLen = 1025;
|
89
|
|
|
switch ($iType) {
|
90
|
|
|
case Sepa::ID1:
|
91
|
|
|
$iMaxLen = 35;
|
92
|
|
|
$strRegEx = '/[^A-Za-z0-9 \.,\+\-\/]/'; // A...Z, a...z, 0...9, blank, dot, comma plus, minus, slash
|
93
|
|
|
$strReplace = '';
|
94
|
|
|
break;
|
95
|
|
|
case Sepa::ID2:
|
96
|
|
|
$iMaxLen = 35;
|
97
|
|
|
$strRegEx = '/[^A-Za-z0-9\.,\+\-\/]/'; // same as ID1 except blank...
|
98
|
|
|
$strReplace = '';
|
99
|
|
|
break;
|
100
|
|
|
case Sepa::MAX35:
|
101
|
|
|
$iMaxLen = 35;
|
102
|
|
|
break;
|
103
|
|
|
case Sepa::MAX70:
|
104
|
|
|
$iMaxLen = 70;
|
105
|
|
|
break;
|
106
|
|
|
case Sepa::MAX140:
|
107
|
|
|
$iMaxLen = 140;
|
108
|
|
|
break;
|
109
|
|
|
case Sepa::MAX1025:
|
110
|
|
|
default:
|
111
|
|
|
break;
|
112
|
|
|
}
|
113
|
|
|
|
114
|
|
|
$strValid = preg_replace($strRegEx, $strReplace, $strValid);
|
115
|
|
|
return substr($strValid ?? '', 0, $iMaxLen);
|
116
|
|
|
}
|
117
|
|
|
|
118
|
|
|
/**
|
119
|
|
|
* Replace some special chars with nearest equivalent.
|
120
|
|
|
* - umlauts, acute, circumflex, ...
|
121
|
|
|
* - square/curly brackets
|
122
|
|
|
* - underscore, at
|
123
|
|
|
* @param string $str text to process
|
124
|
|
|
* @return string
|
125
|
|
|
*/
|
126
|
|
|
public static function replaceSpecialChars(string $str) : string
|
127
|
|
|
{
|
128
|
|
|
$strReplaced = '';
|
129
|
|
|
if (strlen($str) > 0) {
|
130
|
|
|
// replace known special chars
|
131
|
|
|
$aSpecialChars = array(
|
132
|
|
|
'á' => 'a', 'à' => 'a', 'ä' => 'ae', 'â' => 'a', 'ã' => 'a', 'å' => 'a', 'æ' => 'ae',
|
133
|
|
|
'Á' => 'A', 'À' => 'A', 'Ä' => 'Ae', 'Â' => 'A', 'Ã' => 'A', 'Å' => 'A', 'Æ' => 'AE',
|
134
|
|
|
'ç' => 'c', 'Ç' => 'C',
|
135
|
|
|
'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', 'É' => 'E', 'È' => 'E', 'Ê' => 'E', 'Ë' => 'E',
|
136
|
|
|
'ì' => 'i', 'î' => 'i', 'ï' => 'i', 'Í' => 'I', 'Ì' => 'I', 'Î' => 'I', 'Ï' => 'I',
|
137
|
|
|
'ñ' => 'n', 'Ñ' => 'N',
|
138
|
|
|
'ó' => 'o', 'ò' => 'o', 'ö' => 'oe', 'ô' => 'o', 'õ' => 'o', 'ø' => 'o', 'œ' => 'oe',
|
139
|
|
|
'Ó' => 'O', 'Ò' => 'O', 'Ö' => 'Oe', 'Ô' => 'O', 'Õ' => 'O', 'Ø' => 'O', 'Œ' => 'OE',
|
140
|
|
|
'ß' => 'ss', 'š' => 's', 'Š' => 'S',
|
141
|
|
|
'ú' => 'u', 'ù' => 'u', 'ü' => 'ue', 'û' => 'u',
|
142
|
|
|
'Ú' => 'U', 'Ù' => 'U', 'Ü' => 'Ue', 'Û' => 'U',
|
143
|
|
|
'ý' => 'y', 'ÿ' => 'y', 'Ý' => 'Y', 'Ÿ' => 'Y',
|
144
|
|
|
'ž' => 'z', 'Ž' => 'Z',
|
145
|
|
|
'[' => '(', ']' => ')', '{' => '(', '}' => ')',
|
146
|
|
|
'_' => '-', '@' => '(at)', '€' => 'EUR'
|
147
|
|
|
);
|
148
|
|
|
|
149
|
|
|
$strReplaced = strtr($str, $aSpecialChars);
|
150
|
|
|
}
|
151
|
|
|
return $strReplaced;
|
152
|
|
|
}
|
153
|
|
|
|
154
|
|
|
/**
|
155
|
|
|
* Calculates valid delayed date from given date considering SEPA businessdays.
|
156
|
|
|
* @param int $iDaysDelay min count of days the payment delays
|
157
|
|
|
* @param int $dtRequested requested date (unix timestamp)
|
158
|
|
|
* @return int unix timestamp
|
159
|
|
|
*/
|
160
|
|
|
public static function calcDelayedDate(int $iDaysDelay, int $dtRequested = 0) : int
|
161
|
|
|
{
|
162
|
|
|
$dtEarliest = time();
|
163
|
|
|
|
164
|
|
|
// Annotation: should daytime ( < 08:30 / < 18:30 ) bear in mind ?
|
165
|
|
|
$iBDays = 0;
|
166
|
|
|
while ($iBDays < $iDaysDelay) {
|
167
|
|
|
$dtEarliest += 86400; // add day ( 24 * 60 * 60 );
|
168
|
|
|
if (self::isTarget2Day($dtEarliest)) {
|
169
|
|
|
$iBDays++;
|
170
|
|
|
}
|
171
|
|
|
}
|
172
|
|
|
return $dtEarliest > $dtRequested ? $dtEarliest : $dtRequested;
|
173
|
|
|
}
|
174
|
|
|
|
175
|
|
|
/**
|
176
|
|
|
* Check for target2-Day (Sepa-Businessday).
|
177
|
|
|
* Mo...Fr and NOT TARGET1-Day
|
178
|
|
|
* TARGET1 Days:
|
179
|
|
|
* - New Year
|
180
|
|
|
* - Good Day
|
181
|
|
|
* - Easter Monday
|
182
|
|
|
* - 1'st May
|
183
|
|
|
* - 1'st Christmasday
|
184
|
|
|
* - 2'nd Christmasday
|
185
|
|
|
* @param int $dt unix timestamp to check
|
186
|
|
|
* @return bool
|
187
|
|
|
*/
|
188
|
|
|
public static function isTarget2Day(int $dt) : bool
|
189
|
|
|
{
|
190
|
|
|
$bTarget2Day = false;
|
191
|
|
|
$iWeekDay = date('N', $dt);
|
192
|
|
|
if ($iWeekDay <= 5) {
|
193
|
|
|
// no weekend - we check for special target2 day...
|
194
|
|
|
$iYear = intval(date('Y', $dt));
|
195
|
|
|
if (!isset(self::$aTarget2[$iYear])) {
|
196
|
|
|
// just generate yearly days once
|
197
|
|
|
// first the fixed days...
|
198
|
|
|
self::$aTarget2[$iYear] = [
|
199
|
|
|
$iYear . '-01-01', // New Year
|
200
|
|
|
$iYear . '-05-01', // 1'st May
|
201
|
|
|
$iYear . '-12-25', // 1'st Christmas
|
202
|
|
|
$iYear . '-12-26', // 2'nd Christmas
|
203
|
|
|
];
|
204
|
|
|
/*
|
205
|
|
|
* ... and then we have the easter days dynamic to calc
|
206
|
|
|
* https://praxistipps.chip.de/ostern-berechnen-so-entsteht-das-datum-jedes-jahr_101993
|
207
|
|
|
*/
|
208
|
|
|
$a = $iYear % 4;
|
209
|
|
|
$b = $iYear % 7;
|
210
|
|
|
$c = $iYear % 19;
|
211
|
|
|
$d = (19 * $c + 24) % 30;
|
212
|
|
|
$e = (2 * $a + 4 * $b + 6 * $d + 5) % 7;
|
213
|
|
|
$f = intdiv($c + 11 * $d + 22 * $e, 451);
|
214
|
|
|
$iEasterMonth = 3;
|
215
|
|
|
$iEasterSunday = 22 + $d + $e - 7 * $f;
|
216
|
|
|
if ($iEasterSunday > 31) {
|
217
|
|
|
$iEasterSunday -= 31;
|
218
|
|
|
$iEasterMonth++;
|
219
|
|
|
/*
|
220
|
|
|
* Since the earliest possible date for Easter Sunday is March 22nd, this
|
221
|
|
|
* special case mentioned by the calculation rules by Gauß never occurs...
|
222
|
|
|
* ... and therefore cannot be covered by any test case.
|
223
|
|
|
*
|
224
|
|
|
* https://de.wikipedia.org/wiki/Osterdatum
|
225
|
|
|
*
|
226
|
|
|
if ($iEasterSunday == 26) {
|
227
|
|
|
$iEasterSunday = 19;
|
228
|
|
|
$iEasterMonth = 3;
|
229
|
|
|
}
|
230
|
|
|
*/
|
231
|
|
|
}
|
232
|
|
|
$uxtsEasterSunnday = mktime(0, 0, 0, $iEasterMonth, $iEasterSunday, $iYear);
|
233
|
|
|
/*
|
234
|
|
|
* calc friday and monday from sunday: 1day = 60sec * 60min * 24h
|
235
|
|
|
* - Good Day: Easter Sunday - 2days ()
|
236
|
|
|
* - Easter Monday: Easter Sunday + 1day
|
237
|
|
|
*/
|
238
|
|
|
self::$aTarget2[$iYear][] = date('Y-m-d', $uxtsEasterSunnday - 172800);
|
239
|
|
|
self::$aTarget2[$iYear][] = date('Y-m-d', $uxtsEasterSunnday + 86400);
|
240
|
|
|
}
|
241
|
|
|
$bTarget2Day = !in_array(date('Y-m-d', $dt), self::$aTarget2[$iYear]);
|
242
|
|
|
}
|
243
|
|
|
return $bTarget2Day;
|
244
|
|
|
/*
|
245
|
|
|
self::$aTarget2 = [
|
246
|
|
|
// New Year Good Day Easter Monday 1'stMay 2.Christmas
|
247
|
|
|
'2022-01-01', '2022-04-02', '2022-03-26', '2022-03-29', '2022-12-25', '2022-12-26',
|
248
|
|
|
'2023-01-01', '2023-04-07', '2023-04-10', '2023-05-01', '2023-12-25', '2023-12-26',
|
249
|
|
|
'2024-01-01', '2024-03-29', '2024-04-01', '2024-05-01', '2024-12-25', '2024-12-26',
|
250
|
|
|
'2025-01-01', '2025-04-18', '2025-04-21', '2025-05-01', '2025-12-25', '2025-12-26',
|
251
|
|
|
];
|
252
|
|
|
if (intval(date('Y', $dt)) >= array_pop(self::$aTarget2)) {
|
253
|
|
|
trigger_error('No Target2 dates are specified for [' . date('Y-m-d', $dt) . '] so far!', E_USER_ERROR);
|
254
|
|
|
}
|
255
|
|
|
return !($iWeekDay > 5 || in_array(date('Y-m-d', $dt), self::$aTarget2));
|
256
|
|
|
*/
|
257
|
|
|
}
|
258
|
|
|
}
|
259
|
|
|
|