PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / Subscriptions / Repositories / SubscriptionRepository.php

SubscriptionRepository.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at src/Subscriptions/Repositories/SubscriptionRepository.php

601 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Subscriptions\Repositories;
4
5 use Exception;
6 use Give\Donations\Models\Donation;
7 use Give\Donations\ValueObjects\DonationMetaKeys;
8 use Give\Donations\ValueObjects\DonationMode;
9 use Give\Donations\ValueObjects\DonationStatus;
10 use Give\Donations\ValueObjects\DonationType;
11 use Give\Framework\Database\DB;
12 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
13 use Give\Framework\Models\ModelQueryBuilder;
14 use Give\Framework\Support\Facades\DateTime\Temporal;
15 use Give\Helpers\Hooks;
16 use Give\Log\Log;
17 use Give\Subscriptions\Models\Subscription;
18 use Give\Subscriptions\ValueObjects\SubscriptionMode;
19 use Give\Subscriptions\ValueObjects\SubscriptionStatus;
20
21 /**
22 * @since 4.8.0 Add notes repository
23 * @since 2.19.6
24 */
25 class SubscriptionRepository
26 {
27 /**
28 * @var SubscriptionNotesRepository
29 */
30 public $notes;
31
32 /**
33 * @var string[]
34 */
35 private $requiredSubscriptionProperties = [
36 'donorId',
37 'period',
38 'frequency',
39 'amount',
40 'status',
41 'donationFormId',
42 ];
43
44 /**
45 * @since 4.8.0
46 */
47 public function __construct()
48 {
49 $this->notes = give(SubscriptionNotesRepository::class);
50 }
51
52 /**
53 * @since 2.19.6
54 *
55 * @return Subscription|null
56 */
57 public function getById(int $subscriptionId)
58 {
59 return $this->queryById($subscriptionId)->get();
60 }
61
62 /**
63 * @since 2.27.0 Add support for multiple return types.
64 * @since 2.21.0
65 *
66 * @return Subscription|null
67 */
68 public function getByGatewaySubscriptionId(string $gatewaySubscriptionId)
69 {
70 return $this->queryByGatewaySubscriptionId($gatewaySubscriptionId)->get();
71 }
72
73 /**
74 * @since 2.19.6
75 *
76 * @param int $subscriptionId
77 *
78 * @return ModelQueryBuilder<Subscription>
79 */
80 public function queryById(int $subscriptionId): ModelQueryBuilder
81 {
82 return $this->prepareQuery()
83 ->where('id', $subscriptionId);
84 }
85
86 /**
87 * @since 2.21.0
88 *
89 * @return ModelQueryBuilder<Subscription>
90 */
91 public function queryByGatewaySubscriptionId(string $gatewaySubscriptionId): ModelQueryBuilder
92 {
93 return $this->prepareQuery()
94 ->where('profile_id', $gatewaySubscriptionId);
95 }
96
97 /**
98 * @since 2.19.6
99 *
100 * @param int $donationId
101 *
102 * @return ModelQueryBuilder<Subscription>
103 */
104 public function queryByDonationId(int $donationId): ModelQueryBuilder
105 {
106 return $this->prepareQuery()
107 ->where('parent_payment_id', $donationId);
108 }
109
110 /**
111 * @since 2.19.6
112 *
113 * @param int $donorId
114 *
115 * @return ModelQueryBuilder<Subscription>
116 */
117 public function queryByDonorId(int $donorId): ModelQueryBuilder
118 {
119 return $this->prepareQuery()
120 ->where('customer_id', $donorId);
121 }
122
123 /**
124 * @since 4.11.0
125 *
126 * @param int $campaignId
127 *
128 * @return ModelQueryBuilder<Subscription>
129 */
130 public function queryByCampaignId(int $campaignId): ModelQueryBuilder
131 {
132 return $this->prepareQuery()
133 ->where('campaign_id', $campaignId);
134 }
135
136 /**
137 * @deprecated Use give()->subscriptions->notes()->queryBySubscriptionId()->getAll() instead.
138 * @since 2.19.6
139 *
140 * @return object[]
141 */
142 public function getNotesBySubscriptionId(int $id): array
143 {
144 _give_deprecated_function(__METHOD__, '4.6.0', 'give()->subscriptions->notes()->queryBySubscriptionId()->getAll()');
145
146 $notes = array_map(
147 static function ($note) {
148 return array_merge($note->toArray(), [
149 'note' => $note->comment_content,
150 'date' => $note->comment_date,
151 ]);
152 },
153 $this->notes->queryBySubscriptionId($id)->getAll()
154 );
155
156 if (!$notes) {
157 return [];
158 }
159
160 return $notes;
161 }
162
163 /**
164 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
165 * @since 4.11.0 add campaign_id column to insert
166 * @since 2.24.0 add payment_mode column to insert
167 * @since 2.21.0 replace actions with givewp_subscription_creating and givewp_subscription_created
168 * @since 2.19.6
169 *
170 * @return void
171 * @throws Exception
172 */
173 public function insert(Subscription $subscription)
174 {
175 $this->validateSubscription($subscription);
176
177 if ($subscription->renewsAt === null) {
178 $subscription->bumpRenewalDate();
179 }
180
181 // If the subscription is not associated with a campaign, try to find the campaign ID by the form ID
182 if (!$subscription->campaignId) {
183 $campaign = give()->campaigns->getByFormId($subscription->donationFormId);
184 $subscription->campaignId = $campaign ? $campaign->id : null;
185 }
186
187 Hooks::doAction('givewp_subscription_creating', $subscription);
188
189 $dateCreated = Temporal::withoutMicroseconds($subscription->createdAt ?: Temporal::getCurrentDateTime());
190
191 DB::query('START TRANSACTION');
192
193 try {
194 DB::table('give_subscriptions')->insert([
195 'created' => Temporal::getFormattedDateTime($dateCreated),
196 'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
197 'status' => $subscription->status->getValue(),
198 'profile_id' => $subscription->gatewaySubscriptionId ?? '',
199 'customer_id' => $subscription->donorId,
200 'period' => $subscription->period->getValue(),
201 'frequency' => $subscription->frequency,
202 'initial_amount' => $subscription->amount->formatToDecimal(),
203 'recurring_amount' => $subscription->amount->formatToDecimal(),
204 'recurring_fee_amount' => $subscription->feeAmountRecovered !== null ? $subscription->feeAmountRecovered->formatToDecimal(
205 ) : 0,
206 'bill_times' => $subscription->installments,
207 'transaction_id' => $subscription->transactionId ?? '',
208 'product_id' => $subscription->donationFormId,
209 'campaign_id' => $subscription->campaignId,
210 'payment_mode' => $subscription->mode->getValue(),
211 ]);
212 } catch (Exception $exception) {
213 DB::query('ROLLBACK');
214
215 Log::error('Failed creating a subscription', compact('subscription'));
216
217 throw new Exception('Failed creating a subscription', 0, $exception);
218 }
219
220 DB::query('COMMIT');
221
222 $subscriptionId = DB::last_insert_id();
223
224 $subscription->id = $subscriptionId;
225 $subscription->createdAt = $dateCreated;
226
227 Hooks::doAction('givewp_subscription_created', $subscription);
228 }
229
230 /**
231 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
232 * @since 4.11.0 add campaign_id column to update
233 * @since 3.17.0 add expiration column to update
234 * @since 2.24.0 add payment_mode column to update
235 * @since 2.21.0 replace actions with givewp_subscription_updating and givewp_subscription_updated
236 * @since 2.19.6
237 *
238 * @return void
239 * @throws Exception
240 */
241 public function update(Subscription $subscription)
242 {
243 $this->validateSubscription($subscription);
244
245 if ($subscription->renewsAt === null) {
246 throw new InvalidArgumentException('renewsAt is required.');
247 }
248
249 Hooks::doAction('givewp_subscription_updating', $subscription);
250
251 DB::query('START TRANSACTION');
252
253 try {
254 DB::table('give_subscriptions')
255 ->where('id', $subscription->id)
256 ->update([
257 'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
258 'status' => $subscription->status->getValue(),
259 'profile_id' => $subscription->gatewaySubscriptionId,
260 'customer_id' => $subscription->donorId,
261 'period' => $subscription->period->getValue(),
262 'frequency' => $subscription->frequency,
263 'initial_amount' => $subscription->amount->formatToDecimal(),
264 'recurring_amount' => $subscription->amount->formatToDecimal(),
265 'recurring_fee_amount' => isset($subscription->feeAmountRecovered) ? $subscription->feeAmountRecovered->formatToDecimal(
266 ) : 0,
267 'bill_times' => $subscription->installments,
268 'transaction_id' => $subscription->transactionId ?? '',
269 'product_id' => $subscription->donationFormId,
270 'campaign_id' => $subscription->campaignId,
271 'payment_mode' => $subscription->mode->getValue(),
272 ]);
273 } catch (Exception $exception) {
274 DB::query('ROLLBACK');
275
276 Log::error('Failed updating a subscription', compact('subscription'));
277
278 throw new Exception('Failed updating a subscription', 0, $exception);
279 }
280
281 DB::query('COMMIT');
282
283 Hooks::doAction('givewp_subscription_updated', $subscription);
284 }
285
286 /**
287 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
288 * @since 2.21.0 replace actions with givewp_subscription_deleting and givewp_subscription_deleted
289 * @since 2.20.0 consolidate meta deletion into a single query
290 * @since 2.19.6
291 *
292 * @throws Exception
293 */
294 public function delete(Subscription $subscription): bool
295 {
296 Hooks::doAction('givewp_subscription_deleting', $subscription);
297
298 DB::query('START TRANSACTION');
299
300 try {
301 DB::table('give_subscriptions')
302 ->where('id', $subscription->id)
303 ->delete();
304
305 DB::table('give_subscriptionmeta')
306 ->where('subscription_id', $subscription->id)
307 ->delete();
308 } catch (Exception $exception) {
309 DB::query('ROLLBACK');
310
311 Log::error('Failed deleting a subscription', compact('subscription'));
312
313 throw new Exception('Failed deleting a subscription', 0, $exception);
314 }
315
316 DB::query('COMMIT');
317
318 Hooks::doAction('givewp_subscription_deleted', $subscription);
319
320 return true;
321 }
322
323 /**
324 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
325 * @since 4.8.0
326 *
327 * @throws Exception
328 */
329 public function trash(Subscription $subscription): bool
330 {
331 DB::query('START TRANSACTION');
332
333 Hooks::doAction('givewp_subscription_trashing', $subscription);
334
335 try {
336 $previousStatus = DB::table('give_subscriptions')
337 ->where('id', $subscription->id)
338 ->value('status');
339
340 give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_status', $previousStatus);
341 give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_time', time());
342
343 DB::table('give_subscriptions')
344 ->where('id', $subscription->id)
345 ->update(['status' => SubscriptionStatus::TRASHED()->getValue()]);
346 } catch (Exception $exception) {
347 DB::query('ROLLBACK');
348
349 Log::error('Failed trashing a subscription', compact('subscription'));
350
351 throw new Exception('Failed trashing a subscription', 0, $exception);
352 }
353
354 DB::query('COMMIT');
355
356 Hooks::doAction('givewp_subscription_trashed', $subscription);
357
358 return true;
359 }
360
361 /**
362 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
363 * @since 4.12.0
364 *
365 * @throws Exception
366 */
367 public function unTrash(Subscription $subscription): bool
368 {
369 DB::query('START TRANSACTION');
370
371 Hooks::doAction('givewp_subscription_untrashing', $subscription);
372
373 try {
374 $previousStatus = give()->subscription_meta->get_meta($subscription->id, '_wp_trash_meta_status', true);
375
376 // If no previous status was saved, default to 'active'
377 if (empty($previousStatus)) {
378 $previousStatus = SubscriptionStatus::ACTIVE;
379 }
380
381 DB::table('give_subscriptions')
382 ->where('id', $subscription->id)
383 ->update(['status' => $previousStatus]);
384 } catch (Exception $exception) {
385 DB::query('ROLLBACK');
386
387 Log::error('Failed untrashing a subscription', compact('subscription'));
388
389 throw new Exception('Failed untrashing a subscription', 0, $exception);
390 }
391
392 DB::query('COMMIT');
393
394 Hooks::doAction('givewp_subscription_untrashed', $subscription);
395
396 return true;
397 }
398
399 /**
400 * Up to this point the donation is created first and then the subscription, and the donation is stored as the
401 * parent_payment_id of the subscription. This is backwards and should not be the case. But legacy code depends on
402 * this value, so we still need to store it for now.
403 *
404 * This should only be used when creating a new Subscription with its corresponding Donation. Do not add this value
405 * to the Subscription model as it should not be reference moving forward.
406 *
407 * @since 2.24.0 Save payment mode to subscription meta
408 * @since 2.23.0
409 *
410 * @return void
411 */
412 public function updateLegacyParentPaymentId(int $subscriptionId, int $donationId)
413 {
414 $mode = give_get_meta($donationId, DonationMetaKeys::MODE, true) ?? (give_is_test_mode() ? 'test' : 'live');
415
416 DB::table('give_subscriptions')
417 ->where('id', $subscriptionId)
418 ->update([
419 'parent_payment_id' => $donationId,
420 'payment_mode' => $mode,
421 ]);
422 }
423
424 /**
425 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
426 * @since 2.19.6
427 *
428 * @throws Exception
429 */
430 public function updateLegacyColumns(int $subscriptionId, array $columns): bool
431 {
432 foreach (Subscription::propertyKeys() as $key) {
433 if (array_key_exists($key, $columns)) {
434 throw new InvalidArgumentException("'$key' is not a legacy column.");
435 }
436 }
437
438 DB::query('START TRANSACTION');
439
440 try {
441 DB::table('give_subscriptions')
442 ->where('id', $subscriptionId)
443 ->update($columns);
444 } catch (Exception $exception) {
445 DB::query('ROLLBACK');
446
447 Log::error('Failed updating a subscription', compact('subscriptionId', 'columns'));
448
449 throw new Exception('Failed updating a subscription', 0, $exception);
450 }
451
452 DB::query('COMMIT');
453
454 return true;
455 }
456
457 /**
458 * Sets the payment mode for a given subscription
459 *
460 * @since 2.24.0
461 *
462 * @return void
463 */
464 public function updatePaymentMode(int $subscriptionId, SubscriptionMode $mode)
465 {
466 DB::table('give_subscriptions')
467 ->where('id', $subscriptionId)
468 ->update([
469 'payment_mode' => $mode->getValue(),
470 ]);
471 }
472
473 /**
474 * @since 2.23.0 update to no longer rely on parent_payment_id column as it will be deprecated
475 * @since 2.19.6
476 *
477 * @return int|null
478 */
479 public function getInitialDonationId(int $subscriptionId)
480 {
481 $query = DB::table('posts')
482 ->select('ID')
483 ->attachMeta(
484 'give_donationmeta',
485 'ID',
486 'donation_id',
487 [DonationMetaKeys::SUBSCRIPTION_ID, 'subscriptionId'],
488 [DonationMetaKeys::SUBSCRIPTION_INITIAL_DONATION, 'initialDonationId']
489 )
490 ->where('give_donationmeta_attach_meta_subscriptionId.meta_value', $subscriptionId)
491 ->where('give_donationmeta_attach_meta_initialDonationId.meta_value', 1)
492 ->get();
493
494 if (!$query) {
495 return null;
496 }
497
498 return (int)$query->ID;
499 }
500
501 /**
502 * @since 4.11.0 add campaignId to renewal
503 * @since 4.8.1 Remove campaignId from the attributes array since it is auto-generated based on the subscription's form.
504 * @since 4.8.0 Add campaignId support.
505 * @since 3.20.0
506 * @throws Exception
507 */
508 public function createRenewal(Subscription $subscription, array $attributes = []): Donation
509 {
510 $initialDonation = $subscription->initialDonation();
511
512 $donation = Donation::create(
513 array_merge([
514 'subscriptionId' => $subscription->id,
515 'gatewayId' => $subscription->gatewayId,
516 'amount' => $subscription->amount,
517 'status' => DonationStatus::COMPLETE(),
518 'type' => DonationType::RENEWAL(),
519 'donorId' => $subscription->donorId,
520 'formId' => $subscription->donationFormId,
521 'honorific' => $initialDonation->honorific,
522 'firstName' => $initialDonation->firstName,
523 'lastName' => $initialDonation->lastName,
524 'email' => $initialDonation->email,
525 'phone' => $initialDonation->phone,
526 'anonymous' => $initialDonation->anonymous,
527 'levelId' => $initialDonation->levelId,
528 'company' => $initialDonation->company,
529 'comment' => $initialDonation->comment,
530 'billingAddress' => $initialDonation->billingAddress,
531 'feeAmountRecovered' => $subscription->feeAmountRecovered,
532 'exchangeRate' => $initialDonation->exchangeRate,
533 'formTitle' => $initialDonation->formTitle,
534 'mode' => $subscription->mode->isLive() ? DonationMode::LIVE() : DonationMode::TEST(),
535 'donorIp' => $initialDonation->donorIp,
536 'campaignId' => $subscription->campaignId,
537 ], $attributes)
538 );
539
540 $subscription->bumpRenewalDate();
541 $subscription->save();
542
543 return $donation;
544 }
545
546 /**
547 * @since 2.19.6
548 *
549 * @return void
550 */
551 private function validateSubscription(Subscription $subscription)
552 {
553 foreach ($this->requiredSubscriptionProperties as $key) {
554 if (!isset($subscription->$key)) {
555 throw new InvalidArgumentException("'$key' is required.");
556 }
557 }
558
559 if (!$subscription->donor) {
560 throw new InvalidArgumentException("Invalid donorId, Donor does not exist");
561 }
562 }
563
564 /**
565 * @since 4.11.0 add campaign_id column to select
566 * @since 2.19.6
567 *
568 * @return ModelQueryBuilder<Subscription>
569 */
570 public function prepareQuery(): ModelQueryBuilder
571 {
572 $builder = new ModelQueryBuilder(Subscription::class);
573
574 return $builder->from('give_subscriptions')
575 ->select(
576 'id',
577 ['created', 'createdAt'],
578 ['expiration', 'renewsAt'],
579 ['customer_id', 'donorId'],
580 'period',
581 ['frequency', 'frequency'],
582 ['bill_times', 'installments'],
583 ['transaction_id', 'transactionId'],
584 ['payment_mode', 'mode'],
585 ['recurring_amount', 'amount'],
586 ['recurring_fee_amount', 'feeAmount'],
587 'status',
588 ['profile_id', 'gatewaySubscriptionId'],
589 ['product_id', 'donationFormId'],
590 ['campaign_id', 'campaignId']
591 )
592 ->attachMeta(
593 'give_donationmeta',
594 'parent_payment_id',
595 'donation_id',
596 [DonationMetaKeys::GATEWAY, 'gatewayId'],
597 [DonationMetaKeys::CURRENCY, 'currency']
598 );
599 }
600 }
601