PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.8.1
GiveWP – Donation Plugin and Fundraising Platform v4.16.8.1
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 2.3.2 2.30.0 All 255 releases
give / src / Donations / Repositories / DonationRepository.php

DonationRepository.php in GiveWP – Donation Plugin and Fundraising Platform 4.16.8.1, at src/Donations/Repositories/DonationRepository.php

707 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Donations\Repositories;
4
5 use Give\DonationForms\Models\DonationForm;
6 use Give\Donations\Actions\GeneratePurchaseKey;
7 use Give\Donations\Models\Donation;
8 use Give\Donations\ValueObjects\DonationMetaKeys;
9 use Give\Donations\ValueObjects\DonationMode;
10 use Give\Donations\ValueObjects\DonationStatus;
11 use Give\Framework\Database\DB;
12 use Give\Framework\Exceptions\Primitives\Exception;
13 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
14 use Give\Framework\FieldsAPI\Field;
15 use Give\Framework\Models\ModelQueryBuilder;
16 use Give\Framework\QueryBuilder\QueryBuilder;
17 use Give\Framework\Receipts\DonationReceipt;
18 use Give\Framework\Receipts\DonationReceiptBuilder;
19 use Give\Framework\Support\Facades\DateTime\Temporal;
20 use Give\Helpers\Call;
21 use Give\Helpers\Hooks;
22 use Give\Log\Log;
23
24 /**
25 * @since 2.20.0 update amount type, fee recovered, and exchange rate
26 * @since 2.19.6
27 */
28 class DonationRepository
29 {
30 /**
31 * @var DonationNotesRepository
32 */
33 public $notes;
34
35 /**
36 * @since 2.21.0
37 */
38 public function __construct()
39 {
40 $this->notes = give(DonationNotesRepository::class);
41 }
42
43 /**
44 * @since 2.19.6
45 *
46 * @var string[]
47 */
48 private $requiredDonationProperties = [
49 'formId',
50 'status',
51 'gatewayId',
52 'amount',
53 'donorId',
54 'firstName',
55 'type',
56 'email',
57 ];
58
59 /**
60 * Get Donation By ID
61 *
62 * @since 2.19.6
63 *
64 * @return Donation|null
65 */
66 public function getById(int $donationId)
67 {
68 return $this->prepareQuery()
69 ->where('ID', $donationId)
70 ->get();
71 }
72
73 /**
74 * @since 2.21.0
75 * @return Donation|null
76 */
77 public function getByGatewayTransactionId($gatewayTransactionId)
78 {
79 return $this->queryByGatewayTransactionId($gatewayTransactionId)->get();
80 }
81
82 /**
83 * @since 3.16.0
84 */
85 public function getTotalDonationCountByGatewayTransactionId($gatewayTransactionId): int
86 {
87 return $this->queryByGatewayTransactionId($gatewayTransactionId)->count();
88 }
89
90 /**
91 * @since 2.21.0
92 */
93 public function queryByGatewayTransactionId($gatewayTransactionId): ModelQueryBuilder
94 {
95 return $this->prepareQuery()
96 ->where('post_type', 'give_payment')
97 ->where('ID', function (QueryBuilder $builder) use ($gatewayTransactionId) {
98 $builder
99 ->select('donation_id')
100 ->from('give_donationmeta')
101 ->where('meta_key', DonationMetaKeys::GATEWAY_TRANSACTION_ID()->getValue())
102 ->where('meta_value', $gatewayTransactionId);
103 });
104 }
105
106 /**
107 * @since 2.19.6
108 *
109 * @param int $donationId
110 * @return ModelQueryBuilder<Donation>
111 */
112 public function queryById(int $donationId): ModelQueryBuilder
113 {
114 return $this->prepareQuery()
115 ->where('ID', $donationId);
116 }
117
118 /**
119 * @since 2.19.6
120 *
121 * @return Donation[]|null
122 */
123 public function getBySubscriptionId(int $subscriptionId)
124 {
125 return $this->queryBySubscriptionId($subscriptionId)->getAll();
126 }
127
128 /**
129 * @since 2.19.6
130 *
131 * @param int $subscriptionId
132 * @return ModelQueryBuilder<Donation>
133 */
134 public function queryBySubscriptionId(int $subscriptionId): ModelQueryBuilder
135 {
136 $initialDonationId = give()->subscriptions->getInitialDonationId($subscriptionId);
137
138 $renewals = $this->prepareQuery()
139 ->where('post_type', 'give_payment')
140 ->where('post_status', 'give_subscription')
141 ->whereIn('ID', function (QueryBuilder $builder) use ($subscriptionId) {
142 $builder
143 ->select('donation_id')
144 ->from('give_donationmeta')
145 ->where('meta_key', DonationMetaKeys::SUBSCRIPTION_ID)
146 ->where('meta_value', $subscriptionId);
147 });
148
149 return $renewals->orWhere('ID', $initialDonationId)->orderBy('post_date', 'DESC');
150 }
151
152 /**
153 * @since 2.19.6
154 *
155 * @param int $donorId
156 * @return ModelQueryBuilder<Donation>
157 */
158 public function queryByDonorId(int $donorId): ModelQueryBuilder
159 {
160 return $this->prepareQuery()
161 ->where('post_type', 'give_payment')
162 ->whereIn('ID', function (QueryBuilder $builder) use ($donorId) {
163 $builder
164 ->select('donation_id')
165 ->from('give_donationmeta')
166 ->where('meta_key', DonationMetaKeys::DONOR_ID)
167 ->where('meta_value', $donorId);
168 })
169 ->orderBy('post_date', 'DESC');
170 }
171
172 /**
173 * @since 4.0.0
174 */
175 public function getByReceiptId(string $receiptId): ?Donation
176 {
177 return $this->queryByReceiptId($receiptId)->get();
178 }
179
180 /**
181 * @since 4.0.0
182 */
183 public function queryByReceiptId(string $receiptId): ModelQueryBuilder
184 {
185 return $this->prepareQuery()
186 ->where('post_type', 'give_payment')
187 ->whereIn('ID', function (QueryBuilder $builder) use ($receiptId) {
188 $builder
189 ->select('donation_id')
190 ->from('give_donationmeta')
191 ->where('meta_key', DonationMetaKeys::PURCHASE_KEY)
192 ->where('meta_value', $receiptId);
193 });
194 }
195
196 /**
197 * @since 4.8.1 Moved campaignId assignment logic to getCoreDonationMetaForDatabase method to eliminate code duplication.
198 * @since 3.20.0 store meta using native WP functions
199 * @since 2.23.0 retrieve the post_parent instead of relying on parentId property
200 * @since 2.21.0 replace actions with givewp_donation_creating and givewp_donation_created
201 * @since 2.20.0 mutate model and return void
202 * @since 2.19.6
203 *
204 * @return void
205 * @throws Exception|InvalidArgumentException
206 */
207 public function insert(Donation $donation)
208 {
209 $this->validateDonation($donation);
210
211 Hooks::doAction('givewp_donation_creating', $donation);
212
213 $dateCreated = Temporal::withoutMicroseconds($donation->createdAt ?: Temporal::getCurrentDateTime());
214 $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated);
215 $dateUpdated = $donation->updatedAt ?? $dateCreated;
216 $dateUpdatedFormatted = Temporal::getFormattedDateTime($dateUpdated);
217
218 DB::query('START TRANSACTION');
219
220 try {
221 DB::table('posts')
222 ->insert([
223 'post_date' => $dateCreatedFormatted,
224 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted),
225 'post_modified' => $dateUpdatedFormatted,
226 'post_modified_gmt' => get_gmt_from_date($dateUpdatedFormatted),
227 'post_status' => $this->getPersistedDonationStatus($donation)->getValue(),
228 'post_type' => 'give_payment',
229 'post_parent' => $this->deriveLegacyDonationParentId($donation),
230 ]);
231
232 $donationId = DB::last_insert_id();
233
234 $donationMeta = $this->getCoreDonationMetaForDatabase($donation);
235
236 foreach ($donationMeta as $metaKey => $metaValue) {
237 give()->payment_meta->add_meta($donationId, $metaKey, $metaValue);
238 }
239 } catch (Exception $exception) {
240 DB::query('ROLLBACK');
241
242 Log::error('Failed creating a donation', compact('donation'));
243
244 throw new $exception('Failed creating a donation');
245 }
246
247 DB::query('COMMIT');
248
249 $donation->id = $donationId;
250
251 $donation->createdAt = $dateCreated;
252 $donation->updatedAt = $dateUpdated;
253
254 if (!isset($donation->formTitle)) {
255 $donation->formTitle = $this->getFormTitle($donation->formId);
256 }
257
258 if (!isset($donation->purchaseKey)) {
259 $donation->purchaseKey = $donationMeta[DonationMetaKeys::PURCHASE_KEY];
260 }
261
262 Hooks::doAction('givewp_donation_created', $donation);
263 }
264
265 /**
266 * @since 4.6.0 Turn createdAt property updatable
267 * @since 2.23.1 Use give_update_meta() method to update entries on give_donationmeta table
268 * @since 2.23.0 retrieve the post_parent instead of relying on parentId property
269 * @since 2.21.0 replace actions with givewp_donation_updating and givewp_donation_updated
270 * @since 2.20.0 return void
271 * @since 2.19.6
272 *
273 * @return void
274 * @throws Exception|InvalidArgumentException
275 */
276 public function update(Donation $donation)
277 {
278 $this->validateDonation($donation);
279
280 Hooks::doAction('givewp_donation_updating', $donation);
281
282 $dateCreated = Temporal::withoutMicroseconds($donation->createdAt);
283 $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated);
284 $now = Temporal::withoutMicroseconds(Temporal::getCurrentDateTime());
285 $nowFormatted = Temporal::getFormattedDateTime($now);
286
287 DB::query('START TRANSACTION');
288
289 try {
290 DB::table('posts')
291 ->where('ID', $donation->id)
292 ->update([
293 'post_date' => $dateCreatedFormatted,
294 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted),
295 'post_modified' => $nowFormatted,
296 'post_modified_gmt' => get_gmt_from_date($nowFormatted),
297 'post_status' => $this->getPersistedDonationStatus($donation)->getValue(),
298 'post_type' => 'give_payment',
299 'post_parent' => $this->deriveLegacyDonationParentId($donation),
300 ]);
301
302 foreach ($this->getCoreDonationMetaForDatabase($donation) as $metaKey => $metaValue) {
303 give()->payment_meta->update_meta($donation->id, $metaKey, $metaValue);
304 }
305 } catch (Exception $exception) {
306 DB::query('ROLLBACK');
307
308 Log::error('Failed updating a donation', compact('donation'));
309
310 throw new $exception('Failed updating a donation');
311 }
312
313 $donation->updatedAt = $now;
314
315 DB::query('COMMIT');
316
317 Hooks::doAction('givewp_donation_updated', $donation);
318 }
319
320 /**
321 * @since 2.21.0 replace actions with givewp_donation_deleting and givewp_donation_deleted
322 * @since 2.20.0 consolidate meta deletion into a single query
323 * @since 2.19.6
324 *
325 * @throws Exception
326 */
327 public function delete(Donation $donation): bool
328 {
329 DB::query('START TRANSACTION');
330
331 Hooks::doAction('givewp_donation_deleting', $donation);
332
333 try {
334 DB::table('posts')
335 ->where('id', $donation->id)
336 ->delete();
337
338 DB::table('give_donationmeta')
339 ->where('donation_id', $donation->id)
340 ->delete();
341 } catch (Exception $exception) {
342 DB::query('ROLLBACK');
343
344 Log::error('Failed deleting a donation', compact('donation'));
345
346 throw new $exception('Failed deleting a donation');
347 }
348
349 DB::query('COMMIT');
350
351 Hooks::doAction('givewp_donation_deleted', $donation);
352
353 return true;
354 }
355
356 /**
357 * @since 4.6.0
358 *
359 * @throws Exception
360 */
361 public function trash(Donation $donation): bool
362 {
363 DB::query('START TRANSACTION');
364
365 Hooks::doAction('givewp_donation_trashing', $donation);
366
367 try {
368 $previousStatus = DB::table('posts')
369 ->where('ID', $donation->id)
370 ->value('post_status');
371
372 give()->payment_meta->update_meta($donation->id, '_wp_trash_meta_status', $previousStatus);
373 give()->payment_meta->update_meta($donation->id, '_wp_trash_meta_time', time());
374
375 DB::table('posts')
376 ->where('ID', $donation->id)
377 ->update(['post_status' => 'trash']);
378 } catch (Exception $exception) {
379 DB::query('ROLLBACK');
380
381 Log::error('Failed trashing a donation', compact('donation'));
382
383 throw new $exception('Failed trashing a donation');
384 }
385
386 DB::query('COMMIT');
387
388 Hooks::doAction('givewp_donation_trashed', $donation);
389
390 return true;
391 }
392
393 /**
394 * @since 4.12.0
395 *
396 * @throws Exception
397 */
398 public function unTrash(Donation $donation): bool
399 {
400 DB::query('START TRANSACTION');
401
402 Hooks::doAction('givewp_donation_untrashing', $donation);
403
404 try {
405 $previousStatus = give()->payment_meta->get_meta($donation->id, '_wp_trash_meta_status', true);
406
407 // If no previous status was saved, default to 'publish' (completed)
408 if (empty($previousStatus)) {
409 $previousStatus = DonationStatus::COMPLETE;
410 }
411
412 DB::table('posts')
413 ->where('id', $donation->id)
414 ->update(['post_status' => $previousStatus]);
415 } catch (Exception $exception) {
416 DB::query('ROLLBACK');
417
418 Log::error('Failed untrashing a donation', compact('donation'));
419
420 throw new $exception('Failed untrashing a donation');
421 }
422
423 DB::query('COMMIT');
424
425 Hooks::doAction('givewp_donation_untrashed', $donation);
426
427 return true;
428 }
429
430 /**
431 * @since 4.8.1 Consolidated campaignId assignment logic from insert method to eliminate duplication and ensure donation model is updated.
432 * @since 4.0.0 added campaignId
433 * @since 3.9.0 Added meta for phone property
434 * @since 3.2.0 added meta for honorific property
435 * @since 2.20.0 update amount to use new type, and add currency and exchange rate
436 * @since 2.19.6
437 */
438 private function getCoreDonationMetaForDatabase(Donation $donation): array
439 {
440 $meta = [
441 DonationMetaKeys::GATEWAY_TRANSACTION_ID => $donation->gatewayTransactionId,
442 DonationMetaKeys::AMOUNT => give_sanitize_amount_for_db(
443 $donation->amount->formatToDecimal(),
444 ['currency' => $donation->amount->getCurrency()]
445 ),
446 DonationMetaKeys::CURRENCY => $donation->amount->getCurrency()->getCode(),
447 DonationMetaKeys::EXCHANGE_RATE => $donation->exchangeRate,
448 DonationMetaKeys::GATEWAY => $donation->gatewayId,
449 DonationMetaKeys::DONOR_ID => $donation->donorId,
450 DonationMetaKeys::FIRST_NAME => $donation->firstName,
451 DonationMetaKeys::LAST_NAME => $donation->lastName,
452 DonationMetaKeys::EMAIL => $donation->email,
453 DonationMetaKeys::PHONE => $donation->phone,
454 DonationMetaKeys::FORM_ID => $donation->formId,
455 DonationMetaKeys::FORM_TITLE => $donation->formTitle ?? $this->getFormTitle($donation->formId),
456 DonationMetaKeys::MODE => isset($donation->mode) ?
457 $donation->mode->getValue() :
458 $this->getDefaultDonationMode()->getValue(),
459 DonationMetaKeys::PURCHASE_KEY => $donation->purchaseKey ?? Call::invoke(
460 GeneratePurchaseKey::class,
461 $donation->email
462 ),
463 DonationMetaKeys::DONOR_IP => $donation->donorIp ?? give_get_ip(),
464 DonationMetaKeys::LEVEL_ID => $donation->levelId,
465 DonationMetaKeys::ANONYMOUS => (int)$donation->anonymous,
466 DonationMetaKeys::CAMPAIGN_ID => $donation->campaignId,
467 ];
468
469 // If the donation is not associated with a campaign, try to find the campaign ID by the form ID
470 if (!$donation->campaignId) {
471 $campaign = give()->campaigns->getByFormId($donation->formId);
472 $donation->campaignId = $campaign ? $campaign->id : null;
473 }
474
475 if ($donation->campaignId !== null) {
476 $meta[DonationMetaKeys::CAMPAIGN_ID] = $donation->campaignId;
477 }
478
479 if ($donation->feeAmountRecovered !== null) {
480 $meta[DonationMetaKeys::FEE_AMOUNT_RECOVERED] = $donation->feeAmountRecovered->formatToDecimal();
481 }
482
483 if ($donation->billingAddress !== null) {
484 $meta[DonationMetaKeys::BILLING_COUNTRY] = $donation->billingAddress->country;
485 $meta[DonationMetaKeys::BILLING_ADDRESS2] = $donation->billingAddress->address2;
486 $meta[DonationMetaKeys::BILLING_CITY] = $donation->billingAddress->city;
487 $meta[DonationMetaKeys::BILLING_ADDRESS1] = $donation->billingAddress->address1;
488 $meta[DonationMetaKeys::BILLING_STATE] = $donation->billingAddress->state;
489 $meta[DonationMetaKeys::BILLING_ZIP] = $donation->billingAddress->zip;
490 }
491
492 if (isset($donation->subscriptionId)) {
493 $meta[DonationMetaKeys::SUBSCRIPTION_ID] = $donation->subscriptionId;
494 }
495
496 if ($donation->type->isSubscription()) {
497 $meta[DonationMetaKeys::SUBSCRIPTION_INITIAL_DONATION] = 1;
498 $meta[DonationMetaKeys::IS_RECURRING] = 1;
499 }
500
501 if ($donation->company !== null) {
502 $meta[DonationMetaKeys::COMPANY] = $donation->company;
503 }
504
505 if ($donation->comment !== null) {
506 $meta[DonationMetaKeys::COMMENT] = $donation->comment;
507 }
508
509 if ($donation->honorific !== null) {
510 $meta[DonationMetaKeys::HONORIFIC] = $donation->honorific;
511 }
512
513 return $meta;
514 }
515
516 /**
517 * @since 2.19.6
518 *
519 * @return int|null
520 */
521 public function getSequentialId(int $donationId)
522 {
523 $query = DB::table('give_sequential_ordering')->where('payment_id', $donationId)->get();
524
525 if (!$query) {
526 return null;
527 }
528
529 return (int)$query->id;
530 }
531
532 /**
533 * @since 2.19.6
534 *
535 * @return void
536 */
537 private function validateDonation(Donation $donation)
538 {
539 foreach ($this->requiredDonationProperties as $key) {
540 if (!isset($donation->$key)) {
541 throw new InvalidArgumentException("'$key' is required.");
542 }
543 }
544
545 if ( $donation->subscriptionId && $donation->type->isSingle()) {
546 throw new InvalidArgumentException('Subscription ID can only be set for recurring donations.');
547 }
548
549 if ( !$donation->subscriptionId && ( $donation->type->isRenewal() || $donation->type->isSubscription() ) ) {
550 throw new InvalidArgumentException('Subscription ID is required for recurring donations.');
551 }
552
553 if (!$donation->donor) {
554 throw new InvalidArgumentException("Invalid donorId, Donor does not exist");
555 }
556 }
557
558 /**
559 * Provides the donation status with consideration for the donation type. If a donation is a renewal type and its
560 * status is "complete", then the status will return "renewal". In the future, "renewal" will no longer be a valid
561 * status, at which point this function will be unnecessary.
562 *
563 * New renewal donations moving forward should set the type as "renewal" and the status as "complete".
564 *
565 * @since 2.23.0
566 */
567 private function getPersistedDonationStatus(Donation $donation): DonationStatus {
568 if ( $donation->status->isComplete() && $donation->type->isRenewal() ) {
569 return DonationStatus::RENEWAL();
570 }
571
572 return $donation->status;
573 }
574
575 /**
576 * @since 2.19.6
577 */
578 private function getDefaultDonationMode(): DonationMode
579 {
580 $mode = give_is_test_mode() ? 'test' : 'live';
581
582 return new DonationMode($mode);
583 }
584
585 /**
586 * We're moving away from using the parent_id column for donations, but we still need to support it for now as
587 * legacy code still relies on it. It is only stored and should never be used in the model.
588 *
589 * @since 2.23.0
590 */
591 private function deriveLegacyDonationParentId(Donation $donation): int
592 {
593 return $donation->type->isRenewal() ? give()->subscriptions->getInitialDonationId($donation->subscriptionId) : 0;
594 }
595
596 /**
597 * @since 2.19.6
598 */
599 public function getFormTitle(int $formId): string
600 {
601 $form = DB::table('posts')
602 ->where('id', $formId)
603 ->get();
604
605 if (!$form) {
606 return '';
607 }
608
609 return $form->post_title;
610 }
611
612 /**
613 * @since 2.23.0 no longer retrieve the post_parent from the database as parentId is deprecated
614 *
615 * @return ModelQueryBuilder<Donation>
616 */
617 public function prepareQuery(): ModelQueryBuilder
618 {
619 $builder = new ModelQueryBuilder(Donation::class);
620
621 return $builder->from('posts')
622 ->select(
623 ['ID', 'id'],
624 ['post_date', 'createdAt'],
625 ['post_modified', 'updatedAt'],
626 ['post_status', 'status']
627 )
628 ->attachMeta(
629 'give_donationmeta',
630 'ID',
631 'donation_id',
632 ...DonationMetaKeys::getColumnsForAttachMetaQuery()
633 )
634 ->where('post_type', 'give_payment');
635 }
636
637 /**
638 * @since 2.19.6
639 */
640 public function getTotalDonationCountByDonorId(int $donorId): int
641 {
642 return DB::table('posts')
643 ->where('post_type', 'give_payment')
644 ->whereIn('ID', function (QueryBuilder $builder) use ($donorId) {
645 $builder
646 ->select('donation_id')
647 ->from('give_donationmeta')
648 ->where('meta_key', DonationMetaKeys::DONOR_ID)
649 ->where('meta_value', $donorId);
650 })
651 ->count();
652 }
653
654 /**
655 * @since 2.19.6
656 *
657 * @return array|bool|null
658 */
659 public function getAllDonationIdsByDonorId(int $donorId)
660 {
661 return array_column(
662 DB::table('give_donationmeta')
663 ->select('donation_id')
664 ->where('meta_key', DonationMetaKeys::DONOR_ID)
665 ->where('meta_value', $donorId)
666 ->getAll(),
667 'donation_id'
668 );
669 }
670
671 /**
672 * @since 2.23.1 Fixed order by property, see https://github.com/impress-org/givewp/pull/6559
673 * @since 2.21.2
674 *
675 * @return Donation|null
676 */
677 public function getFirstDonation() {
678 return $this->prepareQuery()
679 ->limit(1)
680 ->orderBy('post_date', 'ASC')
681 ->get();
682 }
683
684 /**
685 * @since 2.23.1 Fixed order by property, see https://github.com/impress-org/givewp/pull/6559
686 * @since 2.21.2
687 *
688 * @return Donation|null
689 */
690 public function getLatestDonation() {
691 return $this->prepareQuery()
692 ->limit(1)
693 ->orderBy('post_date', 'DESC')
694 ->get();
695 }
696
697 /**
698 * @since 4.3.0
699 */
700 public function getConfirmationPageReceipt(Donation $donation): DonationReceipt
701 {
702 $receipt = new DonationReceipt($donation);
703
704 return (new DonationReceiptBuilder($receipt))->toConfirmationPage();
705 }
706 }
707