Completed
Push — master ( 26e69a...ab0b77 )
by Robin
03:27
created

Engine::parseTransactionTimestamp()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 2
nop 1
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)) {
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
        foreach (self::$registeredEngines as $engineClass) {
99
            if ($engineClass::isApplicable($string)) {
100
                return new $engineClass();
101
            }
102
        }
103
104
        trigger_error('Unknown mt940 parser loaded, thus reverted to default', E_USER_NOTICE);
105
106
        return new Engine\Unknown();
107
    }
108
109
    /**
110
     * loads the $string into _rawData
111
     * this could be used to move it into handling of streams in the future.
112
     *
113
     * @param string $string
114
     */
115
    public function loadString($string)
116
    {
117
        $this->rawData = trim($string);
118
    }
119
120
    /**
121
     * actual parsing of the data.
122
     *
123
     * @return Statement[]
124
     */
125
    public function parse()
126
    {
127
        $results = [];
128
        foreach ($this->parseStatementData() as $this->currentStatementData) {
129
            $statement = new Statement();
130
            if ($this->debug) {
131
                $statement->rawData = $this->currentStatementData;
132
            }
133
            $statement->setBank($this->parseStatementBank());
134
            $statement->setAccount($this->parseStatementAccount());
135
            $statement->setStartPrice($this->parseStatementStartPrice());
136
            $statement->setEndPrice($this->parseStatementEndPrice());
137
            $statement->setStartTimestamp($this->parseStatementStartTimestamp());
138
            $statement->setEndTimestamp($this->parseStatementEndTimestamp());
139
            $statement->setNumber($this->parseStatementNumber());
140
141
            foreach ($this->parseTransactionData() as $this->currentTransactionData) {
142
                $transaction = new Transaction();
143
                if ($this->debug) {
144
                    $transaction->rawData = $this->currentTransactionData;
145
                }
146
                $transaction->setAccount($this->parseTransactionAccount());
147
                $transaction->setAccountName($this->parseTransactionAccountName());
148
                $transaction->setPrice($this->parseTransactionPrice());
149
                $transaction->setDebitCredit($this->parseTransactionDebitCredit());
150
                $transaction->setCancellation($this->parseTransactionCancellation());
151
                $transaction->setDescription($this->parseTransactionDescription());
152
                $transaction->setValueTimestamp($this->parseTransactionValueTimestamp());
153
                $transaction->setEntryTimestamp($this->parseTransactionEntryTimestamp());
154
                $transaction->setTransactionCode($this->parseTransactionCode());
155
                $statement->addTransaction($transaction);
156
            }
157
            $results[] = $statement;
158
        }
159
160
        return $results;
161
    }
162
163
    /**
164
     * split the rawdata up into statementdata chunks.
165
     *
166
     * @return array
167
     */
168
    protected function parseStatementData()
169
    {
170
        $results = preg_split(
171
                '/(^:20:|^-X{,3}$|\Z)/sm',
172
                $this->getRawData(),
173
                -1,
174
                PREG_SPLIT_NO_EMPTY
175
        );
176
        array_shift($results); // remove the header
177
        return $results;
178
    }
179
180
    /**
181
     * split the statement up into transaction chunks.
182
     *
183
     * @return array
184
     */
185
    protected function parseTransactionData()
186
    {
187
        $results = [];
188
        preg_match_all('/^:61:(.*?)(?=^:61:|^-X{,3}$|\Z)/sm', $this->getCurrentStatementData(), $results);
189
190
        return (!empty($results[0])) ? $results[0] : [];
191
    }
192
193
    /**
194
     * return the actual raw data string.
195
     *
196
     * @return string _rawData
197
     */
198
    public function getRawData()
199
    {
200
        return $this->rawData;
201
    }
202
203
    /**
204
     * return the actual raw data string.
205
     *
206
     * @return string currentStatementData
207
     */
208
    public function getCurrentStatementData()
209
    {
210
        return $this->currentStatementData;
211
    }
212
213
    /**
214
     * return the actual raw data string.
215
     *
216
     * @return string currentTransactionData
217
     */
218
    public function getCurrentTransactionData()
219
    {
220
        return $this->currentTransactionData;
221
    }
222
223
    // statement parsers, these work with currentStatementData
224
225
    /**
226
     * return the actual raw data string.
227
     *
228
     * @return string bank
229
     */
230
    protected function parseStatementBank()
231
    {
232
        return '';
233
    }
234
235
    /**
236
     * uses field 25 to gather accoutnumber.
237
     *
238
     * @return string accountnumber
239
     */
240 View Code Duplication
    protected function parseStatementAccount()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
241
    {
242
        $results = [];
243
        if (preg_match('/:25:([\d\.]+)*/', $this->getCurrentStatementData(), $results)
244
                && !empty($results[1])
245
        ) {
246
            return $this->sanitizeAccount($results[1]);
247
        }
248
249
        // SEPA / IBAN
250
        if (preg_match('/:25:([A-Z0-9]{8}[\d\.]+)*/', $this->getCurrentStatementData(), $results)
251
                && !empty($results[1])
252
        ) {
253
            return $this->sanitizeAccount($results[1]);
254
        }
255
256
        return '';
257
    }
258
259
    /**
260
     * uses field 60F to gather starting amount.
261
     *
262
     * @return float price
263
     */
264
    protected function parseStatementStartPrice()
265
    {
266
        return $this->parseStatementPrice('60F');
267
    }
268
269
    /**
270
     * uses the 62F field to return end price of the statement.
271
     *
272
     * @return float price
273
     */
274
    protected function parseStatementEndPrice()
275
    {
276
        return $this->parseStatementPrice('62F');
277
    }
278
279
    /**
280
     * The actual pricing parser for statements.
281
     *
282
     * @param $key
283
     *
284
     * @return float|string
285
     */
286
    protected function parseStatementPrice($key)
287
    {
288
        $results = [];
289
        if (preg_match('/:'.$key.':([CD])?.*EUR([\d,\.]+)*/', $this->getCurrentStatementData(), $results)
290
                && !empty($results[2])
291
        ) {
292
            $sanitizedPrice = $this->sanitizePrice($results[2]);
293
294
            return (!empty($results[1]) && $results[1] === 'D') ? -$sanitizedPrice : $sanitizedPrice;
295
        }
296
297
        return '';
298
    }
299
300
    /**
301
     * uses the 60F field to determine the date of the statement.
302
     *
303
     * @deprecated will be removed in the next major release and replaced by startTimestamp / endTimestamps
304
     *
305
     * @return int timestamp
306
     */
307
    protected function parseStatementTimestamp()
308
    {
309
        trigger_error('Deprecated in favor of splitting the start and end timestamps for a statement. '.
310
                'Please use parseStatementStartTimestamp($format) or parseStatementEndTimestamp($format) instead. '.
311
                'setTimestamp is now parseStatementStartTimestamp', E_USER_DEPRECATED);
312
313
        return $this->parseStatementStartTimestamp();
314
    }
315
316
    /**
317
     * uses the 60F field to determine the date of the statement.
318
     *
319
     * @return int timestamp
320
     */
321
    protected function parseStatementStartTimestamp()
322
    {
323
        return $this->parseTimestampFromStatement('60F');
324
    }
325
326
    /**
327
     * uses the 62F field to determine the date of the statement.
328
     *
329
     * @return int timestamp
330
     */
331
    protected function parseStatementEndTimestamp()
332
    {
333
        return $this->parseTimestampFromStatement('62F');
334
    }
335
336
    protected function parseTimestampFromStatement($key)
337
    {
338
        $results = [];
339
        if (preg_match('/:'.$key.':[C|D](\d{6})*/', $this->getCurrentStatementData(), $results)
340
                && !empty($results[1])
341
        ) {
342
            return $this->sanitizeTimestamp($results[1], 'ymd');
343
        }
344
345
        return 0;
346
    }
347
348
    /**
349
     * uses the 28C field to determine the statement number.
350
     *
351
     * @return string
352
     */
353
    protected function parseStatementNumber()
354
    {
355
        $results = [];
356
        if (preg_match('/:28C?:(.*)/', $this->getCurrentStatementData(), $results)
357
                && !empty($results[1])
358
        ) {
359
            return trim($results[1]);
360
        }
361
362
        return '';
363
    }
364
365
    // transaction parsers, these work with getCurrentTransactionData
366
    /**
367
     * uses the 86 field to determine account number of the transaction.
368
     *
369
     * @return string
370
     */
371 View Code Duplication
    protected function parseTransactionAccount()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
    {
373
        $results = [];
374
        if (preg_match('/^:86: ?([\d\.]+)\s/im', $this->getCurrentTransactionData(), $results)
375
                && !empty($results[1])
376
        ) {
377
            return $this->sanitizeAccount($results[1]);
378
        }
379
380
        return '';
381
    }
382
383
    /**
384
     * uses the 86 field to determine accountname of the transaction.
385
     *
386
     * @return string
387
     */
388 View Code Duplication
    protected function parseTransactionAccountName()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389
    {
390
        $results = [];
391
        if (preg_match('/:86: ?[\d\.]+ (.+)/', $this->getCurrentTransactionData(), $results)
392
                && !empty($results[1])
393
        ) {
394
            return $this->sanitizeAccountName($results[1]);
395
        }
396
397
        return '';
398
    }
399
400
    /**
401
     * uses the 61 field to determine amount/value of the transaction.
402
     *
403
     * @return float
404
     */
405 View Code Duplication
    protected function parseTransactionPrice()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
406
    {
407
        $results = [];
408
        if (preg_match('/^:61:.*[CD]([\d,\.]+)N/i', $this->getCurrentTransactionData(), $results)
409
                && !empty($results[1])
410
        ) {
411
            return $this->sanitizePrice($results[1]);
412
        }
413
414
        return 0;
415
    }
416
417
    /**
418
     * uses the 61 field to determine debit or credit of the transaction.
419
     *
420
     * @return string
421
     */
422 View Code Duplication
    protected function parseTransactionDebitCredit()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
423
    {
424
        $results = [];
425
        if (preg_match('/^:61:\d+([CD])\d+/', $this->getCurrentTransactionData(), $results)
426
                && !empty($results[1])
427
        ) {
428
            return $this->sanitizeDebitCredit($results[1]);
429
        }
430
431
        return '';
432
    }
433
434
    /**
435
     * Parses the Cancellation flag of a Transaction
436
     *
437
     * @return boolean
438
     */
439
    protected function parseTransactionCancellation () {
440
        return false;
441
    }
442
443
    /**
444
     * uses the 86 field to determine retrieve the full description of the transaction.
445
     *
446
     * @return string
447
     */
448
    protected function parseTransactionDescription()
449
    {
450
        $results = [];
451
        if (preg_match_all('/[\n]:86:(.*?)(?=\n(:6(1|2))|$)/s', $this->getCurrentTransactionData(), $results)
452
                && !empty($results[1])
453
        ) {
454
            return $this->sanitizeDescription(implode(PHP_EOL, $results[1]));
455
        }
456
457
        return '';
458
    }
459
460
    /**
461
     * uses the 61 field to determine the entry timestamp.
462
     *
463
     * @return int
464
     */
465
    protected function parseTransactionEntryTimestamp()
466
    {
467
        return $this->parseTransactionTimestamp('61');
468
    }
469
470
    /**
471
     * uses the 61 field to determine the value timestamp.
472
     *
473
     * @return int
474
     */
475
    protected function parseTransactionValueTimestamp()
476
    {
477
        return $this->parseTransactionTimestamp('61');
478
    }
479
480
    /**
481
     * This does the actual parsing of the transaction timestamp for given $key.
482
     *
483
     * @param string $key
484
     * @return int
485
     */
486
    protected function parseTransactionTimestamp($key)
487
    {
488
        $results = [];
489
        if (preg_match('/^:'.$key.':(\d{6})/', $this->getCurrentTransactionData(), $results)
490
                && !empty($results[1])
491
        ) {
492
            return $this->sanitizeTimestamp($results[1], 'ymd');
493
        }
494
495
        return 0;
496
    }
497
498
    /**
499
     * uses the 61 field to get the bank specific transaction code.
500
     *
501
     * @return string
502
     */
503 View Code Duplication
    protected function parseTransactionCode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
504
    {
505
        $results = [];
506
        if (preg_match('/^:61:.*?N(.{3}).*/', $this->getCurrentTransactionData(), $results)
507
                && !empty($results[1])
508
        ) {
509
            return trim($results[1]);
510
        }
511
512
        return '';
513
    }
514
515
    /**
516
     * @param string $string
517
     *
518
     * @return string
519
     */
520
    protected function sanitizeAccount($string)
521
    {
522
        static $crudeReplacements = [
523
                '.' => '',
524
                ' ' => '',
525
                'GIRO' => 'P',
526
        ];
527
528
        // crude IBAN to 'old' converter
529
        if (Mt940::$removeIBAN
530
                && preg_match('#[A-Z]{2}[0-9]{2}[A-Z]{4}(.*)#', $string, $results)
531
                && !empty($results[1])
532
        ) {
533
            $string = $results[1];
534
        }
535
536
        $account = ltrim(
537
                str_replace(
538
                        array_keys($crudeReplacements),
539
                        array_values($crudeReplacements),
540
                        strip_tags(trim($string))
541
                ),
542
                '0'
543
        );
544
        if ($account != '' && strlen($account) < 9 && strpos($account, 'P') === false) {
545
            $account = 'P'.$account;
546
        }
547
548
        return $account;
549
    }
550
551
    /**
552
     * @param string $string
553
     *
554
     * @return string
555
     */
556
    protected function sanitizeAccountName($string)
557
    {
558
        return preg_replace('/[\r\n]+/', '', trim($string));
559
    }
560
561
    /**
562
     * @param string $string
563
     * @param string $inFormat
564
     *
565
     * @return int
566
     */
567
    protected function sanitizeTimestamp($string, $inFormat = 'ymd')
568
    {
569
        $date = \DateTime::createFromFormat($inFormat, $string);
570
        $date->setTime(0, 0, 0);
571
        if ($date !== false) {
572
            return (int) $date->format('U');
573
        }
574
575
        return 0;
576
    }
577
578
    /**
579
     * @param string $string
580
     *
581
     * @return string
582
     */
583
    protected function sanitizeDescription($string)
584
    {
585
        return preg_replace('/[\r\n]+/', '', trim($string));
586
    }
587
588
    /**
589
     * @param string $string
590
     *
591
     * @return string
592
     */
593
    protected function sanitizeDebitCredit($string)
594
    {
595
        $debitOrCredit = strtoupper(substr((string) $string, 0, 1));
596
        if ($debitOrCredit != Transaction::DEBIT && $debitOrCredit != Transaction::CREDIT) {
597
            trigger_error('wrong value for debit/credit ('.$string.')', E_USER_ERROR);
598
            $debitOrCredit = '';
599
        }
600
601
        return $debitOrCredit;
602
    }
603
604
    /**
605
     * @param string $string
606
     *
607
     * @return float
608
     */
609
    protected function sanitizePrice($string)
610
    {
611
        $floatPrice = ltrim(str_replace(',', '.', strip_tags(trim($string))), '0');
612
613
        return (float) $floatPrice;
614
    }
615
}
616