PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.1
GiveWP – Donation Plugin and Fundraising Platform v4.16.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 2.3.2 2.30.0 2.31.0 2.31.1 All 253 releases
give / src / Subscriptions / Repositories / SubscriptionRepository.php
SubscriptionRepository.php
595 lines 19.0 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.11.0 add campaign_id column to insert
165 * @since 2.24.0 add payment_mode column to insert
166 * @since 2.21.0 replace actions with givewp_subscription_creating and givewp_subscription_created
167 * @since 2.19.6
168 *
169 * @return void
170 * @throws Exception
171 */
172 public function insert(Subscription $subscription)
173 {
174 $this->validateSubscription($subscription);
175
176 if ($subscription->renewsAt === null) {
177 $subscription->bumpRenewalDate();
178 }
179
180 // If the subscription is not associated with a campaign, try to find the campaign ID by the form ID
181 if (!$subscription->campaignId) {
182 $campaign = give()->campaigns->getByFormId($subscription->donationFormId);
183 $subscription->campaignId = $campaign ? $campaign->id : null;
184 }
185
186 Hooks::doAction('givewp_subscription_creating', $subscription);
187
188 $dateCreated = Temporal::withoutMicroseconds($subscription->createdAt ?: Temporal::getCurrentDateTime());
189
190 DB::query('START TRANSACTION');
191
192 try {
193 DB::table('give_subscriptions')->insert([
194 'created' => Temporal::getFormattedDateTime($dateCreated),
195 'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
196 'status' => $subscription->status->getValue(),
197 'profile_id' => $subscription->gatewaySubscriptionId ?? '',
198 'customer_id' => $subscription->donorId,
199 'period' => $subscription->period->getValue(),
200 'frequency' => $subscription->frequency,
201 'initial_amount' => $subscription->amount->formatToDecimal(),
202 'recurring_amount' => $subscription->amount->formatToDecimal(),
203 'recurring_fee_amount' => $subscription->feeAmountRecovered !== null ? $subscription->feeAmountRecovered->formatToDecimal(
204 ) : 0,
205 'bill_times' => $subscription->installments,
206 'transaction_id' => $subscription->transactionId ?? '',
207 'product_id' => $subscription->donationFormId,
208 'campaign_id' => $subscription->campaignId,
209 'payment_mode' => $subscription->mode->getValue(),
210 ]);
211 } catch (Exception $exception) {
212 DB::query('ROLLBACK');
213
214 Log::error('Failed creating a subscription', compact('subscription'));
215
216 throw new $exception('Failed creating a subscription');
217 }
218
219 DB::query('COMMIT');
220
221 $subscriptionId = DB::last_insert_id();
222
223 $subscription->id = $subscriptionId;
224 $subscription->createdAt = $dateCreated;
225
226 Hooks::doAction('givewp_subscription_created', $subscription);
227 }
228
229 /**
230 * @since 4.11.0 add campaign_id column to update
231 * @since 3.17.0 add expiration column to update
232 * @since 2.24.0 add payment_mode column to update
233 * @since 2.21.0 replace actions with givewp_subscription_updating and givewp_subscription_updated
234 * @since 2.19.6
235 *
236 * @return void
237 * @throws Exception
238 */
239 public function update(Subscription $subscription)
240 {
241 $this->validateSubscription($subscription);
242
243 if ($subscription->renewsAt === null) {
244 throw new InvalidArgumentException('renewsAt is required.');
245 }
246
247 Hooks::doAction('givewp_subscription_updating', $subscription);
248
249 DB::query('START TRANSACTION');
250
251 try {
252 DB::table('give_subscriptions')
253 ->where('id', $subscription->id)
254 ->update([
255 'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
256 'status' => $subscription->status->getValue(),
257 'profile_id' => $subscription->gatewaySubscriptionId,
258 'customer_id' => $subscription->donorId,
259 'period' => $subscription->period->getValue(),
260 'frequency' => $subscription->frequency,
261 'initial_amount' => $subscription->amount->formatToDecimal(),
262 'recurring_amount' => $subscription->amount->formatToDecimal(),
263 'recurring_fee_amount' => isset($subscription->feeAmountRecovered) ? $subscription->feeAmountRecovered->formatToDecimal(
264 ) : 0,
265 'bill_times' => $subscription->installments,
266 'transaction_id' => $subscription->transactionId ?? '',
267 'product_id' => $subscription->donationFormId,
268 'campaign_id' => $subscription->campaignId,
269 'payment_mode' => $subscription->mode->getValue(),
270 ]);
271 } catch (Exception $exception) {
272 DB::query('ROLLBACK');
273
274 Log::error('Failed updating a subscription', compact('subscription'));
275
276 throw new $exception('Failed updating a subscription');
277 }
278
279 DB::query('COMMIT');
280
281 Hooks::doAction('givewp_subscription_updated', $subscription);
282 }
283
284 /**
285 * @since 2.21.0 replace actions with givewp_subscription_deleting and givewp_subscription_deleted
286 * @since 2.20.0 consolidate meta deletion into a single query
287 * @since 2.19.6
288 *
289 * @throws Exception
290 */
291 public function delete(Subscription $subscription): bool
292 {
293 Hooks::doAction('givewp_subscription_deleting', $subscription);
294
295 DB::query('START TRANSACTION');
296
297 try {
298 DB::table('give_subscriptions')
299 ->where('id', $subscription->id)
300 ->delete();
301
302 DB::table('give_subscriptionmeta')
303 ->where('subscription_id', $subscription->id)
304 ->delete();
305 } catch (Exception $exception) {
306 DB::query('ROLLBACK');
307
308 Log::error('Failed deleting a subscription', compact('subscription'));
309
310 throw new $exception('Failed deleting a subscription');
311 }
312
313 DB::query('COMMIT');
314
315 Hooks::doAction('givewp_subscription_deleted', $subscription);
316
317 return true;
318 }
319
320 /**
321 * @since 4.8.0
322 *
323 * @throws Exception
324 */
325 public function trash(Subscription $subscription): bool
326 {
327 DB::query('START TRANSACTION');
328
329 Hooks::doAction('givewp_subscription_trashing', $subscription);
330
331 try {
332 $previousStatus = DB::table('give_subscriptions')
333 ->where('id', $subscription->id)
334 ->value('status');
335
336 give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_status', $previousStatus);
337 give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_time', time());
338
339 DB::table('give_subscriptions')
340 ->where('id', $subscription->id)
341 ->update(['status' => SubscriptionStatus::TRASHED()->getValue()]);
342 } catch (Exception $exception) {
343 DB::query('ROLLBACK');
344
345 Log::error('Failed trashing a subscription', compact('subscription'));
346
347 throw new $exception('Failed trashing a subscription');
348 }
349
350 DB::query('COMMIT');
351
352 Hooks::doAction('givewp_subscription_trashed', $subscription);
353
354 return true;
355 }
356
357 /**
358 * @since 4.12.0
359 *
360 * @throws Exception
361 */
362 public function unTrash(Subscription $subscription): bool
363 {
364 DB::query('START TRANSACTION');
365
366 Hooks::doAction('givewp_subscription_untrashing', $subscription);
367
368 try {
369 $previousStatus = give()->subscription_meta->get_meta($subscription->id, '_wp_trash_meta_status', true);
370
371 // If no previous status was saved, default to 'active'
372 if (empty($previousStatus)) {
373 $previousStatus = SubscriptionStatus::ACTIVE;
374 }
375
376 DB::table('give_subscriptions')
377 ->where('id', $subscription->id)
378 ->update(['status' => $previousStatus]);
379 } catch (Exception $exception) {
380 DB::query('ROLLBACK');
381
382 Log::error('Failed untrashing a subscription', compact('subscription'));
383
384 throw new $exception('Failed untrashing a subscription');
385 }
386
387 DB::query('COMMIT');
388
389 Hooks::doAction('givewp_subscription_untrashed', $subscription);
390
391 return true;
392 }
393
394 /**
395 * Up to this point the donation is created first and then the subscription, and the donation is stored as the
396 * parent_payment_id of the subscription. This is backwards and should not be the case. But legacy code depends on
397 * this value, so we still need to store it for now.
398 *
399 * This should only be used when creating a new Subscription with its corresponding Donation. Do not add this value
400 * to the Subscription model as it should not be reference moving forward.
401 *
402 * @since 2.24.0 Save payment mode to subscription meta
403 * @since 2.23.0
404 *
405 * @return void
406 */
407 public function updateLegacyParentPaymentId(int $subscriptionId, int $donationId)
408 {
409 $mode = give_get_meta($donationId, DonationMetaKeys::MODE, true) ?? (give_is_test_mode() ? 'test' : 'live');
410
411 DB::table('give_subscriptions')
412 ->where('id', $subscriptionId)
413 ->update([
414 'parent_payment_id' => $donationId,
415 'payment_mode' => $mode,
416 ]);
417 }
418
419 /**
420 * @since 2.19.6
421 *
422 * @throws Exception
423 */
424 public function updateLegacyColumns(int $subscriptionId, array $columns): bool
425 {
426 foreach (Subscription::propertyKeys() as $key) {
427 if (array_key_exists($key, $columns)) {
428 throw new InvalidArgumentException("'$key' is not a legacy column.");
429 }
430 }
431
432 DB::query('START TRANSACTION');
433
434 try {
435 DB::table('give_subscriptions')
436 ->where('id', $subscriptionId)
437 ->update($columns);
438 } catch (Exception $exception) {
439 DB::query('ROLLBACK');
440
441 Log::error('Failed updating a subscription', compact('subscriptionId', 'columns'));
442
443 throw new $exception('Failed updating a subscription');
444 }
445
446 DB::query('COMMIT');
447
448 return true;
449 }
450
451 /**
452 * Sets the payment mode for a given subscription
453 *
454 * @since 2.24.0
455 *
456 * @return void
457 */
458 public function updatePaymentMode(int $subscriptionId, SubscriptionMode $mode)
459 {
460 DB::table('give_subscriptions')
461 ->where('id', $subscriptionId)
462 ->update([
463 'payment_mode' => $mode->getValue(),
464 ]);
465 }
466
467 /**
468 * @since 2.23.0 update to no longer rely on parent_payment_id column as it will be deprecated
469 * @since 2.19.6
470 *
471 * @return int|null
472 */
473 public function getInitialDonationId(int $subscriptionId)
474 {
475 $query = DB::table('posts')
476 ->select('ID')
477 ->attachMeta(
478 'give_donationmeta',
479 'ID',
480 'donation_id',
481 [DonationMetaKeys::SUBSCRIPTION_ID, 'subscriptionId'],
482 [DonationMetaKeys::SUBSCRIPTION_INITIAL_DONATION, 'initialDonationId']
483 )
484 ->where('give_donationmeta_attach_meta_subscriptionId.meta_value', $subscriptionId)
485 ->where('give_donationmeta_attach_meta_initialDonationId.meta_value', 1)
486 ->get();
487
488 if (!$query) {
489 return null;
490 }
491
492 return (int)$query->ID;
493 }
494
495 /**
496 * @since 4.11.0 add campaignId to renewal
497 * @since 4.8.1 Remove campaignId from the attributes array since it is auto-generated based on the subscription's form.
498 * @since 4.8.0 Add campaignId support.
499 * @since 3.20.0
500 * @throws Exception
501 */
502 public function createRenewal(Subscription $subscription, array $attributes = []): Donation
503 {
504 $initialDonation = $subscription->initialDonation();
505
506 $donation = Donation::create(
507 array_merge([
508 'subscriptionId' => $subscription->id,
509 'gatewayId' => $subscription->gatewayId,
510 'amount' => $subscription->amount,
511 'status' => DonationStatus::COMPLETE(),
512 'type' => DonationType::RENEWAL(),
513 'donorId' => $subscription->donorId,
514 'formId' => $subscription->donationFormId,
515 'honorific' => $initialDonation->honorific,
516 'firstName' => $initialDonation->firstName,
517 'lastName' => $initialDonation->lastName,
518 'email' => $initialDonation->email,
519 'phone' => $initialDonation->phone,
520 'anonymous' => $initialDonation->anonymous,
521 'levelId' => $initialDonation->levelId,
522 'company' => $initialDonation->company,
523 'comment' => $initialDonation->comment,
524 'billingAddress' => $initialDonation->billingAddress,
525 'feeAmountRecovered' => $subscription->feeAmountRecovered,
526 'exchangeRate' => $initialDonation->exchangeRate,
527 'formTitle' => $initialDonation->formTitle,
528 'mode' => $subscription->mode->isLive() ? DonationMode::LIVE() : DonationMode::TEST(),
529 'donorIp' => $initialDonation->donorIp,
530 'campaignId' => $subscription->campaignId,
531 ], $attributes)
532 );
533
534 $subscription->bumpRenewalDate();
535 $subscription->save();
536
537 return $donation;
538 }
539
540 /**
541 * @since 2.19.6
542 *
543 * @return void
544 */
545 private function validateSubscription(Subscription $subscription)
546 {
547 foreach ($this->requiredSubscriptionProperties as $key) {
548 if (!isset($subscription->$key)) {
549 throw new InvalidArgumentException("'$key' is required.");
550 }
551 }
552
553 if (!$subscription->donor) {
554 throw new InvalidArgumentException("Invalid donorId, Donor does not exist");
555 }
556 }
557
558 /**
559 * @since 4.11.0 add campaign_id column to select
560 * @since 2.19.6
561 *
562 * @return ModelQueryBuilder<Subscription>
563 */
564 public function prepareQuery(): ModelQueryBuilder
565 {
566 $builder = new ModelQueryBuilder(Subscription::class);
567
568 return $builder->from('give_subscriptions')
569 ->select(
570 'id',
571 ['created', 'createdAt'],
572 ['expiration', 'renewsAt'],
573 ['customer_id', 'donorId'],
574 'period',
575 ['frequency', 'frequency'],
576 ['bill_times', 'installments'],
577 ['transaction_id', 'transactionId'],
578 ['payment_mode', 'mode'],
579 ['recurring_amount', 'amount'],
580 ['recurring_fee_amount', 'feeAmount'],
581 'status',
582 ['profile_id', 'gatewaySubscriptionId'],
583 ['product_id', 'donationFormId'],
584 ['campaign_id', 'campaignId']
585 )
586 ->attachMeta(
587 'give_donationmeta',
588 'parent_payment_id',
589 'donation_id',
590 [DonationMetaKeys::GATEWAY, 'gatewayId'],
591 [DonationMetaKeys::CURRENCY, 'currency']
592 );
593 }
594 }
595