PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.2.2
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.2.2
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 / Subscription.php
Subscription.php
663 lines 14.5 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\HasDates;
7 use SureCart\Models\Traits\HasPrice;
8 use SureCart\Models\Traits\HasPurchase;
9 use SureCart\Support\Currency;
10 use SureCart\Support\TimeDate;
11
12 /**
13 * Subscription model
14 */
15 class Subscription extends Model {
16 use HasCustomer;
17 use HasPrice;
18 use HasPurchase;
19 use HasDates;
20
21 /**
22 * Rest API endpoint
23 *
24 * @var string
25 */
26 protected $endpoint = 'subscriptions';
27
28 /**
29 * Object name
30 *
31 * @var string
32 */
33 protected $object_name = 'subscription';
34
35 /**
36 * Set the current period attribute
37 *
38 * @param object $value Return request properties.
39 * @return void
40 */
41 public function setCurrentPeriodAttribute( $value ) {
42 $this->setRelation( 'current_period', $value, Period::class );
43 }
44
45 /**
46 * Update the model.
47 *
48 * @param array $attributes Attributes to update.
49 * @return $this|false
50 */
51 protected function update( $attributes = [] ) {
52 // find existing subscription with purchase record.
53 $existing = ( new Subscription() )->with( [ 'purchase' ] )->find( $attributes['id'] ?? $this->attributes['id'] );
54
55 // do the update and also get the purchase record.
56 $this->with( [ 'purchase' ] );
57 $updated = parent::update( $attributes );
58 if ( is_wp_error( $updated ) ) {
59 return $updated;
60 }
61
62 // do the purchase updated event.
63 if ( ! empty( $updated->purchase ) ) {
64 do_action(
65 'surecart/purchase_updated',
66 $updated->purchase,
67 (object) [
68 'data' => (object) [
69 'object' => (object) $updated->purchase->toArray(),
70 'previous_attributes' => (object) array_filter(
71 [
72 // conditionally have the previous product and quantity as the previous attributes.
73 'product' => $updated->purchase->product_id !== $existing->purchase->product_id ? ( $existing->purchase->product_id ?? null ) : null,
74 'quantity' => $updated->purchase->quantity !== $existing->purchase->quantity ? ( $existing->purchase->quantity ?? 1 ) : null,
75 ]
76 ),
77 ],
78 ]
79 );
80 }
81
82 return $this;
83 }
84
85 /**
86 * Cancel a subscription
87 *
88 * @param string $id Model id.
89 * @return $this|\WP_Error
90 */
91 protected function cancel( $id = null ) {
92 if ( $id ) {
93 $this->setAttribute( 'id', $id );
94 }
95
96 if ( $this->fireModelEvent( 'canceling' ) === false ) {
97 return false;
98 }
99
100 if ( empty( $this->attributes['id'] ) ) {
101 return new \WP_Error( 'not_saved', 'Please create the subscription.' );
102 }
103
104 $attributes = $this->attributes;
105 unset( $attributes['id'] );
106
107 $canceled = $this->with(
108 [
109 'purchase',
110 ]
111 )->makeRequest(
112 [
113 'method' => 'PATCH',
114 'query' => $this->query,
115 'body' => [
116 $this->object_name => $attributes,
117 ],
118 ],
119 $this->endpoint . '/' . $this->attributes['id'] . '/cancel/'
120 );
121
122 if ( is_wp_error( $canceled ) ) {
123 return $canceled;
124 }
125
126 $this->resetAttributes();
127
128 $this->fill( $canceled );
129
130 $this->fireModelEvent( 'canceled' );
131
132 // purchase revoked event.
133 if ( ! empty( $this->purchase->revoked ) ) {
134 do_action( 'surecart/purchase_revoked', $this->purchase );
135 }
136
137 return $this;
138 }
139
140 /**
141 * Complete a subscription
142 *
143 * @param string $id Model id.
144 * @return $this|\WP_Error
145 */
146 protected function complete( $id = null ) {
147 if ( $id ) {
148 $this->setAttribute( 'id', $id );
149 }
150
151 if ( $this->fireModelEvent( 'completeing' ) === false ) {
152 return false;
153 }
154
155 if ( empty( $this->attributes['id'] ) ) {
156 return new \WP_Error( 'not_saved', 'Please create the subscription.' );
157 }
158
159 $completed = $this->with(
160 [
161 'purchase',
162 ]
163 )->makeRequest(
164 [
165 'method' => 'PATCH',
166 'query' => $this->query,
167 ],
168 $this->endpoint . '/' . $this->attributes['id'] . '/complete/'
169 );
170
171 if ( is_wp_error( $completed ) ) {
172 return $completed;
173 }
174
175 $this->resetAttributes();
176
177 $this->fill( $completed );
178
179 $this->fireModelEvent( 'completed' );
180
181 return $this;
182 }
183
184 /**
185 * Restore a subscription
186 *
187 * @param string $id Model id.
188 * @return $this|\WP_Error
189 */
190 protected function restore( $id = null ) {
191 if ( $id ) {
192 $this->setAttribute( 'id', $id );
193 }
194
195 if ( $this->fireModelEvent( 'restoring' ) === false ) {
196 return false;
197 }
198
199 if ( empty( $this->attributes['id'] ) ) {
200 return new \WP_Error( 'not_saved', 'Please create the subscription.' );
201 }
202
203 $restored = $this->with( array( 'purchase' ) )
204 ->makeRequest(
205 [
206 'method' => 'PATCH',
207 'query' => $this->query,
208 ],
209 $this->endpoint . '/' . $this->attributes['id'] . '/restore/'
210 );
211
212 if ( is_wp_error( $restored ) ) {
213 return $restored;
214 }
215
216 $this->resetAttributes();
217
218 $this->fill( $restored );
219
220 $this->fireModelEvent( 'restored' );
221
222 // purchase invoked event.
223 if ( ! empty( $this->purchase ) ) {
224 do_action( 'surecart/purchase_invoked', $this->purchase );
225 }
226
227 return $this;
228 }
229
230 /**
231 * Renew a subscription.
232 *
233 * @param string $id Model id.
234 * @return $this|\WP_Error
235 */
236 protected function renew( $id = null ) {
237 if ( $id ) {
238 $this->setAttribute( 'id', $id );
239 }
240
241 if ( $this->fireModelEvent( 'renewing' ) === false ) {
242 return false;
243 }
244
245 if ( empty( $this->attributes['id'] ) ) {
246 return new \WP_Error( 'not_saved', 'Please create the subscription.' );
247 }
248
249 $renewed = \SureCart::request(
250 $this->endpoint . '/' . $this->attributes['id'],
251 [
252 'method' => 'PATCH',
253 'query' => $this->query,
254 'body' => [
255 $this->object_name => [
256 'cancel_at_period_end' => false,
257 ],
258 ],
259 ]
260 );
261
262 if ( is_wp_error( $renewed ) ) {
263 return $renewed;
264 }
265
266 $this->resetAttributes();
267
268 $this->fill( $renewed );
269
270 $this->fireModelEvent( 'renewed' );
271
272 return $this;
273 }
274
275 /**
276 * Preserve a subscription.
277 *
278 * @param string $id Model id.
279 * @return $this|\WP_Error
280 */
281 protected function preserve( $id = null ) {
282 if ( $id ) {
283 $this->setAttribute( 'id', $id );
284 }
285
286 if ( $this->fireModelEvent( 'preserving' ) === false ) {
287 return false;
288 }
289
290 if ( empty( $this->attributes['id'] ) ) {
291 return new \WP_Error( 'not_saved', 'Please create the subscription.' );
292 }
293
294 $preserved = $this->makeRequest(
295 [
296 'method' => 'PATCH',
297 'query' => $this->query,
298 ],
299 $this->endpoint . '/' . $this->attributes['id'] . '/preserve/'
300 );
301
302 if ( is_wp_error( $preserved ) ) {
303 return $preserved;
304 }
305
306 $this->resetAttributes();
307
308 $this->fill( $preserved );
309
310 $this->fireModelEvent( 'preserved' );
311
312 return $this;
313 }
314
315 /**
316 * Preview the upcoming invoice.
317 *
318 * @param string $args Arguments
319 * @return Period|\WP_Error
320 */
321 protected function upcomingPeriod( $args = [] ) {
322 if ( ! empty( $args['id'] ) ) {
323 $this->setAttribute( 'id', $args['id'] );
324 unset( $args['id'] );
325 }
326
327 if ( $this->fireModelEvent( 'previewingUpcomingPeriod' ) === false ) {
328 return false;
329 }
330
331 if ( empty( $this->attributes['id'] ) ) {
332 return new \WP_Error( 'not_saved', 'Please create the subscription' );
333 }
334
335 $upcoming_period = $this->makeRequest(
336 [
337 'method' => 'PATCH',
338 'query' => $this->query,
339 'body' => [
340 $this->object_name => $args,
341 ],
342 ],
343 $this->endpoint . '/' . $this->attributes['id'] . '/upcoming_period/'
344 );
345
346 if ( is_wp_error( $upcoming_period ) ) {
347 return $upcoming_period;
348 }
349
350 $upcoming_period = new Period( $upcoming_period );
351
352 $this->fireModelEvent( 'previewedUpcomingPeriod' );
353
354 return $upcoming_period;
355 }
356
357 /**
358 * Pay off a subscription
359 *
360 * @param string $id Model id.
361 * @return $this|\WP_Error
362 */
363 protected function payOff( $id = null ) {
364 if ( $id ) {
365 $this->setAttribute( 'id', $id );
366 }
367
368 if ( $this->fireModelEvent( 'payingOff' ) === false ) {
369 return false;
370 }
371
372 if ( empty( $this->attributes['id'] ) ) {
373 return new \WP_Error( 'not_saved', 'Please create the subscription' );
374 }
375
376 $paid_off = $this->makeRequest(
377 [
378 'method' => 'PATCH',
379 'query' => $this->query,
380 ],
381 $this->endpoint . '/' . $this->attributes['id'] . '/pay_off/'
382 );
383
384 if ( is_wp_error( $paid_off ) ) {
385 return $paid_off;
386 }
387
388 $this->resetAttributes();
389
390 $this->fill( $paid_off );
391
392 $this->fireModelEvent( 'paidOff' );
393
394 return $this;
395 }
396
397 /**
398 * Is this subscription a lifetime one?
399 *
400 * @return boolean
401 */
402 protected function isLifetime() {
403 return $this->attributes['id'] && empty( $this->attributes['current_period_end_at'] );
404 }
405
406 /**
407 * Can the user upgrade this subscription?
408 *
409 * @return boolean
410 */
411 protected function canBeSwitched() {
412 return apply_filters( 'surecart/subscription/can_be_changed', $this->checkIfCanBeSwitched(), $this );
413 }
414
415 /**
416 * Can the subscription be changed?
417 *
418 * @return boolean
419 */
420 private function checkIfCanBeSwitched() {
421 // updates are not enabled for the account.
422 if ( empty( \SureCart::account()->customer_portal_protocol->subscription_updates_enabled ) ) {
423 return false;
424 }
425 // already set to canceling.
426 if ( $this->attributes['cancel_at_period_end'] ) {
427 return false;
428 }
429 // can't update canceled, incomplete, or past due subscriptions.
430 if ( in_array( $this->attributes['status'], [ 'canceled', 'incomplete', 'past_due' ] ) ) {
431 return false;
432 }
433 // must not be lifetime.
434 if ( $this->isLifetime() ) {
435 return false;
436 }
437 return true;
438 }
439
440 /**
441 * Can we cancel the subscription?
442 *
443 * @return boolean
444 */
445 public function canBeCanceled() {
446 return apply_filters( 'surecart/subscription/can_be_canceled', $this->checkIfCanBeSwitched(), $this );
447 }
448
449 /**
450 * Should delay subscription cancellation?
451 *
452 * @return boolean
453 */
454 public function shouldDelayCancellation(): bool {
455 $protocol = \SureCart::account()->subscription_protocol;
456
457 if ( ! $protocol->cancel_window_enabled || empty( $protocol->cancel_window_days ) || empty( $this->attributes['current_period_end_at'] ) ) {
458 return false;
459 }
460
461 $cancel_window_days = $protocol->cancel_window_days;
462 $now = ( new \DateTime() )->format( 'Y-m-d' );
463 $end = new \DateTime();
464 $end->setTimestamp( $this->attributes['current_period_end_at'] );
465 $end = $end->modify( "-{$cancel_window_days} days" );
466 $end = $end->format( 'Y-m-d' );
467
468 return $now < $end;
469 }
470
471 /**
472 * Can the subscription be canceled?
473 *
474 * @return boolean
475 */
476 private function checkIfCanBeCanceled() {
477 // updates are not enabled for the account.
478 if ( empty( \SureCart::account()->customer_portal_protocol->subscription_cancellations_enabled ) ) {
479 return false;
480 }
481
482 // can't cancel canceled, incomplete, or past due subscriptions.
483 if ( in_array( $this->attributes['status'], [ 'canceled', 'incomplete', 'past_due' ] ) ) {
484 return false;
485 }
486
487 return true;
488 }
489
490 /**
491 * Can we update the quantity?
492 *
493 * @return boolean
494 */
495 protected function canUpdateQuantity() {
496 return apply_filters( 'surecart/subscription/can_update_quantity', $this->checkIfCanBeSwitched(), $this );
497 }
498
499 /**
500 * Check if we can update the quantity.
501 *
502 * @return boolean
503 */
504 private function checkIfCanUpdateQuantity() {
505 // quantity changes are not enabled for this account.
506 if ( empty( \SureCart::account()->customer_portal_protocol->subscription_quantity_updates_enabled ) ) {
507 return false;
508 }
509 return true;
510 }
511
512 /**
513 * Get stats for the subscription
514 *
515 * @param array $args Array of arguments for the statistics.
516 *
517 * @return \SureCart\Models\Statistic;
518 */
519 protected function stats( $args = [] ) {
520 $stat = new Statistic();
521 return $stat->where( $args )->find( 'subscriptions' );
522 }
523
524 /**
525 * Get the current period start at date.
526 *
527 * @return string
528 */
529 public function getCurrentPeriodStartAtDateAttribute() {
530 return ! empty( $this->current_period_start_at ) ? TimeDate::formatDate( $this->current_period_start_at ) : '';
531 }
532
533 /**
534 * Get the current period end at date.
535 *
536 * @return string
537 */
538 public function getCurrentPeriodEndAtDateAttribute() {
539 return ! empty( $this->current_period_end_at ) ? TimeDate::formatDate( $this->current_period_end_at ) : '';
540 }
541
542 /**
543 * Get the current period end at date time.
544 *
545 * @return string
546 */
547 public function getCurrentPeriodEndAtDateTimeAttribute() {
548 return ! empty( $this->current_period_end_at ) ? TimeDate::formatDateAndTime( $this->current_period_end_at ) : '';
549 }
550
551 /**
552 * Get the start at date.
553 *
554 * @return string
555 */
556 public function getStartAtDateAttribute() {
557 return ! empty( $this->start_at ) ? TimeDate::formatDate( $this->start_at ) : '';
558 }
559
560 /**
561 * Get the end at date.
562 *
563 * @return string
564 */
565 public function getEndAtDateAttribute() {
566 return ! empty( $this->end_at ) ? TimeDate::formatDate( $this->end_at ) : '';
567 }
568
569 /**
570 * Get the ended at date.
571 *
572 * @return string
573 */
574 public function getEndedAtDateAttribute() {
575 return ! empty( $this->ended_at ) ? TimeDate::formatDate( $this->ended_at ) : '';
576 }
577
578 /**
579 * Get the restore at date.
580 *
581 * @return string
582 */
583 public function getRestoreAtDateAttribute() {
584 return ! empty( $this->restore_at ) ? TimeDate::formatDate( $this->restore_at ) : '';
585 }
586
587 /**
588 * Get the trial end at date.
589 *
590 * @return string
591 */
592 public function getTrialEndAtDateAttribute() {
593 return ! empty( $this->trial_end_at ) ? TimeDate::formatDate( $this->trial_end_at ) : '';
594 }
595
596 /**
597 * Get the trial end at date time.
598 *
599 * @return string
600 */
601 public function getTrialEndAtAtDateTimeAttribute() {
602 return ! empty( $this->trial_end_at ) ? TimeDate::formatDateAndTime( $this->trial_end_at ) : '';
603 }
604
605 /**
606 * Get the affiliation expires at date.
607 *
608 * @return string
609 */
610 public function getAffiliationExpiresAtDateAttribute() {
611 return ! empty( $this->affiliation_expires_at ) ? TimeDate::formatDate( $this->affiliation_expires_at ) : '';
612 }
613
614 /**
615 * Get the affiliation expires at date and time.
616 *
617 * @return string
618 */
619 public function getAffiliationExpiresAtDateTimeAttribute() {
620 return ! empty( $this->affiliation_expires_at ) ? TimeDate::formatDateAndTime( $this->affiliation_expires_at ) : '';
621 }
622
623 /**
624 * Get the ad hoc display amount.
625 *
626 * @return string|null
627 */
628 public function getAdHocDisplayAmountAttribute() {
629 return $this->ad_hoc_amount ? Currency::format( $this->ad_hoc_amount, $this->currency ) : null;
630 }
631
632 /**
633 * Get the remaining period text attribute.
634 *
635 * @return string
636 */
637 public function getRemainingPeriodTextAttribute() {
638 if ( empty( $this->remaining_period_count ) ) {
639 return '';
640 }
641
642 return sprintf(
643 // translators: %d is the number of remaining payments.
644 _n(
645 '%d payment remaining',
646 '%d payments remaining',
647 $this->remaining_period_count,
648 'surecart'
649 ),
650 $this->remaining_period_count
651 );
652 }
653
654 /**
655 * Get the can modify attribute.
656 *
657 * @return string
658 */
659 public function getCanModifyAttribute() {
660 return apply_filters( 'surecart/subscription/can_modify', true, $this );
661 }
662 }
663