Engine::sanitizeAccount()   B
last analyzed

Complexity

Conditions 7
Paths 4

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 29
rs 8.8333
1
<?php
2
3
namespace Kingsquare\Parser\Banking\Mt940;
4
5
use Kingsquare\Banking\Statement;
6
use Kingsquare\Banking\Transaction;
7
use Kingsquare\Parser\Banking\Mt940;
8
9
/**
10
 * @author Kingsquare ([email protected])
11
 * @license http://opensource.org/licenses/MIT MIT
12
 */
13
abstract class Engine
14
{
15
    private $rawData = '';
16
    protected $currentStatementData = '';
17
    protected $currentTransactionData = '';
18
19
    public $debug = false;
20
21
    protected static $registeredEngines = [
22
        100 => Engine\Abn::class,
23
        200 => Engine\Ing::class,
24
        300 => Engine\Rabo::class,
25
        400 => Engine\Spk::class,
26
        500 => Engine\Triodos::class,
27
        600 => Engine\Knab::class,
28
        700 => Engine\Hsbc::class,
29
        800 => Engine\Bunq::class,
30
        900 => Engine\Penta::class,
31
        1000 => Engine\Asn::class,
32
        1100 => Engine\Kbs::class,
33
        1200 => Engine\Zetb::class,
34
        1300 => Engine\Kontist::class,
35
    ];
36
37
    /**
38
     * reads the firstline of the string to guess which engine to use for parsing.
39
     *
40
     * @param string $string
41
     *
42
     * @return Engine
43
     */
44
    public static function __getInstance($string)
45
    {
46
        $engine = self::detectBank($string);
47
        $engine->loadString($string);
48
49
        return $engine;
50
    }
51
52
    /**
53
     * Register a new Engine.
54
     *
55
     * @param string $engineClass Class name of Engine to be registered
56
     * @param int $priority
57
     */
58
    public static function registerEngine($engineClass, $priority)
59
    {
60
        if (!is_int($priority)) {
0 ignored issues
show
introduced by
The condition is_int($priority) is always true.
Loading history...
61
            trigger_error('Priority must be integer', E_USER_WARNING);
62
63
            return;
64
        }
65
        if (array_key_exists($priority, self::$registeredEngines)) {
66
            trigger_error('Priority already taken', E_USER_WARNING);
67
68
            return;
69
        }
70
        if (!class_exists($engineClass)) {
71
            trigger_error('Engine does not exist', E_USER_WARNING);
72
73
            return;
74
        }
75
        self::$registeredEngines[$priority] = $engineClass;
76
    }
77
78
    /**
79
     * Unregisters all Engines.
80
     */
81
    public static function resetEngines()
82
    {
83
        self::$registeredEngines = [];
84
    }
85
86
    /**
87
     * Checks whether the Engine is applicable for the given string.
88
     *
89
     * @param string $string
90
     *
91
     * @return bool
92
     */
93
    public static function isApplicable($string)
94
    {
95
        return true;
96
    }
97
98
    /**
99
     * @param string $string
100
     *
101
     * @return Engine
102
     */
103
    private static function detectBank($string)
104
    {
105
        ksort(self::$registeredEngines, SORT_NUMERIC);
106
107
        foreach (self::$registeredEngines as $engineClass) {
108
            if ($engineClass::isApplicable($string)) {
109
                return new $engineClass();
110
            }
111
        }
112
113
        trigger_error('Unknown mt940 parser loaded, thus reverted to default');
114
115
        return new Engine\Unknown();
116
    }
117
118
    /**
119
     * loads the $string into _rawData
120
     * this could be used to move it into handling of streams in the future.
121
     *
122
     * @param string $string
123
     */
124
    public function loadString($string)
125
    {
126
        $this->rawData = trim($string);
127
    }
128
129
    /**
130
     * actual parsing of the data.
131
     *
132
     * @return Statement[]
133
     */
134
    public function parse()
135
    {
136
        $results = [];
137
        foreach ($this->parseStatementData() as $this->currentStatementData) {
138
            $statement = new Statement();
139
            if ($this->debug) {
140
                $statement->rawData = $this->currentStatementData;
141
            }
142
            $statement->setBank($this->parseStatementBank());
143
            $statement->setAccount($this->parseStatementAccount());
144
            $statement->setStartPrice($this->parseStatementStartPrice());
145
            $statement->setEndPrice($this->parseStatementEndPrice());
146
            $statement->setStartTimestamp($this->parseStatementStartTimestamp());
147
            $statement->setEndTimestamp($this->parseStatementEndTimestamp());
148
            $statement->setNumber($this->parseStatementNumber());
149
            $statement->setCurrency($this->parseStatementCurrency());
150
151
            foreach ($this->parseTransactionData() as $this->currentTransactionData) {
0 ignored issues
show
Comprehensibility Bug introduced by
$this is overwriting a variable from outer foreach loop.
Loading history...
152
                $transaction = new Transaction();
153
                if ($this->debug) {
154
                    $transaction->rawData = $this->currentTransactionData;
155
                }
156
                $transaction->setAccount($this->parseTransactionAccount());
157
                $transaction->setAccountName($this->parseTransactionAccountName());
158
                $transaction->setPrice($this->parseTransactionPrice());
159
                $transaction->setDebitCredit($this->parseTransactionDebitCredit());
160
                $transaction->setCancellation($this->parseTransactionCancellation());
161
                $transaction->setDescription($this->parseTransactionDescription());
162
                $transaction->setValueTimestamp($this->parseTransactionValueTimestamp());
163
                $transaction->setEntryTimestamp($this->parseTransactionEntryTimestamp());
164
                $transaction->setTransactionCode($this->parseTransactionCode());
165
                $statement->addTransaction($transaction);
166
            }
167
            $results[] = $statement;
168
        }
169
170
        return $results;
171
    }
172
173
    /**
174
     * split the rawdata up into statementdata chunks.
175
     *
176
     * @return array
177
     */
178
    protected function parseStatementData()
179
    {
180
        $results = preg_split(
181
            '/(^:20:|^-X{,3}$|\Z)/m',
182
            $this->getRawData(),
183
            -1,
184
            PREG_SPLIT_NO_EMPTY
185
        );
186
        array_shift($results); // remove the header
187
        return $results;
188
    }
189
190
    /**
191
     * split the statement up into transaction chunks.
192
     *
193
     * @return array
194
     */
195
    protected function parseTransactionData()
196
    {
197
        $results = [];
198
        preg_match_all('/^:61:(.*?)(?=^:61:|^-X{,3}$|\Z)/sm', $this->getCurrentStatementData(), $results);
199
200
        return !empty($results[0]) ? $results[0] : [];
201
    }
202
203
    /**
204
     * return the actual raw data string.
205
     *
206
     * @return string _rawData
207
     */
208
    public function getRawData()
209
    {
210
        return $this->rawData;
211
    }
212
213
    /**
214
     * return the actual raw data string.
215
     *
216
     * @return string currentStatementData
217
     */
218
    public function getCurrentStatementData()
219
    {
220
        return $this->currentStatementData;
221
    }
222
223
    /**
224
     * return the actual raw data string.
225
     *
226
     * @return string currentTransactionData
227
     */
228
    public function getCurrentTransactionData()
229
    {
230
        return $this->currentTransactionData;
231
    }
232
233
    // statement parsers, these work with currentStatementData
234
235
    /**
236
     * return the actual raw data string.
237
     *
238
     * @return string bank
239
     */
240
    protected function parseStatementBank()
241
    {
242
        return '';
243
    }
244
245
    /**
246
     * uses field 25 to gather accoutnumber.
247
     *
248
     * @return string accountnumber
249
     */
250
    protected function parseStatementAccount()
251
    {
252
        $results = [];
253
        if (preg_match('/:25:([\d\.]+)*/', $this->getCurrentStatementData(), $results)
254
            && !empty($results[1])
255
        ) {
256
            return $this->sanitizeAccount($results[1]);
257
        }
258
259
        // SEPA / IBAN
260
        if (preg_match('/:25:([A-Z0-9]{8}[\d\.]+)*/', $this->getCurrentStatementData(), $results)
261
            && !empty($results[1])
262
        ) {
263
            return $this->sanitizeAccount($results[1]);
264
        }
265
266
        return '';
267
    }
268
269
    /**
270
     * uses field 60F to gather starting amount.
271
     *
272
     * @return float price
273
     */
274
    protected function parseStatementStartPrice()
275
    {
276
        return $this->parseStatementPrice('60F');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseStatementPrice('60F') also could return the type string which is incompatible with the documented return type double.
Loading history...
277
    }
278
279
    /**
280
     * uses the 62F field to return end price of the statement.
281
     *
282
     * @return float price
283
     */
284
    protected function parseStatementEndPrice()
285
    {
286
        return $this->parseStatementPrice('62F');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseStatementPrice('62F') also could return the type string which is incompatible with the documented return type double.
Loading history...
287
    }
288
289
    /**
290
     * The actual pricing parser for statements.
291
     *
292
     * @param string $key
293
     *
294
     * @return float|string
295
     */
296
    protected function parseStatementPrice($key)
297
    {
298
        $results = [];
299
        if (preg_match('/:' . $key . ':([CD])?.*[A-Z]{3}([\d,\.]+)*/', $this->getCurrentStatementData(), $results)
300
            && !empty($results[2])
301
        ) {
302
            $sanitizedPrice = $this->sanitizePrice($results[2]);
303
304
            return (!empty($results[1]) && $results[1] === 'D') ? -$sanitizedPrice : $sanitizedPrice;
305
        }
306
307
        return '';
308
    }
309
310
    /**
311
     * The currency initials parser for statements.
312
     * @param string $key
313
     * @return string currency initials
314
     */
315
    protected function parseStatementCurrency($key = '60[FM]')
316
    {
317
        $results = [];
318
        if (preg_match('/:' . $key . ':[CD]?.*([A-Z]{3})([\d,\.]+)*/', $this->getCurrentStatementData(), $results)) {
319
            return $results[1];
320
        }
321
        return '';
322
    }
323
324
    /**
325
     * uses the 60F field to determine the date of the statement.
326
     *
327
     * @deprecated will be removed in the next major release and replaced by startTimestamp / endTimestamps
328
     *
329
     * @return int timestamp
330
     */
331
    protected function parseStatementTimestamp()
332
    {
333
        trigger_error('Deprecated in favor of splitting the start and end timestamps for a statement. ' .
334
            'Please use parseStatementStartTimestamp($format) or parseStatementEndTimestamp($format) instead. ' .
335
            'parseStatementTimestamp is now parseStatementStartTimestamp', E_USER_DEPRECATED);
336
337
        return $this->parseStatementStartTimestamp();
338
    }
339
340
    /**
341
     * uses the 60F field to determine the date of the statement.
342
     *
343
     * @return int timestamp
344
     */
345
    protected function parseStatementStartTimestamp()
346
    {
347
        return $this->parseTimestampFromStatement('60F');
348
    }
349
350
    /**
351
     * uses the 62F field to determine the date of the statement.
352
     *
353
     * @return int timestamp
354
     */
355
    protected function parseStatementEndTimestamp()
356
    {
357
        return $this->parseTimestampFromStatement('62F');
358
    }
359
360
    protected function parseTimestampFromStatement($key)
361
    {
362
        $results = [];
363
        if (preg_match('/:' . $key . ':[C|D](\d{6})*/', $this->getCurrentStatementData(), $results)
364
            && !empty($results[1])
365
        ) {
366
            return $this->sanitizeTimestamp($results[1]);
367
        }
368
369
        return 0;
370
    }
371
372
    /**
373
     * uses the 28C field to determine the statement number.
374
     *
375
     * @return string
376
     */
377
    protected function parseStatementNumber()
378
    {
379
        $results = [];
380
        if (preg_match('/:28C?:(.*)/', $this->getCurrentStatementData(), $results)
381
            && !empty($results[1])
382
        ) {
383
            return trim($results[1]);
384
        }
385
386
        return '';
387
    }
388
389
    // transaction parsers, these work with getCurrentTransactionData
390
391
    /**
392
     * uses the 86 field to determine account number of the transaction.
393
     *
394
     * @return string
395
     */
396
    protected function parseTransactionAccount()
397
    {
398
        $results = [];
399
        if (preg_match('/^:86: ?([\d\.]+)\s/m', $this->getCurrentTransactionData(), $results)
400
            && !empty($results[1])
401
        ) {
402
            return $this->sanitizeAccount($results[1]);
403
        }
404
405
        return '';
406
    }
407
408
    /**
409
     * uses the 86 field to determine accountname of the transaction.
410
     *
411
     * @return string
412
     */
413
    protected function parseTransactionAccountName()
414
    {
415
        $results = [];
416
        if (preg_match('/:86: ?[\d\.]+ (.+)/', $this->getCurrentTransactionData(), $results)
417
            && !empty($results[1])
418
        ) {
419
            return $this->sanitizeAccountName($results[1]);
420
        }
421
422
        return '';
423
    }
424
425
    /**
426
     * uses the 61 field to determine amount/value of the transaction.
427
     *
428
     * @return float
429
     */
430
    protected function parseTransactionPrice()
431
    {
432
        $results = [];
433
        if (preg_match('/^:61:.*?[CD]([\d,\.]+)N/i', $this->getCurrentTransactionData(), $results)
434
            && !empty($results[1])
435
        ) {
436
            return $this->sanitizePrice($results[1]);
437
        }
438
439
        return 0;
440
    }
441
442
    /**
443
     * uses the 61 field to determine debit or credit of the transaction.
444
     *
445
     * @return string
446
     */
447
    protected function parseTransactionDebitCredit()
448
    {
449
        $results = [];
450
        if (preg_match('/^:61:\d+([CD])\d+/', $this->getCurrentTransactionData(), $results)
451
            && !empty($results[1])
452
        ) {
453
            return $this->sanitizeDebitCredit($results[1]);
454
        }
455
456
        return '';
457
    }
458
459
    /**
460
     * Parses the Cancellation flag of a Transaction
461
     *
462
     * @return boolean
463
     */
464
    protected function parseTransactionCancellation()
465
    {
466
        return false;
467
    }
468
469
    /**
470
     * uses the 86 field to determine retrieve the full description of the transaction.
471
     *
472
     * @return string
473
     */
474
    protected function parseTransactionDescription()
475
    {
476
        $results = [];
477
        if (preg_match_all('/[\n]:86:(.*?)(?=\n(:6([12]))|$)/s', $this->getCurrentTransactionData(), $results)
478
            && !empty($results[1])
479
        ) {
480
            return $this->sanitizeDescription(implode(PHP_EOL, $results[1]));
481
        }
482
483
        return '';
484
    }
485
486
    /**
487
     * uses the 61 field to determine the entry timestamp.
488
     *
489
     * @return int
490
     */
491
    protected function parseTransactionEntryTimestamp()
492
    {
493
        return $this->parseTransactionTimestamp('61');
494
    }
495
496
    /**
497
     * uses the 61 field to determine the value timestamp.
498
     *
499
     * @return int
500
     */
501
    protected function parseTransactionValueTimestamp()
502
    {
503
        return $this->parseTransactionTimestamp('61');
504
    }
505
506
    /**
507
     * This does the actual parsing of the transaction timestamp for given $key.
508
     *
509
     * @param string $key
510
     * @return int
511
     */
512
    protected function parseTransactionTimestamp($key)
513
    {
514
        $results = [];
515
        if (preg_match('/^:' . $key . ':(\d{6})/', $this->getCurrentTransactionData(), $results)
516
            && !empty($results[1])
517
        ) {
518
            return $this->sanitizeTimestamp($results[1]);
519
        }
520
521
        return 0;
522
    }
523
524
    /**
525
     * uses the 61 field to get the bank specific transaction code.
526
     *
527
     * @return string
528
     */
529
    protected function parseTransactionCode()
530
    {
531
        $results = [];
532
        if (preg_match('/^:61:.*?N(.{3}).*/', $this->getCurrentTransactionData(), $results)
533
            && !empty($results[1])
534
        ) {
535
            return trim($results[1]);
536
        }
537
        return '';
538
    }
539
540
    /**
541
     * @param string $string
542
     *
543
     * @return string
544
     */
545
    protected function sanitizeAccount($string)
546
    {
547
        static $crudeReplacements = [
548
            '.' => '',
549
            ' ' => '',
550
            'GIRO' => 'P',
551
        ];
552
553
        // crude IBAN to 'old' converter
554
        if (Mt940::$removeIBAN
555
            && preg_match('#[A-Z]{2}[\d]{2}[A-Z]{4}(.*)#', $string, $results)
556
            && !empty($results[1])
557
        ) {
558
            $string = $results[1];
559
        }
560
561
        $account = ltrim(
562
            str_replace(
563
                array_keys($crudeReplacements),
564
                $crudeReplacements,
565
                strip_tags(trim($string))
566
            ),
567
            '0'
568
        );
569
        if ($account !== '' && strlen($account) < 9 && strpos($account, 'P') === false) {
570
            $account = 'P' . $account;
571
        }
572
573
        return $account;
574
    }
575
576
    /**
577
     * @param string $string
578
     *
579
     * @return string
580
     */
581
    protected function sanitizeAccountName($string)
582
    {
583
        return preg_replace('/[\r\n]+/', '', trim($string));
584
    }
585
586
    /**
587
     * @param string $string
588
     * @param string $inFormat
589
     *
590
     * @return int
591
     */
592
    protected function sanitizeTimestamp($string, $inFormat = 'ymd')
593
    {
594
        $date = \DateTime::createFromFormat($inFormat, $string);
595
        $date->setTime(0, 0);
596
        if ($date !== false) {
597
            return (int) $date->format('U');
598
        }
599
600
        return 0;
601
    }
602
603
    /**
604
     * @param string $string
605
     *
606
     * @return string
607
     */
608
    protected function sanitizeDescription($string)
609
    {
610
        return preg_replace('/[\r\n]+/', '', trim($string));
611
    }
612
613
    /**
614
     * @param string $string
615
     *
616
     * @return string
617
     */
618
    protected function sanitizeDebitCredit($string)
619
    {
620
        $debitOrCredit = strtoupper(substr((string) $string, 0, 1));
621
        if ($debitOrCredit !== Transaction::DEBIT && $debitOrCredit !== Transaction::CREDIT) {
622
            trigger_error('wrong value for debit/credit (' . $string . ')', E_USER_ERROR);
623
            $debitOrCredit = '';
624
        }
625
626
        return $debitOrCredit;
627
    }
628
629
    /**
630
     * @param string $string
631
     *
632
     * @return float
633
     */
634
    protected function sanitizePrice($string)
635
    {
636
        $floatPrice = ltrim(str_replace(',', '.', strip_tags(trim($string))), '0');
637
638
        return (float) $floatPrice;
639
    }
640
}
641