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

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