SepaPmtInf   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 64
eloc 171
dl 0
loc 524
rs 3.28
c 0
b 0
f 0

29 Methods

Rating   Name   Duplication   Size   Complexity  
A addChild() 0 12 3
A getCollExecDate() 0 10 3
A setSeqType() 0 3 1
A setName() 0 3 1
B addTransaction() 0 97 10
A setTxCountNode() 0 3 1
A calc() 0 9 3
A errorMsg() 0 4 1
A getId() 0 3 1
A __construct() 0 8 1
A setCI() 0 3 1
A fromArray() 0 14 3
A setCtrlSumNode() 0 3 1
A setBIC() 0 3 1
A setIBAN() 0 3 1
A validate() 0 7 1
A validateIBAN() 0 11 4
A getSeqType() 0 3 1
A getCI() 0 3 1
A setCategoryPurpose() 0 3 1
A getName() 0 3 1
A isValidateSeqType() 0 9 1
A getCategoryPurpose() 0 3 1
A validateBIC() 0 11 4
A validateMandatory() 0 14 5
A validateCI() 0 11 4
A getBIC() 0 3 1
A getIBAN() 0 3 1
A setCollExecDate() 0 11 6

How to fix   Complexity   

Complex Class

Complex classes like SepaPmtInf often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SepaPmtInf, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace SKien\Sepa;
3
4
/**
5
 * Class representing Payment Instruction Info (PII).
6
 *
7
 * #### Usage:
8
 * 1. Create a instance of this class and set all properties using the `SepaPmtInf::setXXX()` methods
9
 *    or `SepaPmtInf::fromArray()`.
10
 * 2. Attach the instance to the `SepaDoc` using the `SepaDoc::addPaymentInstructionInfo()` method.
11
 * 3. Generate the desired transactions (`new SepaTxInf()`) and assign them to this payment info instance.
12
 *
13
 * @package Sepa
14
 * @author Stefanius <[email protected]>
15
 * @copyright MIT License - see the LICENSE file for details
16
 */
17
class SepaPmtInf extends \DOMElement
18
{
19
    use SepaHelper;
20
21
    /** @var string  unique id*/
22
    protected string $strID = '';
23
    /** @var string  full name of applicant*/
24
    protected string $strName = '';
25
    /** @var string  IBAN*/
26
    protected string $strIBAN = '';
27
    /** @var string  BIC*/
28
    protected string $strBIC = '';
29
    /** @var string  CI (Creditor Scheme Identification)*/
30
    protected string $strCI = '';
31
    /** @var string  sequence type (Sepa::FRST, Sepa::SEQ_RECURRENT, Sepa::SEQ_ONE_OFF, Sepa::SEQ_FINAL)*/
32
    protected string $strSeqType = '';
33
    /** @var int requested collection/execution date */
34
    protected int $uxtsCollExecDate = 0;
35
    /** @var string an optional category purpose code (if all payments for the PII belongs to same purpose)     */
36
    protected string $strCategoryPurpose = '';
37
    /** @var SepaDoc parent document */
38
    private SepaDoc $sepaDoc;
39
    /** @var int count of transactions contained in PII */
40
    private int $iTxCount = 0;
41
    /** @var \DOMElement DOM element containing count of transactions */
42
    private ?\DOMElement $xmlTxCount = null;
43
    /** @var float controlsum */
44
    private float $dblCtrlSum = 0.0;
45
    /** @var \DOMElement DOM element containing controlsum */
46
    private ?\DOMElement $xmlCtrlSum = null;
47
48
    /**
49
     * Creating SEPA Payment Instruction Info (PII).
50
     * @param SepaDoc $sepaDoc
51
     */
52
    function __construct(SepaDoc $sepaDoc)
53
    {
54
        // store the parent doc and generate an unique id
55
        parent::__construct("PmtInf");
56
57
        // don't append any child at this point - created element have to be associated with a document after creation
58
        $this->sepaDoc = $sepaDoc;
59
        $this->strID = self::createUID();
60
    }
61
62
    /**
63
     * Validate the object.
64
     * > This method usually dont have to be called from outside. It is
65
     * called, when an instance is attached to a SepaDoc!
66
     * @return int Sepa::OK or error code
67
     * @internal
68
     */
69
    public function validate() : int
70
    {
71
        $iErr = $this->validateIBAN();
72
        $iErr |= $this->validateBIC();
73
        $iErr |= $this->validateCI();
74
        $iErr |= $this->validateMandatory();
75
        return $iErr;
76
    }
77
78
    /**
79
     * Get the error message for given error code.
80
     * Since a payment info can contain multiple errors, the result may contain more than
81
     * one message separated by a separator. <br>
82
     * The separator can be specified to meet the needs of different output destinations.
83
     * Default value is a linefeed.
84
     * @param int $iError   the errorcode
85
     * @param string $strLF     Separator for multiple errors (default: PHP_EOL; posible values: '&lt;br/&gt;', ';', ...)
86
     * @return string
87
     */
88
    public function errorMsg(int $iError, string $strLF = PHP_EOL) : string
89
    {
90
        // route to the Sepa class to get localized message
91
        return Sepa::errorMsgPmtInf($iError, $strLF);
92
    }
93
94
    /**
95
     * Add a transaction.
96
     * <b>Important:</b><br>
97
     * The PPI <b>must</b> be attached to the SepaDoc <b>before</b> the first transaktion is added!
98
     * @param SepaTxInf $oTxInf
99
     * @return int Sepa::OK or error code from SepaTxInf::validate()
100
     */
101
    public function addTransaction(SepaTxInf $oTxInf) : int
102
    {
103
        // element must been added as child to valid parent
104
        if ($this->xmlCtrlSum === null || $this->xmlTxCount === null) {
105
            trigger_error('element not added to parent (you must call SepaDoc::addPaymentInstructionInfo() before)!', E_USER_ERROR);
106
        }
107
        // transaction method have to fit to parent doc
108
        if ($this->sepaDoc->getType() != $oTxInf->getType()) {
109
            return Sepa::ERR_TX_INVALID_TYPE;
110
        }
111
112
        $iErr = $oTxInf->validate();
113
        if ($iErr == Sepa::OK) {
114
            if ($oTxInf->getType() == Sepa::CDD) {
115
                $xmlTx = $this->addChild(null, 'DrctDbtTxInf');
116
117
                $xmlNode = $this->addChild($xmlTx, 'PmtId');
118
                $this->addChild($xmlNode, 'EndToEndId', $oTxInf->getPaymentId());
119
120
                // Instructed Amount
121
                $xmlNode = $this->addChild($xmlTx, 'InstdAmt', sprintf("%01.2f", $oTxInf->getValue()));
122
                $xmlNode->SetAttribute('Ccy', 'EUR');
123
124
                // Mandate Related Information
125
                $xmlNode = $this->addChild($xmlTx, 'DrctDbtTx');
126
                $xmlNode = $this->addChild($xmlNode, 'MndtRltdInf');
127
                $this->addChild($xmlNode, 'MndtId', $oTxInf->getMandateId());
128
                $this->addChild($xmlNode, 'DtOfSgntr', $oTxInf->getDateOfSignature());
129
                $this->addChild($xmlNode, 'AmdmntInd', 'false');
130
131
                // Debitor Information  Name, IBAN, BIC
132
                $xmlNode = $this->addChild($xmlTx, 'DbtrAgt');
133
                $xmlNode = $this->addChild($xmlNode, 'FinInstnId');
134
                $this->addChild($xmlNode, 'BIC', $oTxInf->getBIC());
135
136
                $xmlNode = $this->addChild($xmlTx, 'Dbtr');
137
                $this->addChild($xmlNode, 'Nm', $oTxInf->getName());
138
139
                $xmlNode = $this->addChild($xmlTx, 'DbtrAcct');
140
                $xmlNode = $this->addChild($xmlNode, 'Id');
141
                $this->addChild($xmlNode, 'IBAN', $oTxInf->getIBAN());
142
143
                // Ultimate Debitor if requested
144
                $strUltmtDbtr = $oTxInf->getUltimateName();
145
                if (strlen($strUltmtDbtr) > 0) {
146
                    $xmlNode = $this->addChild($xmlTx, 'UltmtDbtr');
147
                    $this->addChild($xmlNode, 'Nm', $strUltmtDbtr);
148
                }
149
                if (($strPurpose = $oTxInf->getPurpose()) != '') {
150
                    $xmlNode = $this->addChild($xmlTx, 'Purp');
151
                    $this->addChild($xmlNode, 'Cd', $strPurpose);
152
                }
153
            } else {
154
                $xmlTx = $this->addChild(null, 'CdtTrfTxInf');
155
156
                $xmlNode = $this->addChild($xmlTx, 'PmtId');
157
                $this->addChild($xmlNode, 'EndToEndId', $oTxInf->getPaymentId());
158
159
                // Amount
160
                $xmlNode = $this->addChild($xmlTx, 'Amt');
161
                $xmlNode = $this->addChild($xmlNode, 'InstdAmt', sprintf("%01.2f", $oTxInf->getValue()));
162
                $xmlNode->SetAttribute('Ccy', 'EUR');
163
164
                // Creditor Information  Name, IBAN, BIC
165
                $xmlNode = $this->addChild($xmlTx, 'CdtrAgt');
166
                $xmlNode = $this->addChild($xmlNode, 'FinInstnId');
167
                $this->addChild($xmlNode, 'BIC', $oTxInf->getBIC());
168
169
                $xmlNode = $this->addChild($xmlTx, 'Cdtr');
170
                $this->addChild($xmlNode, 'Nm', $oTxInf->getName());
171
172
                $xmlNode = $this->addChild($xmlTx, 'CdtrAcct');
173
                $xmlNode = $this->addChild($xmlNode, 'Id');
174
                $this->addChild($xmlNode, 'IBAN', $oTxInf->getIBAN());
175
176
                // Ultimate Creditor if requested
177
                $strUltmtCbtr = $oTxInf->getUltimateName();
178
                if (strlen($strUltmtCbtr) > 0) {
179
                    $xmlNode = $this->addChild($xmlTx, 'UltmtCbtr');
180
                    $this->addChild($xmlNode, 'Nm', $strUltmtCbtr);
181
                }
182
                if (($strPurpose = $oTxInf->getPurpose()) != '') {
183
                    $xmlNode = $this->addChild($xmlTx, 'Purp');
184
                    $this->addChild($xmlNode, 'Cd', $strPurpose);
185
                }
186
            }
187
188
            // Remittance Information
189
            $xmlNode = $this->addChild($xmlTx, 'RmtInf');
190
            $this->addChild($xmlNode, 'Ustrd', $oTxInf->getDescription());
191
192
            // calculate count and controlsum of transactions
193
            $this->calc($oTxInf->getValue());
194
        } else {
195
            $this->sepaDoc->incInvalidCount();
196
        }
197
        return $iErr;
198
    }
199
200
    /**
201
     * Create a element and set it as child for given parent.
202
     * @param \DOMElement   $xmlParent  parent for the node. If null, child of current instance is created
203
     * @param string        $strNode    nodename
204
     * @param int|string    $value      nodevalue. If empty, no value will be assigned (to create node only containing child elements)
205
     * @return \DOMElement
206
     */
207
    protected function addChild(?\DOMElement $xmlParent, string $strNode, $value = '') : \DOMElement
208
    {
209
        if ($xmlParent == null) {
210
            $xmlParent = $this;
211
        }
212
        $xmlNode = $this->sepaDoc->createElement($strNode);
213
        if (!empty($value)) {
214
            $xmlNode->nodeValue = (string)$value;
215
        }
216
        $xmlParent->appendChild($xmlNode);
217
218
        return $xmlNode;
219
    }
220
221
    /**
222
     * Calculate transaction count and controlsum for PII and update overall in parent doc.
223
     * @param float $dblValue
224
     */
225
    protected function calc(float $dblValue) : void
226
    {
227
        if ($this->xmlTxCount !== null && $this->xmlCtrlSum !== null) {
228
            $this->iTxCount++;
229
            $this->xmlTxCount->nodeValue = (string)$this->iTxCount;
230
            $this->dblCtrlSum += $dblValue;
231
            $this->xmlCtrlSum->nodeValue = sprintf("%01.2f", $this->dblCtrlSum);
232
233
            $this->sepaDoc->calc($dblValue);
234
        }
235
    }
236
237
    /**
238
     * Return the internal unique ID.
239
     * @return string
240
     */
241
    public function getId() : string
242
    {
243
        return $this->strID;
244
    }
245
246
    /**
247
     * Get the collection date.
248
     * @return string
249
     */
250
    public function getCollExecDate(string $type) : string
251
    {
252
        // no delay for CCT
253
        $iDays = 0;
254
        if ($type == Sepa::CDD) {
255
            // CDD: Requested Collection Date depends on sequence type
256
            $this->strSeqType == Sepa::SEQ_RECURRENT ? $iDays = 2 : $iDays = 5;
257
        }
258
        $dtCollect = self::calcDelayedDate($iDays, $this->uxtsCollExecDate);
259
        return date('Y-m-d', $dtCollect);
260
    }
261
262
    /**
263
     * Set the xml node containing transactions count.
264
     * @param \DOMElement $xmlNode
265
     */
266
    public function setTxCountNode(\DOMElement $xmlNode) : void
267
    {
268
        $this->xmlTxCount = $xmlNode;
269
    }
270
271
    /**
272
     * Set the xml node containing control sum.
273
     * @param \DOMElement $xmlNode
274
     */
275
    public function setCtrlSumNode(\DOMElement $xmlNode) : void
276
    {
277
        $this->xmlCtrlSum = $xmlNode;
278
    }
279
280
    /**
281
     * Set properties through associative array.
282
     * Example array:
283
     * ```php
284
     *   $aPPI = [
285
     *       'strName' => '<name>',
286
     *       'strCI' => '<CI>',
287
     *       'strIBAN' => '<IBAN>',
288
     *       'strBIC' => '<BIC>',
289
     *       'strSeqType' => Sepa::SEQ_xxx,
290
     *       'strCollExecDate' => 'setCollExecDate',
291
     *   ];
292
     * ```
293
     * The array does not have to contain all of the properties. The missing properties
294
     * can be set later using the respective `setXXX()` method.
295
     *
296
     * @param array<string> $aProperties    see description
297
     */
298
    public function fromArray(array $aProperties) : void
299
    {
300
        // use the setter methods to ensure that all validations are made!
301
        $aPropertyMap = [
302
            'strName' => 'setName',
303
            'strIBAN' => 'setIBAN',
304
            'strBIC' => 'setBIC',
305
            'strCI' => 'setCI',
306
            'strSeqType' => 'setSeqType',
307
            'strCollExecDate' => 'setCollExecDate',
308
        ];
309
        foreach ($aPropertyMap as $strKey => $strFunc) {
310
            if (isset($aProperties[$strKey])) {
311
                $this->$strFunc($aProperties[$strKey]);
312
            }
313
        }
314
    }
315
316
    /**
317
     * Set full name (lastname, firstname; company name; ...).
318
     * @param string $strName
319
     */
320
    public function setName(string $strName) : void
321
    {
322
        $this->strName = self::validString($strName, Sepa::MAX70);
323
    }
324
325
    /**
326
     * Set IBAN.
327
     * @param string $strIBAN
328
     */
329
    public function setIBAN(string $strIBAN) : void
330
    {
331
        $this->strIBAN = $strIBAN;
332
    }
333
334
    /**
335
     * Set BIC.
336
     * @param string $strBIC
337
     */
338
    public function setBIC(string $strBIC) : void
339
    {
340
        $this->strBIC = $strBIC;
341
    }
342
343
    /**
344
     * Set CI (Creditor Scheme Identification).
345
     * @param string $strCI
346
     */
347
    public function setCI(string $strCI) : void
348
    {
349
        $this->strCI = $strCI;
350
    }
351
352
    /**
353
     * Set sequence type (Sepa::FRST, Sepa::SEQ_RECURRENT, Sepa::SEQ_ONE_OFF, Sepa::SEQ_FINAL).
354
     * @param string $strSeqType
355
     */
356
    public function setSeqType(string $strSeqType) : void
357
    {
358
        $this->strSeqType = $strSeqType;
359
    }
360
361
    /**
362
     * Set the requested collection/execution date.
363
     * If no date set, the next possible date for execution/collection is calculated.
364
     * > <b>Note:</b><br>
365
     * > Banks are not obliged to process order data that was submitted more than 15 calendar days
366
     * BEFORE the execution date.
367
     * @param \DateTime|int|string $date    may be string (format YYYY-MM-DD), int (unixtimestamp) or DateTime - object
368
     */
369
    public function setCollExecDate($date) : void
370
    {
371
        if (is_object($date) && get_class($date) == 'DateTime') {
372
            // DateTime -object
373
            $this->uxtsCollExecDate = $date->getTimestamp();
374
        } else if (is_numeric($date)) {
375
            $this->uxtsCollExecDate = intval($date);
376
        } else  if (is_string($date)) {
377
            $uxts = strtotime($date);
378
            if ($uxts !== false) {
379
                $this->uxtsCollExecDate = $uxts;
380
            }
381
        }
382
    }
383
384
    /**
385
     * Set the payment instruction category purpose.
386
     * This is an optional value!
387
     * If set, only ISO 20022 codes of the ExternalCategoryPurpose1Code list are allowed.
388
     * Referr to the actual list that is available in worksheet '4-CategoryPurpose 'of the Excel
389
     * file provided in the download at
390
     * [www.iso20022.org](https://www.iso20022.org/catalogue-messages/additional-content-messages/external-code-sets)
391
     * > <b>Attention:</b><br>
392
     * > There is no validation whether in this module nor through the provided XSD schemas for
393
     * this value. To avoid rejection of your data, you have to take care for valid values on your own.
394
     * @link ./Category-Purpose-Codes
395
     * @param string $strCategoryPurpose
396
     */
397
    public function setCategoryPurpose(string $strCategoryPurpose) : void
398
    {
399
        $this->strCategoryPurpose = strtoupper(substr($strCategoryPurpose, 0, 4));
400
    }
401
402
    /**
403
     * Get full name (lastname, firstname; company name; ...).
404
     * @return string
405
     */
406
    public function getName() : string
407
    {
408
        return $this->strName;
409
    }
410
411
    /**
412
     * Get IBAN.
413
     * @return string
414
     */
415
    public function getIBAN() : string
416
    {
417
        return $this->strIBAN;
418
    }
419
420
    /**
421
     * Get BIC.
422
     * @return string
423
     */
424
    public function getBIC() : string
425
    {
426
        return $this->strBIC;
427
    }
428
429
    /**
430
     * Get CI (Creditor Scheme Identification).
431
     * @return string
432
     */
433
    public function getCI() : string
434
    {
435
        return $this->strCI;
436
    }
437
438
    /**
439
     * Get sequence type (Sepa::FRST, Sepa::SEQ_RECURRENT, Sepa::SEQ_ONE_OFF, Sepa::SEQ_FINAL).
440
     * @return string
441
     */
442
    public function getSeqType() : string
443
    {
444
        return $this->strSeqType;
445
    }
446
447
    /**
448
     * Get the payment instruction category purpose
449
     * @return string
450
     */
451
    public function getCategoryPurpose() : string
452
    {
453
        return $this->strCategoryPurpose;
454
    }
455
456
    /**
457
     * Validate the IBAN.
458
     * @return int Sepa::OK or errorcode
459
     */
460
    private function validateIBAN() : int
461
    {
462
        $iErr = Sepa::OK;
463
        if (!Sepa::checkValidation(Sepa::V_NO_IBAN_VALIDATION)) {
464
            if (strlen($this->strIBAN) == 0) {
465
                $iErr = Sepa::ERR_PMT_IBAN_MISSING;
466
            } else if (Sepa::validateIBAN($this->strIBAN) != Sepa::OK) {
467
                $iErr = Sepa::ERR_PMT_INVALID_IBAN;
468
            }
469
        }
470
        return $iErr;
471
    }
472
473
    /**
474
     * Validate the BIC.
475
     * @return int Sepa::OK or errorcode
476
     */
477
    private function validateBIC() : int
478
    {
479
        $iErr = Sepa::OK;
480
        if (!Sepa::checkValidation(Sepa::V_NO_BIC_VALIDATION)) {
481
            if (strlen($this->strBIC) == 0) {
482
                $iErr = Sepa::ERR_PMT_BIC_MISSING;
483
            } else if (Sepa::validateBIC($this->strBIC) != Sepa::OK) {
484
                $iErr = Sepa::ERR_PMT_INVALID_BIC;
485
            }
486
        }
487
        return $iErr;
488
    }
489
490
    /**
491
     * Validate the CI.
492
     * @return int Sepa::OK or errorcode
493
     */
494
    private function validateCI() : int
495
    {
496
        $iErr = Sepa::OK;
497
        if (!Sepa::checkValidation(Sepa::V_NO_CI_VALIDATION)) {
498
            if (strlen($this->strCI) == 0) {
499
                $iErr = Sepa::ERR_PMT_CI_MISSING;
500
            } else if (Sepa::validateCI($this->strCI) != Sepa::OK) {
501
                $iErr = Sepa::ERR_PMT_INVALID_CI;
502
            }
503
        }
504
        return $iErr;
505
    }
506
507
    /**
508
     * Validate mandatory fields.
509
     * @return int Sepa::OK or errorcode
510
     */
511
    private function validateMandatory() : int
512
    {
513
        $iErr = Sepa::OK;
514
        if (!Sepa::checkValidation(Sepa::V_IGNORE_MISSING_VALUE)) {
515
            if (strlen($this->strName) == 0) {
516
                $iErr |= Sepa::ERR_PMT_NAME_MISSING;
517
            }
518
            if (strlen($this->strSeqType) == 0) {
519
                $iErr |= Sepa::ERR_PMT_SEQ_TYPE_MISSING;
520
            } else if (!$this->isValidateSeqType($this->strSeqType)) {
521
                $iErr |= Sepa::ERR_PMT_INVALID_SEQ_TYPE;
522
            }
523
        }
524
        return $iErr;
525
    }
526
527
    /**
528
     * Check for valid sequence type.
529
     * @param string $strSeqType type to check
530
     * @return bool
531
     */
532
    private function isValidateSeqType(string $strSeqType) : bool
533
    {
534
        $aValid = [
535
            Sepa::SEQ_FIRST,
536
            Sepa::SEQ_RECURRENT,
537
            Sepa::SEQ_ONE_OFF,
538
            Sepa::SEQ_FINAL
539
        ];
540
        return in_array($strSeqType, $aValid);
541
    }
542
}
543