Order::onAfterWrite()   F
last analyzed

Complexity

Conditions 27
Paths 18433

Size

Total Lines 191
Code Lines 123

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 123
nc 18433
nop 0
dl 0
loc 191
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace AntonyThorpe\SilverShopUnleashed\Extension;
4
5
use DateTime;
6
use SilverStripe\Security\Member;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Security\Member was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use SilverStripe\ORM\FieldType\DBDatetime;
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\FieldType\DBDatetime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use SilverStripe\ORM\DataExtension;
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\DataExtension was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use SilverStripe\ORM\DataObject;
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\DataObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use SilverShop\Extension\ShopConfigExtension;
0 ignored issues
show
Bug introduced by
The type SilverShop\Extension\ShopConfigExtension was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use SilverShop\Model\Address;
0 ignored issues
show
Bug introduced by
The type SilverShop\Model\Address was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use AntonyThorpe\SilverShopUnleashed\UnleashedAPI;
13
use AntonyThorpe\SilverShopUnleashed\Defaults;
14
use AntonyThorpe\SilverShopUnleashed\Utils;
15
16
class Order extends DataExtension
17
{
18
    /**
19
     * Record when an order is sent to Unleashed
20
     * @config
21
     */
22
    private static array $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
23
        'OrderSentToUnleashed' => 'Datetime'
24
    ];
25
26
    /**
27
     * Apply Guid if absent
28
     */
29
    public function onBeforeWrite(): void
30
    {
31
        parent::onBeforeWrite();
32
        if (!$this->getOwner()->getField("Guid")) {
33
            $this->getOwner()->Guid = Utils::createGuid();
34
        }
35
    }
36
37
    /**
38
     * Return the Address Name
39
     */
40
    public function getAddressName(array|Address $address): string
41
    {
42
        if (is_array($address)) {
0 ignored issues
show
introduced by
The condition is_array($address) is always true.
Loading history...
43
            $address_name = $address['StreetAddress'];
44
            if ($address['StreetAddress2']) {
45
                $address_name .= ' ' . $address['StreetAddress2'];
46
            }
47
            $address_name .= ' ' . $address['City'];
48
        } else {
49
            $address_name = $address->Address;
50
            if ($address->AddressLine2) {
51
                $address_name .= ' ' . $address->AddressLine2;
52
            }
53
            $address_name .= ' ' . $address->City;
54
        }
55
        return $address_name;
56
    }
57
58
    /**
59
     * Match the order's shipping address to items returned from Unleashed
60
     */
61
    public function matchCustomerAddress(array $items, array|Address $shipping_address): bool
62
    {
63
        // Obtain the delivery address
64
        $address = $items[0]['Addresses'][0];
65
        if ($address['AddressType'] != "Physical" && isset($items[0]['Addresses'][1])) {
66
            $address = $items[0]['Addresses'][1];
67
        }
68
        return strtoupper($this->getAddressName($shipping_address)) === strtoupper($this->getAddressName($address));
69
    }
70
71
    /**
72
     * add the address components to the body array
73
     * $type is either Postal or Physical
74
     */
75
    public function setBodyAddress(array $body, DataObject $order, string $type): array
76
    {
77
        $countries = (array) ShopConfigExtension::config()->iso_3166_country_codes;
78
79
        if ($type === 'Postal') {
80
            $address = $order->BillingAddress();
81
            $body['Addresses'][] = [
82
                'AddressName' => $this->getAddressName($address),
83
                'AddressType' => $type,
84
                'City' => $address->City,
85
                'Country' => $countries[$address->Country],
86
                'PostalCode' => $address->PostalCode,
87
                'Region' => $address->State,
88
                'StreetAddress' => $address->Address,
89
                'StreetAddress2' => $address->AddressLine2
90
            ];
91
        }
92
93
        if ($type === 'Physical') {
94
            $address = $order->ShippingAddress();
95
            $body['DeliveryCity'] = $address->City;
96
            $body['DeliveryCountry'] = $countries[$address->Country];
97
            $body['DeliveryPostCode'] = $address->PostalCode;
98
            $body['DeliveryRegion'] = $address->State;
99
            $body['DeliveryStreetAddress'] = $address->Address;
100
            $body['DeliveryStreetAddress2'] = $address->AddressLine2;
101
102
            $body['Addresses'][] = [
103
                'AddressName' => $this->getAddressName($address),
104
                'AddressType' => 'Physical',
105
                'City' => $address->City,
106
                'Country' => $countries[$address->Country],
107
                'PostalCode' => $address->PostalCode,
108
                'Region' => $address->State,
109
                'StreetAddress' => $address->Address,
110
                'StreetAddress2' => $address->AddressLine2
111
            ];
112
113
            $body['Addresses'][] = [
114
                'AddressName' => $this->getAddressName($address),
115
                'AddressType' => 'Shipping',
116
                'City' => $address->City,
117
                'Country' => $countries[$address->Country],
118
                'PostalCode' => $address->PostalCode,
119
                'Region' => $address->State,
120
                'StreetAddress' => $address->Address,
121
                'StreetAddress2' => $address->AddressLine2
122
            ];
123
        }
124
125
        return $body;
126
    }
127
128
    /**
129
     * Add the currency code to the body array
130
     */
131
    public function setBodyCurrencyCode(array $body, DataObject $order): array
132
    {
133
        $body['Currency']['CurrencyCode'] = $order->Currency();
134
        return $body;
135
    }
136
137
    /**
138
     * Add the Customer Code/Name (use Company field of BillingAddress to allow for B2B eCommerce sites)
139
     */
140
    public function setBodyCustomerCodeAndName(array $body, DataObject $order): array
141
    {
142
        $billing_address = $order->BillingAddress();
143
        if ($billing_address->Company) {
144
            // use Organisation name
145
            $body['CustomerCode'] = $billing_address->Company;
146
            $body['CustomerName'] = $billing_address->Company;
147
        } else {
148
            // use Contact full name instead
149
            $body['CustomerCode'] = $order->getName();
150
            $body['CustomerName'] = $order->getName();
151
        }
152
        return $body;
153
    }
154
155
    /**
156
     * Set Delivery Method and Delivery Name
157
     * Allow for the SilverShop Shipping module
158
     */
159
    public function setBodyDeliveryMethodAndDeliveryName(array $body, DataObject $order, string $shipping_modifier_class_name): array
160
    {
161
        $shipping_modifier = $order->getModifier($shipping_modifier_class_name);
162
        if (!empty($shipping_modifier)) {
163
            $body['DeliveryMethod'] = $shipping_modifier::config()->product_code;
164
            $body['DeliveryName'] = $shipping_modifier::config()->product_code;
165
        }
166
        return $body;
167
    }
168
169
    /**
170
     * Set Sales Order Lines
171
     */
172
    public function setBodySalesOrderLines(array $body, DataObject $order, string $tax_modifier_class_name, int $rounding_precision): array
173
    {
174
        $line_number = 0;
175
176
        // Sales Order Lines
177
        foreach ($order->Items()->getIterator() as $item) {
178
            // Definitions
179
            $product = $item->Product();
180
            $line_number += 1;
181
            $sales_order_line = [
182
                'DiscountRate' => 0,
183
                'Guid' => $item->Guid,
184
                'LineNumber' => $line_number,
185
                'LineType' => null,
186
                'LineTotal' => round(floatval($item->Total()), $rounding_precision),
187
                'OrderQuantity' => $item->Quantity,
188
                'Product' => [
189
                    'Guid' => $product->Guid
190
                ],
191
                'UnitPrice' => round(floatval($product->getPrice()), $rounding_precision)
192
            ];
193
            if ($tax_modifier_class_name !== '' && $tax_modifier_class_name !== '0') {
194
                $tax_calculator = new $tax_modifier_class_name;
195
                $sales_order_line['LineTax'] = round(
196
                    $tax_calculator->value($item->Total()),
197
                    $rounding_precision
198
                );
199
                $sales_order_line['LineTaxCode'] = $body['Tax']['TaxCode'];
200
            }
201
            $body['SalesOrderLines'][] = $sales_order_line;
202
        }
203
204
        // Add Modifiers that have a product_code
205
        foreach ($order->Modifiers()->sort('Sort')->getIterator() as $modifier) {
206
            $line_total = round(floatval($modifier->Amount), $rounding_precision);
207
208
            if ($modifier::config()->product_code &&
209
                $modifier->Type !== 'Ignored' &&
210
                !empty($line_total)
211
            ) {
212
                $line_number += 1;
213
                $sales_order_line = [
214
                    'DiscountRate' => 0,
215
                    'Guid' => $modifier->Guid,
216
                    'LineNumber' => $line_number,
217
                    'LineTotal' => $line_total,
218
                    'LineType' => null,
219
                    'OrderQuantity' => 1,
220
                    'Product' => [
221
                        'ProductCode' => $modifier::config()->product_code,
222
                    ],
223
                    'UnitPrice' => round(floatval($modifier->Amount), $rounding_precision)
224
                ];
225
                if ($tax_modifier_class_name !== '' && $tax_modifier_class_name !== '0') {
226
                    $tax_calculator = new $tax_modifier_class_name;
227
                    $sales_order_line['LineTax'] = round(
228
                        $tax_calculator->value($modifier->Amount),
229
                        $rounding_precision
230
                    );
231
                    $sales_order_line['LineTaxCode'] = $body['Tax']['TaxCode'];
232
                }
233
                $body['SalesOrderLines'][] = $sales_order_line;
234
            }
235
        }
236
        return $body;
237
    }
238
239
    /**
240
     * Set the Tax Codes
241
     */
242
    public function setBodyTaxCode(array $body, DataObject $order, string $tax_modifier_class_name): array
243
    {
244
        if ($tax_modifier_class_name !== '' && $tax_modifier_class_name !== '0') {
245
            $tax_modifier = $order->getModifier($tax_modifier_class_name);
246
            if (!empty($tax_modifier)) {
247
                $body['Taxable'] = true;
248
                $body['Tax']['TaxCode'] = $tax_modifier::config()->tax_code;
249
            }
250
        }
251
        return $body;
252
    }
253
254
255
    /**
256
     * Calculate the SubTotal and TaxTotal
257
     */
258
    public function setBodySubTotalAndTax(array $body, DataObject $order, string $tax_modifier_class_name, int $rounding_precision): array
259
    {
260
        if ($tax_modifier_class_name !== '' && $tax_modifier_class_name !== '0') {
261
            $tax_modifier = $order->getModifier($tax_modifier_class_name);
262
263
            // Calculate the Tax and Sub Total, which excludes Tax
264
            if (!empty($tax_modifier)) {
265
                $sub_total = 0;
266
                $tax_total = 0;
267
                foreach ($body['SalesOrderLines'] as $item) {
268
                    $sub_total = bcadd(
269
                        (string) $sub_total,
270
                        (string) $item['LineTotal'],
271
                        $rounding_precision
272
                    );
273
                    $tax_total = bcadd(
274
                        (string) $tax_total,
275
                        (string) $item['LineTax'],
276
                        $rounding_precision
277
                    );
278
                }
279
                $body['TaxTotal'] = $tax_total;
280
                $body['SubTotal'] = $sub_total;
281
282
                $rounding = round(floatval($order->Total() - $tax_total - $sub_total), $rounding_precision);
283
                // if there is some rounding, adjust the Tax on the first sales order line
284
                // and adjust the Tax Total by the same amount
285
                if (!empty($rounding)) {
286
                    $body['SalesOrderLines'][0]['LineTax'] = round($body['SalesOrderLines'][0]['LineTax'] + $rounding, $rounding_precision);
287
                    $body['TaxTotal'] = round($body['TaxTotal'] + $rounding, $rounding_precision);
288
                }
289
            }
290
        } else {
291
            $body['SubTotal'] = round(floatval($order->Total()), $rounding_precision);
292
        }
293
        return $body;
294
    }
295
296
    /**
297
     * Send a sales order to Unleashed upon paid status
298
     * May need to create the Customer first
299
     */
300
    public function onAfterWrite(): void
301
    {
302
        parent::onAfterWrite();
303
        $config = $this->getOwner()->config();
304
        $defaults = Defaults::config();
305
306
        if ($defaults->get('send_sales_orders_to_unleashed')
307
            && $this->getOwner()->Status == 'Paid'
308
            && !$this->getOwner()->OrderSentToUnleashed) {
309
            // Definitions
310
            $order = $this->getOwner();
311
            $member = $order->Member();
312
            $date_paid = new DateTime($order->Paid);
313
            $date_placed = new DateTime($order->Placed);
314
            $body = [
315
                'Addresses' => [],
316
                'Currency' => [],
317
                'Customer' => [],
318
                'DiscountRate' => 0,
319
                'Guid' => $order->Guid,
320
                'OrderDate' => $date_placed->format('Y-m-d\TH:i:s'),
321
                'OrderNumber' => $order->Reference,
322
                'OrderStatus' => $defaults->get('order_status'),
323
                'PaymentDueDate' => $date_paid->format('Y-m-d\TH:i:s'),
324
                'PaymentTerm' => $defaults->get('payment_term'),
325
                'PrintPackingSlipInsteadOfInvoice' => $defaults->get('print_packingslip_instead_of_invoice'),
326
                'ReceivedDate' => $date_placed->format('Y-m-d\TH:i:s'),
327
                'SalesOrderLines' => [],
328
                'SellPriceTier' => ShopConfigExtension::current()->CustomerGroup()->Title,
329
                'Taxable' => false,
330
                'Tax'  => [],
331
                'Total' => round(floatval($order->Total()), $config->get('rounding_precision')),
332
            ];
333
334
            $body = $this->setBodyAddress($body, $order, 'Postal');
335
            $body = $this->setBodyAddress($body, $order, 'Physical');
336
            $body = $this->setBodyCurrencyCode($body, $order);
337
            $body = $this->setBodyCustomerCodeAndName($body, $order);
338
            $body = $this->setBodyDeliveryMethodAndDeliveryName($body, $order, $defaults->get('shipping_modifier_class_name'));
339
            $body = $this->setBodyTaxCode($body, $order, $defaults->get('tax_modifier_class_name'));
340
            $body = $this->setBodySalesOrderLines($body, $order, $defaults->get('tax_modifier_class_name'), $config->get('rounding_precision'));
341
            $body = $this->setBodySubTotalAndTax($body, $order, $defaults->get('tax_modifier_class_name'), $config->get('rounding_precision'));
342
343
            // Add optional defaults
344
            if ($defaults->get('created_by')) {
345
                $body['CreatedBy'] = $defaults->get('created_by');
346
            }
347
348
            if ($defaults->get('customer_type')) {
349
                $body['CustomerType'] = $defaults->get('customer_type');
350
            }
351
352
            if ($defaults->get('sales_order_group')) {
353
                $body['SalesOrderGroup'] = $defaults->get('sales_order_group');
354
            }
355
356
            if ($defaults->get('source_id')) {
357
                $body['SourceId'] = $defaults->get('source_id');
358
            }
359
360
            // add phone number if available
361
            if ($order->BillingAddress()->Phone) {
362
                $body['PhoneNumber'] = $order->BillingAddress()->Phone;
363
            }
364
365
            // add required date
366
            $date_required = new DateTime($order->Paid);
367
            if ($defaults->get('expected_days_to_deliver')) {
368
                $date_required->modify('+' . $defaults->get('expected_days_to_deliver') . 'day');
369
            }
370
            $body['RequiredDate'] = $date_required->format('Y-m-d\TH:i:s');
371
372
            if ($order->Notes) {
373
                $body['Comments'] = $order->Notes;
374
            }
375
376
            // Create Member for Guests
377
            if (!$member->exists()) {
378
                $member = Member::create();
379
                $member->FirstName = $order->FirstName;
380
                $member->Surname = $order->Surname;
381
                $member->Email = $order->getLatestEmail();
382
            }
383
384
            // See if New Customer/Guest has previously purchased
385
            if (!$member->Guid) {
386
                $response = UnleashedAPI::sendCall(
387
                    'GET',
388
                    'https://api.unleashedsoftware.com/Customers?contactEmail=' .  $member->Email
389
                );
390
391
                if ($response->getStatusCode() == 200) {
392
                    $contents = (array) json_decode((string) $response->getBody(), true);
393
                    $items = $contents['Items'];
394
                    if ($items && $items[0]) {
395
                        // Email address exists
396
                        $member->Guid = $items[0]['Guid'];
397
                    } else {
398
                        // A Customer is not returned, we have a unique email address.
399
                        // Check to see if the Customer Code exists (note that the Customer Code cannot be doubled up)
400
                        $response = UnleashedAPI::sendCall(
401
                            'GET',
402
                            'https://api.unleashedsoftware.com/Customers?customerCode=' . $body['CustomerCode']
403
                        );
404
405
                        if ($response->getStatusCode() == 200) {
406
                            $contents = json_decode((string) $response->getBody()->getContents(), true);
407
                            $items = $contents['Items'];
408
                            if ($items && $items[0]) {
409
                                // A Customer Code already exists (and the email address is unique).
410
                                // If the address is the same then this is the Customer
411
                                if ($this->matchCustomerAddress($items, $order->ShippingAddress())) {
412
                                    $member->Guid = $items[0]['Guid'];
413
414
                                    //Note the existing email address in the Comment
415
                                    //PUT Customer is not available in Unleashed
416
                                    if ($body['Comments']) {
417
                                        $body['Comments'] .= '.  ';
418
                                    }
419
                                    $body['Comments'] .= _t(
0 ignored issues
show
Bug introduced by
The function _t was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

419
                                    $body['Comments'] .= /** @scrutinizer ignore-call */ _t(
Loading history...
420
                                        'UnleashedAPI.addEmailToCustomerComment',
421
                                        'Add email to Customer: {email_address}',
422
                                        '',
423
                                        ['email_address' => $member->Email]
424
                                    );
425
                                } else {
426
                                    // The Customer Code already exists, we have a unique email address, but
427
                                    // the delivery address is new.
428
                                    // Therefore, we need to create a new Customer with a unique Customer Code.
429
                                    $body['CustomerCode'] .= random_int(10000000, 99999999);
430
                                }
431
                            }
432
                        }
433
                    }
434
                }
435
            }
436
437
            if (!$member->Guid) {
438
                // The Customer Code does not exists in Unleashed and the email address is unique
439
                // therefore create in Unleashed
440
                $member->Guid = Utils::createGuid();
441
                $body_member = [
442
                    'Addresses' => $body['Addresses'],
443
                    'ContactFirstName' => $member->FirstName,
444
                    'ContactLastName' => $member->Surname,
445
                    'CreatedBy' => $body['CreatedBy'],
446
                    'Currency' => $body['Currency'],
447
                    'CustomerCode' => $body['CustomerCode'],
448
                    'CustomerName' => $body['CustomerName'],
449
                    'CustomerType' => $body['CustomerType'],
450
                    'Email' => $member->Email,
451
                    'Guid' => $member->Guid,
452
                    'PaymentTerm' => $body['PaymentTerm'],
453
                    'PhoneNumber' => $body['PhoneNumber'],
454
                    'PrintPackingSlipInsteadOfInvoice' => $body['PrintPackingSlipInsteadOfInvoice'],
455
                    'SellPriceTier' => $body['SellPriceTier'],
456
                    'SourceId' => $body['SourceId'],
457
                    'Taxable' => $body['Taxable'],
458
                    'TaxCode' => $body['Tax']['TaxCode']
459
                ];
460
461
                foreach ($body_member['Addresses'] as $index => $value) {
462
                    $body_member['Addresses'][$index]['IsDefault'] = true;
463
                }
464
465
                $response = UnleashedAPI::sendCall(
466
                    'POST',
467
                    'https://api.unleashedsoftware.com/Customers/' . $member->Guid,
468
                    ['json' => $body_member ]
469
                );
470
471
                if ($response->getReasonPhrase() == 'Created' && $order->Member()->exists()) {
472
                    $member->write();
473
                }
474
            }
475
476
            // Prepare Sales Order data
477
            // Skip if previous calls to Customer have failed and the Guid has not been set
478
            if ($member->Guid) {
479
                $body['Customer']['Guid'] = $member->Guid;
480
481
                $this->getOwner()->extend('updateUnleashedSalesOrder', $body);
482
483
                $response = UnleashedAPI::sendCall(
484
                    'POST',
485
                    'https://api.unleashedsoftware.com/SalesOrders/' . $order->Guid,
486
                    ['json' => $body]
487
                );
488
                if ($response->getReasonPhrase() == 'Created') {
489
                    $this->getOwner()->OrderSentToUnleashed = DBDatetime::now()->Rfc2822();
490
                    $this->getOwner()->write();
491
                }
492
            }
493
        }
494
    }
495
}
496