PluginProbe
seQura / 3.2.0
seQura v3.2.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 3.2.0, at src/Services/Cart/class-cart-service.php

691 lines 18.6 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\ServiceItem;
17 use SeQura\Core\Infrastructure\Logger\LogContextData;
18 use SeQura\WC\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Item\Registration_Item;
19 use SeQura\WC\Core\Extension\Infrastructure\Configuration\Configuration;
20 use SeQura\WC\Dto\Cart_Info;
21 use SeQura\WC\Services\Interface_Logger_Service;
22 use SeQura\WC\Services\Pricing\Interface_Pricing_Service;
23 use SeQura\WC\Services\Product\Interface_Product_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 * Configuration
48 *
49 * @var Configuration
50 */
51 private $configuration;
52
53 /**
54 * Pricing service
55 *
56 * @var Interface_Pricing_Service
57 */
58 private $pricing_service;
59
60 /**
61 * Logger
62 *
63 * @var Interface_Logger_Service
64 */
65 private $logger;
66
67 /**
68 * Constructor
69 */
70 public function __construct(
71 Interface_Product_Service $product_service,
72 Configuration $configuration,
73 Interface_Pricing_Service $pricing_service,
74 Interface_Logger_Service $logger
75 ) {
76 $this->product_service = $product_service;
77 $this->configuration = $configuration;
78 $this->pricing_service = $pricing_service;
79 $this->logger = $logger;
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 ): ?Registration_Item {
194 $registration_amount = $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 Registration_Item(
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, ?Registration_Item $reg_item, int $qty, $item ) {
215 $ref = $product->get_sku() ? $product->get_sku() : $product->get_id();
216 $name = \wp_strip_all_tags( $product->get_title() );
217 if ( $this->configuration->is_enabled_for_services() && $this->product_service->is_service( $product ) ) {
218 /**
219 * Filter the service end date.
220 *
221 * @since 2.0.0
222 */
223 $service_end_date = \apply_filters(
224 'woocommerce_sequra_add_service_end_date',
225 $this->product_service->get_service_end_date( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ),
226 $product,
227 $item
228 );
229
230 $is_duration = 0 === strpos( $service_end_date, 'P' );
231
232 return new ServiceItem(
233 $ref,
234 $name,
235 $this->pricing_service->to_cents( $total_price / $qty ),
236 $qty,
237 $product->is_downloadable(),
238 $this->pricing_service->to_cents( $total_price ) - ( $reg_item ? $reg_item->getTotalWithTax() : 0 ),
239 ! $is_duration ? $service_end_date : null,
240 $is_duration ? $service_end_date : null,
241 null, // supplier.
242 null // rendered.
243 );
244 }
245 return new ProductItem(
246 $ref,
247 $name,
248 $this->pricing_service->to_cents( $total_price / $qty ),
249 $qty,
250 $this->pricing_service->to_cents( $total_price ),
251 $product->is_downloadable(),
252 null, // perishable.
253 null, // personalized.
254 null, // restockable.
255 \wc_get_product_category_list( $product->get_id() ),
256 $product->get_description(),
257 null, // manufacturer.
258 null, // supplier.
259 $product->get_id(),
260 $product->get_permalink(),
261 null // tracking reference.
262 );
263 }
264
265 /**
266 * Get items in cart
267 *
268 * @return array<ProductItem|ServiceItem|RegistrationItem>
269 */
270 public function get_items( ?WC_Order $order ): array {
271
272 $items = array();
273
274 if ( ! $order && null !== WC()->cart ) {
275 // Cart items.
276 foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
277 $product = $this->product_service->get_product_instance( $this->get_product_id_from_item( $cart_item ) );
278 if ( ! $product ) {
279 continue;
280 }
281
282 // Registration item.
283 $reg_item = $this->get_registration_item( $product, (int) $cart_item['quantity'] );
284 if ( $reg_item ) {
285 $items[] = $reg_item;
286 }
287
288 $items[] = $this->get_item(
289 $product,
290 (float) $cart_item['line_subtotal'] + (float) $cart_item['line_subtotal_tax'],
291 $reg_item,
292 (int) $cart_item['quantity'],
293 $cart_item
294 );
295 }
296 } elseif ( $order ) {
297 /**
298 * Order item
299 *
300 * @var WC_Order_Item_Product $item
301 */
302 foreach ( $order->get_items() as $item ) {
303 if ( ! $item instanceof WC_Order_Item_Product ) {
304 continue;
305 }
306 $product = $item->get_product();
307 if ( ! $product ) {
308 continue;
309 }
310
311 // Registration item.
312 $reg_item = $this->get_registration_item( $product, (int) $item->get_quantity( 'edit' ) );
313 if ( $reg_item ) {
314 $items[] = $reg_item;
315 }
316
317 $items[] = $this->get_item(
318 $product,
319 (float) $item->get_subtotal( 'edit' ) + (float) $item->get_subtotal_tax( 'edit' ),
320 $reg_item,
321 (int) $item->get_quantity( 'edit' ),
322 $item
323 );
324 }
325 }
326
327 /**
328 * Filter cart items. Must return an array of ProductItem|ServiceItem
329 *
330 * @since 3.0.0
331 */
332 return \apply_filters( 'sequra_cart_service_get_items', $items, $order );
333 }
334
335 /**
336 * Get products in cart
337 *
338 * @return array<WC_Product>
339 */
340 public function get_products( ?WC_Order $order ): array {
341 $items = array();
342
343 if ( ! $order && null !== WC()->cart ) {
344 // Cart items.
345 foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
346 $product = $this->product_service->get_product_instance( $this->get_product_id_from_item( $cart_item ) );
347 if ( ! $product ) {
348 continue;
349 }
350
351 $items[] = $product;
352 }
353 } elseif ( $order ) {
354 /**
355 * Order item
356 *
357 * @var WC_Order_Item_Product $item
358 */
359 foreach ( $order->get_items() as $item ) {
360 if ( ! $item instanceof WC_Order_Item_Product ) {
361 continue;
362 }
363 $product = $item->get_product();
364 if ( ! $product ) {
365 continue;
366 }
367
368 $items[] = $product;
369 }
370 }
371
372 return $items;
373 }
374
375 /**
376 * Get handling items
377 *
378 * @return HandlingItem[]
379 */
380 public function get_handling_items( ?WC_Order $order = null ): array {
381 $items = array();
382 $shipping_total_with_tax = 0;
383
384 if ( ! $order && null !== WC()->cart ) {
385 $shipping_total_with_tax = (float) WC()->cart->shipping_total + (float) WC()->cart->shipping_tax_total;
386
387 /**
388 * Handling fee. Must contain at least props: name, total
389 *
390 * @var object $fee
391 */
392 foreach ( WC()->cart->get_fees() as $fee ) {
393 $total_with_tax = (float) ( $fee->total ?? 0 );
394 if ( $total_with_tax <= 0 ) {
395 // Fees with negative total are discount items.
396 continue;
397 }
398
399 $items[] = new HandlingItem(
400 $fee->name ?? 'handling',
401 esc_html__( 'Handling cost', 'sequra' ),
402 $this->pricing_service->to_cents( $total_with_tax )
403 );
404 }
405 } elseif ( $order ) {
406 $shipping_total_with_tax = (float) $order->get_shipping_total() + (float) $order->get_shipping_tax();
407
408 /**
409 * Handling fee order item
410 *
411 * @var WC_Order_Item_Fee $fee
412 */
413 foreach ( $order->get_items( 'fee' ) as $fee ) {
414 if ( ! $fee instanceof WC_Order_Item_Fee ) {
415 continue;
416 }
417
418 $total_with_tax = (float) ( $fee->get_total() ?? 0 );
419 if ( $total_with_tax <= 0 ) {
420 // Fees with negative total are discount items.
421 continue;
422 }
423
424 $items[] = new HandlingItem(
425 $fee->get_name(),
426 esc_html__( 'Handling cost', 'sequra' ),
427 $this->pricing_service->to_cents( $total_with_tax )
428 );
429 }
430 }
431
432 if ( $shipping_total_with_tax ) {
433 $items[] = new HandlingItem(
434 'handling',
435 esc_html__( 'Shipping cost', 'sequra' ),
436 $this->pricing_service->to_cents( $shipping_total_with_tax )
437 );
438 }
439
440 return $items;
441 }
442
443 /**
444 * Get discount items
445 *
446 * @return DiscountItem[]
447 */
448 public function get_discount_items( ?WC_Order $order = null ): array {
449 $items = array();
450 if ( ! $order && null !== WC()->cart ) {
451 $cart = WC()->cart;
452 /**
453 * Coupon cart object.
454 *
455 * @var WC_Coupon $coupon
456 */
457 foreach ( $cart->get_coupons() as $coupon ) {
458 $items[] = new DiscountItem(
459 $coupon->get_code(),
460 esc_html__( 'Discount', 'sequra' ),
461 -1 * $this->pricing_service->to_cents(
462 $cart->get_coupon_discount_amount( $coupon->get_code(), false )
463 )
464 );
465 }
466
467 /**
468 * Fee cart object. Must contain at least props: name, total
469 *
470 * @var object $fee
471 */
472 foreach ( $cart->get_fees() as $fee ) {
473 $total_with_tax = (float) ( $fee->total ?? 0 );
474 if ( $total_with_tax >= 0 ) {
475 // Fees with positive total are handling items.
476 continue;
477 }
478
479 $items[] = new DiscountItem(
480 $fee->name ?? 'discount',
481 esc_html__( 'Discount', 'sequra' ),
482 $this->pricing_service->to_cents( $total_with_tax )
483 );
484 }
485 } elseif ( $order ) {
486 /**
487 * Coupon order item
488 *
489 * @var WC_Order_Item_Coupon $coupon
490 */
491 foreach ( $order->get_items( 'coupon' ) as $coupon ) {
492 if ( ! $coupon instanceof WC_Order_Item_Coupon ) {
493 continue;
494 }
495
496 $items[] = new DiscountItem(
497 $coupon->get_code(),
498 $coupon->get_name(),
499 -1 * $this->pricing_service->to_cents(
500 (float) $coupon->get_discount( 'edit' ) + (float) $coupon->get_discount_tax( 'edit' )
501 )
502 );
503 }
504
505 /**
506 * Fee order item
507 *
508 * @var WC_Order_Item_Fee $fee
509 */
510 foreach ( $order->get_items( 'fee' ) as $fee ) {
511 if ( ! $fee instanceof WC_Order_Item_Fee ) {
512 continue;
513 }
514
515 $total_with_tax = (float) ( $fee->get_total() ?? 0 );
516 if ( $total_with_tax >= 0 ) {
517 // Fees with positive total are handling items.
518 continue;
519 }
520
521 $items[] = new DiscountItem(
522 $fee->get_name(),
523 esc_html__( 'Discount', 'sequra' ),
524 $this->pricing_service->to_cents( $total_with_tax )
525 );
526 }
527 }
528 return $items;
529 }
530
531 /**
532 * Get product ID from cart item
533 *
534 * @param array<string, mixed> $cart_item Cart item
535 */
536 private function get_product_id_from_item( $cart_item ): int {
537 return isset( $cart_item['product_id'] ) ? intval( $cart_item['product_id'] ) : 0;
538 }
539
540 /**
541 * Check if cart is eligible for service sale
542 */
543 private function is_eligible_for_service_sale(): bool {
544 if ( ! WC()->cart ) {
545 return false;
546 }
547 $eligible = false;
548 $services_count = 0;
549 foreach ( WC()->cart->cart_contents as $values ) {
550 if ( $this->product_service->is_service( $values['product_id'] ) ) {
551 $services_count += (int) $values['quantity'];
552 $eligible = ( 1 === $services_count );
553 }
554 }
555 /**
556 * Filter if cart is eligible for service sale
557 *
558 * @since 2.0.0
559 */
560 return \apply_filters( 'woocommerce_cart_is_elegible_for_service_sale', $eligible );
561 }
562
563 /**
564 * Check if cart is eligible for product sale
565 */
566 private function is_eligible_for_product_sale(): bool {
567 $eligible = true;
568 if ( WC()->cart ) {
569 global $wp;
570 // Only reject if all products are virtual (don't need shipping).
571 if ( isset( $wp->query_vars['order-pay'] ) ) { // if paying an order.
572 /**
573 * Order
574 *
575 * @var WC_Order $order
576 */
577 $order = \wc_get_order( (int) $wp->query_vars['order-pay'] );
578 if ( ! $order instanceof WC_Order || ! $order->needs_shipping_address() ) {
579 $this->logger->log_debug( 'Order doesn\'t need shipping address seQura will not be offered.', __FUNCTION__, __CLASS__ );
580 $eligible = false;
581 }
582 } elseif ( ! WC()->cart->needs_shipping() ) { // If paying cart.
583 $this->logger->log_debug( 'Cart doesn\'t need shipping seQura will not be offered.', __FUNCTION__, __CLASS__ );
584 $eligible = false;
585 }
586 } else {
587 $eligible = false;
588 }
589 /**
590 * Filter if cart is eligible for product sale
591 *
592 * @since 2.0.0
593 * @deprecated 3.0.0 Use woocommerce_cart_is_eligible_for_product_sale instead
594 */
595 $eligible = \apply_filters_deprecated( 'woocommerce_cart_is_elegible_for_product_sale', array( $eligible ), '3.0.0', 'woocommerce_cart_is_eligible_for_product_sale' );
596
597 /**
598 * Filter if cart is eligible for product sale
599 *
600 * @since 3.0.0
601 */
602 return \apply_filters( 'woocommerce_cart_is_eligible_for_product_sale', $eligible );
603 }
604
605 /**
606 * Check if conditions are met for showing seQura in checkout
607 */
608 public function is_available_in_checkout( ?WC_Order $order = null ): bool {
609 $return = ! empty( WC()->cart ) && $this->configuration->is_available_for_ip();
610 if ( ! $return ) {
611 $this->logger->log_debug( 'seQura is not available for this IP.', __FUNCTION__, __CLASS__ );
612 } else {
613 $is_enabled_for_services = $this->configuration->is_enabled_for_services();
614 if ( $is_enabled_for_services && ! $this->is_eligible_for_service_sale() ) {
615 $this->logger->log_debug( 'Order is not eligible for service sale.', __FUNCTION__, __CLASS__ );
616 $return = false;
617 }
618 if ( ! $is_enabled_for_services && ! $this->is_eligible_for_product_sale() ) {
619 $this->logger->log_debug( 'Order is not eligible for for product sale.', __FUNCTION__, __CLASS__ );
620 $return = false;
621 }
622 if ( $return ) {
623 if ( ! $order instanceof WC_Order ) {
624 foreach ( WC()->cart->get_cart_contents() as $values ) {
625 if ( $this->product_service->is_banned( (int) $values['product_id'] ) ) {
626 $this->logger->log_debug( 'Banned product in the cart. seQura will not be offered', __FUNCTION__, __CLASS__, array( new LogContextData( 'product_id', $values['product_id'] ) ) );
627 $return = false;
628 }
629
630 /**
631 * Filter if item is available in checkout
632 * Can receive an array with the cart item values or the WC_Order_Item instance
633 *
634 * @since 3.0.0
635 */
636 $return = \apply_filters( 'sequra_is_item_available_in_checkout', $return, $values );
637 if ( ! $return ) {
638 break;
639 }
640 }
641 } else {
642 // Order items.
643 foreach ( $order->get_items() as $item ) {
644 if ( method_exists( $item, 'get_product_id' ) ) {
645 /**
646 * Define type for $item to prevent linting errors
647 *
648 * @var WC_Order_Item_Product $item
649 */
650 $product_id = $item->get_product_id();
651 if ( $this->product_service->is_banned( $product_id ) ) {
652 $this->logger->log_debug( 'Banned product in the order. seQura will not be offered', __FUNCTION__, __CLASS__, array( new LogContextData( 'product_id', $product_id ) ) );
653 $return = false;
654 }
655 }
656 /**
657 * Filter if item is available in checkout
658 * Can receive an array with the cart item values or the WC_Order_Item instance
659 *
660 * @since 3.0.0
661 */
662 $return = \apply_filters( 'sequra_is_item_available_in_checkout', $return, $item );
663 if ( ! $return ) {
664 break;
665 }
666 }
667 }
668 }
669 }
670 /**
671 * Filter seQura availability at checkout
672 *
673 * @since 2.0.0
674 */
675 return \apply_filters( 'woocommerce_cart_sq_is_available_in_checkout', $return );
676 }
677
678 /**
679 * Get the total amount of the cart
680 *
681 * @return float|int
682 */
683 public function get_total( $in_cents = true ) {
684 $total = 0;
685 if ( null !== WC()->cart ) {
686 $total = (float) WC()->cart->total;
687 }
688 return $in_cents ? $this->pricing_service->to_cents( $total ) : $total;
689 }
690 }
691