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

1,066 lines 31.3 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\Core\Api\Rest\PickupPointValidateRequest;
14 use Packetery\Module\Carrier;
15 use Packetery\Module\Carrier\PacketaPickupPointsConfig;
16 use Packetery\Module\Options\Provider;
17 use Packetery\Module\Order;
18 use Packetery\Module\Order\PickupPointValidator;
19 use PacketeryLatte\Engine;
20 use PacketeryNette\Http\Request;
21
22 /**
23 * Class Checkout
24 *
25 * @package Packetery
26 */
27 class Checkout {
28
29 private const NONCE_ACTION = 'packetery_checkout';
30 private const NONCE_NAME = '_wpnonce_packetery_checkout';
31
32 private const BUTTON_RENDERER_TABLE_ROW = 'table-row';
33 private const BUTTON_RENDERER_AFTER_RATE = 'after-rate';
34
35 /**
36 * PacketeryLatte engine
37 *
38 * @var Engine
39 */
40 private $latte_engine;
41
42 /**
43 * Options provider.
44 *
45 * @var Provider Options provider.
46 */
47 private $options_provider;
48
49 /**
50 * Carrier repository.
51 *
52 * @var Carrier\Repository Carrier repository.
53 */
54 private $carrierRepository;
55
56 /**
57 * Http request.
58 *
59 * @var Request Http request.
60 */
61 private $httpRequest;
62
63 /**
64 * Order repository.
65 *
66 * @var Order\Repository
67 */
68 private $orderRepository;
69
70 /**
71 * Currency switcher facade.
72 *
73 * @var CurrencySwitcherFacade
74 */
75 private $currencySwitcherFacade;
76
77 /**
78 * Packet auto submitter.
79 *
80 * @var Order\PacketAutoSubmitter
81 */
82 private $packetAutoSubmitter;
83
84 /**
85 * Pickup point validation API.
86 *
87 * @var PickupPointValidator
88 */
89 private $pickupPointValidator;
90
91 /**
92 * OrderFacade.
93 *
94 * @var Order\AttributeMapper
95 */
96 private $mapper;
97
98 /**
99 * RateCalculator.
100 *
101 * @var RateCalculator
102 */
103 private $rateCalculator;
104
105 /**
106 * Internal pickup points config.
107 *
108 * @var PacketaPickupPointsConfig
109 */
110 private $pickupPointsConfig;
111
112 /**
113 * Widget options builder.
114 *
115 * @var WidgetOptionsBuilder
116 */
117 private $widgetOptionsBuilder;
118
119 /**
120 * Carrier entity repository.
121 *
122 * @var Carrier\EntityRepository
123 */
124 private $carrierEntityRepository;
125
126 /**
127 * Checkout constructor.
128 *
129 * @param Engine $latte_engine PacketeryLatte engine.
130 * @param Provider $options_provider Options provider.
131 * @param Carrier\Repository $carrierRepository Carrier repository.
132 * @param Request $httpRequest Http request.
133 * @param Order\Repository $orderRepository Order repository.
134 * @param CurrencySwitcherFacade $currencySwitcherFacade Currency switcher facade.
135 * @param Order\PacketAutoSubmitter $packetAutoSubmitter Packet auto submitter.
136 * @param PickupPointValidator $pickupPointValidator Pickup point validation API.
137 * @param Order\AttributeMapper $mapper OrderFacade.
138 * @param RateCalculator $rateCalculator RateCalculator.
139 * @param PacketaPickupPointsConfig $pickupPointsConfig Internal pickup points config.
140 * @param WidgetOptionsBuilder $widgetOptionsBuilder Widget options builder.
141 * @param Carrier\EntityRepository $carrierEntityRepository Carrier repository.
142 */
143 public function __construct(
144 Engine $latte_engine,
145 Provider $options_provider,
146 Carrier\Repository $carrierRepository,
147 Request $httpRequest,
148 Order\Repository $orderRepository,
149 CurrencySwitcherFacade $currencySwitcherFacade,
150 Order\PacketAutoSubmitter $packetAutoSubmitter,
151 PickupPointValidator $pickupPointValidator,
152 Order\AttributeMapper $mapper,
153 RateCalculator $rateCalculator,
154 PacketaPickupPointsConfig $pickupPointsConfig,
155 WidgetOptionsBuilder $widgetOptionsBuilder,
156 Carrier\EntityRepository $carrierEntityRepository
157 ) {
158 $this->latte_engine = $latte_engine;
159 $this->options_provider = $options_provider;
160 $this->carrierRepository = $carrierRepository;
161 $this->httpRequest = $httpRequest;
162 $this->orderRepository = $orderRepository;
163 $this->currencySwitcherFacade = $currencySwitcherFacade;
164 $this->packetAutoSubmitter = $packetAutoSubmitter;
165 $this->pickupPointValidator = $pickupPointValidator;
166 $this->mapper = $mapper;
167 $this->rateCalculator = $rateCalculator;
168 $this->pickupPointsConfig = $pickupPointsConfig;
169 $this->widgetOptionsBuilder = $widgetOptionsBuilder;
170 $this->carrierEntityRepository = $carrierEntityRepository;
171 }
172
173 /**
174 * Check if chosen shipping rate is bound with Packeta pickup points
175 *
176 * @return bool
177 */
178 public function isPickupPointOrder(): bool {
179 $chosenMethod = $this->getChosenMethod();
180 $carrierId = $this->getCarrierId( $chosenMethod );
181
182 return $carrierId && $this->isPickupPointCarrier( $carrierId );
183 }
184
185 /**
186 * Check if chosen shipping rate is bound with Packeta home delivery
187 *
188 * @return bool
189 */
190 public function isHomeDeliveryOrder(): bool {
191 $chosenMethod = $this->getChosenMethod();
192 $carrierId = $this->getCarrierId( $chosenMethod );
193
194 return $carrierId && $this->carrierRepository->isHomeDeliveryCarrier( $carrierId );
195 }
196
197 /**
198 * Render widget button table row.
199 *
200 * @return void
201 */
202 public function renderWidgetButtonTableRow(): void {
203 if ( ! is_checkout() ) {
204 return;
205 }
206
207 $this->latte_engine->render(
208 PACKETERY_PLUGIN_DIR . '/template/checkout/widget-button-row.latte',
209 [
210 'renderer' => self::BUTTON_RENDERER_TABLE_ROW,
211 'logo' => Plugin::buildAssetUrl( 'public/packeta-symbol.png' ),
212 'translations' => [
213 'packeta' => __( 'Packeta', 'packeta' ),
214 ],
215 ]
216 );
217 }
218
219 /**
220 * Renders widget button and information about chosen pickup point
221 *
222 * @param \WC_Shipping_Rate $shippingRate Shipping rate.
223 */
224 public function renderWidgetButtonAfterShippingRate( \WC_Shipping_Rate $shippingRate ): void {
225 if ( ! is_checkout() ) {
226 return;
227 }
228
229 if ( ! $this->isPacketeryShippingMethod( $shippingRate->get_id() ) ) {
230 return;
231 }
232
233 $this->latte_engine->render(
234 PACKETERY_PLUGIN_DIR . '/template/checkout/widget-button.latte',
235 [
236 'renderer' => self::BUTTON_RENDERER_AFTER_RATE,
237 'logo' => Plugin::buildAssetUrl( 'public/packeta-symbol.png' ),
238 'translations' => [
239 'packeta' => __( 'Packeta', 'packeta' ),
240 ],
241 ]
242 );
243 }
244
245 /**
246 * Creates settings for checkout script.
247 *
248 * @return array
249 */
250 public function createSettings(): array {
251 $carriersConfigForWidget = [];
252 $carriers = $this->carrierEntityRepository->getAllCarriersIncludingNonFeed();
253
254 foreach ( $carriers as $carrier ) {
255 $optionId = Carrier\OptionPrefixer::getOptionId( $carrier->getId() );
256 $defaultPrice = $this->getRateCost(
257 Carrier\Options::createByCarrierId( $carrier->getId() ),
258 $this->getCartContentsTotalIncludingTax(),
259 $this->getCartWeightKg()
260 );
261
262 $carriersConfigForWidget[ $optionId ] = $this->widgetOptionsBuilder->getCarrierForCheckout(
263 $carrier,
264 $defaultPrice,
265 $optionId
266 );
267 }
268
269 return [
270 /**
271 * Filter widget language in checkout.
272 *
273 * @since 1.4.2
274 */
275 'language' => (string) apply_filters( 'packeta_widget_language', substr( get_locale(), 0, 2 ) ),
276 'country' => $this->getCustomerCountry(),
277 'weight' => $this->getCartWeightKg(),
278 'carrierConfig' => $carriersConfigForWidget,
279 // TODO: Settings are not updated on AJAX checkout update. Needs rework due to possible checkout solutions allowing cart update.
280 'isAgeVerificationRequired' => $this->isAgeVerification18PlusRequired(),
281 'pickupPointAttrs' => Order\Attribute::$pickupPointAttrs,
282 'homeDeliveryAttrs' => Order\Attribute::$homeDeliveryAttrs,
283 'appIdentity' => Plugin::getAppIdentity(),
284 'packeteryApiKey' => $this->options_provider->get_api_key(),
285 'widgetAutoOpen' => $this->options_provider->shouldWidgetOpenAutomatically(),
286 'translations' => [
287 'choosePickupPoint' => __( 'Choose pickup point', 'packeta' ),
288 'chooseAddress' => __( 'Check shipping address', 'packeta' ),
289 'addressValidationIsOutOfOrder' => __( 'Address validation is out of order', 'packeta' ),
290 'invalidAddressCountrySelected' => __( 'The selected country does not correspond to the destination country.', 'packeta' ),
291 'selectedShippingAddress' => __( 'Selected shipping address', 'packeta' ),
292 'addressIsValidated' => __( 'Address is validated', 'packeta' ),
293 'addressIsNotValidated' => __( 'Delivery address has not been verified.', 'packeta' ),
294 'addressIsNotValidatedAndRequiredByCarrier' => __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ),
295 ],
296 ];
297 }
298
299 /**
300 * Adds fields to checkout page to save the values later
301 */
302 public function renderHiddenInputFields(): void {
303 $this->latte_engine->render(
304 PACKETERY_PLUGIN_DIR . '/template/checkout/input_fields.latte',
305 [
306 'fields' => array_merge(
307 array_column( Order\Attribute::$pickupPointAttrs, 'name' ),
308 array_column( Order\Attribute::$homeDeliveryAttrs, 'name' )
309 ),
310 ]
311 );
312
313 wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
314 }
315
316 /**
317 * Checks if all pickup point attributes are set, sets an error otherwise.
318 */
319 public function validateCheckoutData(): void {
320 $chosenShippingMethod = $this->getChosenMethod();
321 WC()->session->set( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY, null );
322
323 if ( false === $this->isPacketeryShippingMethod( $chosenShippingMethod ) ) {
324 return;
325 }
326
327 $post = $this->httpRequest->getPost();
328 if ( ! wp_verify_nonce( $post[ self::NONCE_NAME ], self::NONCE_ACTION ) ) {
329 wp_nonce_ays( '' );
330 }
331
332 if ( $this->isShippingRateRestrictedByProductsCategory( $chosenShippingMethod, WC()->cart->get_cart_contents() ) ) {
333 wc_add_notice( __( 'Chosen delivery method is no longer available. Please choose another delivery method.', 'packeta' ), 'error' );
334
335 return;
336 }
337
338 if ( $this->isPickupPointOrder() ) {
339 $error = false;
340 /**
341 * Returns array always.
342 *
343 * @var array $required_attrs
344 */
345 $required_attrs = array_filter(
346 array_combine(
347 array_column( Order\Attribute::$pickupPointAttrs, 'name' ),
348 array_column( Order\Attribute::$pickupPointAttrs, 'required' )
349 )
350 );
351 foreach ( $required_attrs as $attr => $required ) {
352 $attr_value = null;
353 if ( isset( $post[ $attr ] ) ) {
354 $attr_value = $post[ $attr ];
355 }
356 if ( ! $attr_value ) {
357 $error = true;
358 }
359 }
360 if ( $error ) {
361 wc_add_notice( __( 'Pickup point is not chosen.', 'packeta' ), 'error' );
362 }
363
364 if ( ! $error && ! $this->carrierEntityRepository->isValidForCountry(
365 ( $post[ Order\Attribute::CARRIER_ID ] ? $post[ Order\Attribute::CARRIER_ID ] : null ),
366 $this->getCustomerCountry()
367 ) ) {
368 wc_add_notice( __( 'The selected Packeta carrier is not available for the selected delivery country.', 'packeta' ), 'error' );
369 $error = true;
370 }
371
372 if ( ! $error && PickupPointValidator::IS_ACTIVE ) {
373 $pickupPointId = $post[ Order\Attribute::POINT_ID ];
374 $carrierId = ( $post[ Order\Attribute::CARRIER_ID ] ?? null );
375 $carriersForValidation = $chosenShippingMethod;
376 if ( '' === $carrierId ) {
377 $carrierId = Carrier\Repository::INTERNAL_PICKUP_POINTS_ID;
378 $carriersForValidation = Carrier\Repository::INTERNAL_PICKUP_POINTS_ID;
379 }
380 $pickupPointValidationResponse = $this->pickupPointValidator->validate(
381 $this->getPickupPointValidateRequest(
382 $pickupPointId,
383 $carrierId,
384 ( is_numeric( $carrierId ) ? $pickupPointId : null ),
385 $carriersForValidation
386 )
387 );
388 if ( ! $pickupPointValidationResponse->isValid() ) {
389 wc_add_notice( __( 'The selected Packeta pickup point could not be validated. Please select another.', 'packeta' ), 'error' );
390 foreach ( $pickupPointValidationResponse->getErrors() as $validationError ) {
391 $reason = $this->pickupPointValidator->getTranslatedError()[ $validationError['code'] ];
392 // translators: %s: Reason for validation failure.
393 wc_add_notice( sprintf( __( 'Reason: %s', 'packeta' ), $reason ), 'error' );
394 }
395 }
396 }
397 }
398
399 if ( $this->isHomeDeliveryOrder() ) {
400 $carrierId = $this->getCarrierId( $chosenShippingMethod );
401 $optionId = Carrier\OptionPrefixer::getOptionId( $carrierId );
402 $carrierOption = get_option( $optionId );
403
404 $addressValidation = 'none';
405 if ( $carrierOption ) {
406 $addressValidation = ( $carrierOption['address_validation'] ?? $addressValidation );
407 }
408
409 if (
410 'required' === $addressValidation &&
411 (
412 ! isset( $post[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) ||
413 '1' !== $post[ Order\Attribute::ADDRESS_IS_VALIDATED ]
414 )
415 ) {
416 wc_add_notice( __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ), 'error' );
417 }
418 }
419 }
420
421 /**
422 * Saves pickup point and other Packeta information to order.
423 *
424 * @param int $orderId Order id.
425 *
426 * @throws \WC_Data_Exception When invalid data are passed during shipping address update.
427 */
428 public function updateOrderMeta( int $orderId ): void {
429 $chosenMethod = $this->getChosenMethod();
430 if ( false === $this->isPacketeryShippingMethod( $chosenMethod ) ) {
431 return;
432 }
433
434 $post = $this->httpRequest->getPost();
435
436 $propsToSave = [];
437 // Save carrier id for home delivery (we got no id from widget).
438 $carrierId = $this->getCarrierId( $chosenMethod );
439 if ( empty( $post[ Order\Attribute::CARRIER_ID ] ) && $carrierId ) {
440 $propsToSave[ Order\Attribute::CARRIER_ID ] = $carrierId;
441 }
442
443 $wcOrder = $this->orderRepository->getWcOrderById( $orderId );
444 if ( null === $wcOrder ) {
445 return;
446 }
447
448 if ( $this->isPickupPointOrder() ) {
449 if ( PickupPointValidator::IS_ACTIVE ) {
450 $pickupPointValidationError = WC()->session->get( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY );
451 if ( null !== $pickupPointValidationError ) {
452 // translators: %s: Message from downloader.
453 $wcOrder->add_order_note( sprintf( __( 'The selected Packeta pickup point could not be validated, reason: %s.', 'packeta' ), $pickupPointValidationError ) );
454 WC()->session->set( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY, null );
455 }
456 }
457
458 foreach ( Order\Attribute::$pickupPointAttrs as $attr ) {
459 $attrName = $attr['name'];
460 if ( ! isset( $post[ $attrName ] ) ) {
461 continue;
462 }
463 $attrValue = $post[ $attrName ];
464
465 $saveMeta = true;
466 if (
467 ( Order\Attribute::CARRIER_ID === $attrName && ! $attrValue ) ||
468 ( Order\Attribute::POINT_URL === $attrName && ! filter_var( $attrValue, FILTER_VALIDATE_URL ) )
469 ) {
470 $saveMeta = false;
471 }
472 if ( $saveMeta ) {
473 $propsToSave[ $attrName ] = $attrValue;
474 }
475
476 if ( $this->options_provider->replaceShippingAddressWithPickupPointAddress() ) {
477 $this->mapper->toWcOrderShippingAddress( $wcOrder, $attrName, (string) $attrValue );
478 }
479 }
480 $wcOrder->save();
481 }
482
483 $orderEntity = new Core\Entity\Order( (string) $orderId, $carrierId );
484 if (
485 isset( $post[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) &&
486 '1' === $post[ Order\Attribute::ADDRESS_IS_VALIDATED ] &&
487 $this->isHomeDeliveryOrder()
488 ) {
489 $validatedAddress = $this->mapper->toValidatedAddress( $post );
490 $orderEntity->setDeliveryAddress( $validatedAddress );
491 $orderEntity->setAddressValidated( true );
492 }
493
494 if ( 0.0 === $this->getCartWeightKg() && true === $this->options_provider->isDefaultWeightEnabled() ) {
495 $orderEntity->setWeight( $this->options_provider->getDefaultWeight() + $this->options_provider->getPackagingWeight() );
496 }
497
498 $pickupPoint = $this->mapper->toOrderEntityPickupPoint( $orderEntity, $propsToSave );
499 $orderEntity->setPickupPoint( $pickupPoint );
500
501 $this->orderRepository->save( $orderEntity );
502 $this->packetAutoSubmitter->handleEventAsync( Order\PacketAutoSubmitter::EVENT_ON_ORDER_CREATION_FE, $orderId );
503 }
504
505 /**
506 * Registers Packeta checkout hooks
507 */
508 public function register_hooks(): void {
509 // This action works for both classic and Divi templates.
510 add_action( 'woocommerce_review_order_before_submit', [ $this, 'renderHiddenInputFields' ] );
511
512 add_action( 'woocommerce_checkout_process', array( $this, 'validateCheckoutData' ) );
513 add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'updateOrderMeta' ) );
514 if ( ! is_admin() ) {
515 add_filter( 'woocommerce_available_payment_gateways', [ $this, 'filterPaymentGateways' ] );
516 }
517 add_action( 'woocommerce_review_order_before_shipping', array( $this, 'updateShippingRates' ), 10, 2 );
518 add_action( 'woocommerce_cart_calculate_fees', [ $this, 'calculateFees' ] );
519 add_action(
520 'init',
521 function () {
522 /**
523 * Tells if widget button table row should be used.
524 *
525 * @since 1.3.0
526 */
527 if ( $this->options_provider->getCheckoutWidgetButtonLocation() === 'after_transport_methods' ) {
528 add_action( 'woocommerce_review_order_after_shipping', [ $this, 'renderWidgetButtonTableRow' ] );
529 } else {
530 add_action( 'woocommerce_after_shipping_rate', [ $this, 'renderWidgetButtonAfterShippingRate' ] );
531 }
532 }
533 );
534 }
535
536 /**
537 * Updates shipping rates cost based on cart properties.
538 * To test, change the shipping price during the transition from the first to the second step of the cart.
539 */
540 public function updateShippingRates(): void {
541 $packages = WC()->shipping()->get_packages();
542 foreach ( $packages as $i => $package ) {
543 WC()->session->set( 'shipping_for_package_' . $i, false );
544 }
545 }
546
547 /**
548 * Gets customer country from WC cart.
549 *
550 * @return string
551 */
552 public function getCustomerCountry(): string {
553 $country = strtolower( WC()->customer->get_shipping_country() );
554 if ( ! $country ) {
555 $country = strtolower( WC()->customer->get_billing_country() );
556 }
557
558 return $country;
559 }
560
561 /**
562 * Gets cart contents weight in kg.
563 *
564 * @return float
565 */
566 public function getCartWeightKg(): float {
567 $weight = WC()->cart->cart_contents_weight;
568 $weightKg = (float) wc_get_weight( $weight, 'kg' );
569 if ( $weightKg ) {
570 $weightKg += $this->options_provider->getPackagingWeight();
571 }
572
573 return $weightKg;
574 }
575
576 /**
577 * Calculates fees.
578 *
579 * @return void
580 */
581 public function calculateFees(): void {
582 $chosenShippingMethod = $this->getChosenMethod();
583 if ( false === $this->isPacketeryShippingMethod( $chosenShippingMethod ) ) {
584 return;
585 }
586
587 $carrierOptions = Carrier\Options::createByOptionId( $chosenShippingMethod );
588 $chosenCarrier = $this->carrierEntityRepository->getAnyById( $this->getCarrierIdFromShippingMethod( $chosenShippingMethod ) );
589 $maxTaxClass = $this->getTaxClassWithMaxRate();
590
591 if ( $carrierOptions->hasCouponFreeShippingForFeesAllowed() && $this->isFreeShippingCouponApplied() ) {
592 return;
593 }
594
595 if (
596 null !== $chosenCarrier &&
597 $chosenCarrier->supportsAgeVerification() &&
598 null !== $carrierOptions->getAgeVerificationFee() &&
599 $this->isAgeVerification18PlusRequired()
600 ) {
601 $feeAmount = $this->currencySwitcherFacade->getConvertedPrice( $carrierOptions->getAgeVerificationFee() );
602 WC()->cart->fees_api()->add_fee(
603 [
604 'id' => 'packetery-age-verification-fee',
605 'name' => __( 'Age verification fee', 'packeta' ),
606 'amount' => $feeAmount,
607 'taxable' => ! ( false === $maxTaxClass ),
608 'tax_class' => $maxTaxClass,
609 ]
610 );
611 }
612
613 $paymentMethod = WC()->session->get( 'chosen_payment_method' );
614 if ( empty( $paymentMethod ) || false === $this->isCodPaymentMethod( $paymentMethod ) ) {
615 return;
616 }
617
618 $applicableSurcharge = $this->getCODSurcharge( $carrierOptions->toArray(), $this->getCartPrice() );
619 $applicableSurcharge = $this->currencySwitcherFacade->getConvertedPrice( $applicableSurcharge );
620 if ( 0 >= $applicableSurcharge ) {
621 return;
622 }
623
624 $fee = [
625 'id' => 'packetery-cod-surcharge',
626 'name' => __( 'COD surcharge', 'packeta' ),
627 'amount' => $applicableSurcharge,
628 'taxable' => ! ( false === $maxTaxClass ),
629 'tax_class' => $maxTaxClass,
630 ];
631
632 WC()->cart->fees_api()->add_fee( $fee );
633 }
634
635 /**
636 * Gets cart price. Value is cast to float because PHPDoc is not reliable.
637 *
638 * @return float
639 */
640 private function getCartPrice(): float {
641 return (float) WC()->cart->get_subtotal();
642 }
643
644 /**
645 * Prepare shipping rates based on cart properties.
646 *
647 * @return array
648 */
649 public function getShippingRates(): array {
650 $customerCountry = $this->getCustomerCountry();
651 $availableCarriers = $this->carrierEntityRepository->getByCountryIncludingNonFeed( $customerCountry );
652 $cartProducts = WC()->cart->get_cart_contents();
653 $cartPrice = $this->getCartContentsTotalIncludingTax();
654 $cartWeight = $this->getCartWeightKg();
655 $disallowedShippingRateIds = $this->getDisallowedShippingRateIds();
656 $isAgeVerificationRequired = $this->isAgeVerification18PlusRequired();
657
658 $customRates = [];
659 foreach ( $availableCarriers as $carrier ) {
660 if ( $isAgeVerificationRequired && false === $carrier->supportsAgeVerification() ) {
661 continue;
662 }
663
664 $optionId = Carrier\OptionPrefixer::getOptionId( $carrier->getId() );
665 $options = Carrier\Options::createByOptionId( $optionId );
666
667 if ( false === $options->isActive() ) {
668 continue;
669 }
670
671 if ( in_array( $optionId, $disallowedShippingRateIds, true ) ) {
672 continue;
673 }
674
675 if ( $this->isShippingRateRestrictedByProductsCategory( $optionId, $cartProducts ) ) {
676 continue;
677 }
678
679 $cost = $this->getRateCost( $options, $cartPrice, $cartWeight );
680 if ( null !== $cost ) {
681 $rateId = ShippingMethod::PACKETERY_METHOD_ID . ':' . $optionId;
682 $customRates[ $rateId ] = $this->createShippingRate( $options->getName(), $rateId, $cost );
683 }
684 }
685
686 return $customRates;
687 }
688
689 /**
690 * Computes custom rate cost for carrier using cart contents.
691 *
692 * @param Carrier\Options $options Carrier options.
693 * @param float $cartPrice Price.
694 * @param float|int $cartWeight Weight.
695 *
696 * @return ?float
697 */
698 private function getRateCost( Carrier\Options $options, float $cartPrice, $cartWeight ): ?float {
699 return $this->rateCalculator->getShippingRateCost( $options, $cartPrice, $cartWeight, $this->isFreeShippingCouponApplied() );
700 }
701
702 /**
703 * Tells if free shipping coupon is applied.
704 *
705 * @return bool
706 */
707 private function isFreeShippingCouponApplied(): bool {
708 return $this->rateCalculator->isFreeShippingCouponApplied( WC()->cart );
709 }
710
711 /**
712 * Gets applicable COD surcharge.
713 *
714 * @param array $carrierOptions Carrier options.
715 * @param float $cartPrice Cart price.
716 *
717 * @return float
718 */
719 private function getCODSurcharge( array $carrierOptions, float $cartPrice ): float {
720 if ( isset( $carrierOptions['surcharge_limits'] ) ) {
721 foreach ( $carrierOptions['surcharge_limits'] as $weightLimit ) {
722 if ( $cartPrice <= $weightLimit['order_price'] ) {
723 return (float) $weightLimit['surcharge'];
724 }
725 }
726 }
727
728 if ( isset( $carrierOptions['default_COD_surcharge'] ) && is_numeric( $carrierOptions['default_COD_surcharge'] ) ) {
729 return (float) $carrierOptions['default_COD_surcharge'];
730 }
731
732 return 0.0;
733 }
734
735 /**
736 * Get chosen shipping rate id.
737 *
738 * @return string
739 */
740 private function getChosenMethod(): string {
741 $postedShippingMethodArray = $this->httpRequest->getPost( 'shipping_method' );
742
743 if ( null !== $postedShippingMethodArray ) {
744 return $this->removeShippingMethodPrefix( current( $postedShippingMethodArray ) );
745 }
746
747 return $this->calculateShipping();
748 }
749
750 /**
751 * Calculates shipping without using POST data.
752 *
753 * @return string
754 */
755 private function calculateShipping(): string {
756 $chosenShippingRates = WC()->cart->calculate_shipping();
757 $chosenShippingRate = array_shift( $chosenShippingRates );
758
759 if ( $chosenShippingRate instanceof \WC_Shipping_Rate ) {
760 return $this->removeShippingMethodPrefix( $chosenShippingRate->get_id() );
761 }
762
763 return '';
764 }
765
766 /**
767 * Gets carrier id from chosen shipping method.
768 *
769 * @param string $chosenMethod Chosen shipping method.
770 *
771 * @return string|null
772 */
773 private function getCarrierId( string $chosenMethod ): ?string {
774 $carrierId = $this->getCarrierIdFromShippingMethod( $chosenMethod );
775 if ( null === $carrierId ) {
776 return null;
777 }
778
779 if ( $this->pickupPointsConfig->isCompoundCarrierId( $carrierId ) ) {
780 return Carrier\Repository::INTERNAL_PICKUP_POINTS_ID;
781 }
782
783 return $carrierId;
784 }
785
786 /**
787 * Gets feed ID or artificially created ID for internal purposes.
788 *
789 * @param string $chosenMethod Chosen method.
790 *
791 * @return string|null
792 */
793 private function getCarrierIdFromShippingMethod( string $chosenMethod ): ?string {
794 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
795 return null;
796 }
797
798 return Carrier\OptionPrefixer::removePrefix( $chosenMethod );
799 }
800
801 /**
802 * Checks if chosen shipping method is one of packetery.
803 *
804 * @param string $chosenMethod Chosen shipping method.
805 *
806 * @return bool
807 */
808 private function isPacketeryShippingMethod( string $chosenMethod ): bool {
809 $optionId = $this->removeShippingMethodPrefix( $chosenMethod );
810
811 return Carrier\OptionPrefixer::isOptionId( $optionId );
812 }
813
814 /**
815 * Gets ShippingRate's ID of extended id.
816 *
817 * @param string $chosenMethod Chosen shipping method.
818 *
819 * @return string
820 */
821 private function removeShippingMethodPrefix( string $chosenMethod ): string {
822 return str_replace( ShippingMethod::PACKETERY_METHOD_ID . ':', '', $chosenMethod );
823 }
824
825 /**
826 * Create shipping rate.
827 *
828 * @param string $name Name.
829 * @param string $optionId Option ID.
830 * @param float|null $cost Cost.
831 *
832 * @return array
833 */
834 private function createShippingRate( string $name, string $optionId, ?float $cost ): array {
835 return [
836 'label' => $name,
837 'id' => $optionId,
838 'cost' => $cost,
839 'taxes' => '',
840 'calc_tax' => 'per_order',
841 ];
842 }
843
844 /**
845 * Gets disallowed shipping rate ids.
846 *
847 * @return array
848 */
849 private function getDisallowedShippingRateIds(): array {
850 $cartProducts = WC()->cart->get_cart();
851
852 $arraysToMerge = [];
853 foreach ( $cartProducts as $cartProduct ) {
854 $productEntity = Product\Entity::fromPostId( $cartProduct['product_id'] );
855
856 if ( false === $productEntity->isPhysical() ) {
857 continue;
858 }
859
860 $arraysToMerge[] = $productEntity->getDisallowedShippingRateIds();
861 }
862
863 return array_unique( array_merge( [], ...$arraysToMerge ) );
864 }
865
866 /**
867 * Tells if age verification is required by products in cart.
868 *
869 * @return bool
870 */
871 private function isAgeVerification18PlusRequired(): bool {
872 $products = WC()->cart->get_cart();
873
874 foreach ( $products as $product ) {
875 $productEntity = Product\Entity::fromPostId( $product['product_id'] );
876 if ( $productEntity->isPhysical() && $productEntity->isAgeVerification18PlusRequired() ) {
877 return true;
878 }
879 }
880
881 return false;
882 }
883
884 /**
885 * Returns tax_class with the highest tax_rate of cart products, false if no product is taxable.
886 *
887 * @return false|string
888 */
889 private function getTaxClassWithMaxRate() {
890 $products = WC()->cart->get_cart();
891 $taxClasses = [];
892
893 foreach ( $products as $cartProduct ) {
894 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
895 if ( $product->is_taxable() ) {
896 $taxClasses[] = $product->get_tax_class();
897 }
898 }
899
900 if ( empty( $taxClasses ) ) {
901 return false;
902 }
903
904 $taxClasses = array_unique( $taxClasses );
905 if ( 1 === count( $taxClasses ) ) {
906 return $taxClasses[0];
907 }
908
909 $taxRates = [];
910 $customer = WC()->cart->get_customer();
911 foreach ( $taxClasses as $taxClass ) {
912 $taxRates[ $taxClass ] = \WC_Tax::get_rates( $taxClass, $customer );
913 }
914
915 $maxRate = 0;
916 $resultTaxClass = false;
917 foreach ( $taxRates as $taxClassName => $taxClassRates ) {
918 foreach ( $taxClassRates as $rate ) {
919 if ( $rate['rate'] > $maxRate ) {
920 $maxRate = $rate['rate'];
921 $resultTaxClass = $taxClassName;
922 }
923 }
924 }
925
926 return $resultTaxClass;
927 }
928
929 /**
930 * Tells cart contents total price including tax and discounts.
931 *
932 * @return float
933 */
934 private function getCartContentsTotalIncludingTax():float {
935 return (float) WC()->cart->get_cart_contents_total() + (float) WC()->cart->get_cart_contents_tax();
936 }
937
938 /**
939 * Check if given carrier is disabled in products categories in cart
940 *
941 * @param string $shippingRate Shipping rate.
942 * @param array $cartProducts Array of cart products.
943 *
944 * @return bool
945 */
946 private function isShippingRateRestrictedByProductsCategory( string $shippingRate, array $cartProducts ): bool {
947 if ( ! $cartProducts ) {
948 return false;
949 }
950
951 foreach ( $cartProducts as $cartProduct ) {
952 if ( ! isset( $cartProduct['product_id'] ) ) {
953 continue;
954 }
955 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
956 $productCategoryIds = $product->get_category_ids();
957
958 foreach ( $productCategoryIds as $productCategoryId ) {
959 $productCategoryEntity = ProductCategory\Entity::fromTermId( (int) $productCategoryId );
960 $disallowedCategoryShippingRates = $productCategoryEntity->getDisallowedShippingRateIds();
961 if ( in_array( $shippingRate, $disallowedCategoryShippingRates, true ) ) {
962 return true;
963 }
964 }
965 }
966
967 return false;
968 }
969
970 /**
971 * Filters out payment methods, that can not be used.
972 *
973 * @param array $availableGateways Available gateways.
974 *
975 * @return array
976 */
977 public function filterPaymentGateways( array $availableGateways ): array {
978 if ( ! is_checkout() ) {
979 return $availableGateways;
980 }
981
982 $chosenMethod = $this->calculateShipping();
983 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
984 return $availableGateways;
985 }
986
987 $carrier = $this->carrierEntityRepository->getAnyById( $this->getCarrierIdFromShippingMethod( $chosenMethod ) );
988 if ( null === $carrier ) {
989 return $availableGateways;
990 }
991
992 foreach ( $availableGateways as $key => $availableGateway ) {
993 if (
994 $this->isCodPaymentMethod( $availableGateway->id ) &&
995 ! $carrier->supportsCod()
996 ) {
997 unset( $availableGateways[ $key ] );
998 }
999 }
1000
1001 return $availableGateways;
1002 }
1003
1004 /**
1005 * Checks if payment method is a COD one.
1006 *
1007 * @param string $paymentMethod Payment method.
1008 *
1009 * @return bool
1010 */
1011 private function isCodPaymentMethod( string $paymentMethod ): bool {
1012 $codPaymentMethod = $this->options_provider->getCodPaymentMethod();
1013
1014 return ( null !== $codPaymentMethod && ! empty( $paymentMethod ) && $paymentMethod === $codPaymentMethod );
1015 }
1016
1017 /**
1018 * Creates PickupPointValidateRequest object.
1019 *
1020 * @param string $pickupPointId Pickup point id.
1021 * @param ?string $carrierId Carrier id.
1022 * @param ?string $pointCarrierId Carrier pickup point id.
1023 * @param string $chosenShippingMethod WC shipping method id.
1024 *
1025 * @return PickupPointValidateRequest
1026 */
1027 private function getPickupPointValidateRequest(
1028 string $pickupPointId,
1029 ?string $carrierId,
1030 ?string $pointCarrierId,
1031 string $chosenShippingMethod
1032 ): PickupPointValidateRequest {
1033 return new PickupPointValidateRequest(
1034 $pickupPointId,
1035 $carrierId,
1036 $pointCarrierId,
1037 $this->getCustomerCountry(),
1038 $this->getCarrierId( $chosenShippingMethod ),
1039 false,
1040 false,
1041 $this->getCartWeightKg(),
1042 $this->isAgeVerification18PlusRequired(),
1043 null
1044 );
1045 }
1046
1047 /**
1048 * Checks if chosen carrier has pickup points and sets carrier id in provided array.
1049 *
1050 * @param string $carrierId Carrier id.
1051 *
1052 * @return bool
1053 */
1054 public function isPickupPointCarrier( string $carrierId ): bool {
1055 if ( Carrier\Repository::INTERNAL_PICKUP_POINTS_ID === $carrierId ) {
1056 return true;
1057 }
1058 if ( $this->pickupPointsConfig->isVendorCarrierId( $carrierId ) ) {
1059 return true;
1060 }
1061
1062 return $this->carrierRepository->hasPickupPoints( (int) $carrierId );
1063 }
1064
1065 }
1066