PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / trunk
Subscriptions for WooCommerce with Stripe Recurring Payments vtrunk
1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 All 60 releases
subscription / includes / Frontend / Cart.php

Cart.php in Subscriptions for WooCommerce with Stripe Recurring Payments trunk, at includes/Frontend/Cart.php

617 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpringDevs\Subscription\Frontend;
4
5 use SpringDevs\Subscription\Illuminate\Helper;
6 use Automattic\WooCommerce\Blocks\Package;
7 use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
8 use Automattic\WooCommerce\StoreApi\Schemas\V1\CartItemSchema;
9 use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema;
10 use SpringDevs\Subscription\Illuminate\Subscription\Subscription;
11
12 /**
13 * Cart class
14 */
15 class Cart {
16
17 /**
18 * Initialize the class
19 */
20 public function __construct() {
21 add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_to_cart_item_data' ), 10, 2 );
22 add_action( 'woocommerce_blocks_loaded', array( $this, 'define_custom_schema' ) );
23 add_filter( 'woocommerce_cart_item_price', array( $this, 'change_price_cart_html' ), 10, 2 );
24 add_filter( 'woocommerce_cart_item_subtotal', array( $this, 'change_price_cart_html' ), 10, 2 );
25 add_action( 'woocommerce_cart_totals_after_order_total', array( $this, 'add_rows_order_total' ) );
26 add_action( 'woocommerce_review_order_after_order_total', array( $this, 'add_rows_order_total' ) );
27 add_filter( 'woocommerce_add_cart_item_data', array( $this, 'set_renew_status' ), 10, 2 );
28 add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ) );
29 add_filter( 'woocommerce_get_item_data', array( $this, 'set_line_item_meta' ), 10, 2 );
30 add_action( 'woocommerce_before_calculate_totals', array( $this, 'add_calculation_price_filter' ) );
31 add_action( 'woocommerce_calculate_totals', array( $this, 'remove_calculation_price_filter' ) );
32 add_action( 'woocommerce_after_calculate_totals', array( $this, 'remove_calculation_price_filter' ) );
33
34 add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'add_to_cart_validation' ), 10, 4 );
35 add_action( 'woocommerce_store_api_validate_add_to_cart', array( $this, 'add_to_cart_validation_store_api' ), 10, 1 );
36 }
37
38 /**
39 * Add to cart validation.
40 *
41 * @param bool $passed Passed ?.
42 * @param int $product_id Product Id.
43 * @param int $quantity Quantity.
44 * @param int $variation_id Variation Id.
45 *
46 * @return bool
47 */
48 public function add_to_cart_validation( bool $passed, int $product_id, int $quantity, int $variation_id = 0 ): bool {
49 $product_id = $variation_id > 0 ? $variation_id : $product_id;
50 $validation = $this->validate_cart_items( $product_id );
51
52 if ( $validation['failed'] ) {
53 $error_notice = empty( $validation['error_notice'] ) ? __( 'This product cannot be added to the cart.', 'subscription' ) : $validation['error_notice'];
54 wc_add_notice( $error_notice, 'error' );
55 return false;
56 }
57
58 // Validation passed.
59 return $passed;
60 }
61
62 /**
63 * Add to cart validation.
64 *
65 * @param \WC_Product $product Product.
66 * @throws \Exception If validation fails.
67 */
68 public function add_to_cart_validation_store_api( $product ) {
69 $product_id = $product->get_id();
70 $validation = $this->validate_cart_items( $product_id );
71
72 if ( $validation['failed'] ) {
73 $error_notice = empty( $validation['error_notice'] ) ? __( 'This product cannot be added to the cart.', 'subscription' ) : $validation['error_notice'];
74 throw new \Exception( esc_html( $error_notice ) );
75 }
76 }
77
78 /**
79 * Validate cart items.
80 *
81 * @param int $product_id Product Id.
82 * @return array
83 */
84 public function validate_cart_items( $product_id ) {
85 $cart_items = WC()->cart->cart_contents;
86
87 $product = Subscription::get_subs_product( $product_id );
88
89 $error_notice = null;
90 $failed = false;
91 $enabled = $product->is_enabled();
92
93 foreach ( $cart_items as $key => $cart_item ) {
94 if ( isset( $cart_item['subscription'] ) ) {
95 if ( $enabled ) {
96 $error_notice = __( 'You cannot purchase multiple subscriptions at the same time.', 'subscription' );
97 } else {
98 $error_notice = __( 'You cannot purchase a subscription and a non-subscription product at the same time.', 'subscription' );
99 }
100 $failed = true;
101 } elseif ( $enabled ) {
102 $error_notice = __( 'You cannot purchase a subscription along with other products. Please remove other products from your cart first.', 'subscription' );
103 $failed = true;
104 }
105 }
106
107 return [
108 'failed' => (bool) $failed,
109 'error_notice' => $error_notice,
110 ];
111 }
112
113 /**
114 * Add filter before cart calculation.
115 *
116 * @return void
117 */
118 public function add_calculation_price_filter() {
119 add_filter( 'woocommerce_product_get_price', array( $this, 'set_prices_for_calculation' ), 100, 2 );
120 }
121
122 /**
123 * Return 0 if product has trial.
124 *
125 * @param float $price Price.
126 * @param \WC_Product $product Product object.
127 *
128 * @return float
129 */
130 public function set_prices_for_calculation( $price, $product ) {
131 $product = Subscription::get_subs_product( $product );
132 if ( $product->is_enabled() && $product->is_type( 'simple' ) ) {
133 $trial_time_per = $product->get_meta( '_subscrpt_trial_timing_per' );
134 if ( ! empty( $trial_time_per ) && $trial_time_per > 0 && Helper::check_trial( $product->get_id() ) ) {
135 return 0;
136 }
137 }
138
139 return $price;
140 }
141
142 /**
143 * Remove filter after calculate calculation.
144 *
145 * @return void
146 */
147 public function remove_calculation_price_filter() {
148 remove_filter( 'woocommerce_product_get_price', array( $this, 'set_prices_for_calculation' ), 100 );
149 }
150
151 /**
152 * Set line item for display meta details.
153 *
154 * @param array $cart_item_data Cart Item Data.
155 * @param array $cart_item Cart Item.
156 *
157 * @return array
158 */
159 public function set_line_item_meta( $cart_item_data, $cart_item ) {
160 if ( isset( $cart_item['subscription'] ) ) {
161 if ( $cart_item['subscription']['trial'] ) {
162 $cart_item_data[] = array(
163 'key' => __( 'Free Trial', 'subscription' ),
164 'value' => $cart_item['subscription']['trial'],
165 'hidden' => true,
166 '__experimental_woocommerce_blocks_hidden' => false,
167 );
168 }
169 }
170
171 return $cart_item_data;
172 }
173
174 /**
175 * Check cart items if it's valid or not?
176 *
177 * @return void
178 */
179 public function check_cart_items() {
180 if ( subscrpt_pro_activated() ) {
181 return;
182 }
183 $cart_items = WC()->cart->cart_contents;
184 if ( is_array( $cart_items ) ) {
185 foreach ( $cart_items as $key => $value ) {
186 /**
187 * Product Object.
188 *
189 * @var \WC_Product $product
190 */
191 $product = $value['data'];
192 $product = Subscription::get_subs_product( $product );
193 if ( isset( $value['subscription'] ) ) {
194 if ( $product->is_type( 'simple' ) ) {
195 if ( Helper::get_typos( 1, $product->get_meta( '_subscrpt_timing_option' ) ) !== $value['subscription']['type'] || $product->get_trial() !== $value['subscription']['trial'] ) {
196 // remove the item.
197 wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
198 WC()->cart->remove_cart_item( $key );
199 }
200 } else {
201 // remove the item.
202 wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
203 WC()->cart->remove_cart_item( $key );
204 }
205 } elseif ( $product->get_meta( '_subscrpt_enabled' ) ) {
206 // remove the item.
207 wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
208 WC()->cart->remove_cart_item( $key );
209 }
210 }
211 }
212 }
213
214 /**
215 * Define custom schema.
216 *
217 * @return void
218 */
219 public function define_custom_schema() {
220 $this->register_endpoint_data(
221 array(
222 'endpoint' => CartItemSchema::IDENTIFIER,
223 'namespace' => 'sdevs_subscription',
224 'data_callback' => array( $this, 'extend_cart_item_data' ),
225 'schema_callback' => array( $this, 'extend_cart_item_schema' ),
226 'schema_type' => ARRAY_A,
227 )
228 );
229 $this->register_endpoint_data(
230 array(
231 'endpoint' => CartSchema::IDENTIFIER,
232 'namespace' => 'sdevs_subscription',
233 'data_callback' => array( $this, 'extend_cart_data' ),
234 'schema_callback' => array( $this, 'extend_cart_schema' ),
235 'schema_type' => ARRAY_A,
236 )
237 );
238 }
239
240 /**
241 * Register subscription product schema into cart/items endpoint.
242 *
243 * @return array Registered schema.
244 */
245 public function extend_cart_schema() {
246 return array(
247 'recurring_totals' => array(
248 'description' => __( 'List of recurring totals in cart.', 'subscription' ),
249 'type' => 'array',
250 'readonly' => true,
251 'recurring_totals' => array(
252 'price' => array(
253 'description' => __( 'price of the subscription, after any discount that applies to renewals.', 'subscription' ),
254 'type' => array( 'string' ),
255 'readonly' => true,
256 ),
257 'full_price' => array(
258 'description' => __( 'price of the subscription before any discount.', 'subscription' ),
259 'type' => array( 'string' ),
260 'readonly' => true,
261 ),
262 'first_price' => array(
263 'description' => __( 'amount charged today, after all discounts.', 'subscription' ),
264 'type' => array( 'string' ),
265 'readonly' => true,
266 ),
267 'has_recurring_discount' => array(
268 'description' => __( 'Whether a discount applies to renewals.', 'subscription' ),
269 'type' => array( 'boolean' ),
270 'readonly' => true,
271 ),
272 'has_one_time_discount' => array(
273 'description' => __( 'Whether a discount applies to the first payment only.', 'subscription' ),
274 'type' => array( 'boolean' ),
275 'readonly' => true,
276 ),
277 'recurring_limit' => array(
278 'description' => __( 'Number of payments the recurring discount covers. 0 means unlimited.', 'subscription' ),
279 'type' => array( 'number' ),
280 'readonly' => true,
281 ),
282 'time' => array(
283 'description' => __( 'time of the subscription.', 'subscription' ),
284 'type' => array( 'number' ),
285 'readonly' => true,
286 ),
287 'type' => array(
288 'description' => __( 'type of the subscription.', 'subscription' ),
289 'type' => array( 'string' ),
290 'readonly' => true,
291 ),
292 'description' => array(
293 'description' => __( 'price of the subscription description.', 'subscription' ),
294 'type' => array( 'string' ),
295 'readonly' => true,
296 ),
297 'can_user_cancel' => array(
298 'description' => __( 'Allow User Cancellation?', 'subscription' ),
299 'type' => array( 'string' ),
300 'readonly' => true,
301 ),
302 'max_no_payment' => array(
303 'description' => __( 'Maximum Total Payments', 'subscription' ),
304 'type' => array( 'number' ),
305 'readonly' => true,
306 ),
307 ),
308 ),
309 );
310 }
311
312 /**
313 * Register subscription product data into cart/items endpoint.
314 *
315 * @return array $item_data Registered data or empty array if condition is not satisfied.
316 */
317 public function extend_cart_data() {
318 $cart_items = WC()->cart->cart_contents;
319 $recurrings = array();
320 if ( $cart_items ) {
321 foreach ( $cart_items as $cart_item_key => $cart_item ) {
322 if ( isset( $cart_item['subscription'] ) && $cart_item['subscription']['type'] ) {
323 $cart_subscription = $cart_item['subscription'];
324 $start_date = Helper::start_date( $cart_subscription['trial'] );
325 $next_date = Helper::next_date(
326 ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'],
327 $cart_subscription['trial']
328 );
329
330 // Subscription timing & type
331 $time = $cart_subscription['time'];
332 $type = Helper::get_typos( $time, $cart_subscription['type'], true );
333
334 // Discount-aware totals, shared with the classic cart.
335 $price_data = Helper::build_cart_recurring_price_data( $cart_item, $cart_item_key, $type );
336
337 // Description
338 $description = empty( $cart_subscription['trial'] )
339 ? __( 'Next billing on', 'subscription' ) . ': ' . $next_date
340 : __( 'First billing on', 'subscription' ) . ': ' . $start_date;
341
342 $recurrings[] = apply_filters(
343 'subscrpt_cart_recurring_data',
344 array(
345 'price' => $price_data['total'],
346 'full_price' => $price_data['full_total'],
347 'first_price' => $price_data['first_total'],
348 'has_recurring_discount' => $price_data['has_recurring_discount'],
349 'has_one_time_discount' => $price_data['has_one_time_discount'],
350 'recurring_limit' => $price_data['recurring_limit'],
351 'time' => $time,
352 'type' => $type,
353 'description' => $description,
354 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ),
355 'max_no_payment' => $cart_item['data']->get_meta( '_subscrpt_max_no_payment' ),
356 ),
357 $cart_item
358 );
359 }
360 }
361 }
362
363 return $recurrings;
364 }
365
366 /**
367 * Register subscription product schema into cart/items endpoint.
368 *
369 * @return array Registered schema.
370 */
371 public function extend_cart_item_schema() {
372 return array(
373 'time' => array(
374 'description' => __( 'time of the subscription type.', 'subscription' ),
375 'type' => array( 'number', 'null' ),
376 'readonly' => true,
377 ),
378 'type' => array(
379 'description' => __( 'the subscription type.', 'subscription' ),
380 'type' => array( 'string', 'null' ),
381 'readonly' => true,
382 ),
383 'trial' => array(
384 'description' => __( 'the subscription trial.', 'subscription' ),
385 'type' => array( 'string', 'null' ),
386 'readonly' => true,
387 ),
388 'signup_fee' => array(
389 'description' => __( 'Signup Fee amount.', 'subscription' ),
390 'type' => array( 'string', 'null' ),
391 'readonly' => true,
392 ),
393 'cost' => array(
394 'description' => __( 'Recurring amount.', 'subscription' ),
395 'type' => array( 'string', 'null' ),
396 'readonly' => true,
397 ),
398 'max_no_payment' => array(
399 'description' => __( 'Maximum Total Payments', 'subscription' ),
400 'type' => array( 'number' ),
401 'readonly' => true,
402 ),
403 );
404 }
405
406 /**
407 * Register subscription product data into cart/items endpoint.
408 *
409 * @param array $cart_item Current cart item data.
410 *
411 * @return array $item_data Registered data or empty array if condition is not satisfied.
412 */
413 public function extend_cart_item_data( $cart_item ) {
414 $item_data = array(
415 'time' => null,
416 'type' => null,
417 'trial' => null,
418 'signup_fee' => null,
419 'cost' => null,
420 'max_no_payment' => null,
421 );
422
423 if ( isset( $cart_item['subscription'] ) ) {
424 $item_data = $cart_item['subscription'];
425 unset( $item_data['per_cost'] );
426 $item_data['cost'] = (float) $cart_item['subscription']['per_cost'] * $cart_item['quantity'];
427 }
428 if ( ! subscrpt_pro_activated() ) {
429 $item_data['time'] = null;
430 $item_data['signup_fee'] = null;
431 }
432
433 return $item_data;
434 }
435
436 /**
437 * Add product meta on cart item.
438 *
439 * @param array $cart_item_data cart_item_data.
440 * @param int $product_id Product ID.
441 *
442 * @return array
443 */
444 public function add_to_cart_item_data( array $cart_item_data, int $product_id ): array {
445 $product = Subscription::get_subs_product( $product_id );
446 if ( ! $product->is_type( 'simple' ) ) {
447 return $cart_item_data;
448 }
449 if ( $product->is_enabled() ) :
450 $subscription_data = array();
451 $subscription_data['time'] = null;
452 $subscription_data['type'] = $product->get_timing_option();
453 $subscription_data['trial'] = null;
454 if ( $product->has_trial() ) {
455 $subscription_data['trial'] = $product->get_trial();
456 }
457 $subscription_data['signup_fee'] = null;
458 $subscription_data['per_cost'] = $product->get_price();
459 $cart_item_data['subscription'] = apply_filters( 'subscrpt_block_simple_cart_item_data', $subscription_data, $product, $cart_item_data );
460 $cart_item_data['subscription']['max_no_payment'] = $product->get_meta( '_subscrpt_max_no_payment' );
461 endif;
462
463 return $cart_item_data;
464 }
465
466 /**
467 * Register endpoint data with the API.
468 *
469 * @param array $args Endpoint data to register.
470 */
471 protected function register_endpoint_data( $args ) {
472 if ( function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) {
473 woocommerce_store_api_register_endpoint_data( $args );
474 } else {
475 Package::container()->get( ExtendRestApi::class )->register_endpoint_data( $args );
476 }
477 }
478
479 /**
480 * Display formatted price on cart.
481 *
482 * @param string $price price.
483 * @param array $cart_item cart item.
484 *
485 * @return string
486 */
487 public function change_price_cart_html( $price, $cart_item ) {
488 $product = Subscription::get_subs_product( $cart_item['product_id'] );
489 if ( ! $product->is_type( 'simple' ) ) {
490 return $price;
491 }
492
493 if ( $product->is_enabled() ) {
494 return $product->get_price_html();
495 }
496
497 return $price;
498 }
499
500 /**
501 * Display "Recurring totals" on cart
502 *
503 * @return void
504 */
505 public function add_rows_order_total() {
506 $cart_items = WC()->cart->get_cart_contents();
507 $recurrs = Helper::get_recurrs_from_cart( $cart_items );
508 if ( 0 === count( $recurrs ) ) {
509 return;
510 }
511 ?>
512 <tr class="recurring-total">
513 <th><?php esc_html_e( 'Recurring totals', 'subscription' ); ?></th>
514 <td data-title="<?php esc_attr_e( 'Recurring totals', 'subscription' ); ?>">
515 <?php foreach ( $recurrs as $recurr ) : ?>
516 <p>
517 <span><?php echo wp_kses_post( $recurr['price_html'] ); ?></span>
518 <?php if ( $recurr['max_no_payment'] > 0 ) : ?>
519 <span>x <?php echo esc_html( $recurr['max_no_payment'] ); ?></span>
520 <?php endif; ?>
521 <br />
522
523 <small>
524 <?php
525 $billing_text = $recurr['trial_status']
526 ? __( 'First billing on', 'subscription' )
527 : __( 'Next billing on', 'subscription' );
528
529 echo esc_html( $billing_text . ': ' );
530 echo esc_html( $recurr['trial_status'] ? $recurr['start_date'] : $recurr['next_date'] );
531 ?>
532 </small>
533
534 <?php if ( ! empty( $recurr['has_one_time_discount'] ) ) : ?>
535 <br />
536 <small>
537 <?php
538 echo wp_kses_post(
539 sprintf(
540 // translators: 1: amount paid today, 2: amount charged on each renewal.
541 __( 'You pay %1$s today. %2$s will be charged from the next renewal.', 'subscription' ),
542 wc_price( $recurr['first_total'] ?? 0 ),
543 wc_price( $recurr['total'] ?? 0 )
544 )
545 );
546 ?>
547 </small>
548 <?php endif; ?>
549
550 <?php if ( ! empty( $recurr['has_recurring_discount'] ) && (int) ( $recurr['recurring_limit'] ?? 0 ) > 1 ) : ?>
551 <br />
552 <small>
553 <?php
554 echo wp_kses_post(
555 sprintf(
556 // translators: 1: number of discounted payments, 2: full price charged afterwards.
557 __( 'Discount applies to your first %1$s payments. After that, %2$s.', 'subscription' ),
558 number_format_i18n( (int) $recurr['recurring_limit'] ),
559 $recurr['full_price_html'] ?? ''
560 )
561 );
562 ?>
563 </small>
564 <?php endif; ?>
565
566 <?php if ( 'yes' === $recurr['can_user_cancel'] && 0 === (int) $recurr['max_no_payment'] ) : ?>
567 <br />
568 <small><?php esc_html_e( 'You can cancel subscription at any time!', 'subscription' ); ?></small>
569 <?php endif; ?>
570
571 <!-- add how many times will be build if _subscrpt_renewal_limit is not 0 -->
572 <?php if ( (int) $recurr['max_no_payment'] > 0 ) : ?>
573 <br>
574 <small>
575 <?php
576 echo wp_kses_post(
577 sprintf(
578 // translators: 1: number of installments, 2: total amount.
579 __( 'This subscription will be billed in %1$s installments, for a total of %2$s.', 'subscription' ),
580 esc_html( $recurr['max_no_payment'] ),
581 wc_price( $recurr['price'] * (int) $recurr['max_no_payment'] )
582 )
583 );
584 ?>
585 </small>
586 <?php endif; ?>
587 </p>
588 <?php endforeach; ?>
589 </td>
590 </tr>
591 <?php
592 }
593
594 /**
595 * Add renew status.
596 *
597 * @param array $cart_item_data cart_item_data.
598 * @param int $product_id Product ID.
599 *
600 * @return array
601 */
602 public function set_renew_status( $cart_item_data, $product_id ) {
603 $expired = Helper::subscription_exists( $product_id, 'expired' );
604 if ( $expired ) {
605 // Check if maximum payment limit has been reached
606 if ( subscrpt_is_max_payments_reached( $expired ) ) {
607 wc_add_notice( __( 'This subscription has reached its maximum payment limit and cannot be renewed further.', 'subscription' ), 'error' );
608 return $cart_item_data; // Don't add renew status
609 }
610
611 $cart_item_data['renew_subscrpt'] = true;
612 }
613
614 return $cart_item_data;
615 }
616 }
617