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

1,201 lines 35.8 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 'savedData' => get_transient( $this->getTransientNamePacketaCheckoutData() ),
306 'translations' => [
307 'choosePickupPoint' => __( 'Choose pickup point', 'packeta' ),
308 'chooseAddress' => __( 'Check shipping address', 'packeta' ),
309 'addressValidationIsOutOfOrder' => __( 'Address validation is out of order', 'packeta' ),
310 'invalidAddressCountrySelected' => __( 'The selected country does not correspond to the destination country.', 'packeta' ),
311 'selectedShippingAddress' => __( 'Selected shipping address', 'packeta' ),
312 'addressIsValidated' => __( 'Address is validated', 'packeta' ),
313 'addressIsNotValidated' => __( 'Delivery address has not been verified.', 'packeta' ),
314 'addressIsNotValidatedAndRequiredByCarrier' => __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ),
315 ],
316 ];
317 }
318
319 /**
320 * Adds fields to the checkout page to save the values later
321 */
322 public function renderHiddenInputFields(): void {
323 $this->latte_engine->render(
324 PACKETERY_PLUGIN_DIR . '/template/checkout/input_fields.latte',
325 [
326 'fields' => array_merge(
327 array_column( Order\Attribute::$pickupPointAttrs, 'name' ),
328 array_column( Order\Attribute::$homeDeliveryAttrs, 'name' )
329 ),
330 ]
331 );
332 }
333
334 /**
335 * Checks if all pickup point attributes are set, sets an error otherwise.
336 */
337 public function validateCheckoutData(): void {
338 $chosenShippingMethod = $this->getChosenMethod();
339 WC()->session->set( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY, null );
340
341 if ( false === $this->isPacketeryShippingMethod( $chosenShippingMethod ) ) {
342 return;
343 }
344
345 $checkoutData = $this->getPostDataIncludingStoredData( $chosenShippingMethod );
346
347 if ( $this->isShippingRateRestrictedByProductsCategory( $chosenShippingMethod, WC()->cart->get_cart_contents() ) ) {
348 wc_add_notice( __( 'Chosen delivery method is no longer available. Please choose another delivery method.', 'packeta' ), 'error' );
349
350 return;
351 }
352
353 // Cannot be null because of previous condition.
354 $carrierId = $this->getCarrierId( $chosenShippingMethod );
355 $carrierOptions = Carrier\Options::createByCarrierId( $carrierId );
356 $paymentMethod = $this->getChosenPaymentMethod();
357
358 if ( null !== $paymentMethod && $carrierOptions->hasCheckoutPaymentMethodDisallowed( $paymentMethod ) ) {
359 wc_add_notice( __( 'Chosen delivery method is no longer available. Please choose another delivery method.', 'packeta' ), 'error' );
360
361 return;
362 }
363
364 if ( $this->isPickupPointOrder() ) {
365 $error = false;
366 /**
367 * Returns array always.
368 *
369 * @var array $required_attrs
370 */
371 $required_attrs = array_filter(
372 array_combine(
373 array_column( Order\Attribute::$pickupPointAttrs, 'name' ),
374 array_column( Order\Attribute::$pickupPointAttrs, 'required' )
375 )
376 );
377 foreach ( $required_attrs as $attr => $required ) {
378 $attr_value = null;
379 if ( isset( $checkoutData[ $attr ] ) ) {
380 $attr_value = $checkoutData[ $attr ];
381 }
382 if ( ! $attr_value ) {
383 $error = true;
384 }
385 }
386 if ( $error ) {
387 wc_add_notice( __( 'Pickup point is not chosen.', 'packeta' ), 'error' );
388 }
389
390 if (
391 ! $error &&
392 ! $this->carrierEntityRepository->isValidForCountry(
393 $carrierId,
394 $this->getCustomerCountry()
395 )
396 ) {
397 wc_add_notice( __( 'The selected Packeta carrier is not available for the selected delivery country.', 'packeta' ), 'error' );
398 $error = true;
399 }
400
401 if ( ! $error && PickupPointValidator::IS_ACTIVE ) {
402 $pickupPointId = $checkoutData[ Order\Attribute::POINT_ID ];
403 $carriersForValidation = $chosenShippingMethod;
404 if ( '' === $carrierId ) {
405 $carrierId = Entity\Carrier::INTERNAL_PICKUP_POINTS_ID;
406 $carriersForValidation = Entity\Carrier::INTERNAL_PICKUP_POINTS_ID;
407 }
408 $pickupPointValidationResponse = $this->pickupPointValidator->validate(
409 $this->getPickupPointValidateRequest(
410 $pickupPointId,
411 $carrierId,
412 ( is_numeric( $carrierId ) ? $pickupPointId : null ),
413 $carriersForValidation
414 )
415 );
416 if ( ! $pickupPointValidationResponse->isValid() ) {
417 wc_add_notice( __( 'The selected Packeta pickup point could not be validated. Please select another.', 'packeta' ), 'error' );
418 foreach ( $pickupPointValidationResponse->getErrors() as $validationError ) {
419 $reason = $this->pickupPointValidator->getTranslatedError()[ $validationError['code'] ];
420 // translators: %s: Reason for validation failure.
421 wc_add_notice( sprintf( __( 'Reason: %s', 'packeta' ), $reason ), 'error' );
422 }
423 }
424 }
425 }
426
427 if ( $this->isHomeDeliveryOrder() ) {
428 $optionId = Carrier\OptionPrefixer::getOptionId( $carrierId );
429 $carrierOption = get_option( $optionId );
430
431 $addressValidation = 'none';
432 if ( $carrierOption ) {
433 $addressValidation = ( $carrierOption['address_validation'] ?? $addressValidation );
434 }
435
436 if (
437 'required' === $addressValidation &&
438 (
439 ! isset( $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) ||
440 '1' !== $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ]
441 )
442 ) {
443 wc_add_notice( __( 'Delivery address has not been verified. Verification of delivery address is required by this carrier.', 'packeta' ), 'error' );
444 }
445 }
446 }
447
448 /**
449 * Saves pickup point and other Packeta information to order.
450 *
451 * @param int $orderId Order id.
452 *
453 * @throws \WC_Data_Exception When invalid data are passed during shipping address update.
454 */
455 public function updateOrderMeta( int $orderId ): void {
456 $chosenMethod = $this->getChosenMethod();
457 if ( false === $this->isPacketeryShippingMethod( $chosenMethod ) ) {
458 return;
459 }
460
461 $checkoutData = $this->getPostDataIncludingStoredData( $chosenMethod );
462 $propsToSave = [];
463 $carrierId = $this->getCarrierId( $chosenMethod );
464
465 $propsToSave[ Order\Attribute::CARRIER_ID ] = $carrierId;
466
467 $wcOrder = $this->orderRepository->getWcOrderById( $orderId );
468 if ( null === $wcOrder ) {
469 return;
470 }
471
472 if ( $this->isPickupPointOrder() ) {
473 if ( PickupPointValidator::IS_ACTIVE ) {
474 $pickupPointValidationError = WC()->session->get( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY );
475 if ( null !== $pickupPointValidationError ) {
476 // translators: %s: Message from downloader.
477 $wcOrder->add_order_note( sprintf( __( 'The selected Packeta pickup point could not be validated, reason: %s.', 'packeta' ), $pickupPointValidationError ) );
478 WC()->session->set( PickupPointValidator::VALIDATION_HTTP_ERROR_SESSION_KEY, null );
479 }
480 }
481
482 foreach ( Order\Attribute::$pickupPointAttrs as $attr ) {
483 $attrName = $attr['name'];
484 if ( ! isset( $checkoutData[ $attrName ] ) ) {
485 continue;
486 }
487 $attrValue = $checkoutData[ $attrName ];
488
489 $saveMeta = true;
490 if (
491 Order\Attribute::CARRIER_ID === $attrName ||
492 ( Order\Attribute::POINT_URL === $attrName && ! filter_var( $attrValue, FILTER_VALIDATE_URL ) )
493 ) {
494 $saveMeta = false;
495 }
496 if ( $saveMeta ) {
497 $propsToSave[ $attrName ] = $attrValue;
498 }
499
500 if ( $this->options_provider->replaceShippingAddressWithPickupPointAddress() ) {
501 $this->mapper->toWcOrderShippingAddress( $wcOrder, $attrName, (string) $attrValue );
502 }
503 }
504 $wcOrder->save();
505 }
506
507 $orderEntity = new Core\Entity\Order( (string) $orderId, $this->carrierEntityRepository->getAnyById( $carrierId ) );
508 if (
509 isset( $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) &&
510 '1' === $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ] &&
511 $this->isHomeDeliveryOrder()
512 ) {
513 $validatedAddress = $this->mapper->toValidatedAddress( $checkoutData );
514 $orderEntity->setDeliveryAddress( $validatedAddress );
515 $orderEntity->setAddressValidated( true );
516 }
517
518 if ( 0.0 === $this->getCartWeightKg() && true === $this->options_provider->isDefaultWeightEnabled() ) {
519 $orderEntity->setWeight( $this->options_provider->getDefaultWeight() + $this->options_provider->getPackagingWeight() );
520 }
521
522 $pickupPoint = $this->mapper->toOrderEntityPickupPoint( $orderEntity, $propsToSave );
523 $orderEntity->setPickupPoint( $pickupPoint );
524
525 delete_transient( $this->getTransientNamePacketaCheckoutData() );
526 $this->orderRepository->save( $orderEntity );
527 $this->packetAutoSubmitter->handleEventAsync( Order\PacketAutoSubmitter::EVENT_ON_ORDER_CREATION_FE, $orderId );
528 }
529
530 /**
531 * Registers Packeta checkout hooks
532 */
533 public function register_hooks(): void {
534 // This action works for both classic and Divi templates.
535 add_action( 'woocommerce_review_order_before_submit', [ $this, 'renderHiddenInputFields' ] );
536
537 add_action( 'woocommerce_checkout_process', array( $this, 'validateCheckoutData' ) );
538 add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'updateOrderMeta' ) );
539 if ( ! is_admin() ) {
540 add_filter( 'woocommerce_available_payment_gateways', [ $this, 'filterPaymentGateways' ] );
541 }
542 add_action( 'woocommerce_review_order_before_shipping', array( $this, 'updateShippingRates' ), 10, 2 );
543 add_filter( 'woocommerce_cart_shipping_packages', [ $this, 'updateShippingPackages' ] );
544 add_action( 'woocommerce_cart_calculate_fees', [ $this, 'calculateFees' ] );
545 add_action(
546 'init',
547 function () {
548 /**
549 * Tells if widget button table row should be used.
550 *
551 * @since 1.3.0
552 */
553 if ( $this->options_provider->getCheckoutWidgetButtonLocation() === 'after_transport_methods' ) {
554 add_action( 'woocommerce_review_order_after_shipping', [ $this, 'renderWidgetButtonTableRow' ] );
555 } else {
556 add_action( 'woocommerce_after_shipping_rate', [ $this, 'renderWidgetButtonAfterShippingRate' ] );
557 }
558 }
559 );
560 }
561
562 /**
563 * Updates shipping rates cost based on cart properties.
564 * To test, change the shipping price during the transition from the first to the second step of the cart.
565 */
566 public function updateShippingRates(): void {
567 $packages = WC()->shipping()->get_packages();
568 foreach ( $packages as $i => $package ) {
569 WC()->session->set( 'shipping_for_package_' . $i, false );
570 }
571 }
572
573 /**
574 * Updates shipping packages to make WooCommerce caching system work correctly.
575 * Package values are used in WooCommerce method \WC_Shipping::calculate_shipping_for_package().
576 * In order to generate package cache hash correctly by WooCommerce
577 * the package must contain all relevant information related to pricing.
578 *
579 * @param array $packages Packages.
580 *
581 * @return array
582 */
583 public function updateShippingPackages( array $packages ): array {
584 foreach ( $packages as &$package ) {
585 $package['packetery_payment_method'] = $this->getChosenPaymentMethod();
586 }
587
588 return $packages;
589 }
590
591 /**
592 * Gets customer country from WC cart.
593 *
594 * @return string
595 */
596 public function getCustomerCountry(): string {
597 $country = strtolower( WC()->customer->get_shipping_country() );
598 if ( ! $country ) {
599 $country = strtolower( WC()->customer->get_billing_country() );
600 }
601
602 return $country;
603 }
604
605 /**
606 * Gets cart contents weight in kg.
607 *
608 * @return float
609 */
610 public function getCartWeightKg(): float {
611 $weight = WC()->cart->cart_contents_weight;
612 $weightKg = (float) wc_get_weight( $weight, 'kg' );
613 if ( $weightKg ) {
614 $weightKg += $this->options_provider->getPackagingWeight();
615 }
616
617 return $weightKg;
618 }
619
620 /**
621 * Calculates fees.
622 *
623 * @return void
624 */
625 public function calculateFees(): void {
626 $chosenShippingMethod = $this->calculateShipping();
627 if ( false === $this->isPacketeryShippingMethod( $chosenShippingMethod ) ) {
628 return;
629 }
630
631 $carrierOptions = Carrier\Options::createByOptionId( $chosenShippingMethod );
632 $chosenCarrier = $this->carrierEntityRepository->getAnyById( $this->getCarrierIdFromShippingMethod( $chosenShippingMethod ) );
633 $maxTaxClass = $this->getTaxClassWithMaxRate();
634
635 if ( $carrierOptions->hasCouponFreeShippingForFeesAllowed() && $this->isFreeShippingCouponApplied() ) {
636 return;
637 }
638
639 if (
640 null !== $chosenCarrier &&
641 $chosenCarrier->supportsAgeVerification() &&
642 null !== $carrierOptions->getAgeVerificationFee() &&
643 $this->isAgeVerification18PlusRequired()
644 ) {
645 $feeAmount = $this->currencySwitcherFacade->getConvertedPrice( $carrierOptions->getAgeVerificationFee() );
646 WC()->cart->fees_api()->add_fee(
647 [
648 'id' => 'packetery-age-verification-fee',
649 'name' => __( 'Age verification fee', 'packeta' ),
650 'amount' => $feeAmount,
651 'taxable' => ! ( false === $maxTaxClass ),
652 'tax_class' => $maxTaxClass,
653 ]
654 );
655 }
656
657 $paymentMethod = $this->getChosenPaymentMethod();
658 if ( empty( $paymentMethod ) || false === $this->isCodPaymentMethod( $paymentMethod ) ) {
659 return;
660 }
661
662 $applicableSurcharge = $this->getCODSurcharge( $carrierOptions->toArray(), $this->getCartPrice() );
663 $applicableSurcharge = $this->currencySwitcherFacade->getConvertedPrice( $applicableSurcharge );
664 if ( 0 >= $applicableSurcharge ) {
665 return;
666 }
667
668 $fee = [
669 'id' => 'packetery-cod-surcharge',
670 'name' => __( 'COD surcharge', 'packeta' ),
671 'amount' => $applicableSurcharge,
672 'taxable' => ! ( false === $maxTaxClass ),
673 'tax_class' => $maxTaxClass,
674 ];
675
676 WC()->cart->fees_api()->add_fee( $fee );
677 }
678
679 /**
680 * Gets cart price. Value is cast to float because PHPDoc is not reliable.
681 *
682 * @return float
683 */
684 private function getCartPrice(): float {
685 return (float) WC()->cart->get_subtotal();
686 }
687
688 /**
689 * Prepare shipping rates based on cart properties.
690 *
691 * @return array
692 */
693 public function getShippingRates(): array {
694 $customerCountry = $this->getCustomerCountry();
695 $availableCarriers = $this->carrierEntityRepository->getByCountryIncludingNonFeed( $customerCountry );
696 $cartProducts = WC()->cart->get_cart_contents();
697 $cartPrice = $this->getCartContentsTotalIncludingTax();
698 $cartWeight = $this->getCartWeightKg();
699 $disallowedShippingRateIds = $this->getDisallowedShippingRateIds();
700 $isAgeVerificationRequired = $this->isAgeVerification18PlusRequired();
701
702 $customRates = [];
703 foreach ( $availableCarriers as $carrier ) {
704 if ( $isAgeVerificationRequired && false === $carrier->supportsAgeVerification() ) {
705 continue;
706 }
707
708 $optionId = Carrier\OptionPrefixer::getOptionId( $carrier->getId() );
709 $options = Carrier\Options::createByOptionId( $optionId );
710
711 if ( false === $options->isActive() ) {
712 continue;
713 }
714
715 if ( in_array( $optionId, $disallowedShippingRateIds, true ) ) {
716 continue;
717 }
718
719 if ( $this->isShippingRateRestrictedByProductsCategory( $optionId, $cartProducts ) ) {
720 continue;
721 }
722
723 $cost = $this->getRateCost( $options, $cartPrice, $cartWeight );
724 if ( null !== $cost ) {
725 $name = $this->getFormattedShippingMethodName( $options->getName(), $cost );
726 $rateId = ShippingMethod::PACKETERY_METHOD_ID . ':' . $optionId;
727 $customRates[ $rateId ] = $this->createShippingRate( $name, $rateId, $cost );
728 }
729 }
730
731 return $customRates;
732 }
733
734 /**
735 * Computes custom rate cost for carrier using cart contents.
736 *
737 * @param Carrier\Options $options Carrier options.
738 * @param float $cartPrice Price.
739 * @param float|int $cartWeight Weight.
740 *
741 * @return ?float
742 */
743 private function getRateCost( Carrier\Options $options, float $cartPrice, $cartWeight ): ?float {
744 return $this->rateCalculator->getShippingRateCost( $options, $cartPrice, $cartWeight, $this->isFreeShippingCouponApplied() );
745 }
746
747 /**
748 * Returns the shipping method name by price.
749 *
750 * @param string $name Shipping Rate Name.
751 * @param float $cost Shipping Rate Cost.
752 * @return string
753 */
754 private function getFormattedShippingMethodName( string $name, float $cost ): string {
755 if ( 0.0 === $cost ) {
756 return sprintf( '%s: %s', $name, __( 'Free', 'packeta' ) );
757 }
758
759 return $name;
760 }
761
762 /**
763 * Tells if free shipping coupon is applied.
764 *
765 * @return bool
766 */
767 private function isFreeShippingCouponApplied(): bool {
768 return $this->rateCalculator->isFreeShippingCouponApplied( WC()->cart );
769 }
770
771 /**
772 * Gets applicable COD surcharge.
773 *
774 * @param array $carrierOptions Carrier options.
775 * @param float $cartPrice Cart price.
776 *
777 * @return float
778 */
779 private function getCODSurcharge( array $carrierOptions, float $cartPrice ): float {
780 if ( isset( $carrierOptions['surcharge_limits'] ) ) {
781 foreach ( $carrierOptions['surcharge_limits'] as $weightLimit ) {
782 if ( $cartPrice <= $weightLimit['order_price'] ) {
783 return (float) $weightLimit['surcharge'];
784 }
785 }
786 }
787
788 if ( isset( $carrierOptions['default_COD_surcharge'] ) && is_numeric( $carrierOptions['default_COD_surcharge'] ) ) {
789 return (float) $carrierOptions['default_COD_surcharge'];
790 }
791
792 return 0.0;
793 }
794
795 /**
796 * Get chosen shipping rate id.
797 *
798 * @return string
799 */
800 private function getChosenMethod(): string {
801 $postedShippingMethodArray = $this->httpRequest->getPost( 'shipping_method' );
802
803 if ( null !== $postedShippingMethodArray ) {
804 return $this->removeShippingMethodPrefix( current( $postedShippingMethodArray ) );
805 }
806
807 return $this->calculateShipping();
808 }
809
810 /**
811 * Gets chosen payment method.
812 *
813 * @return string|null
814 */
815 private function getChosenPaymentMethod(): ?string {
816 $paymentMethod = WC()->session->get( 'chosen_payment_method' );
817 if ( $paymentMethod ) {
818 return $paymentMethod;
819 }
820
821 return null;
822 }
823
824 /**
825 * Calculates shipping without using POST data.
826 *
827 * @return string
828 */
829 private function calculateShipping(): string {
830 $chosenShippingRates = WC()->cart->calculate_shipping();
831 $chosenShippingRate = array_shift( $chosenShippingRates );
832
833 if ( $chosenShippingRate instanceof \WC_Shipping_Rate ) {
834 return $this->removeShippingMethodPrefix( $chosenShippingRate->get_id() );
835 }
836
837 return '';
838 }
839
840 /**
841 * Gets carrier id from chosen shipping method.
842 * TODO: It's actually an alias of getCarrierIdFromShippingMethod, remove it later.
843 *
844 * @param string $chosenMethod Chosen shipping method.
845 *
846 * @return string|null
847 */
848 private function getCarrierId( string $chosenMethod ): ?string {
849 $carrierId = $this->getCarrierIdFromShippingMethod( $chosenMethod );
850 if ( null === $carrierId ) {
851 return null;
852 }
853
854 return $carrierId;
855 }
856
857 /**
858 * Gets feed ID or artificially created ID for internal purposes.
859 *
860 * @param string $chosenMethod Chosen method.
861 *
862 * @return string|null
863 */
864 private function getCarrierIdFromShippingMethod( string $chosenMethod ): ?string {
865 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
866 return null;
867 }
868
869 return Carrier\OptionPrefixer::removePrefix( $chosenMethod );
870 }
871
872 /**
873 * Checks if chosen shipping method is one of packetery.
874 *
875 * @param string $chosenMethod Chosen shipping method.
876 *
877 * @return bool
878 */
879 private function isPacketeryShippingMethod( string $chosenMethod ): bool {
880 $optionId = $this->removeShippingMethodPrefix( $chosenMethod );
881
882 return Carrier\OptionPrefixer::isOptionId( $optionId );
883 }
884
885 /**
886 * Gets ShippingRate's ID of extended id.
887 *
888 * @param string $chosenMethod Chosen shipping method.
889 *
890 * @return string
891 */
892 private function removeShippingMethodPrefix( string $chosenMethod ): string {
893 return str_replace( ShippingMethod::PACKETERY_METHOD_ID . ':', '', $chosenMethod );
894 }
895
896 /**
897 * Create shipping rate.
898 *
899 * @param string $name Name.
900 * @param string $optionId Option ID.
901 * @param float|null $cost Cost.
902 *
903 * @return array
904 */
905 private function createShippingRate( string $name, string $optionId, ?float $cost ): array {
906 return [
907 'label' => $name,
908 'id' => $optionId,
909 'cost' => $cost,
910 'taxes' => '',
911 'calc_tax' => 'per_order',
912 ];
913 }
914
915 /**
916 * Gets disallowed shipping rate ids.
917 *
918 * @return array
919 */
920 private function getDisallowedShippingRateIds(): array {
921 $cartProducts = WC()->cart->get_cart();
922
923 $arraysToMerge = [];
924 foreach ( $cartProducts as $cartProduct ) {
925 $productEntity = Product\Entity::fromPostId( $cartProduct['product_id'] );
926
927 if ( false === $productEntity->isPhysical() ) {
928 continue;
929 }
930
931 $arraysToMerge[] = $productEntity->getDisallowedShippingRateIds();
932 }
933
934 return array_unique( array_merge( [], ...$arraysToMerge ) );
935 }
936
937 /**
938 * Tells if age verification is required by products in cart.
939 *
940 * @return bool
941 */
942 private function isAgeVerification18PlusRequired(): bool {
943 $products = WC()->cart->get_cart();
944
945 foreach ( $products as $product ) {
946 $productEntity = Product\Entity::fromPostId( $product['product_id'] );
947 if ( $productEntity->isPhysical() && $productEntity->isAgeVerification18PlusRequired() ) {
948 return true;
949 }
950 }
951
952 return false;
953 }
954
955 /**
956 * Returns tax_class with the highest tax_rate of cart products, false if no product is taxable.
957 *
958 * @return false|string
959 */
960 private function getTaxClassWithMaxRate() {
961 $products = WC()->cart->get_cart();
962 $taxClasses = [];
963
964 foreach ( $products as $cartProduct ) {
965 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
966 if ( $product->is_taxable() ) {
967 $taxClasses[] = $product->get_tax_class();
968 }
969 }
970
971 if ( empty( $taxClasses ) ) {
972 return false;
973 }
974
975 $taxClasses = array_unique( $taxClasses );
976 if ( 1 === count( $taxClasses ) ) {
977 return $taxClasses[0];
978 }
979
980 $taxRates = [];
981 $customer = WC()->cart->get_customer();
982 foreach ( $taxClasses as $taxClass ) {
983 $taxRates[ $taxClass ] = \WC_Tax::get_rates( $taxClass, $customer );
984 }
985
986 $maxRate = 0;
987 $resultTaxClass = false;
988 foreach ( $taxRates as $taxClassName => $taxClassRates ) {
989 foreach ( $taxClassRates as $rate ) {
990 if ( $rate['rate'] > $maxRate ) {
991 $maxRate = $rate['rate'];
992 $resultTaxClass = $taxClassName;
993 }
994 }
995 }
996
997 return $resultTaxClass;
998 }
999
1000 /**
1001 * Tells cart contents total price including tax and discounts.
1002 *
1003 * @return float
1004 */
1005 private function getCartContentsTotalIncludingTax():float {
1006 return (float) WC()->cart->get_cart_contents_total() + (float) WC()->cart->get_cart_contents_tax();
1007 }
1008
1009 /**
1010 * Check if given carrier is disabled in products categories in cart
1011 *
1012 * @param string $shippingRate Shipping rate.
1013 * @param array $cartProducts Array of cart products.
1014 *
1015 * @return bool
1016 */
1017 private function isShippingRateRestrictedByProductsCategory( string $shippingRate, array $cartProducts ): bool {
1018 if ( ! $cartProducts ) {
1019 return false;
1020 }
1021
1022 foreach ( $cartProducts as $cartProduct ) {
1023 if ( ! isset( $cartProduct['product_id'] ) ) {
1024 continue;
1025 }
1026 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
1027 $productCategoryIds = $product->get_category_ids();
1028
1029 foreach ( $productCategoryIds as $productCategoryId ) {
1030 $productCategoryEntity = ProductCategory\Entity::fromTermId( (int) $productCategoryId );
1031 $disallowedCategoryShippingRates = $productCategoryEntity->getDisallowedShippingRateIds();
1032 if ( in_array( $shippingRate, $disallowedCategoryShippingRates, true ) ) {
1033 return true;
1034 }
1035 }
1036 }
1037
1038 return false;
1039 }
1040
1041 /**
1042 * Filters out payment methods, that can not be used.
1043 *
1044 * @param array $availableGateways Available gateways.
1045 *
1046 * @return array
1047 */
1048 public function filterPaymentGateways( array $availableGateways ): array {
1049 global $wp;
1050
1051 if ( ! is_checkout() ) {
1052 return $availableGateways;
1053 }
1054
1055 $order = null;
1056 if ( isset( $wp->query_vars['order-pay'] ) && is_numeric( $wp->query_vars['order-pay'] ) ) {
1057 $order = $this->orderRepository->getById( (int) $wp->query_vars['order-pay'], true );
1058 }
1059
1060 if ( $order instanceof Entity\Order ) {
1061 $chosenMethod = Carrier\OptionPrefixer::getOptionId( $order->getCarrier()->getId() );
1062 } else {
1063 $chosenMethod = $this->calculateShipping();
1064 }
1065
1066 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
1067 return $availableGateways;
1068 }
1069
1070 $carrier = $this->carrierEntityRepository->getAnyById( $this->getCarrierIdFromShippingMethod( $chosenMethod ) );
1071 if ( null === $carrier ) {
1072 return $availableGateways;
1073 }
1074
1075 $carrierOptions = Carrier\Options::createByCarrierId( $this->getCarrierId( $chosenMethod ) );
1076 foreach ( $availableGateways as $key => $availableGateway ) {
1077 if (
1078 $this->isCodPaymentMethod( $availableGateway->id ) &&
1079 ! $carrier->supportsCod()
1080 ) {
1081 unset( $availableGateways[ $key ] );
1082 }
1083
1084 if ( $carrierOptions->hasCheckoutPaymentMethodDisallowed( $availableGateway->id ) ) {
1085 unset( $availableGateways[ $key ] );
1086 }
1087 }
1088
1089 return $availableGateways;
1090 }
1091
1092 /**
1093 * Checks if payment method is a COD one.
1094 *
1095 * @param string $paymentMethod Payment method.
1096 *
1097 * @return bool
1098 */
1099 private function isCodPaymentMethod( string $paymentMethod ): bool {
1100 $codPaymentMethod = $this->options_provider->getCodPaymentMethod();
1101
1102 return ( null !== $codPaymentMethod && ! empty( $paymentMethod ) && $paymentMethod === $codPaymentMethod );
1103 }
1104
1105 /**
1106 * Creates PickupPointValidateRequest object.
1107 *
1108 * @param string $pickupPointId Pickup point id.
1109 * @param ?string $carrierId Carrier id.
1110 * @param ?string $pointCarrierId Carrier pickup point id.
1111 * @param string $chosenShippingMethod WC shipping method id.
1112 *
1113 * @return PickupPointValidateRequest
1114 */
1115 private function getPickupPointValidateRequest(
1116 string $pickupPointId,
1117 ?string $carrierId,
1118 ?string $pointCarrierId,
1119 string $chosenShippingMethod
1120 ): PickupPointValidateRequest {
1121 return new PickupPointValidateRequest(
1122 $pickupPointId,
1123 $carrierId,
1124 $pointCarrierId,
1125 $this->getCustomerCountry(),
1126 $this->getCarrierId( $chosenShippingMethod ),
1127 false,
1128 false,
1129 $this->getCartWeightKg(),
1130 $this->isAgeVerification18PlusRequired(),
1131 null
1132 );
1133 }
1134
1135 /**
1136 * Checks if chosen carrier has pickup points and sets carrier id in provided array.
1137 *
1138 * @param string $carrierId Carrier id.
1139 *
1140 * @return bool
1141 */
1142 public function isPickupPointCarrier( string $carrierId ): bool {
1143 if ( $this->pickupPointsConfig->isInternalPickupPointCarrier( $carrierId ) ) {
1144 return true;
1145 }
1146
1147 return $this->carrierRepository->hasPickupPoints( (int) $carrierId );
1148 }
1149
1150 /**
1151 * Gets checkout POST data including stored pickup point if not present in the data.
1152 *
1153 * @param string $chosenShippingMethod Chosen shipping method id.
1154 *
1155 * @return array
1156 */
1157 private function getPostDataIncludingStoredData( string $chosenShippingMethod ): array {
1158 $checkoutData = $this->httpRequest->getPost();
1159 $savedCheckoutData = get_transient( $this->getTransientNamePacketaCheckoutData() );
1160 if ( ! is_array( $savedCheckoutData ) || ! is_array( $savedCheckoutData[ $chosenShippingMethod ] ) ) {
1161 return $checkoutData;
1162 }
1163
1164 $savedCarrierData = $savedCheckoutData[ $chosenShippingMethod ];
1165 if (
1166 empty( $checkoutData[ Order\Attribute::POINT_ID ] ) &&
1167 ! empty( $savedCarrierData[ Order\Attribute::POINT_ID ] )
1168 ) {
1169 foreach ( Order\Attribute::$pickupPointAttrs as $attribute ) {
1170 $checkoutData[ $attribute['name'] ] = $savedCarrierData[ $attribute['name'] ];
1171 }
1172 }
1173
1174 if (
1175 empty( $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) &&
1176 ! empty( $savedCarrierData[ Order\Attribute::ADDRESS_IS_VALIDATED ] )
1177 ) {
1178 foreach ( Order\Attribute::$homeDeliveryAttrs as $attribute ) {
1179 $checkoutData[ $attribute['name'] ] = $savedCarrierData[ $attribute['name'] ];
1180 }
1181 }
1182
1183 if (
1184 empty( $checkoutData[ Order\Attribute::CARRIER_ID ] ) &&
1185 ! empty( $savedCarrierData[ Order\Attribute::CARRIER_ID ] )
1186 ) {
1187 $checkoutData[ Order\Attribute::CARRIER_ID ] = $savedCarrierData[ Order\Attribute::CARRIER_ID ];
1188 }
1189
1190 return $checkoutData;
1191 }
1192
1193 /**
1194 * Gets name of transient for selected pickup point.
1195 */
1196 public function getTransientNamePacketaCheckoutData(): string {
1197 return 'packeta_checkout_data_' . wp_get_session_token();
1198 }
1199
1200 }
1201