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

1,177 lines 35.1 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 *
820 * @param string $chosenMethod Chosen shipping method.
821 *
822 * @return string|null
823 */
824 private function getCarrierId( string $chosenMethod ): ?string {
825 $carrierId = $this->getCarrierIdFromShippingMethod( $chosenMethod );
826 if ( null === $carrierId ) {
827 return null;
828 }
829
830 return $carrierId;
831 }
832
833 /**
834 * Gets feed ID or artificially created ID for internal purposes.
835 *
836 * @param string $chosenMethod Chosen method.
837 *
838 * @return string|null
839 */
840 private function getCarrierIdFromShippingMethod( string $chosenMethod ): ?string {
841 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
842 return null;
843 }
844
845 return Carrier\OptionPrefixer::removePrefix( $chosenMethod );
846 }
847
848 /**
849 * Checks if chosen shipping method is one of packetery.
850 *
851 * @param string $chosenMethod Chosen shipping method.
852 *
853 * @return bool
854 */
855 private function isPacketeryShippingMethod( string $chosenMethod ): bool {
856 $optionId = $this->removeShippingMethodPrefix( $chosenMethod );
857
858 return Carrier\OptionPrefixer::isOptionId( $optionId );
859 }
860
861 /**
862 * Gets ShippingRate's ID of extended id.
863 *
864 * @param string $chosenMethod Chosen shipping method.
865 *
866 * @return string
867 */
868 private function removeShippingMethodPrefix( string $chosenMethod ): string {
869 return str_replace( ShippingMethod::PACKETERY_METHOD_ID . ':', '', $chosenMethod );
870 }
871
872 /**
873 * Create shipping rate.
874 *
875 * @param string $name Name.
876 * @param string $optionId Option ID.
877 * @param float|null $cost Cost.
878 *
879 * @return array
880 */
881 private function createShippingRate( string $name, string $optionId, ?float $cost ): array {
882 return [
883 'label' => $name,
884 'id' => $optionId,
885 'cost' => $cost,
886 'taxes' => '',
887 'calc_tax' => 'per_order',
888 ];
889 }
890
891 /**
892 * Gets disallowed shipping rate ids.
893 *
894 * @return array
895 */
896 private function getDisallowedShippingRateIds(): array {
897 $cartProducts = WC()->cart->get_cart();
898
899 $arraysToMerge = [];
900 foreach ( $cartProducts as $cartProduct ) {
901 $productEntity = Product\Entity::fromPostId( $cartProduct['product_id'] );
902
903 if ( false === $productEntity->isPhysical() ) {
904 continue;
905 }
906
907 $arraysToMerge[] = $productEntity->getDisallowedShippingRateIds();
908 }
909
910 return array_unique( array_merge( [], ...$arraysToMerge ) );
911 }
912
913 /**
914 * Tells if age verification is required by products in cart.
915 *
916 * @return bool
917 */
918 private function isAgeVerification18PlusRequired(): bool {
919 $products = WC()->cart->get_cart();
920
921 foreach ( $products as $product ) {
922 $productEntity = Product\Entity::fromPostId( $product['product_id'] );
923 if ( $productEntity->isPhysical() && $productEntity->isAgeVerification18PlusRequired() ) {
924 return true;
925 }
926 }
927
928 return false;
929 }
930
931 /**
932 * Returns tax_class with the highest tax_rate of cart products, false if no product is taxable.
933 *
934 * @return false|string
935 */
936 private function getTaxClassWithMaxRate() {
937 $products = WC()->cart->get_cart();
938 $taxClasses = [];
939
940 foreach ( $products as $cartProduct ) {
941 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
942 if ( $product->is_taxable() ) {
943 $taxClasses[] = $product->get_tax_class();
944 }
945 }
946
947 if ( empty( $taxClasses ) ) {
948 return false;
949 }
950
951 $taxClasses = array_unique( $taxClasses );
952 if ( 1 === count( $taxClasses ) ) {
953 return $taxClasses[0];
954 }
955
956 $taxRates = [];
957 $customer = WC()->cart->get_customer();
958 foreach ( $taxClasses as $taxClass ) {
959 $taxRates[ $taxClass ] = \WC_Tax::get_rates( $taxClass, $customer );
960 }
961
962 $maxRate = 0;
963 $resultTaxClass = false;
964 foreach ( $taxRates as $taxClassName => $taxClassRates ) {
965 foreach ( $taxClassRates as $rate ) {
966 if ( $rate['rate'] > $maxRate ) {
967 $maxRate = $rate['rate'];
968 $resultTaxClass = $taxClassName;
969 }
970 }
971 }
972
973 return $resultTaxClass;
974 }
975
976 /**
977 * Tells cart contents total price including tax and discounts.
978 *
979 * @return float
980 */
981 private function getCartContentsTotalIncludingTax():float {
982 return (float) WC()->cart->get_cart_contents_total() + (float) WC()->cart->get_cart_contents_tax();
983 }
984
985 /**
986 * Check if given carrier is disabled in products categories in cart
987 *
988 * @param string $shippingRate Shipping rate.
989 * @param array $cartProducts Array of cart products.
990 *
991 * @return bool
992 */
993 private function isShippingRateRestrictedByProductsCategory( string $shippingRate, array $cartProducts ): bool {
994 if ( ! $cartProducts ) {
995 return false;
996 }
997
998 foreach ( $cartProducts as $cartProduct ) {
999 if ( ! isset( $cartProduct['product_id'] ) ) {
1000 continue;
1001 }
1002 $product = WC()->product_factory->get_product( $cartProduct['product_id'] );
1003 $productCategoryIds = $product->get_category_ids();
1004
1005 foreach ( $productCategoryIds as $productCategoryId ) {
1006 $productCategoryEntity = ProductCategory\Entity::fromTermId( (int) $productCategoryId );
1007 $disallowedCategoryShippingRates = $productCategoryEntity->getDisallowedShippingRateIds();
1008 if ( in_array( $shippingRate, $disallowedCategoryShippingRates, true ) ) {
1009 return true;
1010 }
1011 }
1012 }
1013
1014 return false;
1015 }
1016
1017 /**
1018 * Filters out payment methods, that can not be used.
1019 *
1020 * @param array $availableGateways Available gateways.
1021 *
1022 * @return array
1023 */
1024 public function filterPaymentGateways( array $availableGateways ): array {
1025 global $wp;
1026
1027 if ( ! is_checkout() ) {
1028 return $availableGateways;
1029 }
1030
1031 $order = null;
1032 if ( isset( $wp->query_vars['order-pay'] ) && is_numeric( $wp->query_vars['order-pay'] ) ) {
1033 $order = $this->orderRepository->getById( (int) $wp->query_vars['order-pay'], true );
1034 }
1035
1036 if ( $order instanceof Entity\Order ) {
1037 $chosenMethod = Carrier\OptionPrefixer::getOptionId( $order->getCarrier()->getId() );
1038 } else {
1039 $chosenMethod = $this->calculateShipping();
1040 }
1041
1042 if ( ! $this->isPacketeryShippingMethod( $chosenMethod ) ) {
1043 return $availableGateways;
1044 }
1045
1046 $carrier = $this->carrierEntityRepository->getAnyById( $this->getCarrierIdFromShippingMethod( $chosenMethod ) );
1047 if ( null === $carrier ) {
1048 return $availableGateways;
1049 }
1050
1051 $carrierOptions = Carrier\Options::createByCarrierId( $this->getCarrierId( $chosenMethod ) );
1052 foreach ( $availableGateways as $key => $availableGateway ) {
1053 if (
1054 $this->isCodPaymentMethod( $availableGateway->id ) &&
1055 ! $carrier->supportsCod()
1056 ) {
1057 unset( $availableGateways[ $key ] );
1058 }
1059
1060 if ( $carrierOptions->hasCheckoutPaymentMethodDisallowed( $availableGateway->id ) ) {
1061 unset( $availableGateways[ $key ] );
1062 }
1063 }
1064
1065 return $availableGateways;
1066 }
1067
1068 /**
1069 * Checks if payment method is a COD one.
1070 *
1071 * @param string $paymentMethod Payment method.
1072 *
1073 * @return bool
1074 */
1075 private function isCodPaymentMethod( string $paymentMethod ): bool {
1076 $codPaymentMethod = $this->options_provider->getCodPaymentMethod();
1077
1078 return ( null !== $codPaymentMethod && ! empty( $paymentMethod ) && $paymentMethod === $codPaymentMethod );
1079 }
1080
1081 /**
1082 * Creates PickupPointValidateRequest object.
1083 *
1084 * @param string $pickupPointId Pickup point id.
1085 * @param ?string $carrierId Carrier id.
1086 * @param ?string $pointCarrierId Carrier pickup point id.
1087 * @param string $chosenShippingMethod WC shipping method id.
1088 *
1089 * @return PickupPointValidateRequest
1090 */
1091 private function getPickupPointValidateRequest(
1092 string $pickupPointId,
1093 ?string $carrierId,
1094 ?string $pointCarrierId,
1095 string $chosenShippingMethod
1096 ): PickupPointValidateRequest {
1097 return new PickupPointValidateRequest(
1098 $pickupPointId,
1099 $carrierId,
1100 $pointCarrierId,
1101 $this->getCustomerCountry(),
1102 $this->getCarrierId( $chosenShippingMethod ),
1103 false,
1104 false,
1105 $this->getCartWeightKg(),
1106 $this->isAgeVerification18PlusRequired(),
1107 null
1108 );
1109 }
1110
1111 /**
1112 * Checks if chosen carrier has pickup points and sets carrier id in provided array.
1113 *
1114 * @param string $carrierId Carrier id.
1115 *
1116 * @return bool
1117 */
1118 public function isPickupPointCarrier( string $carrierId ): bool {
1119 if ( $this->pickupPointsConfig->isInternalPickupPointCarrier( $carrierId ) ) {
1120 return true;
1121 }
1122
1123 return $this->carrierRepository->hasPickupPoints( (int) $carrierId );
1124 }
1125
1126 /**
1127 * Gets checkout POST data including stored pickup point if not present in the data.
1128 *
1129 * @param string $chosenShippingMethod Chosen shipping method id.
1130 *
1131 * @return array
1132 */
1133 private function getPostDataIncludingStoredData( string $chosenShippingMethod ): array {
1134 $checkoutData = $this->httpRequest->getPost();
1135 $savedCheckoutData = get_transient( $this->getTransientNamePacketaCheckoutData() );
1136 if ( ! is_array( $savedCheckoutData ) || ! is_array( $savedCheckoutData[ $chosenShippingMethod ] ) ) {
1137 return $checkoutData;
1138 }
1139
1140 $savedCarrierData = $savedCheckoutData[ $chosenShippingMethod ];
1141 if (
1142 empty( $checkoutData[ Order\Attribute::POINT_ID ] ) &&
1143 ! empty( $savedCarrierData[ Order\Attribute::POINT_ID ] )
1144 ) {
1145 foreach ( Order\Attribute::$pickupPointAttrs as $attribute ) {
1146 $checkoutData[ $attribute['name'] ] = $savedCarrierData[ $attribute['name'] ];
1147 }
1148 }
1149
1150 if (
1151 empty( $checkoutData[ Order\Attribute::ADDRESS_IS_VALIDATED ] ) &&
1152 ! empty( $savedCarrierData[ Order\Attribute::ADDRESS_IS_VALIDATED ] )
1153 ) {
1154 foreach ( Order\Attribute::$homeDeliveryAttrs as $attribute ) {
1155 $checkoutData[ $attribute['name'] ] = $savedCarrierData[ $attribute['name'] ];
1156 }
1157 }
1158
1159 if (
1160 empty( $checkoutData[ Order\Attribute::CARRIER_ID ] ) &&
1161 ! empty( $savedCarrierData[ Order\Attribute::CARRIER_ID ] )
1162 ) {
1163 $checkoutData[ Order\Attribute::CARRIER_ID ] = $savedCarrierData[ Order\Attribute::CARRIER_ID ];
1164 }
1165
1166 return $checkoutData;
1167 }
1168
1169 /**
1170 * Gets name of transient for selected pickup point.
1171 */
1172 public function getTransientNamePacketaCheckoutData(): string {
1173 return 'packeta_checkout_data_' . wp_get_session_token();
1174 }
1175
1176 }
1177