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

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

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