PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 3.4.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v3.4.1
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
540 lines 12.0 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\Traits\CanFinalize;
9 use SureCart\Models\Traits\HasBillingAddress;
10 use SureCart\Models\Traits\HasDiscount;
11 use SureCart\Models\Traits\HasInvoice;
12 use SureCart\Models\Traits\HasPaymentFailures;
13 use SureCart\Models\Traits\HasPaymentIntent;
14 use SureCart\Models\Traits\HasPaymentMethod;
15 use SureCart\Models\Traits\HasProcessorType;
16 use SureCart\Models\Traits\HasPurchases;
17 use SureCart\Models\Traits\HasShippingAddress;
18 use SureCart\Support\Currency;
19 use SureCart\Support\TimeDate;
20
21 /**
22 * Order model
23 */
24 class Checkout extends Model {
25 use HasCustomer;
26 use HasSubscriptions;
27 use HasDiscount;
28 use HasShippingAddress;
29 use HasPaymentIntent;
30 use HasPaymentMethod;
31 use HasPurchases;
32 use CanFinalize;
33 use HasProcessorType;
34 use HasBillingAddress;
35 use HasPaymentFailures;
36 use HasInvoice;
37
38 /**
39 * Rest API endpoint
40 *
41 * @var string
42 */
43 protected $endpoint = 'checkouts';
44
45 /**
46 * Object name
47 *
48 * @var string
49 */
50 protected $object_name = 'checkout';
51
52 /**
53 * Need to pass the processor type on create
54 *
55 * @param array $attributes Optional attributes.
56 * @param string $type stripe, paypal, etc.
57 */
58 public function __construct( $attributes = [], $type = '' ) {
59 $this->processor_type = $type;
60 parent::__construct( $attributes );
61 }
62
63 /**
64 * Get the display subtotal amount attribute.
65 *
66 * @return string
67 */
68 public function getSubtotalDisplayAmountAttribute() {
69 return ! empty( $this->subtotal_amount ) ? Currency::format( $this->subtotal_amount, $this->currency ) : '';
70 }
71
72
73 /**
74 * Get the display total scratch price attribute.
75 *
76 * @return string
77 */
78 public function getTotalScratchDisplayAmountAttribute() {
79 return Currency::format( - (int) $this->total_savings_amount + (int) $this->total_amount, $this->currency );
80 }
81
82 /**
83 * Get the display subtotal amount attribute.
84 *
85 * @return string
86 */
87 public function getTotalDisplayAmountAttribute() {
88 return Currency::format( (int) $this->total_amount, $this->currency );
89 }
90
91 /**
92 * Get the display full amount attribute.
93 *
94 * @return string
95 */
96 public function getFullDisplayAmountAttribute() {
97 return ! empty( $this->full_amount ) ? Currency::format( $this->full_amount, $this->currency ) : '';
98 }
99
100 /**
101 * Get the display discount amount amount attribute.
102 *
103 * @return string
104 */
105 public function getDiscountDisplayAmountAttribute() {
106 return ! empty( $this->discount_amount ) ? Currency::format( $this->discount_amount, $this->currency ) : '';
107 }
108
109 /**
110 * Get the display bump amount attribute.
111 *
112 * @return string
113 */
114 public function getBumpDisplayAmountAttribute() {
115 return ! empty( $this->bump_amount ) ? Currency::format( $this->bump_amount, $this->currency ) : '';
116 }
117
118 /**
119 * Get the converted total amount attribute.
120 *
121 * @return string
122 */
123 public function getConvertedTotalAmountAttribute() {
124 if ( $this->is_zero_decimal || empty( $this->total_amount ) ) {
125 return $this->total_amount;
126 }
127 return $this->total_amount / 100;
128 }
129
130 /**
131 * Is the checkout an installment.
132 *
133 * @return bool
134 */
135 public function getIsInstallmentAttribute() {
136 return $this->full_amount !== $this->subtotal_amount;
137 }
138
139 /**
140 * Get the line items count attribute.
141 *
142 * @return int
143 */
144 public function getLineItemsCountAttribute() {
145 if ( empty( $this->line_items ) || empty( $this->line_items->data ) ) {
146 return 0;
147 }
148 return array_reduce(
149 $this->line_items->data ?? [],
150 function ( $count, $item ) {
151 return $count + ( $item->quantity ?? 0 );
152 },
153 0
154 );
155 }
156
157 /**
158 * Get the has recurring attribute.
159 *
160 * Do any line items have a recurring price?
161 *
162 * @return bool
163 */
164 public function getHasRecurringAttribute() {
165 return array_reduce(
166 $this->line_items->data ?? [],
167 function ( $carry, $item ) {
168 return $carry || isset( $item->price->recurring_interval );
169 },
170 false
171 );
172 }
173
174 /**
175 * Set attributes during write actions.
176 *
177 * @return void
178 */
179 protected function setWriteAttributes() {
180 $this->setAttribute( 'ip_address', $this->getIPAddress() );
181 if ( isset( $_COOKIE['sc_click_id'] ) ) {
182 $this->setAttribute( 'last_click', $_COOKIE['sc_click_id'] );
183 }
184 }
185
186 /**
187 * Set the upsell funnel attribute
188 *
189 * @param object $value The data array.
190 * @return void
191 */
192 public function setUpsellFunnelAttribute( $value ) {
193 $this->setRelation( 'upsell_funnel', $value, UpsellFunnel::class );
194 }
195
196 /**
197 * Set the upsell funnel attribute
198 *
199 * @param object $value The data array.
200 * @return void
201 */
202 public function setCurrentUpsellAttribute( $value ) {
203 $this->setRelation( 'current_upsell', $value, Upsell::class );
204 }
205
206 /**
207 * Set the recommended bumps attribute
208 *
209 * @param object $value Subscription data array.
210 * @return void
211 */
212 public function setRecommendedBumpsAttribute( $value ) {
213 $this->setCollection( 'recommended_bumps', $value, Bump::class );
214 }
215
216 /**
217 * Create a new model
218 *
219 * @param array $attributes Attributes to create.
220 *
221 * @return $this|\WP_Error|false
222 */
223 protected function create( $attributes = [] ) {
224 $this->setWriteAttributes();
225 return parent::create( $attributes );
226 }
227
228 /**
229 * Update the model
230 *
231 * @param array $attributes Attributes to create.
232 *
233 * @return $this|\WP_Error|false
234 */
235 protected function update( $attributes = [] ) {
236 $this->setWriteAttributes();
237
238 return parent::update( $attributes );
239 }
240
241 /**
242 * Get the IP address of the user
243 *
244 * TOD0: Move this to a helper class.
245 *
246 * @return string
247 */
248 protected function getIPAddress() {
249 return $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
250 }
251
252 /**
253 * Set the product attribute
254 *
255 * @param object $value Product properties.
256 * @return void
257 */
258 public function setLineItemsAttribute( $value ) {
259 $this->setCollection( 'line_items', $value, LineItem::class );
260 }
261
262 /**
263 * Finalize the session for checkout.
264 *
265 * @return $this|\WP_Error
266 */
267 protected function manuallyPay() {
268 if ( $this->fireModelEvent( 'manually_paying' ) === false ) {
269 return false;
270 }
271
272 if ( empty( $this->attributes['id'] ) ) {
273 return new \WP_Error( 'not_saved', 'Please create the checkout session.' );
274 }
275
276 $finalized = \SureCart::request(
277 $this->endpoint . '/' . $this->attributes['id'] . '/manually_pay/',
278 [
279 'method' => 'PATCH',
280 'query' => $this->query,
281 'body' => [
282 $this->object_name => $this->getAttributes(),
283 ],
284 ]
285 );
286
287 if ( is_wp_error( $finalized ) ) {
288 return $finalized;
289 }
290
291 $this->resetAttributes();
292
293 $this->fill( $finalized );
294
295 $this->fireModelEvent( 'manually_paid' );
296
297 return $this;
298 }
299
300 /**
301 * Cancel an checkout
302 *
303 * @return $this|\WP_Error
304 */
305 protected function cancel() {
306 if ( $this->fireModelEvent( 'cancelling' ) === false ) {
307 return false;
308 }
309
310 if ( empty( $this->attributes['id'] ) ) {
311 return new \WP_Error( 'not_saved', 'Please create the order.' );
312 }
313
314 $cancelled = $this->makeRequest(
315 [
316 'method' => 'PATCH',
317 'query' => $this->query,
318 'body' => [
319 $this->object_name => $this->getAttributes(),
320 ],
321 ],
322 $this->endpoint . '/' . $this->attributes['id'] . '/cancel/'
323 );
324
325 if ( is_wp_error( $cancelled ) ) {
326 return $cancelled;
327 }
328
329 $this->resetAttributes();
330
331 $this->fill( $cancelled );
332
333 $this->fireModelEvent( 'cancelled' );
334
335 return $this;
336 }
337
338 /**
339 * Offer a bump.
340 *
341 * @param string|\SureCart\Models\Bump $bump The bump object.
342 *
343 * @return true|\WP_Error
344 */
345 protected function offerBump( $bump ) {
346 if ( $this->fireModelEvent( 'offering_bump' ) === false ) {
347 return false;
348 }
349
350 if ( empty( $this->attributes['id'] ) ) {
351 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
352 }
353
354 $id = is_a( $bump, Bump::class ) ? $bump->id : $bump;
355
356 if ( empty( $id ) ) {
357 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
358 }
359
360 $offered = $this->makeRequest(
361 [
362 'method' => 'PATCH',
363 'query' => $this->query,
364 'body' => [
365 $this->object_name => $this->getAttributes(),
366 ],
367 ],
368 $this->endpoint . '/' . $this->attributes['id'] . '/offer_bump/' . $id
369 );
370
371 if ( is_wp_error( $offered ) ) {
372 return $offered;
373 }
374
375 return true;
376 }
377
378 /**
379 * Offer an upsell.
380 *
381 * @param string|\SureCart\Models\Upsell $upsell The upsell object.
382 *
383 * @return true|\WP_Error
384 */
385 protected function offerUpsell( $upsell ) {
386 if ( $this->fireModelEvent( 'offering_upsell' ) === false ) {
387 return false;
388 }
389
390 if ( empty( $this->attributes['id'] ) ) {
391 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
392 }
393
394 $id = is_a( $upsell, Upsell::class ) ? $upsell->id : $upsell;
395
396 if ( empty( $id ) ) {
397 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
398 }
399
400 $offered = $this->makeRequest(
401 [
402 'method' => 'PATCH',
403 'query' => $this->query,
404 'body' => [
405 $this->object_name => $this->getAttributes(),
406 ],
407 ],
408 $this->endpoint . '/' . $this->attributes['id'] . '/offer_upsell/' . $id
409 );
410
411 if ( is_wp_error( $offered ) ) {
412 return $offered;
413 }
414
415 return true;
416 }
417
418 /**
419 * Decline an upsell.
420 *
421 * @param string|\SureCart\Models\Upsell $upsell The upsell object.
422 *
423 * @return $this|\WP_Error
424 */
425 protected function declineUpsell( $upsell ) {
426 if ( $this->fireModelEvent( 'declining_upsell' ) === false ) {
427 return false;
428 }
429
430 if ( empty( $this->attributes['id'] ) ) {
431 return new \WP_Error( 'not_saved', 'Please create the checkout.' );
432 }
433
434 $id = is_a( $upsell, Upsell::class ) ? $upsell->id : $upsell;
435
436 if ( empty( $id ) ) {
437 return new \WP_Error( 'not_saved', 'Please pass an upsell.' );
438 }
439
440 $declined = $this->makeRequest(
441 [
442 'method' => 'PATCH',
443 'query' => $this->query,
444 'body' => [
445 $this->object_name => $this->getAttributes(),
446 ],
447 ],
448 $this->endpoint . '/' . $this->attributes['id'] . '/decline_upsell/' . $id
449 );
450
451 if ( is_wp_error( $declined ) ) {
452 return $declined;
453 }
454
455 $this->resetAttributes();
456
457 $this->fill( $declined );
458
459 $this->fireModelEvent( 'declined' );
460
461 return $this;
462 }
463
464 /**
465 * Get the billing address attribute
466 *
467 * @return array|null The billing address.
468 */
469 public function getBillingAddressDisplayAttribute() {
470 if ( $this->billing_matches_shipping ) {
471 return $this->shipping_address;
472 }
473
474 return $this->attributes['billing_address'] ?? null;
475 }
476
477 /**
478 * Get the human discount attribute.
479 *
480 * @return string
481 */
482 public function getHumanDiscountAttribute() {
483 if ( empty( $this->discount->coupon ) ) {
484 return '';
485 }
486
487 if ( ! empty( $this->discount->coupon->percent_off ) ) {
488 if ( ! empty( $this->discount->coupon->amount_off ) && ! empty( $this->currency ) ) {
489 return Currency::format( $this->discount->coupon->amount_off, $this->currency );
490 }
491
492 return sprintf( __( '%1d%% off', 'surecart' ), $this->discount->coupon->percent_off | 0 );
493 }
494
495 return '';
496 }
497
498 /**
499 * Get the human discount with duration attribute.
500 *
501 * @return string
502 */
503 public function getHumanDiscountWithDurationAttribute() {
504 if ( ! $this->hasRecurring ) {
505 return $this->human_discount;
506 }
507
508 $duration = $this->discount->coupon->duration ?? '';
509 $duration_in_months = $this->discount->coupon->duration_in_months ?? 0;
510
511 switch ( $duration ) {
512 case 'once':
513 return sprintf( '%s %s', $this->human_discount, __( 'once', 'surecart' ) );
514 case 'repeating':
515 $months_label = sprintf( _n( '%d month', '%d months', $duration_in_months, 'surecart' ), $duration_in_months );
516 return sprintf( '%s for %s', $this->human_discount, $months_label );
517 default:
518 return $this->human_discount;
519 }
520 }
521
522 /**
523 * If the shipping address is required.
524 *
525 * @return bool
526 */
527 public function getShippingAddressRequiredAttribute(): bool {
528 return in_array( $this->shipping_address_accuracy_requirement, [ 'tax', 'full' ], true );
529 }
530
531 /**
532 * Get the Paid at Date attribute.
533 *
534 * @return string
535 */
536 public function getPaidAtDateAttribute() {
537 return ! empty( $this->paid_at ) ? TimeDate::formatDate( $this->paid_at ) : '';
538 }
539 }
540