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 / Donors / Repositories / DonorRepository.php

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

578 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Donors\Repositories;
4
5 use Exception;
6 use Give\Donations\ValueObjects\DonationMetaKeys;
7 use Give\Donors\Exceptions\FailedDonorUpdateException;
8 use Give\Donors\Models\Donor;
9 use Give\Donors\Models\DonorModelQueryBuilder;
10 use Give\Donors\ValueObjects\DonorMetaKeys;
11 use Give\Donors\ValueObjects\DonorType;
12 use Give\Framework\Database\DB;
13 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
14 use Give\Framework\Support\Facades\DateTime\Temporal;
15 use Give\Helpers\Hooks;
16 use Give\Log\Log;
17
18 /**
19 * @since 2.19.6
20 */
21 class DonorRepository
22 {
23 /**
24 * @since 4.4.0
25 *
26 * @var DonorNotesRepository
27 */
28 public $notes;
29
30 /**
31 * @var string[]
32 */
33 private $requiredDonorProperties = [
34 'name',
35 'firstName',
36 'lastName',
37 'email',
38 ];
39
40 /**
41 * @since 4.4.0
42 */
43 public function __construct()
44 {
45 $this->notes = give(DonorNotesRepository::class);
46 }
47
48 /**
49 * Query Donor By ID
50 *
51 * @since 2.24.0 replace ModelQueryBuilder with DonorModelQueryBuilder
52 * @since 2.19.6
53 *
54 * @return DonorModelQueryBuilder<Donor>
55 */
56 public function queryById(int $donorId): DonorModelQueryBuilder
57 {
58 return $this->prepareQuery()
59 ->where('id', $donorId);
60 }
61
62 /**
63 * Get Donor By ID
64 *
65 * @since 2.19.6
66 *
67 * @return Donor|null
68 */
69 public function getById(int $donorId)
70 {
71 return $this->queryById($donorId)->get();
72 }
73
74 /**
75 * Get Donor By WP User ID
76 *
77 * @since 2.19.6
78 *
79 * @return Donor|null
80 */
81 public function getByWpUserId(int $userId)
82 {
83 // user_id can technically be 0 so make sure to return null
84 if (!$userId) {
85 return null;
86 }
87
88 return $this->prepareQuery()
89 ->where('user_id', $userId)
90 ->get();
91 }
92
93 /**
94 * @since 2.19.6
95 *
96 * @return array|bool
97 */
98 public function getAdditionalEmails(int $donorId)
99 {
100 $additionalEmails = DB::table('give_donormeta')
101 ->select(['meta_value', 'email'])
102 ->where('meta_key', DonorMetaKeys::ADDITIONAL_EMAILS)
103 ->where('donor_id', $donorId)
104 ->getAll();
105
106 if (!$additionalEmails) {
107 return null;
108 }
109
110 return array_column($additionalEmails, 'email');
111 }
112
113 /**
114 * @since 3.20.0 store meta using native WP functions
115 * @since 3.7.0 Add support to "phone" property
116 * @since 2.24.0 add support for $donor->totalAmountDonated and $donor->totalNumberOfDonation
117 * @since 2.21.0 add actions givewp_donor_creating and givewp_donor_created
118 * @since 2.20.0 mutate model and return void
119 * @since 2.19.6
120 *
121 * @return void
122 * @throws Exception
123 */
124 public function insert(Donor $donor)
125 {
126 $this->validateDonor($donor);
127
128 Hooks::doAction('givewp_donor_creating', $donor);
129
130 $dateCreated = Temporal::withoutMicroseconds($donor->createdAt ?: Temporal::getCurrentDateTime());
131
132 DB::query('START TRANSACTION');
133
134 $args = [
135 'date_created' => Temporal::getFormattedDateTime($dateCreated),
136 'user_id' => $donor->userId ?? 0,
137 'email' => $donor->email,
138 'name' => $donor->name,
139 ];
140
141 if (isset($donor->phone)) {
142 $args['phone'] = $donor->phone;
143 }
144
145 if (isset($donor->totalAmountDonated)) {
146 $args['purchase_value'] = $donor->totalAmountDonated->formatToDecimal();
147 }
148
149 if (isset($donor->totalNumberOfDonations)) {
150 $args['purchase_count'] = $donor->totalNumberOfDonations;
151 }
152
153 try {
154 DB::table('give_donors')
155 ->insert($args);
156
157 $donorId = DB::last_insert_id();
158
159 foreach ($this->getCoreDonorMeta($donor) as $metaKey => $metaValue) {
160 give()->donor_meta->add_meta($donorId, $metaKey, $metaValue);
161 }
162
163 if (isset($donor->additionalEmails)) {
164 foreach ($donor->additionalEmails as $additionalEmail) {
165 give()->donor_meta->add_meta($donorId, DonorMetaKeys::ADDITIONAL_EMAILS, $additionalEmail);
166 }
167 }
168
169 if (isset($donor->addresses)) {
170 $this->updateAddresses($donor, $donorId);
171 }
172 } catch (Exception $exception) {
173 DB::query('ROLLBACK');
174
175 Log::error('Failed creating a donor', compact('donor'));
176
177 throw new $exception('Failed creating a donor');
178 }
179
180 DB::query('COMMIT');
181
182 $donor->id = $donorId;
183 $donor->createdAt = $dateCreated;
184
185 Hooks::doAction('givewp_donor_created', $donor);
186 }
187
188 /**
189 * @since 4.4.0 Add support for addresses
190 * @since 3.7.0 Add support to "phone" property
191 * @since 2.24.0 add support for $donor->totalAmountDonated and $donor->totalNumberOfDonation
192 * @since 2.23.1 use give()->donor_meta to update meta so data is upserted
193 * @since 2.21.0 add actions givewp_donor_updating and givewp_donor_updated
194 * @since 2.20.0 return void
195 * @since 2.19.6
196 *
197 * @return void
198 * @throws Exception
199 */
200 public function update(Donor $donor)
201 {
202 $this->validateDonor($donor);
203
204 Hooks::doAction('givewp_donor_updating', $donor);
205
206 DB::query('START TRANSACTION');
207
208 $args = [
209 'user_id' => $donor->userId,
210 'email' => $donor->email,
211 'phone' => $donor->phone,
212 'name' => $donor->name,
213 ];
214
215 if (isset($donor->totalAmountDonated) && $donor->isDirty('totalAmountDonated')) {
216 $args['purchase_value'] = $donor->totalAmountDonated->formatToDecimal();
217 }
218
219 if (isset($donor->totalNumberOfDonations) && $donor->isDirty('totalNumberOfDonations')) {
220 $args['purchase_count'] = $donor->totalNumberOfDonations;
221 }
222
223 try {
224 DB::table('give_donors')
225 ->where('id', $donor->id)
226 ->update($args);
227
228 foreach ($this->getCoreDonorMeta($donor) as $metaKey => $metaValue) {
229 give()->donor_meta->update_meta($donor->id, $metaKey, $metaValue);
230 }
231
232 if (isset($donor->additionalEmails) && $donor->isDirty('additionalEmails')) {
233 $this->updateAdditionalEmails($donor);
234 }
235
236 if (isset($donor->addresses) && $donor->isDirty('addresses')) {
237 $this->updateAddresses($donor, $donor->id);
238 }
239 } catch (Exception $exception) {
240 DB::query('ROLLBACK');
241
242 Log::error('Failed updating a donor', compact('donor'));
243
244 throw new FailedDonorUpdateException($donor, 0, $exception);
245 }
246
247 DB::query('COMMIT');
248
249 Hooks::doAction('givewp_donor_updated', $donor);
250 }
251
252 /**
253 * @since 2.19.6
254 *
255 * @throws Exception
256 */
257 public function updateLegacyColumns(int $donorId, array $columns): bool
258 {
259 DB::query('START TRANSACTION');
260
261 foreach (Donor::propertyKeys() as $key) {
262 if (array_key_exists($key, $columns)) {
263 throw new InvalidArgumentException("'$key' is not a legacy column.");
264 }
265 }
266
267 try {
268 DB::table('give_donors')
269 ->where('id', $donorId)
270 ->update($columns);
271 } catch (Exception $exception) {
272 DB::query('ROLLBACK');
273
274 Log::error('Failed updating a donor', compact('donorId', 'columns'));
275
276 throw new $exception('Failed updating a donor');
277 }
278
279 DB::query('COMMIT');
280
281 return true;
282 }
283
284 /**
285 *
286 * @since 2.21.0 add actions givewp_donor_deleting and givewp_donor_deleted
287 * @since 2.20.0 consolidate meta deletion into a single query
288 * @since 2.19.6
289 *
290 * @throws Exception
291 */
292 public function delete(Donor $donor): bool
293 {
294 DB::query('START TRANSACTION');
295
296 Hooks::doAction('givewp_donor_deleting', $donor);
297
298 try {
299 DB::table('give_donors')
300 ->where('id', $donor->id)
301 ->delete();
302
303 DB::table('give_donormeta')
304 ->where('donor_id', $donor->id)
305 ->delete();
306 } catch (Exception $exception) {
307 DB::query('ROLLBACK');
308
309 Log::error('Failed deleting a donor', compact('donor'));
310
311 throw new $exception('Failed deleting a donor');
312 }
313
314 DB::query('COMMIT');
315
316 Hooks::doAction('givewp_donor_deleted', $donor);
317
318 return true;
319 }
320
321 /**
322 * @since 4.4.0 Add avatarId and company to core donor meta
323 * @since 2.19.6
324 */
325 private function getCoreDonorMeta(Donor $donor): array
326 {
327 return [
328 DonorMetaKeys::FIRST_NAME => $donor->firstName,
329 DonorMetaKeys::LAST_NAME => $donor->lastName,
330 DonorMetaKeys::PREFIX => $donor->prefix ?? null,
331 DonorMetaKeys::AVATAR_ID => $donor->avatarId ?? null,
332 DonorMetaKeys::COMPANY => $donor->company ?? null,
333 ];
334 }
335
336 /**
337 * @since 2.19.6
338 *
339 * @return void
340 */
341 private function validateDonor(Donor $donor)
342 {
343 foreach ($this->requiredDonorProperties as $key) {
344 if (!isset($donor->$key)) {
345 throw new InvalidArgumentException("'$key' is required.");
346 }
347 }
348 }
349
350 /**
351 * @since 2.21.1 optimize query by skipping prepareQuery until found
352 * @since 2.19.6
353 *
354 * @return Donor|null
355 */
356 public function getByEmail(string $email)
357 {
358 $queryByPrimaryEmail = DB::table('give_donors')
359 ->select(
360 'id',
361 'email'
362 )
363 ->where('email', $email)
364 ->get();
365
366 if ($queryByPrimaryEmail) {
367 return $this->queryById($queryByPrimaryEmail->id)->get();
368 }
369
370 return $this->getByAdditionalEmail($email);
371 }
372
373 /**
374 * @since 2.19.6
375 *
376 * @return Donor|null
377 */
378 public function getByAdditionalEmail(string $email)
379 {
380 $donorMetaObject = DB::table('give_donormeta')
381 ->select(['donor_id', 'id'])
382 ->where('meta_key', DonorMetaKeys::ADDITIONAL_EMAILS)
383 ->where('meta_value', $email)
384 ->get();
385
386 if (!$donorMetaObject) {
387 return null;
388 }
389
390 return $this->getById($donorMetaObject->id);
391 }
392
393 /**
394 * @since 3.7.0 Add support to "phone" property
395 * @since 2.24.0 replace ModelQueryBuilder with DonorModelQueryBuilder
396 * @since 2.19.6
397 *
398 * @return DonorModelQueryBuilder<Donor>
399 */
400 public function prepareQuery(): DonorModelQueryBuilder
401 {
402 $builder = new DonorModelQueryBuilder(Donor::class);
403
404 return $builder->from('give_donors')
405 ->select(
406 'id',
407 ['user_id', 'userId'],
408 'email',
409 'phone',
410 'name',
411 ['purchase_value', 'totalAmountDonated'],
412 ['purchase_count', 'totalNumberOfDonations'],
413 ['payment_ids', 'paymentIds'],
414 ['date_created', 'createdAt'],
415 'token',
416 ['verify_key', 'verifyKey'],
417 ['verify_throttle', 'verifyThrottle']
418 )
419 ->attachMeta(
420 'give_donormeta',
421 'ID',
422 'donor_id',
423 ...DonorMetaKeys::getColumnsForAttachMetaQueryWithoutExtraMetadata()
424 );
425 }
426
427 /**
428 * Additional emails are assigned to the same additional_email meta key.
429 * In order to update them we need to delete and re-insert.
430 *
431 * @since 4.4.0 Remove all additional emails and re-insert only the new ones
432 * @since 3.20.0 store meta using native WP functions
433 * @since 2.19.6
434 *
435 * @return void
436 */
437 private function updateAdditionalEmails(Donor $donor)
438 {
439 DB::table('give_donormeta')
440 ->where('donor_id', $donor->id)
441 ->where('meta_key', DonorMetaKeys::ADDITIONAL_EMAILS)
442 ->delete();
443
444 foreach ($donor->additionalEmails as $additionalEmail) {
445 give()->donor_meta->add_meta($donor->id, DonorMetaKeys::ADDITIONAL_EMAILS, $additionalEmail);
446 }
447 }
448
449 /**
450 * Addresses are stored as indexed meta keys.
451 * In order to update them we need to delete all address-related meta keys and re-insert.
452 *
453 * @since 4.4.0
454 */
455 private function updateAddresses(Donor $donor, ?int $donorId): void
456 {
457 $id = $donorId ?? $donor->id;
458 $prefix = DB::prefix('give_donormeta');
459
460 $addressMetaKeys = [
461 DonorMetaKeys::ADDRESS_LINE1,
462 DonorMetaKeys::ADDRESS_LINE2,
463 DonorMetaKeys::ADDRESS_CITY,
464 DonorMetaKeys::ADDRESS_STATE,
465 DonorMetaKeys::ADDRESS_COUNTRY,
466 DonorMetaKeys::ADDRESS_ZIP,
467 ];
468
469 $likeConditions = implode(' OR ', array_fill(0, count($addressMetaKeys), 'meta_key LIKE %s'));
470 $likeValues = array_map(function($key) { return $key . '*'; }, $addressMetaKeys);
471
472 $sql = DB::prepare(
473 "DELETE FROM {$prefix}
474 WHERE donor_id = %d
475 AND ({$likeConditions})",
476 array_merge([$id], $likeValues)
477 );
478
479 try {
480 DB::query( str_replace('*', '%', $sql));
481 } catch (Exception $e) {
482 Log::error('Failed deleting donor addresses', compact('donor', 'id', 'sql'));
483 }
484
485 foreach ($donor->addresses as $index => $address) {
486 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_LINE1 . $index, $address->address1);
487 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_LINE2 . $index, $address->address2);
488 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_CITY . $index, $address->city);
489 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_STATE . $index, $address->state);
490 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_COUNTRY . $index, $address->country);
491 give()->donor_meta->add_meta($id, DonorMetaKeys::ADDRESS_ZIP . $index, $address->zip);
492 }
493 }
494
495 /**
496 * @since 4.12.0 filter by status
497 * @since 2.20.0
498 *
499 * @return string|null
500 */
501 public function getDonorLatestDonationDate(int $donorId)
502 {
503 $donation = DB::table('posts')
504 ->select('post_date')
505 ->leftJoin('give_donationmeta', 'ID', 'donation_id')
506 ->where('post_type', 'give_payment')
507 ->where('meta_key', DonationMetaKeys::DONOR_ID)
508 ->where('meta_value', $donorId)
509 ->whereIn('post_status', ['publish', 'give_subscription'])
510 ->orderBy('CAST(post_date AS DATETIME)', 'DESC')
511 ->limit(1)
512 ->get();
513
514 if ($donation) {
515 return $donation->post_date;
516 }
517
518 return null;
519 }
520
521 /**
522 * @since 2.24.0 change return to DonorType
523 * @since 2.20.0
524 *
525 * @return DonorType|null
526 */
527 public function getDonorType(int $donorId)
528 {
529 $donor = DB::table('give_donors')
530 ->select(
531 'id',
532 ['purchase_count', 'donationCount'],
533 ['payment_ids', 'paymentIds']
534 )
535 ->where('id', $donorId)
536 ->get();
537
538 if (!$donor) {
539 return null;
540 }
541
542 if (!$donor->donationCount) {
543 return DonorType::NEW();
544 }
545
546 // Donation IDs
547 $ids = strpos($donor->paymentIds, ',')
548 ? explode(',', $donor->paymentIds)
549 : [$donor->paymentIds];
550
551 // Recurring
552 $recurringDonations = DB::table('posts')
553 ->leftJoin('give_donationmeta', 'id', 'donation_id')
554 ->whereIn('donation_id', $ids)
555 ->where('meta_key', DonationMetaKeys::IS_RECURRING)
556 ->where('meta_value', '1')
557 ->count();
558
559 if ($recurringDonations) {
560 return DonorType::SUBSCRIBER();
561 }
562
563 if ((int)$donor->donationCount > 1) {
564 return DonorType::REPEAT();
565 }
566
567 return DonorType::SINGLE();
568 }
569
570 /**
571 * @since 2.20.0
572 */
573 public function getDonorsCount(): int
574 {
575 return DB::table('give_donors')->count();
576 }
577 }
578