PluginProbe
Packeta / 1.3.2
Packeta v1.3.2
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / src / Packetery / Module / Checkout.php

Checkout.php in Packeta 1.3.2, at src/Packetery/Module/Checkout.php

858 lines 24.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Packeta plugin class for checkout.
4 *
5 * @package Packetery
6 */
7
8 declare( strict_types=1 );
9
10 namespace Packetery\Module;
11
12 use Packetery\Core;
13 use Packetery\Module\Options\Provider;
14 use PacketeryLatte\Engine;
15 use PacketeryNette\Http\Request;
16
17 /**
18 * Class Checkout
19 *
20 * @package Packetery
21 */
22 class Checkout {
23
24 public const CARRIER_PREFIX = 'packetery_carrier_';
25 private const NONCE_ACTION = 'packetery_checkout';
26
27 const ATTR_POINT_ID = 'packetery_point_id';
28 const ATTR_POINT_NAME = 'packetery_point_name';
29 const ATTR_POINT_CITY = 'packetery_point_city';
30 const ATTR_POINT_ZIP = 'packetery_point_zip';
31 const ATTR_POINT_STREET = 'packetery_point_street';
32 const ATTR_POINT_PLACE = 'packetery_point_place'; // Business name of pickup point.
33 const ATTR_CARRIER_ID = 'packetery_carrier_id';
34 const ATTR_POINT_URL = 'packetery_point_url';
35
36 const BUTTON_RENDERER_TABLE_ROW = 'table-row';
37 const BUTTON_RENDERER_AFTER_RATE = 'after-rate';
38
39 /**
40 * Tells if hidden fields should be rendered at default place.
41 *
42 * @var bool
43 */
44 private $shouldRenderHiddenFieldsAtDefaultPlace = true;
45
46 /**
47 * Pickup point attributes configuration.
48 *
49 * @var array[]
50 */
51 public static $pickupPointAttrs = array(
52 'id' => array(
53 'name' => self::ATTR_POINT_ID,
54 'required' => true,
55 ),
56 'name' => array(
57 'name' => self::ATTR_POINT_NAME,
58 'required' => true,
59 ),
60 'city' => array(
61 'name' => self::ATTR_POINT_CITY,
62 'required' => true,
63 ),
64 'zip' => array(
65 'name' => self::ATTR_POINT_ZIP,
66 'required' => true,
67 ),
68 'street' => array(
69 'name' => self::ATTR_POINT_STREET,
70 'required' => true,
71 ),
72 'place' => array(
73 'name' => self::ATTR_POINT_PLACE,
74 'required' => false,
75 ),
76 'carrierId' => array(
77 'name' => self::ATTR_CARRIER_ID,
78 'required' => false,
79 ),
80 'url' => array(
81 'name' => self::ATTR_POINT_URL,
82 'required' => true,
83 ),
84 );
85
86 /**
87 * Home delivery attributes configuration.
88 *
89 * @var array[]
90 */
91 private static $homeDeliveryAttrs = [
92 'isValidated' => [
93 'name' => 'packetery_address_isValidated', // Name of checkout hidden form field. Must be unique in entire form.
94 'isWidgetResultField' => false, // Is attribute included in widget result address? By default, it is.
95 ],
96 'houseNumber' => [ // post type address field called 'houseNumber'.
97 'name' => 'packetery_address_houseNumber',
98 ],
99 'street' => [
100 'name' => 'packetery_address_street',
101 ],
102 'city' => [
103 'name' => 'packetery_address_city',
104 ],
105 'postCode' => [
106 'name' => 'packetery_address_postCode',
107 'widgetResultField' => 'postcode', // Widget returns address object containing specified field. By default, it is the array key 'postCode', but in this case it is 'postcode'.
108 ],
109 'county' => [
110 'name' => 'packetery_address_county',
111 ],
112 'country' => [
113 'name' => 'packetery_address_country',
114 ],
115 'latitude' => [
116 'name' => 'packetery_address_latitude',
117 ],
118 'longitude' => [
119 'name' => 'packetery_address_longitude',
120 ],
121 ];
122
123 /**
124 * PacketeryLatte engine
125 *
126 * @var Engine
127 */
128 private $latte_engine;
129
130 /**
131 * Options provider.
132 *
133 * @var Provider Options provider.
134 */
135 private $options_provider;
136
137 /**
138 * Carrier repository.
139 *
140 * @var Carrier\Repository Carrier repository.
141 */
142 private $carrierRepository;
143
144 /**
145 * Http request.
146 *
147 * @var Request Http request.
148 */
149 private $httpRequest;
150
151 /**
152 * Order repository.
153 *
154 * @var Order\Repository
155 */
156 private $orderRepository;
157
158 /**
159 * Checkout constructor.
160 *
161 * @param Engine $latte_engine PacketeryLatte engine.
162 * @param Provider $options_provider Options provider.
163 * @param Carrier\Repository $carrierRepository Carrier repository.
164 * @param Request $httpRequest Http request.
165 * @param Order\Repository $orderRepository Order repository.
166 */
167 public function __construct(
168 Engine $latte_engine,
169 Provider $options_provider,
170 Carrier\Repository $carrierRepository,
171 Request $httpRequest,
172 Order\Repository $orderRepository
173 ) {
174 $this->latte_engine = $latte_engine;
175 $this->options_provider = $options_provider;
176 $this->carrierRepository = $carrierRepository;
177 $this->httpRequest = $httpRequest;
178 $this->orderRepository = $orderRepository;
179 }
180
181 /**
182 * Check if chosen shipping rate is bound with Packeta pickup points
183 *
184 * @return bool
185 */
186 public function isPickupPointOrder(): bool {
187 $chosenMethod = $this->getChosenMethod();
188 $carrierId = $this->getCarrierId( $chosenMethod );
189
190 return $carrierId && $this->carrierRepository->isPickupPointCarrier( $carrierId );
191 }
192
193 /**
194 * Check if chosen shipping rate is bound with Packeta home delivery
195 *
196 * @return bool
197 */
198 public function isHomeDeliveryOrder(): bool {
199 $chosenMethod = $this->getChosenMethod();
200 $carrierId = $this->getCarrierId( $chosenMethod );
201
202 return $carrierId && $this->carrierRepository->isHomeDeliveryCarrier( $carrierId );
203 }
204
205 /**
206 * Render widget button table row.
207 *
208 * @return void
209 */
210 public function renderWidgetButtonTableRow(): void {
211 if ( ! is_checkout() ) {
212 return;
213 }
214
215 if ( $this->shouldRenderHiddenFieldsAtDefaultPlace ) {
216 $this->renderHiddenInputFields();
217 }
218
219 $this->latte_engine->render(
220 PACKETERY_PLUGIN_DIR . '/template/checkout/widget-button-row.latte',
221 [
222 'renderer' => self::BUTTON_RENDERER_TABLE_ROW,
223 'logo' => Plugin::buildAssetUrl( 'public/packeta-symbol.png' ),
224 'translations' => [
225 'packeta' => __( 'Packeta', 'packeta' ),
226 ],
227 ]
228 );
229 }
230
231 /**
232 * Renders widget button and information about chosen pickup point
233 *
234 * @param \WC_Shipping_Rate $shippingRate Shipping rate.
235 */
236 public function renderWidgetButtonAfterShippingRate( \WC_Shipping_Rate $shippingRate ): void {
237 if ( ! is_checkout() ) {
238 return;
239 }
240
241 if ( ! $this->isPacketeryOrder( $shippingRate->get_id() ) ) {
242 return;
243 }
244
245 static $hiddenInputsRendered = false;
246 static $hiddenFieldsRenderedFor = null;
247
248 if ( $this->shouldRenderHiddenFieldsAtDefaultPlace && ( ! $hiddenInputsRendered || $shippingRate->get_id() === $hiddenFieldsRenderedFor ) ) {
249 $this->renderHiddenInputFields();
250 $hiddenInputsRendered = true;
251 $hiddenFieldsRenderedFor = $shippingRate->get_id();
252 }
253
254 $this->latte_engine->render(
255 PACKETERY_PLUGIN_DIR . '/template/checkout/widget-button.latte',
256 [
257 'renderer' => self::BUTTON_RENDERER_AFTER_RATE,
258 'logo' => Plugin::buildAssetUrl( 'public/packeta-symbol.png' ),
259 'translations' => [
260 'packeta' => __( 'Packeta', 'packeta' ),
261 ],
262 ]
263 );
264 }
265
266 /**
267 * Gets widget carriers param.
268 *
269 * @param bool $isPickupPoints Is context pickup point related.
270 * @param string $carrierId Carrier id.
271 *
272 * @return string|null
273 */
274 public static function getWidgetCarriersParam( bool $isPickupPoints, string $carrierId ): ?string {
275 if ( $isPickupPoints ) {
276 return ( is_numeric( $carrierId ) ? $carrierId : Carrier\Repository::INTERNAL_PICKUP_POINTS_ID );
277 }
278
279 return null;
280 }
281
282 /**
283 * Creates settings for checkout script.
284 *
285 * @return array
286 */
287 public function createSettings(): array {
288 $carrierConfig = [];
289 $carriers = $this->carrierRepository->getAllIncludingZpoints();
290
291 foreach ( $carriers as $carrier ) {
292 $optionId = self::CARRIER_PREFIX . $carrier['id'];
293 $carrierConfig[ $optionId ] = [
294 'id' => $carrier['id'],
295 'is_pickup_points' => $carrier['is_pickup_points'],
296 ];
297
298 if ( $carrier['is_pickup_points'] ) {
299 $carrierConfig[ $optionId ]['carriers'] = self::getWidgetCarriersParam( (bool) $carrier['is_pickup_points'], (string) $carrier['id'] );
300 }
301
302 if ( ! $carrier['is_pickup_points'] ) {
303 $carrierOption = get_option( $optionId );
304
305 $addressValidation = 'none';
306 if ( $carrierOption ) {
307 $addressValidation = ( $carrierOption['address_validation'] ?? $addressValidation );
308 }
309
310 $carrierConfig[ $optionId ]['address_validation'] = $addressValidation;
311 }
312 }
313
314 return [
315 'language' => substr( get_locale(), 0, 2 ),
316 'country' => $this->getCustomerCountry(),
317 'weight' => $this->getCartWeightKg(),
318 'carrierConfig' => $carrierConfig,
319 'pickupPointAttrs' => self::$pickupPointAttrs,
320 'homeDeliveryAttrs' => self::$homeDeliveryAttrs,
321 'appIdentity' => Plugin::getAppIdentity(),
322 'packeteryApiKey' => $this->options_provider->get_api_key(),
323 'translations' => [
324 'choosePickupPoint' => __( 'Choose pickup point', 'packeta' ),
325 'chooseAddress' => __( 'Check shipping address', 'packeta' ),
326 'addressValidationIsOutOfOrder' => __( 'Address validation is out of order', 'packeta' ),
327 'invalidAddressCountrySelected' => __( 'The selected country does not correspond to the destination country.', 'packeta' ),
328 'selectedShippingAddress' => __( 'Selected shipping address', 'packeta' ),
329 'addressIsValidated' => __( 'Address is validated', 'packeta' ),
330 'addressIsNotValidated' => __( 'Delivery address has not been verified.', 'packeta' ),
331 'addressIsNotValidatedAndRequiredByCarrier' => __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ),
332 ],
333 ];
334 }
335
336 /**
337 * Adds fields to checkout page to save the values later
338 */
339 public function renderHiddenInputFields(): void {
340 $this->latte_engine->render(
341 PACKETERY_PLUGIN_DIR . '/template/checkout/input_fields.latte',
342 [ 'fields' => array_merge( array_column( self::$pickupPointAttrs, 'name' ), array_column( self::$homeDeliveryAttrs, 'name' ) ) ]
343 );
344
345 wp_nonce_field( self::NONCE_ACTION );
346 }
347
348 /**
349 * Checks if all pickup point attributes are set, sets an error otherwise.
350 */
351 public function validateCheckoutData(): void {
352 $chosenMethod = $this->getChosenMethod();
353 if ( false === $this->isPacketeryOrder( $chosenMethod ) ) {
354 return;
355 }
356
357 $post = $this->httpRequest->getPost();
358 if ( ! wp_verify_nonce( $post['_wpnonce'], self::NONCE_ACTION ) ) {
359 wp_nonce_ays( '' );
360 }
361
362 if ( $this->isPickupPointOrder() ) {
363 $error = false;
364 $required_attrs = array_filter(
365 array_combine(
366 array_column( self::$pickupPointAttrs, 'name' ),
367 array_column( self::$pickupPointAttrs, 'required' )
368 )
369 );
370 foreach ( $required_attrs as $attr => $required ) {
371 $attr_value = null;
372 if ( isset( $post[ $attr ] ) ) {
373 $attr_value = $post[ $attr ];
374 }
375 if ( ! $attr_value ) {
376 $error = true;
377 }
378 }
379 $carrierId = null;
380 if ( isset( $post['carrier_id'] ) ) {
381 $carrierId = $post['carrier_id'];
382 }
383 $pointCarrierId = null;
384 if ( isset( $post['point_carrier_id'] ) ) {
385 $pointCarrierId = $post['point_carrier_id'];
386 }
387 if ( $carrierId && ! $pointCarrierId ) {
388 $error = true;
389 }
390 if ( ! $carrierId && $pointCarrierId ) {
391 $error = true;
392 }
393 if ( $error ) {
394 wc_add_notice( __( 'Pick up point is not chosen.', 'packeta' ), 'error' );
395 }
396 }
397
398 if ( $this->isHomeDeliveryOrder() ) {
399 $carrierId = $this->getCarrierId( $chosenMethod );
400 $optionId = self::CARRIER_PREFIX . $carrierId;
401 $carrierOption = get_option( $optionId );
402
403 $addressValidation = 'none';
404 if ( $carrierOption ) {
405 $addressValidation = ( $carrierOption['address_validation'] ?? $addressValidation );
406 }
407
408 if (
409 'required' === $addressValidation &&
410 (
411 ! isset( $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] ) ||
412 '1' !== $post[ self::$homeDeliveryAttrs['isValidated']['name'] ]
413 )
414 ) {
415 wc_add_notice( __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ), 'error' );
416 }
417 }
418 }
419
420 /**
421 * Saves pickup point and other Packeta information to order.
422 *
423 * @param int $orderId Order id.
424 *
425 * @throws \WC_Data_Exception When invalid data are passed during shipping address update.
426 */
427 public function updateOrderMeta( int $orderId ): void {
428 $chosenMethod = $this->getChosenMethod();
429 if ( false === $this->isPacketeryOrder( $chosenMethod ) ) {
430 return;
431 }
432
433 $post = $this->httpRequest->getPost();
434
435 $propsToSave = [];
436 // Save carrier id for home delivery (we got no id from widget).
437 $carrierId = $this->getCarrierId( $chosenMethod );
438 if ( empty( $post[ self::ATTR_CARRIER_ID ] ) && $carrierId ) {
439 $propsToSave[ self::ATTR_CARRIER_ID ] = $carrierId;
440 }
441
442 if ( $this->isPickupPointOrder() ) {
443 $wcOrder = wc_get_order( $orderId );
444 if ( ! $wcOrder instanceof \WC_Order ) {
445 return;
446 }
447
448 foreach ( self::$pickupPointAttrs as $attr ) {
449 $attrName = $attr['name'];
450 if ( ! isset( $post[ $attrName ] ) ) {
451 continue;
452 }
453 $attrValue = $post[ $attrName ];
454
455 $saveMeta = true;
456 if (
457 ( self::ATTR_CARRIER_ID === $attrName && ! $attrValue ) ||
458 ( self::ATTR_POINT_URL === $attrName && ! filter_var( $attrValue, FILTER_VALIDATE_URL ) )
459 ) {
460 $saveMeta = false;
461 }
462 if ( $saveMeta ) {
463 $propsToSave[ $attrName ] = $attrValue;
464 }
465
466 if ( $this->options_provider->replaceShippingAddressWithPickupPointAddress() ) {
467 self::updateShippingAddressProperty( $wcOrder, $attrName, (string) $attrValue );
468 }
469 }
470 $wcOrder->save();
471 }
472
473 $orderEntity = new Core\Entity\Order( (string) $orderId, $carrierId );
474 if (
475 isset( $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] ) &&
476 '1' === $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] &&
477 $this->isHomeDeliveryOrder()
478 ) {
479 $validatedAddress = new Core\Entity\Address(
480 $post[ self::$homeDeliveryAttrs['street']['name'] ],
481 $post[ self::$homeDeliveryAttrs['city']['name'] ],
482 $post[ self::$homeDeliveryAttrs['postCode']['name'] ]
483 );
484 $validatedAddress->setCounty( $post[ self::$homeDeliveryAttrs['county']['name'] ] );
485 $validatedAddress->setHouseNumber( $post[ self::$homeDeliveryAttrs['houseNumber']['name'] ] );
486 $validatedAddress->setLatitude( $post[ self::$homeDeliveryAttrs['latitude']['name'] ] );
487 $validatedAddress->setLongitude( $post[ self::$homeDeliveryAttrs['longitude']['name'] ] );
488
489 $orderEntity->setDeliveryAddress( $validatedAddress );
490 $orderEntity->setAddressValidated( true );
491 }
492
493 self::updateOrderEntityFromPropsToSave( $orderEntity, $propsToSave );
494 $this->orderRepository->save( $orderEntity );
495 }
496
497 /**
498 * Updates order entity from props to save-
499 *
500 * @param Core\Entity\Order $orderEntity Order entity.
501 * @param array $propsToSave Props to save.
502 *
503 * @return void
504 */
505 public static function updateOrderEntityFromPropsToSave( Core\Entity\Order $orderEntity, array $propsToSave ): void {
506 $orderEntityPickupPoint = $orderEntity->getPickupPoint();
507 if ( null === $orderEntityPickupPoint ) {
508 $orderEntityPickupPoint = new Core\Entity\PickupPoint();
509 }
510
511 foreach ( $propsToSave as $attrName => $attrValue ) {
512 switch ( $attrName ) {
513 case self::ATTR_CARRIER_ID:
514 $orderEntity->setCarrierId( $attrValue );
515 break;
516 case self::ATTR_POINT_ID:
517 $orderEntityPickupPoint->setId( $attrValue );
518 break;
519 case self::ATTR_POINT_NAME:
520 $orderEntityPickupPoint->setName( $attrValue );
521 break;
522 case self::ATTR_POINT_URL:
523 $orderEntityPickupPoint->setUrl( $attrValue );
524 break;
525 case self::ATTR_POINT_STREET:
526 $orderEntityPickupPoint->setStreet( $attrValue );
527 break;
528 case self::ATTR_POINT_ZIP:
529 $orderEntityPickupPoint->setZip( $attrValue );
530 break;
531 case self::ATTR_POINT_CITY:
532 $orderEntityPickupPoint->setCity( $attrValue );
533 break;
534 }
535 }
536
537 $orderEntity->setPickupPoint( $orderEntityPickupPoint );
538 }
539
540 /**
541 * Registers Packeta checkout hooks
542 */
543 public function register_hooks(): void {
544 $activeTheme = strtolower( wp_get_theme()->get_stylesheet() );
545
546 if ( in_array( $activeTheme, [ 'divi', 'divi_child' ], true ) ) {
547 add_action( 'woocommerce_review_order_before_submit', [ $this, 'renderHiddenInputFields' ] );
548 $this->shouldRenderHiddenFieldsAtDefaultPlace = false;
549 }
550
551 add_action( 'woocommerce_checkout_process', array( $this, 'validateCheckoutData' ) );
552 add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'updateOrderMeta' ) );
553 add_action( 'woocommerce_review_order_before_shipping', array( $this, 'updateShippingRates' ), 10, 2 );
554 add_action( 'woocommerce_cart_calculate_fees', [ $this, 'calculateFees' ] );
555 add_action(
556 'init',
557 function () {
558 /**
559 * Tells if widget button table row should be used.
560 *
561 * @since 1.3.0
562 */
563 if ( $this->options_provider->getCheckoutWidgetButtonLocation() === 'after_transport_methods' ) {
564 add_action( 'woocommerce_review_order_after_shipping', [ $this, 'renderWidgetButtonTableRow' ] );
565 } else {
566 add_action( 'woocommerce_after_shipping_rate', [ $this, 'renderWidgetButtonAfterShippingRate' ] );
567 }
568 }
569 );
570 }
571
572 /**
573 * Updates shipping rates cost based on cart properties.
574 */
575 public function updateShippingRates(): void {
576 $customRates = $this->getShippingRates();
577
578 $packages = WC()->shipping()->get_packages();
579 foreach ( $packages as $i => $package ) {
580 if ( ! empty( $package['rates'] ) ) {
581 foreach ( $package['rates'] as $key => $rate ) {
582 if ( isset( $customRates[ $rate->get_id() ] ) ) {
583 $rate->set_cost( $customRates[ $rate->get_id() ]['cost'] );
584 WC()->shipping->packages[ $i ]['rates'][ $key ] = $rate;
585 }
586 }
587 }
588 }
589 }
590
591 /**
592 * Gets customer country from WC cart.
593 *
594 * @return string
595 */
596 public function getCustomerCountry(): string {
597 $country = strtolower( WC()->customer->get_shipping_country() );
598 if ( ! $country ) {
599 $country = strtolower( WC()->customer->get_billing_country() );
600 }
601
602 return $country;
603 }
604
605 /**
606 * Gets cart contents weight in kg.
607 *
608 * @return float|int
609 */
610 public function getCartWeightKg() {
611 $weight = WC()->cart->cart_contents_weight;
612 $weightKg = wc_get_weight( $weight, 'kg' );
613 if ( $weightKg ) {
614 $weightKg += $this->options_provider->getPackagingWeight();
615 }
616
617 return $weightKg;
618 }
619
620 /**
621 * Calculates fees.
622 *
623 * @return void
624 */
625 public function calculateFees(): void {
626 $chosenShippingMethod = $this->getChosenMethod();
627 if ( false === $this->isPacketeryOrder( $chosenShippingMethod ) ) {
628 return;
629 }
630
631 $carrierOptions = get_option( $chosenShippingMethod );
632 if ( ! $carrierOptions ) {
633 return;
634 }
635
636 $isCod = false;
637 $codPaymentMethod = $this->options_provider->getCodPaymentMethod();
638 $chosenPaymentMethod = WC()->session->get( 'chosen_payment_method' );
639 if ( null !== $codPaymentMethod && ! empty( $chosenPaymentMethod ) && $chosenPaymentMethod === $codPaymentMethod ) {
640 $isCod = true;
641 }
642
643 if ( false === $isCod ) {
644 return;
645 }
646
647 $applicableSurcharge = $this->getCODSurcharge( $carrierOptions, $this->getCartPrice() );
648
649 // WooCommerce currency-switcher.com compatibility.
650 $applicableSurcharge = $this->applyFilterWoocsExchangeValue( $applicableSurcharge );
651
652 if ( 0 >= $applicableSurcharge ) {
653 return;
654 }
655
656 $fee = [
657 'id' => 'packetery-cod-surcharge',
658 'name' => __( 'COD surcharge', 'packeta' ),
659 'amount' => $applicableSurcharge,
660 ];
661
662 WC()->cart->fees_api()->add_fee( $fee );
663 }
664
665 /**
666 * Gets cart price. Value is cast to float because PHPDoc is not reliable.
667 *
668 * @return float
669 */
670 private function getCartPrice(): float {
671 return (float) WC()->cart->get_subtotal();
672 }
673
674 /**
675 * Prepare shipping rates based on cart properties.
676 *
677 * @return array
678 */
679 public function getShippingRates(): array {
680 $customerCountry = $this->getCustomerCountry();
681 $availableCarriers = $this->carrierRepository->getByCountryIncludingZpoints( $customerCountry );
682 $carrierOptions = [];
683 foreach ( $availableCarriers as $carrier ) {
684 $optionId = self::CARRIER_PREFIX . $carrier->getId();
685 $carrierOptions[ $optionId ] = get_option( $optionId );
686 }
687
688 $cartPrice = $this->getCartPrice();
689 $cartWeight = $this->getCartWeightKg();
690
691 $customRates = [];
692 foreach ( $carrierOptions as $optionId => $options ) {
693 if ( is_array( $options ) && true === $options['active'] ) {
694 $cost = $this->getRateCost( $options, $cartPrice, $cartWeight );
695 if ( null !== $cost ) {
696 $customRates[ $optionId ] = [
697 'label' => $options['name'],
698 'id' => $optionId,
699 'cost' => $cost,
700 'taxes' => '',
701 'calc_tax' => 'per_order',
702 ];
703 }
704 }
705 }
706
707 return $customRates;
708 }
709
710 /**
711 * Computes custom rate cost for carrier using cart contents.
712 *
713 * @param array $carrierOptions Carrier options.
714 * @param float $cartPrice Price.
715 * @param float|int $cartWeight Weight.
716 *
717 * @return int|float|null
718 */
719 private function getRateCost( array $carrierOptions, float $cartPrice, $cartWeight ) {
720 $cost = null;
721
722 foreach ( $carrierOptions['weight_limits'] as $weightLimit ) {
723 if ( $cartWeight <= $weightLimit['weight'] ) {
724 $cost = $weightLimit['price'];
725 break;
726 }
727 }
728
729 if ( null === $cost ) {
730 return null;
731 }
732
733 if ( $carrierOptions['free_shipping_limit'] && $cartPrice >= $carrierOptions['free_shipping_limit'] ) {
734 $cost = 0;
735 }
736
737 // WooCommerce currency-switcher.com compatibility.
738 return $this->applyFilterWoocsExchangeValue( (float) $cost );
739 }
740
741 /**
742 * Gets applicable COD surcharge.
743 *
744 * @param array $carrierOptions Carrier options.
745 * @param float $cartPrice Cart price.
746 *
747 * @return float
748 */
749 private function getCODSurcharge( array $carrierOptions, float $cartPrice ): float {
750 if ( isset( $carrierOptions['surcharge_limits'] ) ) {
751 foreach ( $carrierOptions['surcharge_limits'] as $weightLimit ) {
752 if ( $cartPrice <= $weightLimit['order_price'] ) {
753 return (float) $weightLimit['surcharge'];
754 }
755 }
756 }
757
758 if ( isset( $carrierOptions['default_COD_surcharge'] ) && is_numeric( $carrierOptions['default_COD_surcharge'] ) ) {
759 return (float) $carrierOptions['default_COD_surcharge'];
760 }
761
762 return 0.0;
763 }
764
765 /**
766 * Get chosen shipping rate id.
767 *
768 * @return string
769 */
770 private function getChosenMethod(): string {
771 $chosenShippingRates = WC()->cart->calculate_shipping();
772 $chosenShippingRate = ( $chosenShippingRates[0] ?? null );
773
774 if ( $chosenShippingRate instanceof \WC_Shipping_Rate ) {
775 return $chosenShippingRate->get_id();
776 }
777
778 return '';
779 }
780
781 /**
782 * Gets carrier id from chosen shipping method.
783 *
784 * @param string $chosenMethod Chosen shipping method.
785 *
786 * @return string|null
787 */
788 public function getCarrierId( string $chosenMethod ): ?string {
789 if ( ! $this->isPacketeryOrder( $chosenMethod ) ) {
790 return null;
791 }
792
793 $carrierId = str_replace( self::CARRIER_PREFIX, '', $chosenMethod );
794 if ( strpos( $carrierId, 'zpoint' ) === 0 ) {
795 return Carrier\Repository::INTERNAL_PICKUP_POINTS_ID;
796 }
797
798 return $carrierId;
799 }
800
801
802 /**
803 * Checks if chosen shipping method is one of packetery.
804 *
805 * @param string $chosenMethod Chosen shipping method.
806 *
807 * @return bool
808 */
809 private function isPacketeryOrder( string $chosenMethod ): bool {
810 return ( strpos( $chosenMethod, self::CARRIER_PREFIX ) === 0 );
811 }
812
813 /**
814 * WooCommerce currency-switcher.com compatibility.
815 *
816 * @param float $value Value of the surcharge or transport price.
817 * @return float
818 */
819 private function applyFilterWoocsExchangeValue( float $value ): float {
820 if ( 0 < $value ) {
821 /**
822 * Applies woocs_exchange_value filters.
823 *
824 * @since 1.2.7
825 */
826 $value = (float) apply_filters( 'woocs_exchange_value', $value );
827 }
828
829 return $value;
830 }
831
832 /**
833 * Update order shipping.
834 *
835 * @param \WC_Order $wcOrder WC Order.
836 * @param string $attributeName Attribute name.
837 * @param string $value Value.
838 *
839 * @return void
840 * @throws \WC_Data_Exception When shipping input is invalid.
841 */
842 public static function updateShippingAddressProperty( \WC_Order $wcOrder, string $attributeName, string $value ): void {
843 if ( self::ATTR_POINT_STREET === $attributeName ) {
844 $wcOrder->set_shipping_address_1( $value );
845 $wcOrder->set_shipping_address_2( '' );
846 }
847 if ( self::ATTR_POINT_PLACE === $attributeName ) {
848 $wcOrder->set_shipping_company( $value );
849 }
850 if ( self::ATTR_POINT_CITY === $attributeName ) {
851 $wcOrder->set_shipping_city( $value );
852 }
853 if ( self::ATTR_POINT_ZIP === $attributeName ) {
854 $wcOrder->set_shipping_postcode( $value );
855 }
856 }
857 }
858