OrderExtension::onBeforeWrite()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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