Completed
Push — master ( 16cb5f...9fa3e0 )
by Iurii
01:11
created

Shippo::setShippingMethodsCheckout()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 7
nc 4
nop 2
1
<?php
2
3
/**
4
 * @package Shippo
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\shippo\models;
11
12
use gplcart\core\Model;
13
use gplcart\core\models\Language as LanguageModel,
14
    gplcart\core\models\User as UserModel,
15
    gplcart\core\models\Price as PriceModel,
16
    gplcart\core\models\State as StateModel,
17
    gplcart\core\models\Currency as CurrencyModel,
18
    gplcart\core\models\Address as AddressModel,
19
    gplcart\core\models\Store as StoreModel,
20
    gplcart\core\models\Shipping as ShippingModel;
21
use gplcart\core\helpers\Session as SessionHelper,
22
    gplcart\core\helpers\Convertor as ConvertorHelper;
23
use gplcart\modules\shippo\models\Api as ShippoApiModel;
24
25
/**
26
 * Manages basic behaviors and data related to Shippo module
27
 */
28
class Shippo extends Model
29
{
30
31
    /**
32
     * An array of Shippo module settings
33
     * @var array
34
     */
35
    protected $settings = array();
36
37
    /**
38
     * Api model instance
39
     * @var \gplcart\modules\shippo\models\Api $api
40
     */
41
    protected $api;
42
43
    /**
44
     * Language model instance
45
     * @var \gplcart\core\models\Language $language
46
     */
47
    protected $language;
48
49
    /**
50
     * User model class instance
51
     * @var \gplcart\core\models\User $user
52
     */
53
    protected $user;
54
55
    /**
56
     * Price model class instance
57
     * @var \gplcart\core\models\Price $price
58
     */
59
    protected $price;
60
61
    /**
62
     * Shipping model class instance
63
     * @var \gplcart\core\models\Shipping $shipping
64
     */
65
    protected $shipping;
66
67
    /**
68
     * State model class instance
69
     * @var \gplcart\core\models\State $state
70
     */
71
    protected $state;
72
73
    /**
74
     * Currency model class instance
75
     * @var \gplcart\core\models\Currency $currency
76
     */
77
    protected $currency;
78
79
    /**
80
     * Address model class instance
81
     * @var \gplcart\core\models\Address $address
82
     */
83
    protected $address;
84
85
    /**
86
     * Store model instance
87
     * @var \gplcart\core\models\Store $store
88
     */
89
    protected $store;
90
91
    /**
92
     * Convertor class instance
93
     * @var \gplcart\core\helpers\Convertor $convertor
94
     */
95
    protected $convertor;
96
97
    /**
98
     * Session class instance
99
     * @var \gplcart\core\helpers\Session $session
100
     */
101
    protected $session;
102
103
    /**
104
     * @param ShippoApiModel $api
105
     * @param LanguageModel $language
106
     * @param UserModel $user
107
     * @param PriceModel $price
108
     * @param CurrencyModel $currency
109
     * @param AddressModel $address
110
     * @param StoreModel $store
111
     * @param StateModel $state
112
     * @param ShippingModel $shipping
113
     * @param SessionHelper $session
114
     * @param ConvertorHelper $convertor
115
     */
116
    public function __construct(ShippoApiModel $api, LanguageModel $language,
117
            UserModel $user, PriceModel $price, CurrencyModel $currency,
118
            AddressModel $address, StoreModel $store, StateModel $state,
119
            ShippingModel $shipping, SessionHelper $session,
120
            ConvertorHelper $convertor)
121
    {
122
        parent::__construct();
123
124
        $this->api = $api;
125
        $this->user = $user;
126
        $this->price = $price;
127
        $this->state = $state;
128
        $this->store = $store;
129
        $this->session = $session;
130
        $this->address = $address;
131
        $this->currency = $currency;
132
        $this->language = $language;
133
        $this->shipping = $shipping;
134
        $this->convertor = $convertor;
135
136
        $this->settings = $this->config->module('shippo');
137
    }
138
139
    /**
140
     * Returns an array of carrier names keyed by id
141
     * @return array
142
     */
143
    public function getCarrierNames()
144
    {
145
        return require __DIR__ . '/../config/carriers.php';
146
    }
147
148
    /**
149
     * Returns an array of service names keyed by id
150
     * @return array
151
     */
152
    public function getServiceNames()
153
    {
154
        return require __DIR__ . '/../config/services.php';
155
    }
156
157
    /**
158
     * Calculates shipping rates and sets available shipping methods on checkout page
159
     * @param array $data
160
     */
161
    public function calculate(array &$data)
162
    {
163
        if ($data['request_shipping_methods']) {
164
            $address = $this->getSourceAddress($data);
165
            $rates = $this->getRates($address, $data['cart'], $data['order']);
166
            $this->setShippingMethodsCheckout($data, $rates);
167
        }
168
    }
169
170
    /**
171
     * Validates order shipping rates before an order is created
172
     * @param array $order
173
     * @param array $options
174
     * @param array $result
175
     */
176
    public function validate(array &$order, $options, array &$result)
177
    {
178
        $method = $this->shipping->get($order['shipping']);
179
180
        if (empty($method['module']) || $method['module'] !== 'shippo') {
181
            return null;
182
        }
183
184
        $error_result = array(
185
            'severity' => 'danger',
186
            'redirect' => '', // Stay on the same page
187
            'message' => $this->language->text('Please recalculate shipping rates')
188
        );
189
190
        // Forbid further processing if shipping component has not been set
191
        if (!isset($order['data']['components']['shipping']['price']) && empty($options['admin'])) {
192
            $result = $error_result;
193
            return null;
194
        }
195
196
        $rates = $this->getRates($order['shipping_address'], $order['cart'], $order);
197
198
        $this->setShippingMethod($method, $rates, $order['currency']);
199
200
        // Forbid further processing and redirect back if shipping rates don't match
201
        // Validate only "normal" submits.
202
        // In admin mode shipping prices can be adjusted by administrator
203
        if (empty($options['admin']) && isset($method['price']) && $method['price'] != $order['data']['components']['shipping']['price']) {
204
            $result = $error_result;
205
            return null;
206
        }
207
208
        // Save Shippo request in the order data to get later labels etc.
209
        $order['data']['shippo'] = $method['data'];
210
    }
211
212
    /**
213
     * Returns an array of cached rates
214
     * @param array|integer $address
215
     * @param array $cart
216
     * @param array $order
217
     * @return array
218
     */
219
    public function getRates($address, array $cart, array $order)
220
    {
221
        if (!is_array($address)) {
222
            $address = $this->address->get($address);
223
        }
224
225
        $to_address = $this->getShippoAddress($address);
226
227
        if (empty($to_address)) {
228
            return array();
229
        }
230
231
        $session_limit = 10;
232
        $session_rates = $this->session->get('shippo', array());
233
234
        if (count($session_rates) > $session_limit) {
235
            $this->session->delete('shippo');
236
        }
237
238
        $cache_id = $this->getCacheKey($this->settings['sender'], $to_address);
239
240
        if (!empty($session_rates[$cache_id])) {
241
            return $session_rates[$cache_id];
242
        }
243
244
        if ($this->api->isValidAddress($to_address) !== true) {
245
            return $this->getDefaultRates();
246
        }
247
248
        $parcel = $this->getParcel($cart, $order);
249
        $response = $this->api->getRates($this->settings['sender'], $to_address, $parcel);
250
251
        if (empty($response)) {
252
            return $this->getDefaultRates();
253
        }
254
255
        $session_rates[$cache_id] = $response;
256
        $this->session->set('shippo', $session_rates);
257
258
        return $response;
259
    }
260
261
    /**
262
     * Returns a unique cache key for a combination of recipient and sender addresses
263
     * @param array $from
264
     * @param array $to
265
     * @return string
266
     */
267
    protected function getCacheKey(array $from, $to)
268
    {
269
        ksort($to);
270
        ksort($from);
271
272
        return md5(json_encode(array($from, $to)));
273
    }
274
275
    /**
276
     * Sets shipping methods available for the order shipping address
277
     * @param array $data
278
     * @param array $rates
279
     */
280
    protected function setShippingMethodsCheckout(array &$data, array $rates)
281
    {
282
        foreach ($data['shipping_methods'] as &$method) {
283
284
            if (empty($method['module']) || $method['module'] !== 'shippo') {
285
                continue;
286
            }
287
288
            if (!$this->setShippingMethod($method, $rates, $data['order']['currency'])) {
289
                unset($data['shipping_methods'][$method['id']]);
290
            }
291
        }
292
293
        // Show cheapest items first
294
        gplcart_array_sort($data['shipping_methods'], 'price');
295
    }
296
297
    /**
298
     * Adjust price and label for the given shipping method
299
     * @param array $method
300
     * @param array $rates
301
     * @param string $currency
302
     * @return boolean
303
     */
304
    protected function setShippingMethod(array &$method, array $rates, $currency)
305
    {
306
        $service_id = $this->getShippoServiceId($method['id']);
307
308
        if (empty($rates[$service_id])) {
309
            return false;
310
        }
311
312
        $converted = $this->currency->convert($rates[$service_id]['amount_local'], $rates[$service_id]['currency_local'], $currency);
313
        $price = $this->price->format($converted, $currency, false, true);
314
315
        $method['title'] .= " - $price";
316
317
        if (isset($rates[$service_id]['days'])) {
318
            $method['description'] = $this->language->text('Estimated delivery time: @num day(s)', array('@num' => $rates[$service_id]['days']));
319
        }
320
321
        $method['data'] = $rates[$service_id];
322
        $method['price'] = $this->price->amount($converted, $currency);
323
324
        return true;
325
    }
326
327
    /**
328
     * Returns the default rates if unabled to calculate via Shippo API
329
     * @return array
330
     */
331
    protected function getDefaultRates()
332
    {
333
        if (empty($this->settings['default']['method'])) {
334
            return array();
335
        }
336
337
        $service_id = $this->getShippoServiceId($this->settings['default']['method']);
338
339
        return array(
340
            $service_id => array(
341
                'currency' => 'USD',
342
                'amount' => $this->settings['default']['price']
343
            )
344
        );
345
    }
346
347
    /**
348
     * Converts the system method ID into Shippo's service ID
349
     * @param string $system_method_id
350
     * @return string
351
     */
352
    protected function getShippoServiceId($system_method_id)
353
    {
354
        return str_replace('shippo_', '', $system_method_id);
355
    }
356
357
    /**
358
     * Converts an address into Shippo's format
359
     * @param array $data
360
     * @return array
361
     */
362
    protected function getShippoAddress(array $data)
363
    {
364
        if (empty($data)) {
365
            return array();
366
        }
367
368
        $name = array();
369
        if (isset($data['first_name'])) {
370
            $name[] = $data['first_name'];
371
        }
372
        if (isset($data['middle_name'])) {
373
            $name[] = $data['middle_name'];
374
        }
375
        if (isset($data['last_name'])) {
376
            $name[] = $data['last_name'];
377
        }
378
379
        $city = '';
380
        if (isset($data['city_name'])) {
381
            $city = $data['city_name'];
382
        } else if (isset($data['city_id']) && !is_numeric($data['city_id'])) {
383
            $city = $data['city_id'];
384
        }
385
386
        $state = '';
387
        if (isset($data['state_name'])) {
388
            $state = $data['state_name'];
389
        } else if (isset($data['state_id'])) {
390
            $state_data = $this->state->get($data['state_id']);
391
            $state = isset($state_data['name']) ? $state_data['name'] : '';
392
        }
393
394
        if (!empty($data['country'])) {
395
            $country = $data['country'];
396
        } else {
397
            $store = $this->store->getDefault(true);
398
            $country = $store['data']['country'];
399
        }
400
401
        return array(
402
            'city' => $city,
403
            'state' => $state,
404
            'country' => $country,
405
            'name' => implode(' ', $name),
406
            'email' => $this->user->getSession('email'),
407
            'phone' => isset($data['phone']) ? $data['phone'] : '',
408
            'zip' => isset($data['postcode']) ? $data['postcode'] : '',
409
            'company' => isset($data['company']) ? $data['company'] : '',
410
            'street1' => isset($data['address_1']) ? $data['address_1'] : '',
411
            'street2' => isset($data['address_2']) ? $data['address_2'] : '',
412
        );
413
    }
414
415
    /**
416
     * Returns an array of parcel data in Shippo's format
417
     * @param array $cart
418
     * @param array $order
419
     * @return array
420
     */
421
    protected function getParcel(array $cart, array $order)
422
    {
423
        $dimensions = $this->getDimensions($cart, $order);
424
        $dimensions += $this->settings['default'];
425
426
        return array(
427
            'width' => $dimensions['width'],
428
            'height' => $dimensions['height'],
429
            'length' => $dimensions['length'],
430
            'weight' => $dimensions['weight'],
431
            'mass_unit' => $order['weight_unit'],
432
            'distance_unit' => $order['size_unit']
433
        );
434
    }
435
436
    /**
437
     * Returns total dimensions of all products in the order
438
     * @param array $cart
439
     * @param array $order
440
     * @return array
441
     */
442
    protected function getDimensions(array $cart, array $order)
443
    {
444
        $width = $height = $length = $weight = array();
445
446
        foreach ($cart['items'] as $item) {
447
            $product = $item['product'];
448
            $width[] = (float) $this->convertor->convert($product['width'], $product['size_unit'], $order['size_unit']);
449
            $height[] = (float) $this->convertor->convert($product['height'], $product['size_unit'], $order['size_unit']);
450
            $length[] = (float) $this->convertor->convert($product['length'], $product['size_unit'], $order['size_unit']);
451
            $weight[] = (float) $this->convertor->convert($product['weight'], $product['weight_unit'], $order['weight_unit']);
452
        }
453
454
        $result = array(
455
            'height' => max($height),
456
            'length' => max($length),
457
            'width' => array_sum($width),
458
            'weight' => array_sum($weight),
459
        );
460
461
        if (count(array_filter($result)) != count($result)) {
462
            return array();
463
        }
464
465
        return $result;
466
    }
467
468
    /**
469
     * Sets a source shipping address
470
     * @param array $data
471
     * @return array
472
     */
473
    protected function getSourceAddress(array $data)
474
    {
475
        if (!empty($data['order']['shipping_address'])) {
476
            $address_id = $data['order']['shipping_address'];
477
            return $this->address->get($address_id);
478
        }
479
480
        if (!empty($data['show_shipping_address_form']) && !empty($data['address']['shipping'])) {
481
            return $data['address']['shipping'];
482
        }
483
484
        return array();
485
    }
486
487
}
488