PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.6.1
GiveWP – Donation Plugin and Fundraising Platform v4.6.1
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 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / API / REST / V3 / Routes / Donations / DonationController.php
give / src / API / REST / V3 / Routes / Donations Last commit date
ValueObjects 11 months ago DonationController.php 11 months ago DonationNotesController.php 11 months ago
DonationController.php
990 lines
1 <?php
2
3 namespace Give\API\REST\V3\Routes\Donations;
4
5 use DateTime;
6 use Give\API\REST\V3\Routes\CURIE;
7 use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationAnonymousMode;
8 use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationRoute;
9 use Give\Donations\Models\Donation;
10 use Give\Donations\Properties\BillingAddress;
11 use Give\Donations\ValueObjects\DonationStatus;
12 use Give\Donations\ViewModels\DonationViewModel;
13 use Give\Framework\Exceptions\Primitives\Exception;
14 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
15 use Give\Framework\PaymentGateways\CommandHandlers\PaymentRefundedHandler;
16 use Give\Framework\PaymentGateways\Commands\PaymentRefunded;
17 use Give\Framework\PaymentGateways\Contracts\PaymentGatewayRefundable;
18 use Give\Framework\Support\ValueObjects\Money;
19 use WP_Error;
20 use WP_REST_Controller;
21 use WP_REST_Request;
22 use WP_REST_Response;
23 use WP_REST_Server;
24
25 /**
26 * @since 4.6.0
27 */
28 class DonationController extends WP_REST_Controller
29 {
30 /**
31 * @var string
32 */
33 protected $namespace;
34
35 /**
36 * @var string
37 */
38 protected $rest_base;
39
40 /**
41 * @since 4.6.0
42 */
43 public function __construct()
44 {
45 $this->namespace = DonationRoute::NAMESPACE;
46 $this->rest_base = DonationRoute::BASE;
47 }
48
49 /**
50 * @since 4.6.0
51 */
52 public function register_routes()
53 {
54 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
55 [
56 'methods' => WP_REST_Server::READABLE,
57 'callback' => [$this, 'get_item'],
58 'permission_callback' => [$this, 'permissionsCheck'],
59 'args' => [
60 '_embed' => [
61 'description' => __('Whether to embed related resources in the response. It can be true when we want to embed all available resources, or a string like "givewp:donor" when we wish to embed only a specific one.',
62 'give'),
63 'type' => [
64 'string',
65 'boolean'
66 ],
67 'default' => false,
68 ],
69 'id' => [
70 'type' => 'integer',
71 'required' => true,
72 ],
73 'includeSensitiveData' => [
74 'type' => 'boolean',
75 'default' => false,
76 ],
77 'anonymousDonations' => [
78 'type' => 'string',
79 'default' => 'exclude',
80 'enum' => [
81 'exclude',
82 'include',
83 'redact',
84 ],
85 ],
86 ],
87 'schema' => [$this, 'get_public_item_schema'],
88 ],
89 [
90 'methods' => WP_REST_Server::EDITABLE,
91 'callback' => [$this, 'update_item'],
92 'permission_callback' => [$this, 'update_item_permissions_check'],
93 'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::EDITABLE),
94 'schema' => [$this, 'get_public_item_schema'],
95 ],
96 [
97 'methods' => WP_REST_Server::DELETABLE,
98 'callback' => [$this, 'delete_item'],
99 'permission_callback' => [$this, 'delete_item_permissions_check'],
100 'args' => [
101 'id' => [
102 'type' => 'integer',
103 'required' => true,
104 ],
105 'force' => [
106 'type' => 'boolean',
107 'default' => false,
108 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
109 ],
110 ],
111 'schema' => [$this, 'get_public_item_schema'],
112 ],
113 ]);
114
115 register_rest_route($this->namespace, '/' . $this->rest_base, [
116 [
117 'methods' => WP_REST_Server::READABLE,
118 'callback' => [$this, 'get_items'],
119 'permission_callback' => [$this, 'permissionsCheck'],
120 'args' => $this->get_collection_params(),
121 'schema' => [$this, 'get_public_item_schema'],
122 ],
123 [
124 'methods' => WP_REST_Server::DELETABLE,
125 'callback' => [$this, 'delete_items'],
126 'permission_callback' => [$this, 'delete_items_permissions_check'],
127 'args' => [
128 'ids' => [
129 'description' => __('Array of donation IDs to delete', 'give'),
130 'type' => 'array',
131 'items' => [
132 'type' => 'integer',
133 ],
134 'required' => true,
135 ],
136 'force' => [
137 'type' => 'boolean',
138 'default' => false,
139 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
140 ],
141 ],
142 'schema' => [$this, 'get_public_item_schema'],
143 ],
144 ]);
145
146 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/refund', [
147 [
148 'methods' => WP_REST_Server::EDITABLE,
149 'callback' => [$this, 'refund_item'],
150 'permission_callback' => [$this, 'refund_item_permissions_check'],
151 'args' => [
152 'id' => [
153 'type' => 'integer',
154 'required' => true,
155 ],
156 ],
157 'schema' => [$this, 'get_public_item_schema'],
158 ],
159 ]);
160 }
161
162 /**
163 * @since 4.6.0
164 */
165 public function get_items($request)
166 {
167 $includeSensitiveData = $request->get_param('includeSensitiveData');
168 $donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
169 $page = $request->get_param('page');
170 $perPage = $request->get_param('per_page');
171 $sortColumn = $this->getSortColumn($request->get_param('sort'));
172 $sortDirection = $request->get_param('direction');
173 $mode = $request->get_param('mode');
174 $status = $request->get_param('status');
175
176 $query = Donation::query();
177
178 if ($campaignId = $request->get_param('campaignId')) {
179 // Filter by CampaignId
180 $query->where('give_donationmeta_attach_meta_campaignId.meta_value', $campaignId);
181 }
182
183 if ($donorId = $request->get_param('donorId')) {
184 $query->where('give_donationmeta_attach_meta_donorId.meta_value', $donorId);
185 }
186
187 if ($donationAnonymousMode->isExcluded()) {
188 // Exclude anonymous donations from results
189 $query->where('give_donationmeta_attach_meta_anonymous.meta_value', 0);
190 }
191
192 // Include only current payment "mode"
193 $query->where('give_donationmeta_attach_meta_mode.meta_value', $mode);
194
195 // Filter by status if not 'any'
196 if ( ! in_array('any', (array)$status, true)) {
197 $query->whereIn('post_status', (array)$status);
198 }
199
200 $query
201 ->limit($perPage)
202 ->offset(($page - 1) * $perPage)
203 ->orderBy($sortColumn, $sortDirection);
204
205 $donations = $query->getAll() ?? [];
206 $donations = array_map(function ($donation) use ($includeSensitiveData, $donationAnonymousMode, $request) {
207 $item = (new DonationViewModel($donation))
208 ->anonymousMode($donationAnonymousMode)
209 ->includeSensitiveData($includeSensitiveData)
210 ->exports();
211
212 return $this->prepare_response_for_collection(
213 $this->prepare_item_for_response($item, $request)
214 );
215 }, $donations);
216
217 $totalDonations = empty($donations) ? 0 : Donation::query()->count();
218 $totalPages = (int)ceil($totalDonations / $perPage);
219
220 $response = rest_ensure_response($donations);
221 $response->header('X-WP-Total', $totalDonations);
222 $response->header('X-WP-TotalPages', $totalPages);
223
224 $base = add_query_arg(
225 map_deep($request->get_query_params(), function ($value) {
226 if (is_bool($value)) {
227 $value = $value ? 'true' : 'false';
228 }
229
230 return urlencode($value);
231 }),
232 rest_url(DonationRoute::BASE)
233 );
234
235 if ($page > 1) {
236 $prevPage = $page - 1;
237
238 if ($prevPage > $totalPages) {
239 $prevPage = $totalPages;
240 }
241
242 $response->link_header('prev', add_query_arg('page', $prevPage, $base));
243 }
244
245 if ($totalPages > $page) {
246 $nextPage = $page + 1;
247 $response->link_header('next', add_query_arg('page', $nextPage, $base));
248 }
249
250 return $response;
251 }
252
253 /**
254 * @since 4.6.0
255 */
256 public function get_item($request)
257 {
258 $donation = Donation::find($request->get_param('id'));
259 $includeSensitiveData = $request->get_param('includeSensitiveData');
260 $donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
261
262 if ( ! $donation || ($donation->anonymous && $donationAnonymousMode->isExcluded())) {
263 return new WP_Error('donation_not_found', __('Donation not found', 'give'), ['status' => 404]);
264 }
265
266 $item = (new DonationViewModel($donation))
267 ->anonymousMode($donationAnonymousMode)
268 ->includeSensitiveData($includeSensitiveData)
269 ->exports();
270
271 $response = $this->prepare_item_for_response($item, $request);
272
273 return rest_ensure_response($response);
274 }
275
276 /**
277 * Update a single donation.
278 *
279 * @since 4.6.0
280 */
281 public function update_item($request): WP_REST_Response
282 {
283 $donation = Donation::find($request->get_param('id'));
284
285 if (!$donation) {
286 return new WP_REST_Response(__('Donation not found', 'give'), 404);
287 }
288
289 $nonEditableFields = [
290 'id',
291 'updatedAt',
292 'purchaseKey',
293 'donorIp',
294 'type',
295 'mode',
296 'gatewayTransactionId',
297 ];
298
299 foreach ($request->get_params() as $key => $value) {
300 if (!in_array($key, $nonEditableFields, true)) {
301 if (in_array($key, $donation::propertyKeys(), true)) {
302 try {
303 $processedValue = $this->processFieldValue($key, $value);
304 if ($donation->isPropertyTypeValid($key, $processedValue)) {
305 $donation->$key = $processedValue;
306 }
307 } catch (Exception $e) {
308 continue;
309 }
310 }
311 }
312 }
313
314 if ($donation->isDirty()) {
315 $donation->save();
316 }
317
318 $item = (new DonationViewModel($donation))
319 ->includeSensitiveData(true)
320 ->anonymousMode(new DonationAnonymousMode('include'))
321 ->exports();
322
323 $response = $this->prepare_item_for_response($item, $request);
324
325 return rest_ensure_response($response);
326 }
327
328 /**
329 * Refund a single donation.
330 *
331 * @since 4.6.0
332 */
333 public function refund_item($request)
334 {
335 $donation = Donation::find($request->get_param('id'));
336
337 if (!$donation) {
338 return new WP_REST_Response(__('Donation not found', 'give'), 404);
339 }
340
341 $gateway = $donation->gateway();
342
343 if (!$gateway->supportsRefund()) {
344 return new WP_REST_Response(__('Refunds are not supported for this gateway', 'give'), 400);
345 }
346
347 try {
348 /** @var PaymentGatewayRefundable $gateway */
349 $command = $gateway->refundDonation($donation);
350
351 if ($command instanceof PaymentRefunded) {
352 $handler = new PaymentRefundedHandler($command);
353 $handler->handle($donation);
354 }
355
356 $response = $this->prepare_item_for_response($donation->toArray(), $request);
357
358 return rest_ensure_response($response);
359 } catch (\Exception $exception) {
360 return new WP_REST_Response(__('Failed to refund donation', 'give'), 500);
361 }
362 }
363
364 /**
365 * Process field values for special data types before setting them on the donation model.
366 *
367 * @since 4.6.0
368 */
369 private function processFieldValue(string $key, $value)
370 {
371 switch ($key) {
372 case 'amount':
373 case 'feeAmountRecovered':
374 if (is_array($value)) {
375 // Handle Money object array format: ['amount' => 100.00, 'currency' => 'USD']
376 if (isset($value['amount']) && isset($value['currency'])) {
377 return Money::fromDecimal($value['amount'], $value['currency']);
378 }
379 }
380 return $value;
381
382 case 'status':
383 if (is_string($value)) {
384 return new DonationStatus($value);
385 }
386 return $value;
387
388 case 'billingAddress':
389 if (is_array($value)) {
390 return BillingAddress::fromArray($value);
391 }
392 return $value;
393
394 case 'createdAt':
395 try {
396 if (is_string($value)) {
397 return new DateTime( $value, wp_timezone());
398 } elseif (is_array($value)) {
399 return new DateTime($value['date'], new \DateTimeZone($value['timezone']));
400 }
401 } catch (\Exception $e) {
402 throw new InvalidArgumentException("Invalid date format for {$key}: {$value}.");
403 }
404 return $value;
405
406 default:
407 return $value;
408 }
409 }
410
411 /**
412 * Delete a single donation.
413 *
414 * @since 4.6.0
415 */
416 public function delete_item($request): WP_REST_Response
417 {
418 $donation = Donation::find($request->get_param('id'));
419 $force = $request->get_param('force');
420
421 if (!$donation) {
422 return new WP_REST_Response(['message' => __('Donation not found', 'give')], 404);
423 }
424
425 $item = (new DonationViewModel($donation))
426 ->includeSensitiveData(true)
427 ->anonymousMode(new DonationAnonymousMode('include'))
428 ->exports();
429
430 if ($force) {
431 // Permanently delete the donation
432 $deleted = $donation->delete();
433
434 if (!$deleted) {
435 return new WP_REST_Response(['message' => __('Failed to delete donation', 'give')], 500);
436 }
437
438 } else {
439 // Move the donation to trash (soft delete)
440 $trashed = $donation->trash();
441
442 if (!$trashed) {
443 return new WP_REST_Response(['message' => __('Failed to trash donation', 'give')], 500);
444 }
445 }
446
447 return new WP_REST_Response(['deleted' => true, 'previous' => $item], 200);
448 }
449
450 /**
451 * Delete multiple donations.
452 *
453 * @since 4.6.0
454 */
455 public function delete_items($request): WP_REST_Response
456 {
457 $ids = $request->get_param('ids');
458 $force = $request->get_param('force');
459 $deleted = [];
460 $errors = [];
461
462 foreach ($ids as $id) {
463 $donation = Donation::find($id);
464
465 if (!$donation) {
466 $errors[] = ['id' => $id, 'message' => __('Donation not found', 'give')];
467 continue;
468 }
469
470 $item = (new DonationViewModel($donation))
471 ->includeSensitiveData(true)
472 ->anonymousMode(new DonationAnonymousMode('include'))
473 ->exports();
474
475 if ($force) {
476 if ($donation->delete()) {
477 $deleted[] = ['id' => $id, 'previous' => $item];
478 } else {
479 $errors[] = ['id' => $id, 'message' => __('Failed to delete donation', 'give')];
480 }
481 } else {
482 $trashed = $donation->trash();
483
484 if ($trashed) {
485 $deleted[] = ['id' => $id, 'previous' => $item];
486 } else {
487 $errors[] = ['id' => $id, 'message' => __('Failed to trash donation', 'give')];
488 }
489 }
490 }
491
492 return new WP_REST_Response([
493 'deleted' => $deleted,
494 'errors' => $errors,
495 'total_requested' => count($ids),
496 'total_deleted' => count($deleted),
497 'total_errors' => count($errors),
498 ], 200);
499 }
500
501 /**
502 * @since 4.6.0
503 */
504 public function getSortColumn(string $sortColumn): string
505 {
506 $sortColumnsMap = [
507 'id' => 'ID',
508 'createdAt' => 'post_date',
509 'updatedAt' => 'post_modified',
510 'status' => 'post_status',
511 'amount' => 'give_donationmeta_attach_meta_amount.meta_value',
512 'feeAmountRecovered' => 'give_donationmeta_attach_meta_feeAmountRecovered.meta_value',
513 'donorId' => 'give_donationmeta_attach_meta_donorId.meta_value',
514 'firstName' => 'give_donationmeta_attach_meta_firstName.meta_value',
515 'lastName' => 'give_donationmeta_attach_meta_lastName.meta_value',
516 ];
517
518 return $sortColumnsMap[$sortColumn];
519 }
520
521 /**
522 * @since 4.6.0
523 */
524 public function get_collection_params(): array
525 {
526 $params = parent::get_collection_params();
527
528 $params['page']['default'] = 1;
529 $params['per_page']['default'] = 30;
530
531 // Remove default parameters not being used
532 unset($params['context']);
533 unset($params['search']);
534
535 $params += [
536 'sort' => [
537 'type' => 'string',
538 'default' => 'id',
539 'enum' => [
540 'id',
541 'createdAt',
542 'updatedAt',
543 'status',
544 'amount',
545 'feeAmountRecovered',
546 'donorId',
547 'firstName',
548 'lastName',
549 ],
550 ],
551 'direction' => [
552 'type' => 'string',
553 'default' => 'DESC',
554 'enum' => ['ASC', 'DESC'],
555 ],
556 'mode' => [
557 'type' => 'string',
558 'default' => 'live',
559 'enum' => ['live', 'test'],
560 ],
561 'status' => [
562 'type' => 'array',
563 'items' => [
564 'type' => 'string',
565 'enum' => [
566 'any',
567 'publish',
568 'give_subscription',
569 'pending',
570 'processing',
571 'refunded',
572 'revoked',
573 'failed',
574 'cancelled',
575 'abandoned',
576 'preapproval',
577 ],
578 ],
579 'default' => ['any'],
580 ],
581 'campaignId' => [
582 'type' => 'integer',
583 'default' => 0,
584 ],
585 'donorId' => [
586 'type' => 'integer',
587 'default' => 0,
588 ],
589 'includeSensitiveData' => [
590 'type' => 'boolean',
591 'default' => false,
592 ],
593 'anonymousDonations' => [
594 'type' => 'string',
595 'default' => 'exclude',
596 'enum' => [
597 'exclude',
598 'include',
599 'redact',
600 ],
601 ],
602 'force' => [
603 'type' => 'boolean',
604 'default' => false,
605 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
606 ],
607 ];
608
609 return $params;
610 }
611
612 /**
613 * @since 4.6.0
614 * @throws Exception
615 */
616 public function prepare_item_for_response($item, $request): WP_REST_Response
617 {
618 $donationId = $request->get_param('id') ?? $item['id'] ?? null;
619
620 if ($donationId) {
621 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $donationId));
622 $donor_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'donors', $item['donorId']));
623 $campaign_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'campaigns', $item['campaignId']));
624 $links = [
625 'self' => ['href' => $self_url],
626 CURIE::relationUrl('donor') => [
627 'href' => $donor_url,
628 'embeddable' => true,
629 ],
630 CURIE::relationUrl('campaign') => [
631 'href' => $campaign_url,
632 'embeddable' => true,
633 ],
634 ];
635 } else {
636 $links = [];
637 }
638
639 $response = new WP_REST_Response($item);
640 if (!empty($links)) {
641 $response->add_links($links);
642 }
643
644 return $response;
645 }
646
647 /**
648 * @since 4.6.0
649 */
650 public function permissionsCheck(WP_REST_Request $request)
651 {
652 $includeSensitiveData = $request->get_param('includeSensitiveData');
653 $includeAnonymousDonations = $request->get_param('anonymousDonations');
654 $canEditDonations = $this->canEditDonations();
655
656 if ($includeSensitiveData && !$canEditDonations) {
657 return new WP_Error(
658 'rest_forbidden',
659 esc_html__('You do not have permission to include sensitive data.', 'give'),
660 ['status' => $this->authorizationStatusCode()]
661 );
662 }
663
664 if ($includeAnonymousDonations !== null) {
665 $anonymousMode = new DonationAnonymousMode($includeAnonymousDonations);
666
667 if ($anonymousMode->isIncluded() && !$canEditDonations) {
668 return new WP_Error(
669 'rest_forbidden',
670 esc_html__('You do not have permission to include anonymous donations.', 'give'),
671 ['status' => $this->authorizationStatusCode()]
672 );
673 }
674 }
675
676 return true;
677 }
678
679 /**
680 * @since 4.6.0
681 */
682 public function update_item_permissions_check($request)
683 {
684 if ($this->canEditDonations()) {
685 return true;
686 }
687
688 return new WP_Error(
689 'rest_forbidden',
690 esc_html__('You do not have permission to update donations.', 'give'),
691 ['status' => $this->authorizationStatusCode()]
692 );
693 }
694
695 /**
696 * @since 4.6.0
697 */
698 public function delete_item_permissions_check($request)
699 {
700 if ($this->canDeleteDonations()) {
701 return true;
702 }
703
704 return new WP_Error(
705 'rest_forbidden',
706 esc_html__('You do not have permission to delete donations.', 'give'),
707 ['status' => $this->authorizationStatusCode()]
708 );
709 }
710
711 /**
712 * @since 4.6.0
713 */
714 public function delete_items_permissions_check($request)
715 {
716 if ($this->canDeleteDonations()) {
717 return true;
718 }
719
720 return new WP_Error(
721 'rest_forbidden',
722 esc_html__('You do not have permission to delete donations.', 'give'),
723 ['status' => $this->authorizationStatusCode()]
724 );
725 }
726
727 /**
728 * @since 4.6.0
729 */
730 public function refund_item_permissions_check($request)
731 {
732 if ($this->canRefundDonations()) {
733 return true;
734 }
735
736 return new WP_Error(
737 'rest_forbidden',
738 esc_html__('You do not have permission to refund donations.', 'give'),
739 ['status' => $this->authorizationStatusCode()]
740 );
741 }
742
743 /**
744 * Check if current user can edit donations.
745 *
746 * @since 4.6.0
747 */
748 private function canEditDonations(): bool
749 {
750 return current_user_can('manage_options')
751 || (
752 current_user_can('edit_give_payments')
753 && current_user_can('view_give_payments')
754 );
755 }
756
757 /**
758 * Check if current user can delete donations.
759 *
760 * @since 4.6.0
761 */
762 private function canDeleteDonations(): bool
763 {
764 return current_user_can('manage_options') || current_user_can('delete_give_payments');
765 }
766
767 /**
768 * Check if current user can refund donations.
769 *
770 * @since 4.6.0
771 */
772 private function canRefundDonations(): bool
773 {
774 return current_user_can('manage_options') || current_user_can('edit_give_payments');
775 }
776
777 /**
778 * @since 4.6.0
779 */
780 public function authorizationStatusCode(): int
781 {
782 return is_user_logged_in() ? 403 : 401;
783 }
784
785 /**
786 * @since 4.6.1 Change type of billing address properties to accept null values
787 * @since 4.6.0
788 */
789 public function get_item_schema(): array
790 {
791 return [
792 'title' => 'donation',
793 'type' => 'object',
794 'properties' => [
795 'id' => [
796 'type' => 'integer',
797 'description' => esc_html__('Donation ID', 'give'),
798 ],
799 'donorId' => [
800 'type' => 'integer',
801 'description' => esc_html__('Donor ID', 'give'),
802 ],
803 'firstName' => [
804 'type' => 'string',
805 'description' => esc_html__('Donor first name', 'give'),
806 'format' => 'text-field',
807 ],
808 'lastName' => [
809 'type' => 'string',
810 'description' => esc_html__('Donor last name', 'give'),
811 'format' => 'text-field',
812 ],
813 'honorific' => [
814 'type' => ['string', 'null'],
815 'description' => esc_html__('Donor honorific/prefix', 'give'),
816 'enum' => give_get_option('title_prefixes', array_values(give_get_default_title_prefixes())),
817 ],
818 'email' => [
819 'type' => 'string',
820 'description' => esc_html__('Donor email', 'give'),
821 'format' => 'email',
822 ],
823 'phone' => [
824 'type' => ['string', 'null'],
825 'description' => esc_html__('Donor phone', 'give'),
826 'format' => 'text-field',
827 ],
828 'company' => [
829 'type' => ['string', 'null'],
830 'description' => esc_html__('Donor company', 'give'),
831 'format' => 'text-field',
832 ],
833 'amount' => [
834 'type' => ['object', 'null'],
835 'properties' => [
836 'amount' => [
837 'type' => 'number',
838 ],
839 'amountInMinorUnits' => [
840 'type' => 'integer',
841 ],
842 'currency' => [
843 'type' => 'string',
844 'format' => 'text-field',
845 ],
846 ],
847 'description' => esc_html__('Donation amount', 'give'),
848 ],
849 'feeAmountRecovered' => [
850 'type' => ['object', 'null'],
851 'properties' => [
852 'amount' => [
853 'type' => 'number',
854 ],
855 'amountInMinorUnits' => [
856 'type' => 'integer',
857 ],
858 'currency' => [
859 'type' => 'string',
860 'format' => 'text-field',
861 ],
862 ],
863 'description' => esc_html__('Fee amount recovered', 'give'),
864 ],
865 'eventTicketsAmount' => [
866 'type' => ['object', 'null'],
867 'readonly' => true,
868 'properties' => [
869 'amount' => [
870 'type' => 'number',
871 ],
872 'amountInMinorUnits' => [
873 'type' => 'integer',
874 ],
875 'currency' => [
876 'type' => 'string',
877 'format' => 'text-field',
878 ],
879 ],
880 'description' => esc_html__('Event tickets amount', 'give'),
881 ],
882 'status' => [
883 'type' => 'string',
884 'description' => esc_html__('Donation status', 'give'),
885 'enum' => array_values(DonationStatus::toArray()),
886 ],
887 'gatewayId' => [
888 'type' => 'string',
889 'description' => esc_html__('Payment gateway ID', 'give'),
890 'format' => 'text-field',
891 ],
892 'mode' => [
893 'type' => 'string',
894 'description' => esc_html__('Donation mode (live or test)', 'give'),
895 'enum' => ['live', 'test'],
896 ],
897 'anonymous' => [
898 'type' => 'boolean',
899 'description' => esc_html__('Whether the donation is anonymous', 'give'),
900 ],
901 'billingAddress' => [
902 'type' => ['object', 'null'],
903 'description' => esc_html__('Billing address', 'give'),
904 'properties' => [
905 'address1' => ['type' => ['string', 'null'], 'format' => 'text-field'],
906 'address2' => ['type' => ['string', 'null'], 'format' => 'text-field'],
907 'city' => ['type' => ['string', 'null'], 'format' => 'text-field'],
908 'state' => ['type' => ['string', 'null'], 'format' => 'text-field'],
909 'country' => ['type' => ['string', 'null'], 'format' => 'text-field'],
910 'zip' => ['type' => ['string', 'null'], 'format' => 'text-field'],
911 ],
912 ],
913 'donorIp' => [
914 'type' => ['string', 'null'],
915 'description' => esc_html__('Donor IP address (sensitive data)', 'give'),
916 'format' => 'text-field',
917 ],
918 'purchaseKey' => [
919 'type' => ['string', 'null'],
920 'description' => esc_html__('Purchase key (sensitive data)', 'give'),
921 'format' => 'text-field',
922 ],
923 'createdAt' => [
924 'type' => ['object', 'null'],
925 'properties' => [
926 'date' => [
927 'type' => 'string',
928 'description' => esc_html__('Date', 'give'),
929 'format' => 'date-time',
930 ],
931 'timezone' => [
932 'type' => 'string',
933 'description' => esc_html__('Timezone of the date', 'give'),
934 'format' => 'text-field',
935 ],
936 'timezone_type' => [
937 'type' => 'integer',
938 'description' => esc_html__('Timezone type', 'give'),
939 ],
940 ],
941 'description' => esc_html__('Donation creation date', 'give'),
942 'format' => 'date-time',
943 ],
944 'updatedAt' => [
945 'type' => ['object', 'null'],
946 'properties' => [
947 'date' => [
948 'type' => 'string',
949 'description' => esc_html__('Date', 'give'),
950 'format' => 'date-time',
951 ],
952 'timezone' => [
953 'type' => 'string',
954 'description' => esc_html__('Timezone of the date', 'give'),
955 'format' => 'text-field',
956 ],
957 'timezone_type' => [
958 'type' => 'integer',
959 'description' => esc_html__('Timezone type', 'give'),
960 ],
961 ],
962 'description' => esc_html__('Donation last update date', 'give'),
963 'format' => 'date-time',
964 ],
965 'customFields' => [
966 'type' => 'array',
967 'readonly' => true,
968 'description' => esc_html__('Custom fields (sensitive data)', 'give'),
969 'items' => [
970 'type' => 'object',
971 'properties' => [
972 'label' => [
973 'type' => 'string',
974 'description' => esc_html__('Field label', 'give'),
975 'format' => 'text-field',
976 ],
977 'value' => [
978 'type' => 'string',
979 'description' => esc_html__('Field value', 'give'),
980 'format' => 'text-field',
981 ],
982 ],
983 ],
984 ],
985 ],
986 'required' => ['id', 'donorId', 'amount', 'currency', 'status', 'gatewayId', 'mode', 'createdAt'],
987 ];
988 }
989 }
990