Passed
Pull Request — master (#57)
by
unknown
01:16
created

Engine::parseStatementNumber()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
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
    ];
30
31
    /**
32
     * reads the firstline of the string to guess which engine to use for parsing.
33
     *
34
     * @param string $string
35
     *
36
     * @return Engine
37
     */
38
    public static function __getInstance($string)
39
    {
40
        $engine = self::detectBank($string);
41
        $engine->loadString($string);
42
43
        return $engine;
44
    }
45
46
    /**
47
     * Register a new Engine.
48
     *
49
     * @param string $engineClass Class name of Engine to be registered
50
     * @param int $priority
51
     */
52
    public static function registerEngine($engineClass, $priority)
53
    {
54
        if (!is_int($priority)) {
0 ignored issues
show
introduced by
The condition is_int($priority) is always true.
Loading history...
55
            trigger_error('Priority must be integer', E_USER_WARNING);
56
57
            return;
58
        }
59
        if (array_key_exists($priority, self::$registeredEngines)) {
60
            trigger_error('Priority already taken', E_USER_WARNING);
61
62
            return;
63
        }
64
        if (!class_exists($engineClass)) {
65
            trigger_error('Engine does not exist', E_USER_WARNING);
66
67
            return;
68
        }
69
        self::$registeredEngines[$priority] = $engineClass;
70
    }
71
72
    /**
73
     * Unregisters all Engines.
74
     */
75
    public static function resetEngines()
76
    {
77
        self::$registeredEngines = [];
78
    }
79
80
    /**
81
     * Checks whether the Engine is applicable for the given string.
82
     *
83
     * @param string $string
84
     *
85
     * @return bool
86
     */
87
    public static function isApplicable($string)
88
    {
89
        return true;
90
    }
91
92
    /**
93
     * @param string $string
94
     *
95
     * @return Engine
96
     */
97
    private static function detectBank($string)
98
    {
99
        ksort(self::$registeredEngines, SORT_NUMERIC);
100
101
        foreach (self::$registeredEngines as $engineClass) {
102
            if ($engineClass::isApplicable($string)) {
103
                return new $engineClass();
104
            }
105
        }
106
107
        trigger_error('Unknown mt940 parser loaded, thus reverted to default', E_USER_NOTICE);
108
109
        return new Engine\Unknown();
110
    }
111
112
    /**
113
     * loads the $string into _rawData
114
     * this could be used to move it into handling of streams in the future.
115
     *
116
     * @param string $string
117
     */
118
    public function loadString($string)
119
    {
120
        $this->rawData = trim($string);
121
    }
122
123
    /**
124
     * actual parsing of the data.
125
     *
126
     * @return Statement[]
127
     */
128
    public function parse()
129
    {
130
        $results = [];
131
        foreach ($this->parseStatementData() as $this->currentStatementData) {
132
            $statement = new Statement();
133
            if ($this->debug) {
134
                $statement->rawData = $this->currentStatementData;
135
            }
136
            $statement->setBank($this->parseStatementBank());
137
            $statement->setAccount($this->parseStatementAccount());
138
            $statement->setStartPrice($this->parseStatementStartPrice());
139
            $statement->setEndPrice($this->parseStatementEndPrice());
140
            $statement->setStartTimestamp($this->parseStatementStartTimestamp());
141
            $statement->setEndTimestamp($this->parseStatementEndTimestamp());
142
            $statement->setNumber($this->parseStatementNumber());
143
            $statement->setCurrency($this->parseStatementCurrency());
144
145
            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...
146
                $transaction = new Transaction();
147
                if ($this->debug) {
148
                    $transaction->rawData = $this->currentTransactionData;
149
                }
150
                $transaction->setAccount($this->parseTransactionAccount());
151
                $transaction->setAccountName($this->parseTransactionAccountName());
152
                $transaction->setPrice($this->parseTransactionPrice());
153
                $transaction->setDebitCredit($this->parseTransactionDebitCredit());
154
                $transaction->setCancellation($this->parseTransactionCancellation());
155
                $transaction->setDescription($this->parseTransactionDescription());
156
                $transaction->setValueTimestamp($this->parseTransactionValueTimestamp());
157
                $transaction->setEntryTimestamp($this->parseTransactionEntryTimestamp());
158
                $transaction->setTransactionCode($this->parseTransactionCode());
159
                $transaction->setVirtualAccount($this->parseVirtualAccount());
160
                $statement->addTransaction($transaction);
161
            }
162
            $results[] = $statement;
163
        }
164
165
        return $results;
166
    }
167
168
    /**
169
     * split the rawdata up into statementdata chunks.
170
     *
171
     * @return array
172
     */
173
    protected function parseStatementData()
174
    {
175
        $results = preg_split(
176
            '/(^:20:|^-X{,3}$|\Z)/m',
177
            $this->getRawData(),
178
            -1,
179
            PREG_SPLIT_NO_EMPTY
180
        );
181
        array_shift($results); // remove the header
0 ignored issues
show
Bug introduced by
It seems like $results can also be of type false; however, parameter $array of array_shift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

181
        array_shift(/** @scrutinizer ignore-type */ $results); // remove the header
Loading history...
182
        return $results;
183
    }
184
185
    /**
186
     * split the statement up into transaction chunks.
187
     *
188
     * @return array
189
     */
190
    protected function parseTransactionData()
191
    {
192
        $results = [];
193
        preg_match_all('/^:61:(.*?)(?=^:61:|^-X{,3}$|\Z)/sm', $this->getCurrentStatementData(), $results);
194
195
        return !empty($results[0]) ? $results[0] : [];
196
    }
197
198
    /**
199
     * return the actual raw data string.
200
     *
201
     * @return string _rawData
202
     */
203
    public function getRawData()
204
    {
205
        return $this->rawData;
206
    }
207
208
    /**
209
     * return the actual raw data string.
210
     *
211
     * @return string currentStatementData
212
     */
213
    public function getCurrentStatementData()
214
    {
215
        return $this->currentStatementData;
216
    }
217
218
    /**
219
     * return the actual raw data string.
220
     *
221
     * @return string currentTransactionData
222
     */
223
    public function getCurrentTransactionData()
224
    {
225
        return $this->currentTransactionData;
226
    }
227
228
    // statement parsers, these work with currentStatementData
229
230
    /**
231
     * return the actual raw data string.
232
     *
233
     * @return string bank
234
     */
235
    protected function parseStatementBank()
236
    {
237
        return '';
238
    }
239
240
    /**
241
     * uses field 25 to gather accoutnumber.
242
     *
243
     * @return string accountnumber
244
     */
245
    protected function parseStatementAccount()
246
    {
247
        $results = [];
248
        if (preg_match('/:25:([\d\.]+)*/', $this->getCurrentStatementData(), $results)
249
            && !empty($results[1])
250
        ) {
251
            return $this->sanitizeAccount($results[1]);
252
        }
253
254
        // SEPA / IBAN
255
        if (preg_match('/:25:([A-Z0-9]{8}[\d\.]+)*/', $this->getCurrentStatementData(), $results)
256
            && !empty($results[1])
257
        ) {
258
            return $this->sanitizeAccount($results[1]);
259
        }
260
261
        return '';
262
    }
263
264
    /**
265
     * uses field 60F to gather starting amount.
266
     *
267
     * @return float price
268
     */
269
    protected function parseStatementStartPrice()
270
    {
271
        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...
272
    }
273
274
    /**
275
     * uses the 62F field to return end price of the statement.
276
     *
277
     * @return float price
278
     */
279
    protected function parseStatementEndPrice()
280
    {
281
        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...
282
    }
283
284
    /**
285
     * The actual pricing parser for statements.
286
     *
287
     * @param string $key
288
     *
289
     * @return float|string
290
     */
291
    protected function parseStatementPrice($key)
292
    {
293
        $results = [];
294
        if (preg_match('/:' . $key . ':([CD])?.*[A-Z]{3}([\d,\.]+)*/', $this->getCurrentStatementData(), $results)
295
            && !empty($results[2])
296
        ) {
297
            $sanitizedPrice = $this->sanitizePrice($results[2]);
298
299
            return (!empty($results[1]) && $results[1] === 'D') ? -$sanitizedPrice : $sanitizedPrice;
300
        }
301
302
        return '';
303
    }
304
305
    /**
306
     * The currency initials parser for statements.
307
     * @param string $key
308
     * @return string currency initials
309
     */
310
    protected function parseStatementCurrency($key = '60[FM]')
311
    {
312
        $results = [];
313
        if (preg_match('/:' . $key . ':[CD]?.*([A-Z]{3})([\d,\.]+)*/', $this->getCurrentStatementData(), $results)) {
314
            return $results[1];
315
        }
316
        return '';
317
    }
318
319
    /**
320
     * uses the 60F field to determine the date of the statement.
321
     *
322
     * @deprecated will be removed in the next major release and replaced by startTimestamp / endTimestamps
323
     *
324
     * @return int timestamp
325
     */
326
    protected function parseStatementTimestamp()
327
    {
328
        trigger_error('Deprecated in favor of splitting the start and end timestamps for a statement. ' .
329
            'Please use parseStatementStartTimestamp($format) or parseStatementEndTimestamp($format) instead. ' .
330
            'parseStatementTimestamp is now parseStatementStartTimestamp', E_USER_DEPRECATED);
331
332
        return $this->parseStatementStartTimestamp();
333
    }
334
335
    /**
336
     * uses the 60F field to determine the date of the statement.
337
     *
338
     * @return int timestamp
339
     */
340
    protected function parseStatementStartTimestamp()
341
    {
342
        return $this->parseTimestampFromStatement('60F');
343
    }
344
345
    /**
346
     * uses the 62F field to determine the date of the statement.
347
     *
348
     * @return int timestamp
349
     */
350
    protected function parseStatementEndTimestamp()
351
    {
352
        return $this->parseTimestampFromStatement('62F');
353
    }
354
355
    protected function parseTimestampFromStatement($key)
356
    {
357
        $results = [];
358
        if (preg_match('/:' . $key . ':[C|D](\d{6})*/', $this->getCurrentStatementData(), $results)
359
            && !empty($results[1])
360
        ) {
361
            return $this->sanitizeTimestamp($results[1], 'ymd');
362
        }
363
364
        return 0;
365
    }
366
367
    /**
368
     * uses the 28C field to determine the statement number.
369
     *
370
     * @return string
371
     */
372
    protected function parseStatementNumber()
373
    {
374
        $results = [];
375
        if (preg_match('/:28C?:(.*)/', $this->getCurrentStatementData(), $results)
376
            && !empty($results[1])
377
        ) {
378
            return trim($results[1]);
379
        }
380
381
        return '';
382
    }
383
384
    // transaction parsers, these work with getCurrentTransactionData
385
386
    /**
387
     * uses the 86 field to determine account number of the transaction.
388
     *
389
     * @return string
390
     */
391
    protected function parseTransactionAccount()
392
    {
393
        $results = [];
394
        if (preg_match('/^:86: ?([\d\.]+)\s/im', $this->getCurrentTransactionData(), $results)
395
            && !empty($results[1])
396
        ) {
397
            return $this->sanitizeAccount($results[1]);
398
        }
399
400
        return '';
401
    }
402
403
    /**
404
     * uses the 86 field to determine accountname of the transaction.
405
     *
406
     * @return string
407
     */
408
    protected function parseTransactionAccountName()
409
    {
410
        $results = [];
411
        if (preg_match('/:86: ?[\d\.]+ (.+)/', $this->getCurrentTransactionData(), $results)
412
            && !empty($results[1])
413
        ) {
414
            return $this->sanitizeAccountName($results[1]);
415
        }
416
417
        return '';
418
    }
419
420
    /**
421
     * uses the 61 field to determine amount/value of the transaction.
422
     *
423
     * @return float
424
     */
425
    protected function parseTransactionPrice()
426
    {
427
        $results = [];
428
        if (preg_match('/^:61:.*?[CD]([\d,\.]+)N/i', $this->getCurrentTransactionData(), $results)
429
            && !empty($results[1])
430
        ) {
431
            return $this->sanitizePrice($results[1]);
432
        }
433
434
        return 0;
435
    }
436
437
    /**
438
     * uses the 61 field to determine debit or credit of the transaction.
439
     *
440
     * @return string
441
     */
442
    protected function parseTransactionDebitCredit()
443
    {
444
        $results = [];
445
        if (preg_match('/^:61:\d+([CD])\d+/', $this->getCurrentTransactionData(), $results)
446
            && !empty($results[1])
447
        ) {
448
            return $this->sanitizeDebitCredit($results[1]);
449
        }
450
451
        return '';
452
    }
453
454
    /**
455
     * Parses the Cancellation flag of a Transaction
456
     *
457
     * @return boolean
458
     */
459
    protected function parseTransactionCancellation()
460
    {
461
        return false;
462
    }
463
464
    /**
465
     * uses the 86 field to determine retrieve the full description of the transaction.
466
     *
467
     * @return string
468
     */
469
    protected function parseTransactionDescription()
470
    {
471
        $results = [];
472
        if (preg_match_all('/[\n]:86:(.*?)(?=\n(:6(1|2))|$)/s', $this->getCurrentTransactionData(), $results)
473
            && !empty($results[1])
474
        ) {
475
            return $this->sanitizeDescription(implode(PHP_EOL, $results[1]));
476
        }
477
478
        return '';
479
    }
480
481
    /**
482
     * retrieve the virtual account from full description of the transaction.
483
     *
484
     * @return string
485
     */
486
    protected function parseVirtualAccount()
487
    {
488
        $results = [];
489
        if (preg_match('/[\n].*?(?:\/VA\/(.+)$)/m', $this->getCurrentTransactionData(), $results)) {
490
            return $this->sanitizeVirtualAccount($results[1]);
491
        }
492
493
        return '';
494
    }
495
496
497
    /**
498
     * uses the 61 field to determine the entry timestamp.
499
     *
500
     * @return int
501
     */
502
    protected function parseTransactionEntryTimestamp()
503
    {
504
        return $this->parseTransactionTimestamp('61');
505
    }
506
507
    /**
508
     * uses the 61 field to determine the value timestamp.
509
     *
510
     * @return int
511
     */
512
    protected function parseTransactionValueTimestamp()
513
    {
514
        return $this->parseTransactionTimestamp('61');
515
    }
516
517
    /**
518
     * This does the actual parsing of the transaction timestamp for given $key.
519
     *
520
     * @param string $key
521
     * @return int
522
     */
523
    protected function parseTransactionTimestamp($key)
524
    {
525
        $results = [];
526
        if (preg_match('/^:' . $key . ':(\d{6})/', $this->getCurrentTransactionData(), $results)
527
            && !empty($results[1])
528
        ) {
529
            return $this->sanitizeTimestamp($results[1], 'ymd');
530
        }
531
532
        return 0;
533
    }
534
535
    /**
536
     * uses the 61 field to get the bank specific transaction code.
537
     *
538
     * @return string
539
     */
540
    protected function parseTransactionCode()
541
    {
542
        $results = [];
543
        if (preg_match('/^:61:.*?N(.{3}).*/', $this->getCurrentTransactionData(), $results)
544
            && !empty($results[1])
545
        ) {
546
            return trim($results[1]);
547
        }
548
549
        return '';
550
    }
551
552
    /**
553
     * @param string $string
554
     *
555
     * @return string
556
     */
557
    protected function sanitizeAccount($string)
558
    {
559
        static $crudeReplacements = [
560
            '.' => '',
561
            ' ' => '',
562
            'GIRO' => 'P',
563
        ];
564
565
        // crude IBAN to 'old' converter
566
        if (Mt940::$removeIBAN
567
            && preg_match('#[A-Z]{2}[\d]{2}[A-Z]{4}(.*)#', $string, $results)
568
            && !empty($results[1])
569
        ) {
570
            $string = $results[1];
571
        }
572
573
        $account = ltrim(
574
            str_replace(
575
                array_keys($crudeReplacements),
576
                array_values($crudeReplacements),
577
                strip_tags(trim($string))
578
            ),
579
            '0'
580
        );
581
        if ($account !== '' && strlen($account) < 9 && strpos($account, 'P') === false) {
582
            $account = 'P' . $account;
583
        }
584
585
        return $account;
586
    }
587
588
    /**
589
     * @param string $string
590
     *
591
     * @return string
592
     */
593
    protected function sanitizeAccountName($string)
594
    {
595
        return preg_replace('/[\r\n]+/', '', trim($string));
596
    }
597
598
    /**
599
     * @param string $string
600
     * @param string $inFormat
601
     *
602
     * @return int
603
     */
604
    protected function sanitizeTimestamp($string, $inFormat = 'ymd')
605
    {
606
        $date = \DateTime::createFromFormat($inFormat, $string);
607
        $date->setTime(0, 0, 0);
608
        if ($date !== false) {
609
            return (int)$date->format('U');
610
        }
611
612
        return 0;
613
    }
614
615
    /**
616
     * @param string $string
617
     *
618
     * @return string
619
     */
620
    protected function sanitizeDescription($string)
621
    {
622
        return preg_replace('/[\r\n]+/', '', trim($string));
623
    }
624
625
    /**
626
     * @param string $string
627
     *
628
     * @return string
629
     */
630
    protected function sanitizeDebitCredit($string)
631
    {
632
        $debitOrCredit = strtoupper(substr((string)$string, 0, 1));
633
        if ($debitOrCredit !== Transaction::DEBIT && $debitOrCredit !== Transaction::CREDIT) {
634
            trigger_error('wrong value for debit/credit (' . $string . ')', E_USER_ERROR);
635
            $debitOrCredit = '';
636
        }
637
638
        return $debitOrCredit;
639
    }
640
641
    /**
642
     * @param string $string
643
     *
644
     * @return float
645
     */
646
    protected function sanitizePrice($string)
647
    {
648
        $floatPrice = ltrim(str_replace(',', '.', strip_tags(trim($string))), '0');
649
650
        return (float)$floatPrice;
651
    }
652
653
    /**
654
     * @param string $string
655
     *
656
     * @return string
657
     */
658
    protected function sanitizeVirtualAccount($string)
659
    {
660
        return trim($string);
661
    }
662
663
}
664