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

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

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