PluginProbe
seQura / 4.0.0
seQura v4.0.0
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / src / Services / Cart / class-cart-service.php

class-cart-service.php in seQura 4.0.0, at src/Services/Cart/class-cart-service.php

710 lines 19.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cart service
4 *
5 * @package SeQura/WC
6 * @subpackage SeQura/WC/Services
7 */
8
9 namespace SeQura\WC\Services\Cart;
10
11 use DateTime;
12 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\DiscountItem;
13 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\HandlingItem;
14 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\OtherPaymentItem;
15 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\ProductItem;
16 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\RegistrationItem;
17 use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\ServiceItem;
18 use SeQura\Core\Infrastructure\Logger\LogContextData;
19 use SeQura\WC\Dto\Cart_Info;
20 use SeQura\WC\Services\Log\Interface_Logger_Service;
21 use SeQura\WC\Services\Pricing\Interface_Pricing_Service;
22 use SeQura\WC\Services\Product\Interface_Product_Service;
23 use SeQura\WC\Services\Shopper\Interface_Shopper_Service;
24 use WC_Coupon;
25 use WC_Order;
26 use WC_Order_Item_Coupon;
27 use WC_Order_Item_Fee;
28 use WC_Order_Item_Product;
29 use WC_Order_Refund;
30 use WC_Product;
31
32 /**
33 * Handle use cases related to cart
34 */
35 class Cart_Service implements Interface_Cart_Service {
36
37 private const SESSION_CART_INFO = 'sequra_cart_info';
38
39 /**
40 * Product service
41 *
42 * @var Interface_Product_Service
43 */
44 private $product_service;
45
46 /**
47 * Pricing service
48 *
49 * @var Interface_Pricing_Service
50 */
51 private $pricing_service;
52
53 /**
54 * Logger
55 *
56 * @var Interface_Logger_Service
57 */
58 private $logger;
59
60 /**
61 * Shopper service
62 *
63 * @var Interface_Shopper_Service
64 */
65 private $shopper_service;
66
67 /**
68 * Constructor
69 */
70 public function __construct(
71 Interface_Product_Service $product_service,
72 Interface_Pricing_Service $pricing_service,
73 Interface_Logger_Service $logger,
74 Interface_Shopper_Service $shopper_service
75 ) {
76 $this->product_service = $product_service;
77 $this->pricing_service = $pricing_service;
78 $this->logger = $logger;
79 $this->shopper_service = $shopper_service;
80 }
81
82 /**
83 * Get refund items
84 *
85 * @return OtherPaymentItem[]
86 */
87 public function get_refund_items( ?WC_Order $order = null ): array {
88 $items = array();
89 /**
90 * Order refund
91 *
92 * @var WC_Order_Refund $refund
93 */
94 foreach ( $order->get_refunds() as $refund ) {
95 $items[] = $this->create_refund_item( $refund->get_id(), $refund->get_amount( 'edit' ) );
96 }
97 return $items;
98 }
99
100 /**
101 * Create refund item instance
102 */
103 public function create_refund_item( ?int $id, float $amount ): OtherPaymentItem {
104 $cents = $this->pricing_service->to_cents( $amount );
105 if ( ! $id ) {
106 $id = $cents;
107 }
108 return new OtherPaymentItem( 'r_' . $id, 'Refund', -1 * $cents );
109 }
110
111 /**
112 * Get closest desired first charge date from cart items
113 */
114 private function update_first_charge_on( int $product_id, ?string &$first_charge_on ) {
115 $date = $this->product_service->get_desired_first_charge_date( $product_id );
116 if ( $date instanceof DateTime ) {
117 $formatted_date = $date->format( DateTime::ATOM );
118 $first_charge_on = $first_charge_on ? min( $first_charge_on, $formatted_date ) : $formatted_date;
119 }
120 }
121
122 /**
123 * Get closest desired first charge date from cart items
124 */
125 public function get_desired_first_charge_on( ?WC_Order $order = null ): ?string {
126 $first_charge_on = null;
127 if ( ! $order && null !== WC()->cart ) {
128 foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
129 $product_id = $this->get_product_id_from_item( $cart_item );
130 $this->update_first_charge_on( $product_id, $first_charge_on );
131 }
132 } elseif ( $order ) {
133 /**
134 * Order item
135 *
136 * @var WC_Order_Item_Product $item
137 */
138 foreach ( $order->get_items() as $item ) {
139 if ( ! $item instanceof WC_Order_Item_Product ) {
140 continue;
141 }
142 $product = $item->get_product();
143 if ( $product ) {
144 $this->update_first_charge_on( $product->get_id(), $first_charge_on );
145 }
146 }
147 }
148 return $first_charge_on;
149 }
150
151 /**
152 * Get seQura cart info data from session. If not exists, then initialize it.
153 *
154 * @param bool $initialize_if_not_exists If true, then initialize cart info if not exists.
155 */
156 public function get_cart_info_from_session( $initialize_if_not_exists = true ): ?Cart_Info {
157 $_session = WC()->session;
158 $raw_data = $_session->get( self::SESSION_CART_INFO, null );
159 if ( $raw_data ) {
160 return Cart_Info::from_array( $raw_data );
161 }
162
163 if ( ! $initialize_if_not_exists ) {
164 return null;
165 }
166
167 $cart_info = new Cart_Info();
168 $_session->set( self::SESSION_CART_INFO, $cart_info->to_array() );
169 return $cart_info;
170 }
171
172 /**
173 * Attempt to clear seQura cart info data from session.
174 */
175 public function clear_cart_info_from_session(): void {
176 if ( WC()->session ) {
177 WC()->session->set( self::SESSION_CART_INFO, null );
178 }
179 }
180
181 /**
182 * Check if cart info is valid
183 *
184 * @param ?Cart_Info $cart_info
185 */
186 public function is_cart_info_valid( $cart_info ): bool {
187 return $cart_info instanceof Cart_Info && $cart_info->ref;
188 }
189
190 /**
191 * Get registration item instance
192 */
193 private function get_registration_item( WC_Product $product, int $qty ): ?RegistrationItem {
194 $registration_amount = (int) $this->product_service->get_registration_amount( $product, true );
195 if ( $registration_amount <= 0 ) {
196 return null;
197 }
198
199 $ref = $product->get_sku() ? $product->get_sku() : $product->get_id();
200 $name = \wp_strip_all_tags( $product->get_title() );
201
202 return new RegistrationItem(
203 "$ref-reg",
204 "Reg. $name",
205 $registration_amount * $qty
206 );
207 }
208 /**
209 * Get item instance
210 *
211 * @param mixed $item The product item.
212 * @return ProductItem|ServiceItem
213 */
214 private function get_item( WC_Product $product, float $total_price, ?RegistrationItem $reg_item, int $qty, $item, string $country ) {
215 if ( $this->product_service->is_enabled_for_services( $country ) && $this->product_service->is_service( $product ) ) {
216 return $this->get_service_item( $product, $total_price, $qty, $reg_item, $item );
217 }
218 return $this->get_product_item( $product, $total_price, $qty );
219 }
220
221 /**
222 * Get ProductItem instance
223 */
224 private function get_product_item( WC_Product $product, float $total_price, int $qty ): ProductItem {
225 return new ProductItem(
226 $this->product_service->get_reference( $product ),
227 $this->product_service->get_name( $product ),
228 $this->pricing_service->to_cents( $total_price / $qty ),
229 $qty,
230 $this->pricing_service->to_cents( $total_price ),
231 $product->is_downloadable(),
232 null,
233 null,
234 null,
235 \wc_get_product_category_list( $product->get_id() ),
236 $product->get_description(),
237 null,
238 null,
239 $product->get_id(),
240 $product->get_permalink(),
241 null
242 );
243 }
244
245 /**
246 * Get ServiceItem instance
247 *
248 * @param mixed $item The product item.
249 */
250 private function get_service_item( WC_Product $product, float $total_price, int $qty, ?RegistrationItem $reg_item, $item ): ServiceItem {
251 /**
252 * Filter the service end date.
253 *
254 * @since 2.0.0
255 */
256 $service_end_date = \apply_filters(
257 'woocommerce_sequra_add_service_end_date',
258 $this->product_service->get_service_end_date( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ),
259 $product,
260 $item
261 );
262 $service_end_date = strval( $service_end_date );
263
264 $is_duration = 0 === strpos( $service_end_date, 'P' );
265
266 return new ServiceItem(
267 $this->product_service->get_reference( $product ),
268 $this->product_service->get_name( $product ),
269 $this->pricing_service->to_cents( $total_price / $qty ),
270 $qty,
271 $product->is_downloadable(),
272 $this->pricing_service->to_cents( $total_price ) - ( $reg_item ? $reg_item->getTotalWithTax() : 0 ),
273 ! $is_duration ? $service_end_date : null,
274 $is_duration ? $service_end_date : null,
275 null, // supplier.
276 null // rendered.
277 );
278 }
279
280 /**
281 * Get items in cart
282 *
283 * @return array<ProductItem|ServiceItem|RegistrationItem>
284 */
285 public function get_items( ?WC_Order $order ): array {
286
287 $items = array();
288 $country = $this->shopper_service->get_country( $order );
289
290 if ( ! $order && null !== WC()->cart ) {
291 // Cart items.
292 foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
293 $product = $this->product_service->get_product_instance( $this->get_product_id_from_item( $cart_item ) );
294 if ( ! $product ) {
295 continue;
296 }
297
298 // Registration item.
299 $reg_item = $this->get_registration_item( $product, (int) $cart_item['quantity'] );
300 if ( $reg_item ) {
301 $items[] = $reg_item;
302 }
303
304 $items[] = $this->get_item(
305 $product,
306 (float) $cart_item['line_subtotal'] + (float) $cart_item['line_subtotal_tax'],
307 $reg_item,
308 (int) $cart_item['quantity'],
309 $cart_item,
310 $country
311 );
312 }
313 } elseif ( $order ) {
314 /**
315 * Order item
316 *
317 * @var WC_Order_Item_Product $item
318 */
319 foreach ( $order->get_items() as $item ) {
320 if ( ! $item instanceof WC_Order_Item_Product ) {
321 continue;
322 }
323 $product = $item->get_product();
324 if ( ! $product ) {
325 continue;
326 }
327
328 // Registration item.
329 $reg_item = $this->get_registration_item( $product, (int) $item->get_quantity( 'edit' ) );
330 if ( $reg_item ) {
331 $items[] = $reg_item;
332 }
333
334 $items[] = $this->get_item(
335 $product,
336 (float) $item->get_subtotal( 'edit' ) + (float) $item->get_subtotal_tax( 'edit' ),
337 $reg_item,
338 (int) $item->get_quantity( 'edit' ),
339 $item,
340 $country
341 );
342 }
343 }
344
345 /**
346 * Filter cart items. Must return an array of ProductItem|ServiceItem
347 *
348 * @since 3.0.0
349 */
350 return \apply_filters( 'sequra_cart_service_get_items', $items, $order );
351 }
352
353 /**
354 * Get products in cart
355 *
356 * @return array<WC_Product>
357 */
358 public function get_products( ?WC_Order $order ): array {
359 $items = array();
360
361 if ( ! $order && null !== WC()->cart ) {
362 // Cart items.
363 foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
364 $product = $this->product_service->get_product_instance( $this->get_product_id_from_item( $cart_item ) );
365 if ( ! $product ) {
366 continue;
367 }
368
369 $items[] = $product;
370 }
371 } elseif ( $order ) {
372 /**
373 * Order item
374 *
375 * @var WC_Order_Item_Product $item
376 */
377 foreach ( $order->get_items() as $item ) {
378 if ( ! $item instanceof WC_Order_Item_Product ) {
379 continue;
380 }
381 $product = $item->get_product();
382 if ( ! $product ) {
383 continue;
384 }
385
386 $items[] = $product;
387 }
388 }
389
390 return $items;
391 }
392
393 /**
394 * Get handling items
395 *
396 * @return HandlingItem[]
397 */
398 public function get_handling_items( ?WC_Order $order = null ): array {
399 $items = array();
400 $shipping_total_with_tax = 0;
401
402 if ( ! $order && null !== WC()->cart ) {
403 $shipping_total_with_tax = (float) WC()->cart->shipping_total + (float) WC()->cart->shipping_tax_total;
404
405 /**
406 * Handling fee. Must contain at least props: name, total
407 *
408 * @var object $fee
409 */
410 foreach ( WC()->cart->get_fees() as $fee ) {
411 $total_with_tax = (float) ( $fee->total ?? 0 );
412 if ( $total_with_tax <= 0 ) {
413 // Fees with negative total are discount items.
414 continue;
415 }
416
417 $items[] = new HandlingItem(
418 $fee->name ?? 'handling',
419 esc_html__( 'Handling cost', 'sequra' ),
420 $this->pricing_service->to_cents( $total_with_tax )
421 );
422 }
423 } elseif ( $order ) {
424 $shipping_total_with_tax = (float) $order->get_shipping_total() + (float) $order->get_shipping_tax();
425
426 /**
427 * Handling fee order item
428 *
429 * @var WC_Order_Item_Fee $fee
430 */
431 foreach ( $order->get_items( 'fee' ) as $fee ) {
432 if ( ! $fee instanceof WC_Order_Item_Fee ) {
433 continue;
434 }
435
436 $total_with_tax = (float) ( $fee->get_total() ?? 0 );
437 if ( $total_with_tax <= 0 ) {
438 // Fees with negative total are discount items.
439 continue;
440 }
441
442 $items[] = new HandlingItem(
443 $fee->get_name(),
444 esc_html__( 'Handling cost', 'sequra' ),
445 $this->pricing_service->to_cents( $total_with_tax )
446 );
447 }
448 }
449
450 if ( $shipping_total_with_tax ) {
451 $items[] = new HandlingItem(
452 'handling',
453 esc_html__( 'Shipping cost', 'sequra' ),
454 $this->pricing_service->to_cents( $shipping_total_with_tax )
455 );
456 }
457
458 return $items;
459 }
460
461 /**
462 * Get discount items
463 *
464 * @return DiscountItem[]
465 */
466 public function get_discount_items( ?WC_Order $order = null ): array {
467 $items = array();
468 if ( ! $order && null !== WC()->cart ) {
469 $cart = WC()->cart;
470 /**
471 * Coupon cart object.
472 *
473 * @var WC_Coupon $coupon
474 */
475 foreach ( $cart->get_coupons() as $coupon ) {
476 $items[] = new DiscountItem(
477 $coupon->get_code(),
478 esc_html__( 'Discount', 'sequra' ),
479 -1 * $this->pricing_service->to_cents(
480 $cart->get_coupon_discount_amount( $coupon->get_code(), false )
481 )
482 );
483 }
484
485 /**
486 * Fee cart object. Must contain at least props: name, total
487 *
488 * @var object $fee
489 */
490 foreach ( $cart->get_fees() as $fee ) {
491 $total_with_tax = (float) ( $fee->total ?? 0 );
492 if ( $total_with_tax >= 0 ) {
493 // Fees with positive total are handling items.
494 continue;
495 }
496
497 $items[] = new DiscountItem(
498 $fee->name ?? 'discount',
499 esc_html__( 'Discount', 'sequra' ),
500 $this->pricing_service->to_cents( $total_with_tax )
501 );
502 }
503 } elseif ( $order ) {
504 /**
505 * Coupon order item
506 *
507 * @var WC_Order_Item_Coupon $coupon
508 */
509 foreach ( $order->get_items( 'coupon' ) as $coupon ) {
510 if ( ! $coupon instanceof WC_Order_Item_Coupon ) {
511 continue;
512 }
513
514 $items[] = new DiscountItem(
515 $coupon->get_code(),
516 $coupon->get_name(),
517 -1 * $this->pricing_service->to_cents(
518 (float) $coupon->get_discount( 'edit' ) + (float) $coupon->get_discount_tax( 'edit' )
519 )
520 );
521 }
522
523 /**
524 * Fee order item
525 *
526 * @var WC_Order_Item_Fee $fee
527 */
528 foreach ( $order->get_items( 'fee' ) as $fee ) {
529 if ( ! $fee instanceof WC_Order_Item_Fee ) {
530 continue;
531 }
532
533 $total_with_tax = (float) ( $fee->get_total() ?? 0 );
534 if ( $total_with_tax >= 0 ) {
535 // Fees with positive total are handling items.
536 continue;
537 }
538
539 $items[] = new DiscountItem(
540 $fee->get_name(),
541 esc_html__( 'Discount', 'sequra' ),
542 $this->pricing_service->to_cents( $total_with_tax )
543 );
544 }
545 }
546 return $items;
547 }
548
549 /**
550 * Get product ID from cart item
551 *
552 * @param array<string, mixed> $cart_item Cart item
553 */
554 private function get_product_id_from_item( $cart_item ): int {
555 return isset( $cart_item['product_id'] ) ? intval( $cart_item['product_id'] ) : 0;
556 }
557
558 /**
559 * Check if cart is eligible for service sale
560 */
561 private function is_eligible_for_service_sale(): bool {
562 if ( ! WC()->cart ) {
563 return false;
564 }
565 $eligible = false;
566 $services_count = 0;
567 foreach ( WC()->cart->cart_contents as $values ) {
568 if ( $this->product_service->is_service( $values['product_id'] ) ) {
569 $services_count += (int) $values['quantity'];
570 $eligible = ( 1 === $services_count );
571 }
572 }
573 /**
574 * Filter if cart is eligible for service sale
575 *
576 * @since 2.0.0
577 */
578 return \apply_filters( 'woocommerce_cart_is_elegible_for_service_sale', $eligible );
579 }
580
581 /**
582 * Check if cart is eligible for product sale
583 */
584 private function is_eligible_for_product_sale(): bool {
585 $eligible = true;
586 if ( WC()->cart ) {
587 global $wp;
588 // Only reject if all products are virtual (don't need shipping).
589 if ( isset( $wp->query_vars['order-pay'] ) ) { // if paying an order.
590 /**
591 * Order
592 *
593 * @var WC_Order $order
594 */
595 $order = \wc_get_order( (int) $wp->query_vars['order-pay'] );
596 if ( ! $order instanceof WC_Order || ! $order->needs_shipping_address() ) {
597 $this->logger->log_debug( 'Order doesn\'t need shipping address seQura will not be offered.', __FUNCTION__, __CLASS__ );
598 $eligible = false;
599 }
600 } elseif ( ! WC()->cart->needs_shipping() ) { // If paying cart.
601 $this->logger->log_debug( 'Cart doesn\'t need shipping seQura will not be offered.', __FUNCTION__, __CLASS__ );
602 $eligible = false;
603 }
604 } else {
605 $eligible = false;
606 }
607 /**
608 * Filter if cart is eligible for product sale
609 *
610 * @since 2.0.0
611 * @deprecated 3.0.0 Use woocommerce_cart_is_eligible_for_product_sale instead
612 */
613 $eligible = \apply_filters_deprecated( 'woocommerce_cart_is_elegible_for_product_sale', array( $eligible ), '3.0.0', 'woocommerce_cart_is_eligible_for_product_sale' );
614
615 /**
616 * Filter if cart is eligible for product sale
617 *
618 * @since 3.0.0
619 */
620 return \apply_filters( 'woocommerce_cart_is_eligible_for_product_sale', $eligible );
621 }
622
623 /**
624 * Check if conditions are met for showing seQura in checkout
625 */
626 public function is_available_in_checkout( ?WC_Order $order = null ): bool {
627 $return = ! empty( WC()->cart ) && $this->shopper_service->is_ip_allowed();
628 if ( ! $return ) {
629 $this->logger->log_debug( 'seQura is not available for this IP.', __FUNCTION__, __CLASS__ );
630 } else {
631 $country = $this->shopper_service->get_country( $order );
632 $is_enabled_for_services = $this->product_service->is_enabled_for_services( $country );
633 if ( $is_enabled_for_services && ! $this->is_eligible_for_service_sale() ) {
634 $this->logger->log_debug( 'Order is not eligible for service sale.', __FUNCTION__, __CLASS__ );
635 $return = false;
636 }
637 if ( ! $is_enabled_for_services && ! $this->is_eligible_for_product_sale() ) {
638 $this->logger->log_debug( 'Order is not eligible for for product sale.', __FUNCTION__, __CLASS__ );
639 $return = false;
640 }
641 if ( $return ) {
642 if ( ! $order instanceof WC_Order ) {
643 foreach ( WC()->cart->get_cart_contents() as $values ) {
644 if ( $this->product_service->is_banned( (int) $values['product_id'] ) ) {
645 $this->logger->log_debug( 'Banned product in the cart. seQura will not be offered', __FUNCTION__, __CLASS__, array( new LogContextData( 'product_id', $values['product_id'] ) ) );
646 $return = false;
647 }
648
649 /**
650 * Filter if item is available in checkout
651 * Can receive an array with the cart item values or the WC_Order_Item instance
652 *
653 * @since 3.0.0
654 */
655 $return = \apply_filters( 'sequra_is_item_available_in_checkout', $return, $values );
656 if ( ! $return ) {
657 break;
658 }
659 }
660 } else {
661 // Order items.
662 foreach ( $order->get_items() as $item ) {
663 if ( method_exists( $item, 'get_product_id' ) ) {
664 /**
665 * Define type for $item to prevent linting errors
666 *
667 * @var WC_Order_Item_Product $item
668 */
669 $product_id = $item->get_product_id();
670 if ( $this->product_service->is_banned( $product_id ) ) {
671 $this->logger->log_debug( 'Banned product in the order. seQura will not be offered', __FUNCTION__, __CLASS__, array( new LogContextData( 'product_id', $product_id ) ) );
672 $return = false;
673 }
674 }
675 /**
676 * Filter if item is available in checkout
677 * Can receive an array with the cart item values or the WC_Order_Item instance
678 *
679 * @since 3.0.0
680 */
681 $return = \apply_filters( 'sequra_is_item_available_in_checkout', $return, $item );
682 if ( ! $return ) {
683 break;
684 }
685 }
686 }
687 }
688 }
689 /**
690 * Filter seQura availability at checkout
691 *
692 * @since 2.0.0
693 */
694 return \apply_filters( 'woocommerce_cart_sq_is_available_in_checkout', $return );
695 }
696
697 /**
698 * Get the total amount of the cart
699 *
700 * @return float|int
701 */
702 public function get_total( $in_cents = true ) {
703 $total = 0;
704 if ( null !== WC()->cart ) {
705 $total = (float) WC()->cart->total;
706 }
707 return $in_cents ? $this->pricing_service->to_cents( $total ) : $total;
708 }
709 }
710