Passed
Pull Request — master (#54)
by
unknown
01:06
created

Engine::parseStatementPrice()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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