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

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