PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.1.0
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.1.0
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 All 281 releases
surecart / app / src / Models / Checkout.php
Checkout.php
798 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SureCart\Models;
4
5 use SureCart\Models\Traits\HasCustomer;
6 use SureCart\Models\Traits\HasSubscriptions;
7 use SureCart\Models\LineItem;
8 use SureCart\Models\Fee;
9 use SureCart\Models\Traits\CanFinalize;
10 use SureCart\Models\Traits\HasBillingAddress;
11 use SureCart\Models\Traits\HasDiscount;
12 use SureCart\Models\Traits\HasInvoice;
13 use SureCart\Models\Traits\HasPaymentFailures;
14 use SureCart\Models\Traits\HasPaymentIntent;
15 use SureCart\Models\Traits\HasPaymentMethod;
16 use SureCart\Models\Traits\HasProcessorType;
17 use SureCart\Models\Traits\HasPurchases;
18 use SureCart\Models\Traits\HasShippingAddress;
19 use SureCart\Models\Traits\HasShippingChoices;
20 use SureCart\Support\Currency;
21 use SureCart\Support\TimeDate;
22
23 /**
24 * Order model
25 */
26 class Checkout extends Model {
27 use HasCustomer;
28 use HasSubscriptions;
29 use HasDiscount;
30 use HasShippingAddress;
31 use HasShippingChoices;
32 use HasPaymentIntent;
33 use HasPaymentMethod;
34 use HasPurchases;
35 use CanFinalize;
36 use HasProcessorType;
37 use HasBillingAddress;
38 use HasPaymentFailures;
39 use HasInvoice;
40
41 /**
42 * Rest API endpoint
43 *
44 * @var string
45 */
46 protected $endpoint = 'checkouts';
47
48 /**
49 * Object name
50 *
51 * @var string
52 */
53 protected $object_name = 'checkout';
54
55 /**
56 * Need to pass the processor type on create
57 *
58 * @param array $attributes Optional attributes.
59 * @param string $type stripe, paypal, etc.
60 */
61 public function __construct( $attributes = [], $type = '' ) {
62 $this->processor_type = $type;
63 parent::__construct( $attributes );
64 }
65
66 /**
67 * Get the display subtotal amount attribute.
68 *
69 * @return string
70 */
71 public function getAmountDueDisplayAmountAttribute() {
72 return Currency::format( $this->amount_due ?? 0, $this->currency );
73 }
74
75 /**
76 * Get the display subtotal amount attribute.
77 *
78 * @return string
79 */
80 public function getSubtotalDisplayAmountAttribute() {
81 return ! empty( $this->subtotal_amount ) ? Currency::format( $this->subtotal_amount, $this->currency ) : '';
82 }
83
84 /**
85 * Get the display discounts amount attribute.
86 *
87 * @return string
88 */
89 public function getDiscountsDisplayAmountAttribute() {
90 return ! empty( $this->discount_amount ) ? Currency::format( $this->discount_amount, $this->currency ) : '';
91 }
92
93 /**
94 * Get the display tax reverse charged amount attribute.
95 *
96 * @return string
97 */
98 public function getTaxReverseChargedDisplayAmountAttribute() {
99 return ! empty( $this->tax_reverse_charged_amount ) ? Currency::format( $this->tax_reverse_charged_amount, $this->currency ) : '';
100 }
101
102 /**
103 * Get the display tax reverse charged amount attribute.
104 *
105 * @return string
106 */
107 public function getDiscountsDisplayAttribute() {
108 return ! empty( $this->discounts ) ? Currency::format( $this->discounts, $this->currency ) : '';
109 }
110
111 /**
112 * Get the display scratch amount attribute.
113 *
114 * @return string
115 */
116 public function getScratchDisplayAmountAttribute() {
117 return Currency::format( (int) ( -$this->total_savings_amount + $this->total_amount ), $this->currency );
118 }
119
120 /**
121 * Get the display subtotal amount attribute.
122 *
123 * @return string
124 */
125 public function getTrialDisplayAmountAttribute() {
126 return ! empty( $this->trial_amount ) ? Currency::format( $this->trial_amount, $this->currency ) : '';
127 }
128
129 /**
130 * Get the display remaining amount due attribute.
131 *
132 * @return string
133 */
134 public function getRemainingAmountDueDisplayAmountAttribute() {
135 return ! empty( $this->remaining_amount_due ) ? Currency::format( $this->remaining_amount_due, $this->currency ) : '';
136 }
137
138 /**
139 * Get the display refunded amount attribute.
140 *
141 * @return string
142 */
143 public function getRefundedDisplayAmountAttribute() {
144 return ! empty( $this->refunded_amount ) ? Currency::format( $this->refunded_amount, $this->currency ) : '';
145 }
146
147 /**
148 * Get the display net paid amount attribute.
149 *
150 * @return string
151 */
152 public function getNetPaidDisplayAmountAttribute() {
153 return ! empty( $this->net_paid_amount ) ? Currency::format( $this->net_paid_amount, $this->currency ) : '';
154 }
155
156 /**
157 * Get the display subtotal amount attribute.
158 *
159 * @return string
160 */
161 public function getTotalDisplayAmountAttribute() {
162 return Currency::format( (int) $this->total_amount, $this->currency );
163 }
164
165 /**
166 * Get the display total savings amount attribute.
167 *
168 * @return string
169 */
170 public function getTotalSavingsDisplayAmountAttribute() {
171 return Currency::format( (int) $this->total_savings_amount, $this->currency );
172 }
173
174 /**
175 * Get the display total scratch price attribute.
176 *
177 * @return string
178 */
179 public function getTotalScratchDisplayAmountAttribute() {
180 return Currency::format( - (int) $this->total_savings_amount + (int) $this->total_amount, $this->currency );
181 }
182
183 /**
184 * Get the subtotal scratch amount (sum of all line items' scratch amounts).
185 *
186 * @return int
187 */
188 public function getSubtotalScratchAmountAttribute() {
189 if ( empty( $this->line_items ) || empty( $this->line_items->data ) ) {
190 return 0;
191 }
192 return array_reduce(
193 $this->line_items->data ?? [],
194 function ( $sum, $item ) {
195 return $sum + (int) ( $item->scratch_amount ?? 0 );
196 },
197 0
198 );
199 }
200
201 /**
202 * Get the display subtotal scratch amount attribute.
203 *
204 * @return string
205 */
206 public function getSubtotalScratchDisplayAmountAttribute() {
207 $scratch = $this->subtotal_scratch_amount;
208 return ! empty( $scratch ) ? Currency::format( $scratch, $this->currency ) : '';
209 }
210
211 /**
212 * Check if the checkout has a subtotal scratch amount different from the subtotal.
213 *
214 * @return bool
215 */
216 public function getHasSubtotalScratchAmountAttribute() {
217 $scratch = $this->subtotal_scratch_amount;
218 $subtotal = (int) ( $this->subtotal_amount ?? 0 );
219 return $scratch > 0 && $scratch > $subtotal;
220 }
221
222 /**
223 * Get the converts currency attribute.
224 * We should convert currency by default.
225 *
226 * @return bool
227 */
228 public function getShowConvertedTotalAttribute() {
229 return apply_filters( 'surecart_checkout_show_converted_total', true, $this );
230 }
231
232 /**
233 * Get the display full amount attribute.
234 *
235 * @return string
236 */
237 public function getFullDisplayAmountAttribute() {
238 return ! empty( $this->full_amount ) ? Currency::format( $this->full_amount, $this->currency ) : '';
239 }
240
241 /**
242 * Get the display discount amount amount attribute.
243 *
244 * @return string
245 */
246 public function getDiscountDisplayAmountAttribute() {
247 return ! empty( $this->discount_amount ) ? Currency::format( $this->discount_amount, $this->currency ) : '';
248 }
249
250 /**
251 * Get the display bump amount attribute.
252 *
253 * @return string
254 */
255 public function getBumpDisplayAmountAttribute() {
256 return ! empty( $this->bump_amount ) ? Currency::format( $this->bump_amount, $this->currency ) : '';
257 }
258
259 /**
260 * Get the converted total amount attribute.
261 *
262 * @return string
263 */
264 public function getConvertedTotalAmountAttribute() {
265 if ( $this->is_zero_decimal || empty( $this->total_amount ) ) {
266 return $this->total_amount;
267 }
268 return $this->total_amount / 100;
269 }
270
271 /**
272 * Is the checkout an installment.
273 *
274 * @return bool
275 */
276 public function getIsInstallmentAttribute() {
277 return $this->full_amount !== $this->subtotal_amount;
278 }
279
280 /**
281 * Get the line items count attribute.
282 *
283 * @return int
284 */
285 public function getLineItemsCountAttribute() {
286 if ( empty( $this->line_items ) || empty( $this->line_items->data ) ) {
287 return 0;
288 }
289 return array_reduce(
290 $this->line_items->data ?? [],
291 function ( $count, $item ) {
292 return $count + ( $item->quantity ?? 0 );
293 },
294 0
295 );
296 }
297
298 /**
299 * Get the has recurring attribute.
300 *
301 * Do any line items have a recurring price?
302 *
303 * @return bool
304 */
305 public function getHasRecurringAttribute() {
306 return array_reduce(
307 $this->line_items->data ?? [],
308 function ( $carry, $item ) {
309 return $carry || isset( $item->price->recurring_interval );
310 },
311 false
312 );
313 }
314
315 /**
316 * Set attributes during write actions.
317 *
318 * @return void
319 */
320 protected function setWriteAttributes() {
321 $this->setAttribute( 'ip_address', $this->getIPAddress() );
322 if ( isset( $_COOKIE['sc_click_id'] ) ) {
323 $this->setAttribute( 'last_click', $_COOKIE['sc_click_id'] );
324 }
325 }
326
327 /**
328 * Set the upsell funnel attribute
329 *
330 * @param object $value The data array.
331 * @return void
332 */
333 public function setUpsellFunnelAttribute( $value ) {
334 $this->setRelation( 'upsell_funnel', $value, UpsellFunnel::class );
335 }
336
337 /**
338 * Set the upsell funnel attribute
339 *
340 * @param object $value The data array.
341 * @return void
342 */
343 public function setCurrentUpsellAttribute( $value ) {
344 $this->setRelation( 'current_upsell', $value, Upsell::class );
345 }
346
347 /**
348 * Set the recommended bumps attribute
349 *
350 * @param object $value Subscription data array.
351 * @return void
352 */
353 public function setRecommendedBumpsAttribute( $value ) {
354 $this->setCollection( 'recommended_bumps', $value, Bump::class );
355 }
356
357 /**
358 * Set the checkout fees attribute
359 *
360 * @param object $value Subscription data array.
361 * @return void
362 */
363 public function setCheckoutFeesAttribute( $value ) {
364 $this->setCollection( 'checkout_fees', $value, Fee::class );
365 }
366
367 /**
368 * Set the shipping fees attribute
369 *
370 * @param object $value Subscription data array.
371 * @return void
372 */
373 public function setShippingFeesAttribute( $value ) {
374 $this->setCollection( 'shipping_fees', $value, Fee::class );
375 }
376
377 /**
378 * Get the display checkout fees amount attribute.
379 *
380 * @return string
381 */
382 public function getCheckoutFeesDisplayAmountAttribute() {
383 return ! empty( $this->checkout_fees_amount ) ? Currency::format( $this->checkout_fees_amount, $this->currency ) : '';
384 }
385
386 /**
387 * Get the display shipping fees amount attribute.
388 *
389 * @return string
390 */
391 public function getShippingFeesDisplayAmountAttribute() {
392 return ! empty( $this->shipping_fees_amount ) ? Currency::format( $this->shipping_fees_amount, $this->currency ) : '';
393 }
394
395 /**
396 * Create a new model
397 *
398 * @param array $attributes Attributes to create.
399 *
400 * @return $this|\WP_Error|false
401 */
402 protected function create( $attributes = [] ) {
403 $this->setWriteAttributes();
404 return parent::create( $attributes );
405 }
406
407 /**
408 * Update the model
409 *
410 * @param array $attributes Attributes to create.
411 *
412 * @return $this|\WP_Error|false
413 */
414 protected function update( $attributes = [] ) {
415 $this->setWriteAttributes();
416
417 return parent::update( $attributes );
418 }
419
420 /**
421 * Get the IP address of the user
422 *
423 * TOD0: Move this to a helper class.
424 *
425 * @return string
426 */
427 protected function getIPAddress() {
428 return $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
429 }
430
431 /**
432 * Set the product attribute
433 *
434 * @param object $value Product properties.
435 * @return void
436 */
437 public function setLineItemsAttribute( $value ) {
438 $this->setCollection( 'line_items', $value, LineItem::class );
439 }
440
441 /**
442 * Finalize the session for checkout.
443 *
444 * @return $this|\WP_Error
445 */
446 protected function manuallyPay() {
447 if ( $this->fireModelEvent( 'manually_paying' ) === false ) {
448 return false;
449 }
450
451 if ( empty( $this->attributes['id'] ) ) {
452 return new \WP_Error( 'not_saved', 'Please create the checkout session.' );
453 }
454
455 $finalized = \SureCart::request(
456 $this->endpoint . '/' . $this->attributes['id'] . '/manually_pay/',
457 [
458 'method' => 'PATCH',
459 'query' => $this->query,
460 'body' => [
461 $this->object_name => $this->getAttributes(),
462 ],
463 ]
464 );
465
466 if ( is_wp_error( $finalized ) ) {
467 return $finalized;
468 }
469
470 $this->resetAttributes();
471
472 $this->fill( $finalized );
473
474 $this->fireModelEvent( 'manually_paid' );
475
476 return $this;
477 }
478
479 /**
480 * Cancel an checkout
481 *
482 * @return $this|\WP_Error
483 */
484 protected function cancel() {
485 if ( $this->fireModelEvent( 'cancelling' ) === false ) {
486 return false;
487 }
488
489 if ( empty( $this->attributes['id'] ) ) {
490 return new \WP_Error( 'not_saved', 'Please create the order.' );
491 }
492
493 $cancelled = $this->makeRequest(
494 [
495 'method' => 'PATCH',
496 'query' => $this->query,
497 'body' => [
498 $this->object_name => $this->getAttributes(),
499 ],
500 ],
501 $this->endpoint . '/' . $this->attributes['id'] . '/cancel/'
502 );
503
504 if ( is_wp_error( $cancelled ) ) {
505 return $cancelled;
506 }
507
508 $this->resetAttributes();
509
510 $this->fill( $cancelled );
511
512 $this->fireModelEvent( 'cancelled' );
513
514 return $this;
515 }
516
517 /**
518 * Offer a bump.
519 *
520 * @param string|\SureCart\Models\Bump $bump The bump object.
521 *
522 * @return true|\WP_Error
523 */
524 protected function offerBump( $bump ) {
525 if ( $this->fireModelEvent( 'offering_bump' ) === false ) {
526 return false;
527 }
528
529 if ( empty( $this->attributes['id'] ) ) {
530 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
531 }
532
533 $id = is_a( $bump, Bump::class ) ? $bump->id : $bump;
534
535 if ( empty( $id ) ) {
536 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
537 }
538
539 $offered = $this->makeRequest(
540 [
541 'method' => 'PATCH',
542 'query' => $this->query,
543 'body' => [
544 $this->object_name => $this->getAttributes(),
545 ],
546 ],
547 $this->endpoint . '/' . $this->attributes['id'] . '/offer_bump/' . $id
548 );
549
550 if ( is_wp_error( $offered ) ) {
551 return $offered;
552 }
553
554 return true;
555 }
556
557 /**
558 * Offer an upsell.
559 *
560 * @param string|\SureCart\Models\Upsell $upsell The upsell object.
561 *
562 * @return true|\WP_Error
563 */
564 protected function offerUpsell( $upsell ) {
565 if ( $this->fireModelEvent( 'offering_upsell' ) === false ) {
566 return false;
567 }
568
569 if ( empty( $this->attributes['id'] ) ) {
570 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
571 }
572
573 $id = is_a( $upsell, Upsell::class ) ? $upsell->id : $upsell;
574
575 if ( empty( $id ) ) {
576 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
577 }
578
579 $offered = $this->makeRequest(
580 [
581 'method' => 'PATCH',
582 'query' => $this->query,
583 'body' => [
584 $this->object_name => $this->getAttributes(),
585 ],
586 ],
587 $this->endpoint . '/' . $this->attributes['id'] . '/offer_upsell/' . $id
588 );
589
590 if ( is_wp_error( $offered ) ) {
591 return $offered;
592 }
593
594 return true;
595 }
596
597 /**
598 * Decline an upsell.
599 *
600 * @param string|\SureCart\Models\Upsell $upsell The upsell object.
601 *
602 * @return $this|\WP_Error
603 */
604 protected function declineUpsell( $upsell ) {
605 if ( $this->fireModelEvent( 'declining_upsell' ) === false ) {
606 return false;
607 }
608
609 if ( empty( $this->attributes['id'] ) ) {
610 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
611 }
612
613 $id = is_a( $upsell, Upsell::class ) ? $upsell->id : $upsell;
614
615 if ( empty( $id ) ) {
616 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
617 }
618
619 $declined = $this->makeRequest(
620 [
621 'method' => 'PATCH',
622 'query' => $this->query,
623 'body' => [
624 $this->object_name => $this->getAttributes(),
625 ],
626 ],
627 $this->endpoint . '/' . $this->attributes['id'] . '/decline_upsell/' . $id
628 );
629
630 if ( is_wp_error( $declined ) ) {
631 return $declined;
632 }
633
634 $this->resetAttributes();
635
636 $this->fill( $declined );
637
638 $this->fireModelEvent( 'declined' );
639
640 return $this;
641 }
642
643 /**
644 * Get the human discount attribute.
645 *
646 * @return string
647 */
648 public function getHumanDiscountAttribute() {
649 if ( empty( $this->discount->coupon ) ) {
650 return '';
651 }
652
653 if ( ! empty( $this->discount->coupon->percent_off ) ) {
654 if ( ! empty( $this->discount->coupon->amount_off ) && ! empty( $this->currency ) ) {
655 return Currency::format( $this->discount->coupon->amount_off, $this->currency );
656 }
657
658 // translators: %1d is the percentage discount.
659 return sprintf( __( '%1d%% off', 'surecart' ), $this->discount->coupon->percent_off | 0 );
660 }
661
662 return '';
663 }
664
665 /**
666 * Get the human discount with duration attribute.
667 *
668 * @return string
669 */
670 public function getHumanDiscountWithDurationAttribute() {
671 if ( ! $this->hasRecurring ) {
672 return $this->human_discount;
673 }
674
675 $duration = $this->discount->coupon->duration ?? '';
676 $duration_in_months = $this->discount->coupon->duration_in_months ?? 0;
677
678 switch ( $duration ) {
679 case 'once':
680 return sprintf( '%s %s', $this->human_discount, __( 'once', 'surecart' ) );
681 case 'repeating':
682 // translators: %d is the number of months.
683 $months_label = sprintf( _n( '%d month', '%d months', $duration_in_months, 'surecart' ), $duration_in_months );
684 return sprintf( '%s for %s', $this->human_discount, $months_label );
685 default:
686 return $this->human_discount;
687 }
688 }
689
690 /**
691 * Get the shipping display amount attribute.
692 *
693 * @return string
694 */
695 public function getShippingDisplayAmountAttribute() {
696 return $this->shipping_amount ? Currency::format( $this->shipping_amount, $this->currency ) : __( 'Free', 'surecart' );
697 }
698
699 /**
700 * If the shipping address is required.
701 *
702 * @return bool
703 */
704 public function getShippingAddressRequiredAttribute(): bool {
705 return in_array( $this->shipping_address_accuracy_requirement, [ 'tax', 'full' ], true );
706 }
707
708 /**
709 * Get the paid display amount attribute.
710 *
711 * @return string
712 */
713 public function getPaidDisplayAmountAttribute() {
714 return Currency::format( $this->paid_amount, $this->currency );
715 }
716
717 /**
718 * Get the Paid at Date attribute.
719 *
720 * @return string
721 */
722 public function getPaidAtDateAttribute() {
723 return ! empty( $this->paid_at ) ? TimeDate::formatDate( $this->paid_at ) : '';
724 }
725
726 /**
727 * Get the proration display amount attribute.
728 *
729 * @return string
730 */
731 public function getProrationDisplayAmountAttribute() {
732 return Currency::format( $this->proration_amount, $this->currency );
733 }
734
735 /**
736 * Get the applied balance display amount attribute.
737 *
738 * @return string
739 */
740 public function getAppliedBalanceDisplayAmountAttribute() {
741 return Currency::format( $this->applied_balance_amount, $this->currency );
742 }
743
744 /**
745 * Get the credited balance display amount attribute.
746 *
747 * @return string
748 */
749 public function getCreditedBalanceDisplayAmountAttribute() {
750 return Currency::format( $this->credited_balance_amount, $this->currency );
751 }
752
753 /**
754 * Get the store currency amount due display amount attribute.
755 *
756 * @return string
757 */
758 public function getAmountDueDefaultCurrencyDisplayAmountAttribute() {
759 return Currency::format( $this->amount_due, $this->currency, [ 'convert' => false ] );
760 }
761
762 /**
763 * Get the store currency amount due display amount attribute.
764 *
765 * @return string
766 */
767 public function getCurrentCurrencyAttribute() {
768 return Currency::getCurrentCurrency();
769 }
770
771 /**
772 * Get the tax exclusive display amount attribute.
773 *
774 * @return string
775 */
776 public function getTaxExclusiveDisplayAmountAttribute() {
777 return Currency::format( $this->tax_exclusive_amount, $this->currency );
778 }
779
780 /**
781 * Get the tax inclusive display amount attribute.
782 *
783 * @return string
784 */
785 public function getTaxInclusiveDisplayAmountAttribute() {
786 return Currency::format( $this->tax_inclusive_amount, $this->currency );
787 }
788
789 /**
790 * Get the tax display amount attribute.
791 *
792 * @return string
793 */
794 public function getTaxDisplayAmountAttribute() {
795 return Currency::format( $this->tax_amount, $this->currency );
796 }
797 }
798