Passed
Push — main ( ec9792...45a540 )
by Daniel
02:11
created

ElectornicInvoiceWrite::setProduceMiddleXml()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 49
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 43
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 49
rs 8.6097
1
<?php
2
3
/**
4
 *
5
 * The MIT License (MIT)
6
 *
7
 * Copyright (c) 2024 Daniel Popiniuc
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a copy
10
 * of this software and associated documentation files (the "Software"), to deal
11
 * in the Software without restriction, including without limitation the rights
12
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
 * copies of the Software, and to permit persons to whom the Software is
14
 * furnished to do so, subject to the following conditions:
15
 *
16
 * The above copyright notice and this permission notice shall be included in all
17
 * copies or substantial portions of the Software.
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
 * SOFTWARE.
26
 *
27
 */
28
29
namespace danielgp\efactura;
30
31
class ElectornicInvoiceWrite
32
{
33
34
    use TraitVersions;
35
36
    protected \XMLWriter $objXmlWriter;
37
38
    private function loadSettingsAndManageDefaults(array $arrayData, array $arrayFeatures): array
39
    {
40
        // if no DocumentNameSpaces seen take Default ones from local configuration
41
        $this->getSettingsFromFileIntoMemory($arrayFeatures['Comments']);
42
        $arrayDefaults = $this->getDefaultsIntoDataSet($arrayData, $arrayFeatures['SchemaLocation']);
43
        if ($arrayDefaults !== []) {
44
            $arrayData = array_merge($arrayData, $arrayDefaults['Root']);
45
            if (!array_key_exists('CustomizationID', $arrayData['Header']['CommonBasicComponents-2'])) {
46
                $arrayData['Header']['CommonBasicComponents-2']['CustomizationID'] = 'urn:cen.eu:en16931:2017'
47
                    . '#compliant#urn:efactura.mfinante.ro:CIUS-RO:' . $arrayDefaults['CIUS-RO'];
48
                $arrayData['Header']['CommonBasicComponents-2']['UBLVersionID']    = $arrayDefaults['UBL'];
49
            }
50
        }
51
        return $arrayData;
52
    }
53
54
    private function setCategorizedVerifications(array $arrayDataIn)
55
    {
56
        $strCategoryToReturn    = '';
57
        $key                    = implode('_', [$arrayDataIn['commentParentKey'], $arrayDataIn['tag']]);
58
        $arrayKeyMapping        = [
59
            'Lines_AllowanceCharge'        => 'ArrayElementsOrdered',
60
            'Delivery_DeliveryLocation_ID' => 'SingleElementWithAttribute',
61
        ];
62
        $arrayTagMapping        = [
63
            'EmbeddedDocumentBinaryObject' => 'SingleElementWithAttribute',
64
            'EndpointID'                   => 'SingleElementWithAttribute',
65
            'AdditionalItemProperty'       => 'MultipleElementsOrdered',
66
            'CommodityClassification'      => 'MultipleElementsOrdered',
67
            'PartyTaxScheme'               => 'MultipleElementsOrdered',
68
            'StandardItemIdentification'   => 'MultipleElementsOrdered',
69
            'TaxSubtotal'                  => 'MultipleElementsOrdered'
70
        ];
71
        $arrayCommentParrentKey = [
72
            'AccountingCustomerParty_PartyIdentification',
73
            'AccountingSupplierParty_PartyIdentification',
74
            'Lines_Item_SellersItemIdentification',
75
            'Lines_Item_StandardItemIdentification',
76
            'Lines_Item_CommodityClassification',
77
            'PayeeParty_PartyIdentification'
78
        ];
79
        if (array_key_exists($key, $arrayKeyMapping)) {
80
            $strCategoryToReturn = $arrayKeyMapping[$key];
81
        } elseif (array_key_exists($arrayDataIn['tag'], $arrayTagMapping)) {
82
            $strCategoryToReturn = $arrayTagMapping[$arrayDataIn['tag']];
83
        } elseif (in_array($arrayDataIn['commentParentKey'], $arrayCommentParrentKey)) {
84
            $strCategoryToReturn = 'SingleElementWithAttribute';
85
        } elseif ($arrayDataIn['matches'] !== []) {
86
            $strCategoryToReturn = 'SingleElementWithAttribute';
87
        } elseif (is_array($arrayDataIn['data'])) {
88
            $strCategoryToReturn = 'ElementsOrdered';
89
        } else {
90
            $strCategoryToReturn = 'SingleElementWithAttribute';
91
        }
92
        return $strCategoryToReturn;
93
    }
94
95
    private function setDocumentTag(array $arrayDocumentData): void
96
    {
97
        $this->objXmlWriter->startElement($arrayDocumentData['DocumentTagName']);
98
        foreach ($arrayDocumentData['DocumentNameSpaces'] as $key => $value) {
99
            if ($key === '') {
100
                $strValue = sprintf($value, $arrayDocumentData['DocumentTagName']);
101
                $this->objXmlWriter->writeAttributeNS(null, 'xmlns', null, $strValue);
102
            } else {
103
                $this->objXmlWriter->writeAttributeNS('xmlns', $key, null, $value);
104
            }
105
        }
106
        if (array_key_exists('SchemaLocation', $arrayDocumentData)) {
107
            $this->objXmlWriter->writeAttribute('xsi:schemaLocation', $arrayDocumentData['SchemaLocation']);
108
        }
109
    }
110
111
    private function setElementComment(string $strKey): void
112
    {
113
        if (array_key_exists($strKey, $this->arraySettings['Comments'])) {
114
            $elementComment = $this->arraySettings['Comments'][$strKey];
115
            if (is_array($elementComment)) {
116
                foreach ($elementComment as $value) {
117
                    $this->objXmlWriter->writeComment($value);
118
                }
119
            } else {
120
                $this->objXmlWriter->writeComment($elementComment);
121
            }
122
        }
123
    }
124
125
    private function setElementsOrdered(array $arrayInput): void
126
    {
127
        $this->setElementComment($arrayInput['commentParentKey']);
128
        $this->objXmlWriter->startElement('cac:' . $arrayInput['tag']);
129
        $this->setExtraElement($arrayInput, 'Start');
130
        $arrayCustomOrder = $this->arraySettings['CustomOrder'][$arrayInput['commentParentKey']];
131
        foreach ($arrayCustomOrder as $value) { // get the values in expected order
132
            if (array_key_exists($value, $arrayInput['data'])) { // because certain value are optional
133
                $key         = implode('_', [$arrayInput['commentParentKey'], $value]);
134
                $matches     = [];
135
                preg_match('/^.*(Amount|Quantity)$/', $value, $matches, PREG_OFFSET_CAPTURE);
136
                $strCategory = $this->setCategorizedVerifications([
137
                    'commentParentKey' => $arrayInput['commentParentKey'],
138
                    'data'             => $arrayInput['data'][$value],
139
                    'matches'          => $matches,
140
                    'tag'              => $value,
141
                ]);
142
                switch ($strCategory) {
143
                    case 'ArrayElementsOrdered':
144
                        foreach ($arrayInput['data'][$value] as $value2) {
145
                            $this->setElementsOrdered([
146
                                'commentParentKey' => $key,
147
                                'data'             => $value2,
148
                                'tag'              => $value,
149
                            ]);
150
                        }
151
                        break;
152
                    case 'ElementsOrdered':
153
                        $this->setElementsOrdered([
154
                            'commentParentKey' => $key,
155
                            'data'             => $arrayInput['data'][$value],
156
                            'tag'              => $value,
157
                        ]);
158
                        break;
159
                    case 'MultipleElementsOrdered':
160
                        $this->setMultipleElementsOrdered([
161
                            'commentParentKey' => $key,
162
                            'data'             => $arrayInput['data'][$value],
163
                            'tag'              => $value,
164
                        ]);
165
                        break;
166
                    case 'SingleElementWithAttribute':
167
                        $this->setSingleElementWithAttribute([
168
                            'commentParentKey' => $arrayInput['commentParentKey'],
169
                            'data'             => $arrayInput['data'][$value],
170
                            'tag'              => $value,
171
                        ]);
172
                        break;
173
                }
174
            }
175
        }
176
        $this->setExtraElement($arrayInput, 'End');
177
        $this->objXmlWriter->endElement(); // $key
178
    }
179
180
    private function setExtraElement(array $arrayInput, string $strType): void
181
    {
182
        if (in_array($arrayInput['tag'], ['AccountingCustomerParty', 'AccountingSupplierParty'])) {
183
            switch ($strType) {
184
                case 'End':
185
                    $this->objXmlWriter->endElement();
186
                    break;
187
                case 'Start':
188
                    $this->objXmlWriter->startElement('cac:Party');
189
                    break;
190
            }
191
        }
192
    }
193
194
    private function setHeaderCommonBasicComponents(array $arrayElementWithData): void
195
    {
196
        $arrayCustomOrdered = $this->arraySettings['CustomOrder']['Header_CBC'];
197
        foreach ($arrayCustomOrdered as $value) {
198
            if (array_key_exists($value, $arrayElementWithData)) {
199
                $this->setElementComment($value);
200
                $this->objXmlWriter->writeElement('cbc:' . $value, $arrayElementWithData[$value]);
201
            }
202
        }
203
    }
204
205
    private function setManageComment(string $strCommentParentKey, array $arrayIn): string
206
    {
207
        if (str_starts_with($strCommentParentKey, 'AllowanceCharge')) {
208
            $arrayCommentPieces = explode('_', $strCommentParentKey);
209
            // carefully manage a child to decide on comment tag
210
            $strChargeIndicator = $arrayIn['ChargeIndicator'];
211
            if (in_array($strChargeIndicator, ['0', '1'])) {
212
                $strChargeIndicator = [
213
                    '0' => 'false',
214
                    '1' => 'true',
215
                    ][$arrayIn['ChargeIndicator']];
216
            }
217
            array_splice($arrayCommentPieces, 0, 1, 'AllowanceCharge~ChargeIndicator'
218
                . ucfirst($strChargeIndicator));
219
            $strCommentParentKey = implode('_', $arrayCommentPieces);
220
        }
221
        return $strCommentParentKey;
222
    }
223
224
    private function setMultipleElementsOrdered(array $arrayData): void
225
    {
226
        foreach ($arrayData['data'] as $value) {
227
            $strCommentParentKey = $this->setManageComment($arrayData['commentParentKey'], $value);
228
            $this->setElementsOrdered([
229
                'commentParentKey' => $strCommentParentKey,
230
                'data'             => $value,
231
                'tag'              => $arrayData['tag'],
232
            ]);
233
        }
234
    }
235
236
    protected function setNumericValue(string $strTag, array $arrayDataIn): string|float
237
    {
238
        $sReturn      = $arrayDataIn['value'];
239
        $arrayRawTags = ['CreditedQuantity', 'EndpointID', 'InvoicedQuantity', 'ItemClassificationCode', 'PriceAmount'];
240
        if (is_numeric($arrayDataIn['value']) && !in_array($strTag, $arrayRawTags)) {
241
            $fmt = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL);
242
            $fmt->setAttribute(\NumberFormatter::GROUPING_USED, 0);
243
            $fmt->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 0);
244
            // if contains currencyID consider 2 decimals as minimum
245
            if (in_array('currencyID', array_keys($arrayDataIn))) {
246
                $fmt->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 2);
247
            }
248
            $fmt->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 2);
249
            $sReturn = $fmt->format($arrayDataIn['value']);
250
        }
251
        return $sReturn;
252
    }
253
254
    private function setPrepareXml(string $strFile, int $intIdent = 4): void
255
    {
256
        $this->objXmlWriter = new \XMLWriter();
257
        $this->objXmlWriter->openURI($strFile);
258
        $this->objXmlWriter->setIndent(true);
259
        $this->objXmlWriter->setIndentString(str_repeat(' ', $intIdent));
260
        $this->objXmlWriter->startDocument('1.0', 'UTF-8');
261
    }
262
263
    private function setProduceMiddleXml(array $arrayData): void
264
    {
265
        $arrayAggregates             = $arrayData['Header']['CommonAggregateComponents-2'];
266
        $arrayOptionalElementsHeader = [
267
            'InvoicePeriod'               => 'Single',
268
            'OrderReference'              => 'Single',
269
            'BillingReference'            => 'Single',
270
            'DespatchDocumentReference'   => 'Single',
271
            'ReceiptDocumentReference'    => 'Single',
272
            'OriginatorDocumentReference' => 'Single',
273
            'ContractDocumentReference'   => 'Single',
274
            'AdditionalDocumentReference' => 'Multiple',
275
            'ProjectReference'            => 'Single',
276
            'AccountingSupplierParty'     => 'SingleCompany',
277
            'AccountingCustomerParty'     => 'SingleCompany',
278
            'PayeeParty'                  => 'Single',
279
            'TaxRepresentativeParty'      => 'Single',
280
            'Delivery'                    => 'Single',
281
            'PaymentMeans'                => 'Multiple',
282
            'PaymentTerms'                => 'Single',
283
            'DocumentReference'           => 'Single',
284
            'AllowanceCharge'             => 'Multiple',
285
            'TaxTotal'                    => 'Multiple',
286
            'LegalMonetaryTotal'          => 'Single',
287
        ];
288
        foreach ($arrayOptionalElementsHeader as $key => $strLogicType) {
289
            if (array_key_exists($key, $arrayAggregates)) {
290
                switch ($strLogicType) {
291
                    case 'Multiple':
292
                        $this->setMultipleElementsOrdered([
293
                            'commentParentKey' => $key,
294
                            'data'             => $arrayAggregates[$key],
295
                            'tag'              => $key,
296
                        ]);
297
                        break;
298
                    case 'Single':
299
                        $this->setElementsOrdered([
300
                            'commentParentKey' => $key,
301
                            'data'             => $arrayAggregates[$key],
302
                            'tag'              => $key,
303
                        ]);
304
                        break;
305
                    case 'SingleCompany':
306
                        $this->setElementsOrdered([
307
                            'commentParentKey' => $key,
308
                            'data'             => $arrayAggregates[$key]['Party'],
309
                            'tag'              => $key,
310
                        ]);
311
                        break;
312
                }
313
            }
314
        }
315
    }
316
317
    private function setSingleComment(array $arrayInput): void
318
    {
319
        if (array_key_exists('commentParentKey', $arrayInput)) {
320
            $this->setElementComment(implode('_', [$arrayInput['commentParentKey'], $arrayInput['tag']]));
321
            if (str_ends_with($arrayInput['tag'], 'Quantity')) {
322
                $this->setElementComment(implode('_', [$arrayInput['commentParentKey'], $arrayInput['tag']
323
                    . 'UnitOfMeasure']));
324
            }
325
        }
326
    }
327
328
    private function setSingleElementWithAttribute(array $arrayInput): void
329
    {
330
        $this->setSingleComment($arrayInput);
331
        if (is_array($arrayInput['data']) && array_key_exists('value', $arrayInput['data'])) {
332
            $this->objXmlWriter->startElement('cbc:' . $arrayInput['tag']);
333
            foreach ($arrayInput['data'] as $key => $value) {
334
                if ($key !== 'value') { // if is not value, must be an attribute
335
                    $this->objXmlWriter->writeAttribute($key, $value);
336
                }
337
            }
338
            $this->objXmlWriter->writeRaw($this->setNumericValue($arrayInput['tag'], $arrayInput['data']));
339
            $this->objXmlWriter->endElement();
340
        } else {
341
            $this->objXmlWriter->writeElement('cbc:' . $arrayInput['tag'], $arrayInput['data']);
342
        }
343
    }
344
345
    public function writeElectronicInvoice(string $strFile, array $inData, array $arrayFeatures): void
346
    {
347
        $arrayData = $this->loadSettingsAndManageDefaults($inData, $arrayFeatures);
348
        if (!array_key_exists('Ident', $arrayFeatures)) {
349
            $arrayFeatures['Ident'] = 4;
350
        }
351
        $this->setPrepareXml($strFile, $arrayFeatures['Ident']);
352
        $this->setDocumentTag($arrayData);
353
        $this->setHeaderCommonBasicComponents($arrayData['Header']['CommonBasicComponents-2']);
354
        $this->setProduceMiddleXml($arrayData);
355
        // multiple Lines
356
        $this->setMultipleElementsOrdered([
357
            'commentParentKey' => 'Lines',
358
            'data'             => $arrayData['Lines'],
359
            'tag'              => $arrayData['DocumentTagName'] . 'Line',
360
        ]);
361
        $this->objXmlWriter->endElement(); // Invoice or CreditNote
362
        $this->objXmlWriter->flush();
363
    }
364
}
365