PluginProbe
Packeta / 1.3.1
Packeta v1.3.1
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.1, at src/Packetery/Module/Checkout.php

854 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 $post = $this->httpRequest->getPost();
353 if ( ! wp_verify_nonce( $post['_wpnonce'], self::NONCE_ACTION ) ) {
354 wp_nonce_ays( '' );
355 }
356
357 if ( $this->isPickupPointOrder() ) {
358 $error = false;
359 $required_attrs = array_filter(
360 array_combine(
361 array_column( self::$pickupPointAttrs, 'name' ),
362 array_column( self::$pickupPointAttrs, 'required' )
363 )
364 );
365 foreach ( $required_attrs as $attr => $required ) {
366 $attr_value = null;
367 if ( isset( $post[ $attr ] ) ) {
368 $attr_value = $post[ $attr ];
369 }
370 if ( ! $attr_value ) {
371 $error = true;
372 }
373 }
374 $carrierId = null;
375 if ( isset( $post['carrier_id'] ) ) {
376 $carrierId = $post['carrier_id'];
377 }
378 $pointCarrierId = null;
379 if ( isset( $post['point_carrier_id'] ) ) {
380 $pointCarrierId = $post['point_carrier_id'];
381 }
382 if ( $carrierId && ! $pointCarrierId ) {
383 $error = true;
384 }
385 if ( ! $carrierId && $pointCarrierId ) {
386 $error = true;
387 }
388 if ( $error ) {
389 wc_add_notice( __( 'Pick up point is not chosen.', 'packeta' ), 'error' );
390 }
391 }
392
393 if ( $this->isHomeDeliveryOrder() ) {
394 $chosenMethod = $this->getChosenMethod();
395 $carrierId = $this->getCarrierId( $chosenMethod );
396 $optionId = self::CARRIER_PREFIX . $carrierId;
397 $carrierOption = get_option( $optionId );
398
399 $addressValidation = 'none';
400 if ( $carrierOption ) {
401 $addressValidation = ( $carrierOption['address_validation'] ?? $addressValidation );
402 }
403
404 if (
405 'required' === $addressValidation &&
406 (
407 ! isset( $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] ) ||
408 '1' !== $post[ self::$homeDeliveryAttrs['isValidated']['name'] ]
409 )
410 ) {
411 wc_add_notice( __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ), 'error' );
412 }
413 }
414 }
415
416 /**
417 * Saves pickup point and other Packeta information to order.
418 *
419 * @param int $orderId Order id.
420 *
421 * @throws \WC_Data_Exception When invalid data are passed during shipping address update.
422 */
423 public function updateOrderMeta( int $orderId ): void {
424 $chosenMethod = $this->getChosenMethod();
425 if ( false === $this->isPacketeryOrder( $chosenMethod ) ) {
426 return;
427 }
428
429 $post = $this->httpRequest->getPost();
430
431 $propsToSave = [];
432 // Save carrier id for home delivery (we got no id from widget).
433 $carrierId = $this->getCarrierId( $chosenMethod );
434 if ( empty( $post[ self::ATTR_CARRIER_ID ] ) && $carrierId ) {
435 $propsToSave[ self::ATTR_CARRIER_ID ] = $carrierId;
436 }
437
438 if ( $this->isPickupPointOrder() ) {
439 $wcOrder = wc_get_order( $orderId );
440 if ( ! $wcOrder instanceof \WC_Order ) {
441 return;
442 }
443
444 foreach ( self::$pickupPointAttrs as $attr ) {
445 $attrName = $attr['name'];
446 if ( ! isset( $post[ $attrName ] ) ) {
447 continue;
448 }
449 $attrValue = $post[ $attrName ];
450
451 $saveMeta = true;
452 if (
453 ( self::ATTR_CARRIER_ID === $attrName && ! $attrValue ) ||
454 ( self::ATTR_POINT_URL === $attrName && ! filter_var( $attrValue, FILTER_VALIDATE_URL ) )
455 ) {
456 $saveMeta = false;
457 }
458 if ( $saveMeta ) {
459 $propsToSave[ $attrName ] = $attrValue;
460 }
461
462 if ( $this->options_provider->replaceShippingAddressWithPickupPointAddress() ) {
463 self::updateShippingAddressProperty( $wcOrder, $attrName, (string) $attrValue );
464 }
465 }
466 $wcOrder->save();
467 }
468
469 $orderEntity = new Core\Entity\Order( (string) $orderId, $carrierId );
470 if (
471 isset( $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] ) &&
472 '1' === $post[ self::$homeDeliveryAttrs['isValidated']['name'] ] &&
473 $this->isHomeDeliveryOrder()
474 ) {
475 $validatedAddress = new Core\Entity\Address(
476 $post[ self::$homeDeliveryAttrs['street']['name'] ],
477 $post[ self::$homeDeliveryAttrs['city']['name'] ],
478 $post[ self::$homeDeliveryAttrs['postCode']['name'] ]
479 );
480 $validatedAddress->setCounty( $post[ self::$homeDeliveryAttrs['county']['name'] ] );
481 $validatedAddress->setHouseNumber( $post[ self::$homeDeliveryAttrs['houseNumber']['name'] ] );
482 $validatedAddress->setLatitude( $post[ self::$homeDeliveryAttrs['latitude']['name'] ] );
483 $validatedAddress->setLongitude( $post[ self::$homeDeliveryAttrs['longitude']['name'] ] );
484
485 $orderEntity->setDeliveryAddress( $validatedAddress );
486 $orderEntity->setAddressValidated( true );
487 }
488
489 self::updateOrderEntityFromPropsToSave( $orderEntity, $propsToSave );
490 $this->orderRepository->save( $orderEntity );
491 }
492
493 /**
494 * Updates order entity from props to save-
495 *
496 * @param Core\Entity\Order $orderEntity Order entity.
497 * @param array $propsToSave Props to save.
498 *
499 * @return void
500 */
501 public static function updateOrderEntityFromPropsToSave( Core\Entity\Order $orderEntity, array $propsToSave ): void {
502 $orderEntityPickupPoint = $orderEntity->getPickupPoint();
503 if ( null === $orderEntityPickupPoint ) {
504 $orderEntityPickupPoint = new Core\Entity\PickupPoint();
505 }
506
507 foreach ( $propsToSave as $attrName => $attrValue ) {
508 switch ( $attrName ) {
509 case self::ATTR_CARRIER_ID:
510 $orderEntity->setCarrierId( $attrValue );
511 break;
512 case self::ATTR_POINT_ID:
513 $orderEntityPickupPoint->setId( $attrValue );
514 break;
515 case self::ATTR_POINT_NAME:
516 $orderEntityPickupPoint->setName( $attrValue );
517 break;
518 case self::ATTR_POINT_URL:
519 $orderEntityPickupPoint->setUrl( $attrValue );
520 break;
521 case self::ATTR_POINT_STREET:
522 $orderEntityPickupPoint->setStreet( $attrValue );
523 break;
524 case self::ATTR_POINT_ZIP:
525 $orderEntityPickupPoint->setZip( $attrValue );
526 break;
527 case self::ATTR_POINT_CITY:
528 $orderEntityPickupPoint->setCity( $attrValue );
529 break;
530 }
531 }
532
533 $orderEntity->setPickupPoint( $orderEntityPickupPoint );
534 }
535
536 /**
537 * Registers Packeta checkout hooks
538 */
539 public function register_hooks(): void {
540 $activeTheme = strtolower( wp_get_theme()->get_stylesheet() );
541
542 if ( in_array( $activeTheme, [ 'divi', 'divi_child' ], true ) ) {
543 add_action( 'woocommerce_review_order_before_submit', [ $this, 'renderHiddenInputFields' ] );
544 $this->shouldRenderHiddenFieldsAtDefaultPlace = false;
545 }
546
547 add_action( 'woocommerce_checkout_process', array( $this, 'validateCheckoutData' ) );
548 add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'updateOrderMeta' ) );
549 add_action( 'woocommerce_review_order_before_shipping', array( $this, 'updateShippingRates' ), 10, 2 );
550 add_action( 'woocommerce_cart_calculate_fees', [ $this, 'calculateFees' ] );
551 add_action(
552 'init',
553 function () {
554 /**
555 * Tells if widget button table row should be used.
556 *
557 * @since 1.3.0
558 */
559 if ( $this->options_provider->getCheckoutWidgetButtonLocation() === 'after_transport_methods' ) {
560 add_action( 'woocommerce_review_order_after_shipping', [ $this, 'renderWidgetButtonTableRow' ] );
561 } else {
562 add_action( 'woocommerce_after_shipping_rate', [ $this, 'renderWidgetButtonAfterShippingRate' ] );
563 }
564 }
565 );
566 }
567
568 /**
569 * Updates shipping rates cost based on cart properties.
570 */
571 public function updateShippingRates(): void {
572 $customRates = $this->getShippingRates();
573
574 $packages = WC()->shipping()->get_packages();
575 foreach ( $packages as $i => $package ) {
576 if ( ! empty( $package['rates'] ) ) {
577 foreach ( $package['rates'] as $key => $rate ) {
578 if ( isset( $customRates[ $rate->get_id() ] ) ) {
579 $rate->set_cost( $customRates[ $rate->get_id() ]['cost'] );
580 WC()->shipping->packages[ $i ]['rates'][ $key ] = $rate;
581 }
582 }
583 }
584 }
585 }
586
587 /**
588 * Gets customer country from WC cart.
589 *
590 * @return string
591 */
592 public function getCustomerCountry(): string {
593 $country = strtolower( WC()->customer->get_shipping_country() );
594 if ( ! $country ) {
595 $country = strtolower( WC()->customer->get_billing_country() );
596 }
597
598 return $country;
599 }
600
601 /**
602 * Gets cart contents weight in kg.
603 *
604 * @return float|int
605 */
606 public function getCartWeightKg() {
607 $weight = WC()->cart->cart_contents_weight;
608 $weightKg = wc_get_weight( $weight, 'kg' );
609 if ( $weightKg ) {
610 $weightKg += $this->options_provider->getPackagingWeight();
611 }
612
613 return $weightKg;
614 }
615
616 /**
617 * Calculates fees.
618 *
619 * @return void
620 */
621 public function calculateFees(): void {
622 $chosenShippingMethod = $this->getChosenMethod();
623 if ( false === $this->isPacketeryOrder( $chosenShippingMethod ) ) {
624 return;
625 }
626
627 $carrierOptions = get_option( $chosenShippingMethod );
628 if ( ! $carrierOptions ) {
629 return;
630 }
631
632 $isCod = false;
633 $codPaymentMethod = $this->options_provider->getCodPaymentMethod();
634 $chosenPaymentMethod = WC()->session->get( 'chosen_payment_method' );
635 if ( null !== $codPaymentMethod && ! empty( $chosenPaymentMethod ) && $chosenPaymentMethod === $codPaymentMethod ) {
636 $isCod = true;
637 }
638
639 if ( false === $isCod ) {
640 return;
641 }
642
643 $applicableSurcharge = $this->getCODSurcharge( $carrierOptions, $this->getCartPrice() );
644
645 // WooCommerce currency-switcher.com compatibility.
646 $applicableSurcharge = $this->applyFilterWoocsExchangeValue( $applicableSurcharge );
647
648 if ( 0 >= $applicableSurcharge ) {
649 return;
650 }
651
652 $fee = [
653 'id' => 'packetery-cod-surcharge',
654 'name' => __( 'COD surcharge', 'packeta' ),
655 'amount' => $applicableSurcharge,
656 ];
657
658 WC()->cart->fees_api()->add_fee( $fee );
659 }
660
661 /**
662 * Gets cart price. Value is cast to float because PHPDoc is not reliable.
663 *
664 * @return float
665 */
666 private function getCartPrice(): float {
667 return (float) WC()->cart->get_subtotal();
668 }
669
670 /**
671 * Prepare shipping rates based on cart properties.
672 *
673 * @return array
674 */
675 public function getShippingRates(): array {
676 $customerCountry = $this->getCustomerCountry();
677 $availableCarriers = $this->carrierRepository->getByCountryIncludingZpoints( $customerCountry );
678 $carrierOptions = [];
679 foreach ( $availableCarriers as $carrier ) {
680 $optionId = self::CARRIER_PREFIX . $carrier->getId();
681 $carrierOptions[ $optionId ] = get_option( $optionId );
682 }
683
684 $cartPrice = $this->getCartPrice();
685 $cartWeight = $this->getCartWeightKg();
686
687 $customRates = [];
688 foreach ( $carrierOptions as $optionId => $options ) {
689 if ( is_array( $options ) && true === $options['active'] ) {
690 $cost = $this->getRateCost( $options, $cartPrice, $cartWeight );
691 if ( null !== $cost ) {
692 $customRates[ $optionId ] = [
693 'label' => $options['name'],
694 'id' => $optionId,
695 'cost' => $cost,
696 'taxes' => '',
697 'calc_tax' => 'per_order',
698 ];
699 }
700 }
701 }
702
703 return $customRates;
704 }
705
706 /**
707 * Computes custom rate cost for carrier using cart contents.
708 *
709 * @param array $carrierOptions Carrier options.
710 * @param float $cartPrice Price.
711 * @param float|int $cartWeight Weight.
712 *
713 * @return int|float|null
714 */
715 private function getRateCost( array $carrierOptions, float $cartPrice, $cartWeight ) {
716 $cost = null;
717
718 foreach ( $carrierOptions['weight_limits'] as $weightLimit ) {
719 if ( $cartWeight <= $weightLimit['weight'] ) {
720 $cost = $weightLimit['price'];
721 break;
722 }
723 }
724
725 if ( null === $cost ) {
726 return null;
727 }
728
729 if ( $carrierOptions['free_shipping_limit'] && $cartPrice >= $carrierOptions['free_shipping_limit'] ) {
730 $cost = 0;
731 }
732
733 // WooCommerce currency-switcher.com compatibility.
734 return $this->applyFilterWoocsExchangeValue( (float) $cost );
735 }
736
737 /**
738 * Gets applicable COD surcharge.
739 *
740 * @param array $carrierOptions Carrier options.
741 * @param float $cartPrice Cart price.
742 *
743 * @return float
744 */
745 private function getCODSurcharge( array $carrierOptions, float $cartPrice ): float {
746 if ( isset( $carrierOptions['surcharge_limits'] ) ) {
747 foreach ( $carrierOptions['surcharge_limits'] as $weightLimit ) {
748 if ( $cartPrice <= $weightLimit['order_price'] ) {
749 return (float) $weightLimit['surcharge'];
750 }
751 }
752 }
753
754 if ( isset( $carrierOptions['default_COD_surcharge'] ) && is_numeric( $carrierOptions['default_COD_surcharge'] ) ) {
755 return (float) $carrierOptions['default_COD_surcharge'];
756 }
757
758 return 0.0;
759 }
760
761 /**
762 * Get chosen shipping rate id.
763 *
764 * @return string
765 */
766 private function getChosenMethod(): string {
767 $chosenShippingRates = WC()->cart->calculate_shipping();
768 $chosenShippingRate = ( $chosenShippingRates[0] ?? null );
769
770 if ( $chosenShippingRate instanceof \WC_Shipping_Rate ) {
771 return $chosenShippingRate->get_id();
772 }
773
774 return '';
775 }
776
777 /**
778 * Gets carrier id from chosen shipping method.
779 *
780 * @param string $chosenMethod Chosen shipping method.
781 *
782 * @return string|null
783 */
784 public function getCarrierId( string $chosenMethod ): ?string {
785 if ( ! $this->isPacketeryOrder( $chosenMethod ) ) {
786 return null;
787 }
788
789 $carrierId = str_replace( self::CARRIER_PREFIX, '', $chosenMethod );
790 if ( strpos( $carrierId, 'zpoint' ) === 0 ) {
791 return Carrier\Repository::INTERNAL_PICKUP_POINTS_ID;
792 }
793
794 return $carrierId;
795 }
796
797
798 /**
799 * Checks if chosen shipping method is one of packetery.
800 *
801 * @param string $chosenMethod Chosen shipping method.
802 *
803 * @return bool
804 */
805 private function isPacketeryOrder( string $chosenMethod ): bool {
806 return ( strpos( $chosenMethod, self::CARRIER_PREFIX ) === 0 );
807 }
808
809 /**
810 * WooCommerce currency-switcher.com compatibility.
811 *
812 * @param float $value Value of the surcharge or transport price.
813 * @return float
814 */
815 private function applyFilterWoocsExchangeValue( float $value ): float {
816 if ( 0 < $value ) {
817 /**
818 * Applies woocs_exchange_value filters.
819 *
820 * @since 1.2.7
821 */
822 $value = (float) apply_filters( 'woocs_exchange_value', $value );
823 }
824
825 return $value;
826 }
827
828 /**
829 * Update order shipping.
830 *
831 * @param \WC_Order $wcOrder WC Order.
832 * @param string $attributeName Attribute name.
833 * @param string $value Value.
834 *
835 * @return void
836 * @throws \WC_Data_Exception When shipping input is invalid.
837 */
838 public static function updateShippingAddressProperty( \WC_Order $wcOrder, string $attributeName, string $value ): void {
839 if ( self::ATTR_POINT_STREET === $attributeName ) {
840 $wcOrder->set_shipping_address_1( $value );
841 $wcOrder->set_shipping_address_2( '' );
842 }
843 if ( self::ATTR_POINT_PLACE === $attributeName ) {
844 $wcOrder->set_shipping_company( $value );
845 }
846 if ( self::ATTR_POINT_CITY === $attributeName ) {
847 $wcOrder->set_shipping_city( $value );
848 }
849 if ( self::ATTR_POINT_ZIP === $attributeName ) {
850 $wcOrder->set_shipping_postcode( $value );
851 }
852 }
853 }
854