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 / API / REST / V3 / Routes / Donations / DonationController.php

DonationController.php in GiveWP – Donation Plugin and Fundraising Platform 4.16.8.1, at src/API/REST/V3/Routes/Donations/DonationController.php

1,275 lines 47.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\API\REST\V3\Routes\Donations;
4
5 use Exception;
6 use Give\API\REST\V3\Routes\Donations\DataTransferObjects\DonationCreateData;
7 use Give\API\REST\V3\Routes\Donations\Exceptions\DonationValidationException;
8 use Give\API\REST\V3\Routes\Donations\Fields\DonationFields;
9 use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationAnonymousMode;
10 use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationRoute;
11 use Give\API\REST\V3\Support\CURIE;
12 use Give\API\REST\V3\Support\Item;
13 use Give\API\REST\V3\Support\Schema\SchemaTypes;
14 use Give\Donations\Models\Donation;
15 use Give\Donations\ValueObjects\DonationMode;
16 use Give\Donations\ValueObjects\DonationStatus;
17 use Give\Donations\ValueObjects\DonationType;
18 use Give\API\REST\V3\Routes\Donations\ViewModels\DonationViewModel;
19 use Give\Framework\PaymentGateways\CommandHandlers\PaymentRefundedHandler;
20 use Give\Framework\PaymentGateways\Commands\PaymentRefunded;
21 use Give\Framework\PaymentGateways\Contracts\PaymentGatewayRefundable;
22 use Give\Framework\Permissions\Facades\UserPermissions;
23 use WP_Error;
24 use WP_REST_Controller;
25 use WP_REST_Request;
26 use WP_REST_Response;
27 use WP_REST_Server;
28
29 /**
30 * @since 4.6.0
31 */
32 class DonationController extends WP_REST_Controller
33 {
34 /**
35 * @var string
36 */
37 protected $namespace;
38
39 /**
40 * @var string
41 */
42 protected $rest_base;
43
44 /**
45 * @since 4.6.0
46 */
47 public function __construct()
48 {
49 $this->namespace = DonationRoute::NAMESPACE;
50 $this->rest_base = DonationRoute::BASE;
51 }
52
53 /**
54 *
55 * @since 4.14.0 replaced permissionsCheck with get_item_permissions_check and get_items_permissions_check
56 * @since 4.9.0 Move schema key to the route level instead of defining it for each endpoint (which is incorrect)
57 * @since 4.6.0
58 */
59 public function register_routes()
60 {
61 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
62 [
63 'methods' => WP_REST_Server::READABLE,
64 'callback' => [$this, 'get_item'],
65 'permission_callback' => [$this, 'get_item_permissions_check'],
66 'args' => [
67 '_embed' => [
68 'description' => __(
69 '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.',
70 'give'
71 ),
72 'type' => [
73 'string',
74 'boolean',
75 ],
76 'default' => false,
77 ],
78 'id' => [
79 'type' => 'integer',
80 'required' => true,
81 ],
82 'includeSensitiveData' => [
83 'type' => 'boolean',
84 'default' => false,
85 ],
86 'anonymousDonations' => [
87 'type' => 'string',
88 'default' => 'exclude',
89 'enum' => [
90 'exclude',
91 'include',
92 'redact',
93 ],
94 ],
95 ],
96 ],
97 [
98 'methods' => WP_REST_Server::EDITABLE,
99 'callback' => [$this, 'update_item'],
100 'permission_callback' => [$this, 'update_item_permissions_check'],
101 'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::EDITABLE),
102 ],
103 [
104 'methods' => WP_REST_Server::DELETABLE,
105 'callback' => [$this, 'delete_item'],
106 'permission_callback' => [$this, 'delete_item_permissions_check'],
107 'args' => [
108 'id' => [
109 'type' => 'integer',
110 'required' => true,
111 ],
112 'force' => [
113 'type' => 'boolean',
114 'default' => false,
115 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
116 ],
117 ],
118 ],
119 'schema' => [$this, 'get_public_item_schema'],
120 ]);
121
122 register_rest_route($this->namespace, '/' . $this->rest_base, [
123 [
124 'methods' => WP_REST_Server::READABLE,
125 'callback' => [$this, 'get_items'],
126 'permission_callback' => [$this, 'get_items_permissions_check'],
127 'args' => $this->get_collection_params(),
128 ],
129 [
130 'methods' => WP_REST_Server::CREATABLE,
131 'callback' => [$this, 'create_item'],
132 'permission_callback' => [$this, 'create_item_permissions_check'],
133 'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::CREATABLE),
134 ],
135 [
136 'methods' => WP_REST_Server::DELETABLE,
137 'callback' => [$this, 'delete_items'],
138 'permission_callback' => [$this, 'delete_items_permissions_check'],
139 'args' => [
140 'ids' => [
141 'description' => __('Array of donation IDs to delete', 'give'),
142 'type' => 'array',
143 'items' => [
144 'type' => 'integer',
145 ],
146 'required' => true,
147 ],
148 'force' => [
149 'type' => 'boolean',
150 'default' => false,
151 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
152 ],
153 ],
154 ],
155 'schema' => [$this, 'get_public_item_schema'],
156 ]);
157
158 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/refund', [
159 [
160 'methods' => WP_REST_Server::EDITABLE,
161 'callback' => [$this, 'refund_item'],
162 'permission_callback' => [$this, 'refund_item_permissions_check'],
163 'args' => [
164 'id' => [
165 'type' => 'integer',
166 'required' => true,
167 ],
168 'includeSensitiveData' => [
169 'type' => 'boolean',
170 'default' => false,
171 ],
172 'anonymousDonations' => [
173 'type' => 'string',
174 'default' => 'exclude',
175 'enum' => [
176 'exclude',
177 'include',
178 'redact',
179 ],
180 ],
181 ],
182 ],
183 'schema' => [$this, 'get_public_item_schema'],
184 ]);
185 }
186
187 /**
188 * @since 4.6.0
189 */
190 public function get_items($request)
191 {
192 $includeSensitiveData = $request->get_param('includeSensitiveData');
193 $donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
194 $page = $request->get_param('page');
195 $perPage = $request->get_param('per_page');
196 $sortColumn = $this->getSortColumn($request->get_param('sort'));
197 $sortDirection = $request->get_param('direction');
198 $mode = $request->get_param('mode');
199 $status = $request->get_param('status');
200
201 $query = Donation::query();
202
203 if ($campaignId = $request->get_param('campaignId')) {
204 // Filter by CampaignId
205 $query->where('give_donationmeta_attach_meta_campaignId.meta_value', $campaignId);
206 }
207
208 if ($donorId = $request->get_param('donorId')) {
209 $query->where('give_donationmeta_attach_meta_donorId.meta_value', $donorId);
210 }
211
212 if ($subscriptionId = $request->get_param('subscriptionId')) {
213 $query->where('give_donationmeta_attach_meta_subscriptionId.meta_value', $subscriptionId);
214 }
215
216 if ($donationAnonymousMode->isExcluded()) {
217 // Exclude anonymous donations from results
218 $query->where('give_donationmeta_attach_meta_anonymous.meta_value', 0);
219 }
220
221 // Include only current payment "mode"
222 $query->where('give_donationmeta_attach_meta_mode.meta_value', $mode);
223
224 // Filter by status if not 'any'
225 if (!in_array('any', (array)$status, true)) {
226 $query->whereIn('post_status', (array)$status);
227 }
228
229 $query
230 ->limit($perPage)
231 ->offset(($page - 1) * $perPage)
232 ->orderBy($sortColumn, $sortDirection);
233
234 $donations = $query->getAll() ?? [];
235 $donations = array_map(function ($donation) use ($includeSensitiveData, $donationAnonymousMode, $request) {
236 $item = (new DonationViewModel($donation))
237 ->anonymousMode($donationAnonymousMode)
238 ->includeSensitiveData($includeSensitiveData)
239 ->exports();
240
241 return $this->prepare_response_for_collection(
242 $this->prepare_item_for_response($item, $request)
243 );
244 }, $donations);
245
246 $totalDonations = empty($donations) ? 0 : Donation::query()->count();
247 $totalPages = (int)ceil($totalDonations / $perPage);
248
249 $response = rest_ensure_response($donations);
250 $response->header('X-WP-Total', $totalDonations);
251 $response->header('X-WP-TotalPages', $totalPages);
252
253 $base = add_query_arg(
254 map_deep($request->get_query_params(), function ($value) {
255 if (is_bool($value)) {
256 $value = $value ? 'true' : 'false';
257 }
258
259 return urlencode($value);
260 }),
261 rest_url(DonationRoute::BASE)
262 );
263
264 if ($page > 1) {
265 $prevPage = $page - 1;
266
267 if ($prevPage > $totalPages) {
268 $prevPage = $totalPages;
269 }
270
271 $response->link_header('prev', add_query_arg('page', $prevPage, $base));
272 }
273
274 if ($totalPages > $page) {
275 $nextPage = $page + 1;
276 $response->link_header('next', add_query_arg('page', $nextPage, $base));
277 }
278
279 return $response;
280 }
281
282 /**
283 * @since 4.6.0
284 */
285 public function get_item($request)
286 {
287 $donation = Donation::find($request->get_param('id'));
288 $includeSensitiveData = $request->get_param('includeSensitiveData');
289 $donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
290
291 if (!$donation || ($donation->anonymous && $donationAnonymousMode->isExcluded())) {
292 return new WP_Error('donation_not_found', __('Donation not found', 'give'), ['status' => 404]);
293 }
294
295 $item = (new DonationViewModel($donation))
296 ->anonymousMode($donationAnonymousMode)
297 ->includeSensitiveData($includeSensitiveData)
298 ->exports();
299
300 $response = $this->prepare_item_for_response($item, $request);
301
302 return rest_ensure_response($response);
303 }
304
305 /**
306 * Create a single donation.
307 *
308 * @since 4.8.0
309 */
310 public function create_item($request): WP_REST_Response
311 {
312 try {
313 $data = DonationCreateData::fromRequest($request);
314 $donation = $data->isRenewal() ? $data->createRenewal() : $data->createDonation();
315
316 $item = (new DonationViewModel($donation))
317 ->includeSensitiveData(true)
318 ->anonymousMode(new DonationAnonymousMode('include'))
319 ->exports();
320
321 $response = $this->prepare_item_for_response($item, $request);
322 $response->set_status(201);
323
324 return rest_ensure_response($response);
325 } catch (DonationValidationException $e) {
326 return new WP_REST_Response([
327 'message' => $e->getMessage(),
328 'error' => $e->getErrorCode()
329 ], $e->getStatusCode());
330 } catch (\Exception $e) {
331 return new WP_REST_Response([
332 'message' => __('Failed to create donation', 'give'),
333 'error' => $e->getMessage()
334 ], 400);
335 }
336 }
337
338 /**
339 * Update a single donation.
340 *
341 * @since 4.7.0 Add support for updating custom fields
342 * @since 4.6.0
343 *
344 * @return WP_REST_Response|WP_Error
345 */
346 public function update_item($request)
347 {
348 $donation = Donation::find($request->get_param('id'));
349
350 if (!$donation) {
351 return new WP_REST_Response(__('Donation not found', 'give'), 404);
352 }
353
354 $nonEditableFields = [
355 'id',
356 'updatedAt',
357 'purchaseKey',
358 'donorIp',
359 'type',
360 'mode',
361 'gatewayTransactionId',
362 ];
363
364 foreach ($request->get_params() as $key => $value) {
365 if (!in_array($key, $nonEditableFields, true)) {
366 if (in_array($key, $donation::propertyKeys(), true)) {
367 try {
368 $processedValue = DonationFields::processValue($key, $value);
369 if ($donation->isPropertyTypeValid($key, $processedValue)) {
370 $donation->$key = $processedValue;
371 }
372 } catch (Exception $e) {
373 continue;
374 }
375 }
376 }
377 }
378
379 if ($donation->isDirty()) {
380 $donation->save();
381 }
382
383 $item = (new DonationViewModel($donation))
384 ->includeSensitiveData(true)
385 ->anonymousMode(new DonationAnonymousMode('include'))
386 ->exports();
387
388 $fieldsUpdate = $this->update_additional_fields_for_object($item, $request);
389 if (is_wp_error($fieldsUpdate)) {
390 return $fieldsUpdate;
391 }
392
393 $response = $this->prepare_item_for_response($item, $request);
394
395 return rest_ensure_response($response);
396 }
397
398 /**
399 * Refund a single donation.
400 *
401 * @since 4.6.0
402 */
403 public function refund_item($request)
404 {
405 $donation = Donation::find($request->get_param('id'));
406
407 if (!$donation) {
408 return new WP_REST_Response(__('Donation not found', 'give'), 404);
409 }
410
411 $gateway = $donation->gateway();
412
413 if (!$gateway->supportsRefund()) {
414 return new WP_REST_Response(__('Refunds are not supported for this gateway', 'give'), 400);
415 }
416
417 try {
418 /** @var PaymentGatewayRefundable $gateway */
419 $command = $gateway->refundDonation($donation);
420
421 if ($command instanceof PaymentRefunded) {
422 $handler = new PaymentRefundedHandler($command);
423 $handler->handle($donation);
424 }
425
426 $includeSensitiveData = $request->get_param('includeSensitiveData');
427 $donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
428
429 $item = (new DonationViewModel($donation))
430 ->includeSensitiveData($includeSensitiveData)
431 ->anonymousMode($donationAnonymousMode)
432 ->exports();
433
434 $response = $this->prepare_item_for_response($item, $request);
435
436 return rest_ensure_response($response);
437 } catch (\Exception $exception) {
438 return new WP_REST_Response([
439 'message' => __('Failed to refund donation', 'give'),
440 'error' => $exception->getMessage(),
441 'code' => $exception->getCode(),
442 ], 500);
443 }
444 }
445
446 /**
447 * Delete a single donation.
448 *
449 * @since 4.6.0
450 */
451 public function delete_item($request): WP_REST_Response
452 {
453 $donation = Donation::find($request->get_param('id'));
454 $force = $request->get_param('force');
455
456 if (!$donation) {
457 return new WP_REST_Response(['message' => __('Donation not found', 'give')], 404);
458 }
459
460 $item = (new DonationViewModel($donation))
461 ->includeSensitiveData(true)
462 ->anonymousMode(new DonationAnonymousMode('include'))
463 ->exports();
464
465 if ($force) {
466 // Permanently delete the donation
467 $deleted = $donation->delete();
468
469 if (!$deleted) {
470 return new WP_REST_Response(['message' => __('Failed to delete donation', 'give')], 500);
471 }
472 } else {
473 // Move the donation to trash (soft delete)
474 $trashed = $donation->trash();
475
476 if (!$trashed) {
477 return new WP_REST_Response(['message' => __('Failed to trash donation', 'give')], 500);
478 }
479 }
480
481 return new WP_REST_Response(['deleted' => true, 'previous' => $item], 200);
482 }
483
484 /**
485 * Delete multiple donations.
486 *
487 * @since 4.6.0
488 */
489 public function delete_items($request): WP_REST_Response
490 {
491 $ids = $request->get_param('ids');
492 $force = $request->get_param('force');
493 $deleted = [];
494 $errors = [];
495
496 foreach ($ids as $id) {
497 $donation = Donation::find($id);
498
499 if (!$donation) {
500 $errors[] = ['id' => $id, 'message' => __('Donation not found', 'give')];
501 continue;
502 }
503
504 $item = (new DonationViewModel($donation))
505 ->includeSensitiveData(true)
506 ->anonymousMode(new DonationAnonymousMode('include'))
507 ->exports();
508
509 if ($force) {
510 if ($donation->delete()) {
511 $deleted[] = ['id' => $id, 'previous' => $item];
512 } else {
513 $errors[] = ['id' => $id, 'message' => __('Failed to delete donation', 'give')];
514 }
515 } else {
516 $trashed = $donation->trash();
517
518 if ($trashed) {
519 $deleted[] = ['id' => $id, 'previous' => $item];
520 } else {
521 $errors[] = ['id' => $id, 'message' => __('Failed to trash donation', 'give')];
522 }
523 }
524 }
525
526 return new WP_REST_Response([
527 'deleted' => $deleted,
528 'errors' => $errors,
529 'total_requested' => count($ids),
530 'total_deleted' => count($deleted),
531 'total_errors' => count($errors),
532 ], 200);
533 }
534
535 /**
536 * @since 4.13.0 updated the amount sort columns to CAST as DECIMAL
537 * @since 4.6.0
538 */
539 public function getSortColumn(string $sortColumn): string
540 {
541 $sortColumnsMap = [
542 'id' => 'ID',
543 'createdAt' => 'post_date',
544 'updatedAt' => 'post_modified',
545 'status' => 'post_status',
546 'amount' => 'CAST(give_donationmeta_attach_meta_amount.meta_value AS DECIMAL(10, 2))',
547 'feeAmountRecovered' => 'CAST(give_donationmeta_attach_meta_feeAmountRecovered.meta_value AS DECIMAL(10, 2))',
548 'donorId' => 'give_donationmeta_attach_meta_donorId.meta_value',
549 'firstName' => 'give_donationmeta_attach_meta_firstName.meta_value',
550 'lastName' => 'give_donationmeta_attach_meta_lastName.meta_value',
551 ];
552
553 return $sortColumnsMap[$sortColumn];
554 }
555
556 /**
557 * @since 4.6.0
558 */
559 public function get_collection_params(): array
560 {
561 $params = parent::get_collection_params();
562
563 $params['page']['default'] = 1;
564 $params['per_page']['default'] = 30;
565
566 // Remove default parameters not being used
567 unset($params['context']);
568 unset($params['search']);
569
570 $params += [
571 'sort' => [
572 'type' => 'string',
573 'default' => 'id',
574 'enum' => [
575 'id',
576 'createdAt',
577 'updatedAt',
578 'status',
579 'amount',
580 'feeAmountRecovered',
581 'donorId',
582 'firstName',
583 'lastName',
584 ],
585 ],
586 'direction' => [
587 'type' => 'string',
588 'default' => 'DESC',
589 'enum' => ['ASC', 'DESC'],
590 ],
591 'mode' => [
592 'type' => 'string',
593 'default' => 'live',
594 'enum' => ['live', 'test'],
595 ],
596 'status' => [
597 'type' => 'array',
598 'items' => [
599 'type' => 'string',
600 'enum' => [
601 'any',
602 'publish',
603 'give_subscription',
604 'pending',
605 'processing',
606 'refunded',
607 'revoked',
608 'failed',
609 'cancelled',
610 'abandoned',
611 'preapproval',
612 ],
613 ],
614 'default' => ['any'],
615 ],
616 'campaignId' => [
617 'type' => 'integer',
618 'default' => 0,
619 ],
620 'donorId' => [
621 'type' => 'integer',
622 'default' => 0,
623 ],
624 'subscriptionId' => [
625 'type' => 'integer',
626 'default' => 0,
627 ],
628 'includeSensitiveData' => [
629 'type' => 'boolean',
630 'default' => false,
631 ],
632 'anonymousDonations' => [
633 'type' => 'string',
634 'default' => 'exclude',
635 'enum' => [
636 'exclude',
637 'include',
638 'redact',
639 ],
640 ],
641 'force' => [
642 'type' => 'boolean',
643 'default' => false,
644 'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
645 ],
646 ];
647
648 return $params;
649 }
650
651 /**
652 * @since 4.13.0 updated embeddable links
653 * @since 4.7.0 Add support for adding custom fields to the response
654 * @since 4.6.0
655 * @throws Exception
656 */
657 public function prepare_item_for_response($item, $request): WP_REST_Response
658 {
659 $donationId = $request->get_param('id') ?? $item['id'] ?? null;
660
661 if ($donationId && $donation = Donation::find($donationId)) {
662 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $donationId));
663
664 $links = [
665 'self' => ['href' => $self_url]
666 ];
667
668 if (!empty($item['donorId'])) {
669 $donor_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'donors', $item['donorId']));
670 $donor_url = add_query_arg([
671 'mode' => $request->get_param('mode'),
672 'includeSensitiveData' => $request->get_param('includeSensitiveData'),
673 'anonymousDonors' => $request->get_param('anonymousDonations'),
674 ], $donor_url);
675
676 $links[CURIE::relationUrl('donor')] = [
677 'href' => $donor_url,
678 'embeddable' => true,
679 ];
680 }
681
682 if (!empty($item['campaignId'])) {
683 $campaign_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'campaigns', $item['campaignId']));
684 $campaign_url = add_query_arg([
685 'mode' => $request->get_param('mode'),
686 ], $campaign_url);
687
688 $links[CURIE::relationUrl('campaign')] = [
689 'href' => $campaign_url,
690 'embeddable' => true,
691 ];
692 }
693
694 if (!empty($item['formId'])) {
695 $form_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'forms', $item['formId']));
696 $form_url = add_query_arg([
697 'mode' => $request->get_param('mode'),
698 ], $form_url);
699
700 $links[CURIE::relationUrl('form')] = [
701 'href' => $form_url,
702 'embeddable' => true,
703 ];
704 }
705
706 // Add subscription link when subscriptionId is greater than 0
707 if (isset($item['subscriptionId']) && $item['subscriptionId'] > 0) {
708 $subscription_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'subscriptions', $item['subscriptionId']));
709 $links[CURIE::relationUrl('subscription')] = [
710 'href' => $subscription_url,
711 'embeddable' => true,
712 ];
713 }
714 } else {
715 $links = [];
716 }
717
718 $responseItem = Item::formatDatesForResponse(
719 $item,
720 ['createdAt', 'updatedAt']
721 );
722
723 $response = new WP_REST_Response($responseItem);
724 if (!empty($links)) {
725 $response->add_links($links);
726 }
727
728 $response->data = $this->add_additional_fields_to_object($response->data, $request);
729
730 return $response;
731 }
732
733 /**
734 * @since 4.14.0
735 */
736 public function get_item_permissions_check($request)
737 {
738 return $this->validationForGetMethods($request);
739 }
740
741 /**
742 * @since 4.14.0
743 */
744 public function get_items_permissions_check($request)
745 {
746 return $this->validationForGetMethods($request);
747 }
748
749 /**
750 * @since 4.14.0 update method name to validationForGetMethods, replace logic with UserPermissions facade and add canViewDonations check
751 * @since 4.6.0
752 */
753 public function validationForGetMethods(WP_REST_Request $request)
754 {
755 $includeSensitiveData = $request->get_param('includeSensitiveData');
756 $includeAnonymousDonations = $request->get_param('anonymousDonations');
757 $canViewDonations = UserPermissions::donations()->canView();
758
759 if ($includeSensitiveData && !$canViewDonations) {
760 return new WP_Error(
761 'rest_forbidden',
762 __('You do not have permission to include sensitive data.', 'give'),
763 ['status' => $this->authorizationStatusCode()]
764 );
765 }
766
767 if ($includeAnonymousDonations !== null) {
768 $anonymousMode = new DonationAnonymousMode($includeAnonymousDonations);
769
770 if ($anonymousMode->isIncluded() && !$canViewDonations) {
771 return new WP_Error(
772 'rest_forbidden',
773 __('You do not have permission to include anonymous donations.', 'give'),
774 ['status' => $this->authorizationStatusCode()]
775 );
776 }
777 }
778
779 return true;
780 }
781
782 /**
783 * @since 4.6.0
784 */
785 public function update_item_permissions_check($request)
786 {
787 if ($this->canEditDonations()) {
788 return true;
789 }
790
791 return new WP_Error(
792 'rest_forbidden',
793 __('You do not have permission to update donations.', 'give'),
794 ['status' => $this->authorizationStatusCode()]
795 );
796 }
797
798 /**
799 * @since 4.6.0
800 */
801 public function create_item_permissions_check($request)
802 {
803 if ($this->canEditDonations()) {
804 return true;
805 }
806
807 return new WP_Error(
808 'rest_forbidden',
809 __('You do not have permission to create donations.', 'give'),
810 ['status' => $this->authorizationStatusCode()]
811 );
812 }
813
814 /**
815 * @since 4.8.0
816 */
817 public function delete_item_permissions_check($request)
818 {
819 if ($this->canDeleteDonations()) {
820 return true;
821 }
822
823 return new WP_Error(
824 'rest_forbidden',
825 __('You do not have permission to delete donations.', 'give'),
826 ['status' => $this->authorizationStatusCode()]
827 );
828 }
829
830 /**
831 * @since 4.6.0
832 */
833 public function delete_items_permissions_check($request)
834 {
835 if ($this->canDeleteDonations()) {
836 return true;
837 }
838
839 return new WP_Error(
840 'rest_forbidden',
841 __('You do not have permission to delete donations.', 'give'),
842 ['status' => $this->authorizationStatusCode()]
843 );
844 }
845
846 /**
847 * @since 4.6.0
848 */
849 public function refund_item_permissions_check($request)
850 {
851 if ($this->canRefundDonations()) {
852 return true;
853 }
854
855 return new WP_Error(
856 'rest_forbidden',
857 __('You do not have permission to refund donations.', 'give'),
858 ['status' => $this->authorizationStatusCode()]
859 );
860 }
861
862 /**
863 * Check if current user can edit donations.
864 *
865 * @since 4.14.0 replace logic with UserPermissions facade
866 * @since 4.6.0
867 */
868 private function canEditDonations(): bool
869 {
870 return UserPermissions::donations()->canEdit();
871 }
872
873 /**
874 * Check if current user can delete donations.
875 *
876 * @since 4.14.0 replace logic with UserPermissions facade
877 * @since 4.6.0
878 */
879 private function canDeleteDonations(): bool
880 {
881 return UserPermissions::donations()->canDelete();
882 }
883
884 /**
885 * Check if current user can refund donations.
886 *
887 * @since 4.14.0 replace logic with UserPermissions facade
888 * @since 4.6.0
889 */
890 private function canRefundDonations(): bool
891 {
892 return UserPermissions::donations()->canEdit();
893 }
894
895 /**
896 * @since 4.6.0
897 */
898 public function authorizationStatusCode(): int
899 {
900 return is_user_logged_in() ? 403 : 401;
901 }
902
903 /**
904 * @since 4.13.0 Updated schema to match actual response, add schema description
905 * @since 4.8.0 Change default status to complete
906 * @since 4.7.0 Change title to givewp/donation and add custom fields schema
907 * @since 4.6.1 Change type of billing address properties to accept null values
908 * @since 4.6.0
909 */
910 public function get_item_schema(): array
911 {
912 $schema = [
913 '$schema' => 'http://json-schema.org/draft-04/schema#',
914 'title' => 'givewp/donation',
915 'description' => esc_html__('Donation routes for CRUD operations', 'give'),
916 'type' => 'object',
917 'properties' => [
918 'id' => [
919 'type' => 'integer',
920 'description' => esc_html__('Donation ID', 'give'),
921 'readonly' => true,
922 ],
923 'donorId' => [
924 'type' => 'integer',
925 'description' => esc_html__('Donor ID', 'give'),
926 ],
927 'firstName' => [
928 'type' => 'string',
929 'description' => esc_html__('Donor first name', 'give'),
930 'format' => 'text-field',
931 ],
932 'lastName' => [
933 'type' => ['string', 'null'],
934 'description' => esc_html__('Donor last name', 'give'),
935 'format' => 'text-field',
936 ],
937 'honorific' => [
938 'type' => ['string', 'null'],
939 'description' => esc_html__('Donor honorific/prefix', 'give'),
940 'enum' => $this->get_honorific_prefixes(),
941 ],
942 'email' => [
943 'type' => 'string',
944 'description' => esc_html__('Donor email', 'give'),
945 'format' => 'email',
946 ],
947 'phone' => [
948 'type' => ['string', 'null'],
949 'description' => esc_html__('Donor phone', 'give'),
950 'format' => 'text-field',
951 ],
952 'company' => [
953 'type' => ['string', 'null'],
954 'description' => esc_html__('Donor company', 'give'),
955 'format' => 'text-field',
956 ],
957 'amount' => SchemaTypes::money()->description(esc_html__('Donation amount', 'give'))->toArray(),
958 'feeAmountRecovered' => SchemaTypes::money()->nullable()->description(esc_html__('Fee amount recovered', 'give'))->toArray(),
959 'eventTicketsAmount' => SchemaTypes::money()->nullable()->readonly()->description(esc_html__('Event tickets amount', 'give'))->toArray(),
960 'status' => [
961 'type' => 'string',
962 'description' => esc_html__('Donation status', 'give'),
963 'enum' => array_values(DonationStatus::toArray()),
964 'default' => DonationStatus::COMPLETE,
965 ],
966 'type' => [
967 'type' => 'string',
968 'description' => esc_html__('Donation type', 'give'),
969 'enum' => array_values(DonationType::toArray()),
970 'default' => DonationType::SINGLE,
971 'required' => true,
972 ],
973 'gatewayId' => [
974 'type' => 'string',
975 'description' => esc_html__('Payment gateway ID', 'give'),
976 'format' => 'text-field',
977 ],
978 'mode' => [
979 'type' => 'string',
980 'description' => esc_html__('Donation mode (live or test)', 'give'),
981 'enum' => array_values(DonationMode::toArray()),
982 ],
983 'anonymous' => [
984 'type' => 'boolean',
985 'description' => esc_html__('Whether the donation is anonymous', 'give'),
986 'default' => false,
987 ],
988 'campaignId' => [
989 'type' => 'integer',
990 'description' => esc_html__('Campaign ID', 'give'),
991 ],
992 'formId' => [
993 'type' => 'integer',
994 'description' => esc_html__('Form ID', 'give'),
995 ],
996 'formTitle' => [
997 'type' => 'string',
998 'description' => esc_html__('Form title', 'give'),
999 'format' => 'text-field',
1000 ],
1001 'subscriptionId' => [
1002 'type' => ['integer', 'null'],
1003 'description' => esc_html__('Subscription ID', 'give'),
1004 ],
1005 'levelId' => [
1006 'type' => ['string', 'null'],
1007 'description' => esc_html__('Level ID', 'give'),
1008 'format' => 'text-field',
1009 ],
1010 'gatewayTransactionId' => [
1011 'type' => ['string', 'null'],
1012 'description' => esc_html__('Gateway transaction ID', 'give'),
1013 'format' => 'text-field',
1014 ],
1015 'exchangeRate' => [
1016 'type' => 'string',
1017 'description' => esc_html__('Exchange rate', 'give'),
1018 'format' => 'text-field',
1019 'default' => '1',
1020 ],
1021 'comment' => [
1022 'type' => ['string', 'null'],
1023 'description' => esc_html__('Donation comment', 'give'),
1024 'format' => 'text-field',
1025 ],
1026 'billingAddress' => [
1027 'type' => ['object', 'null'],
1028 'description' => esc_html__('Billing address', 'give'),
1029 'properties' => [
1030 'address1' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1031 'address2' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1032 'city' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1033 'state' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1034 'country' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1035 'zip' => ['type' => ['string', 'null'], 'format' => 'text-field'],
1036 ],
1037 ],
1038 'donorIp' => [
1039 'type' => ['string', 'null'],
1040 'description' => esc_html__('Donor IP address (sensitive data)', 'give'),
1041 'format' => 'text-field',
1042 ],
1043 'purchaseKey' => [
1044 'type' => ['string', 'null'],
1045 'description' => esc_html__('Purchase key (sensitive data)', 'give'),
1046 'format' => 'text-field',
1047 ],
1048 'createdAt' => [
1049 'type' => ['string', 'null'],
1050 'description' => esc_html__('Created at Date and Time string', 'give'),
1051 'format' => 'date-time',
1052 ],
1053 'updatedAt' => [
1054 'type' => ['string', 'null'],
1055 'description' => esc_html__('Created at Date and Time string', 'give'),
1056 'format' => 'date-time',
1057 ],
1058 'updateRenewalDate' => [
1059 'type' => 'boolean',
1060 'description' => esc_html__('Whether to update the subscription renewal date with the createdAt date when creating subscription or renewal donations', 'give'),
1061 'default' => false,
1062 ],
1063 'customFields' => [
1064 'type' => 'array',
1065 'readonly' => true,
1066 'description' => esc_html__('Custom fields (sensitive data)', 'give'),
1067 'items' => [
1068 'type' => 'object',
1069 'properties' => [
1070 'label' => [
1071 'type' => 'string',
1072 'description' => esc_html__('Field label', 'give'),
1073 'format' => 'text-field',
1074 ],
1075 'value' => [
1076 'type' => 'string',
1077 'description' => esc_html__('Field value', 'give'),
1078 'format' => 'text-field',
1079 ],
1080 ],
1081 ],
1082 ],
1083 'gateway' => [
1084 'type' => 'object',
1085 'readonly' => true,
1086 'properties' => [
1087 'id' => [
1088 'type' => 'string',
1089 'description' => esc_html__('Gateway ID', 'give'),
1090 ],
1091 'name' => [
1092 'type' => 'string',
1093 'description' => esc_html__('Gateway name', 'give'),
1094 ],
1095 'label' => [
1096 'type' => 'string',
1097 'description' => esc_html__('Payment method label', 'give'),
1098 ],
1099 'transactionUrl' => [
1100 'type' => 'string',
1101 'description' => esc_html__('Gateway transaction URL', 'give'),
1102 'format' => 'uri',
1103 ],
1104 ],
1105 ],
1106 'eventTickets' => [
1107 'type' => ['array', 'null'],
1108 'readonly' => true,
1109 'description' => esc_html__('Event tickets', 'give'),
1110 'items' => [
1111 'type' => 'object',
1112 'properties' => [
1113 'id' => [
1114 'type' => 'integer',
1115 'description' => esc_html__('Event ticket ID', 'give'),
1116 ],
1117 'eventId' => [
1118 'type' => 'integer',
1119 'description' => esc_html__('Event ID', 'give'),
1120 ],
1121 'ticketTypeId' => [
1122 'type' => 'integer',
1123 'description' => esc_html__('Ticket type ID', 'give'),
1124 ],
1125 'donationId' => [
1126 'type' => 'integer',
1127 'description' => esc_html__('Donation ID', 'give'),
1128 ],
1129 'amount' => SchemaTypes::money()->description(esc_html__('Event ticket amount', 'give'))->toArray(),
1130 'createdAt' => [
1131 'type' => 'string',
1132 'description' => esc_html__('Created at Date and Time string', 'give'),
1133 'format' => 'date-time',
1134 ],
1135 'updatedAt' => [
1136 'type' => 'string',
1137 'description' => esc_html__('Updated at Date and Time string', 'give'),
1138 'format' => 'date-time',
1139 ],
1140 'event' => [
1141 'type' => 'object',
1142 'properties' => [
1143 'id' => [
1144 'type' => 'integer',
1145 'description' => esc_html__('Event ID', 'give'),
1146 ],
1147 'title' => [
1148 'type' => 'string',
1149 'description' => esc_html__('Event title', 'give'),
1150 ],
1151 'description' => [
1152 'type' => 'string',
1153 'description' => esc_html__('Event description', 'give'),
1154 ],
1155 'startDateTime' => [
1156 'type' => 'string',
1157 'description' => esc_html__('Event start date and time', 'give'),
1158 'format' => 'date-time',
1159 ],
1160 'endDateTime' => [
1161 'type' => 'string',
1162 'description' => esc_html__('Event end date and time', 'give'),
1163 'format' => 'date-time',
1164 ],
1165 'ticketCloseDateTime' => [
1166 'type' => 'string',
1167 'description' => esc_html__('Event ticket close date and time', 'give'),
1168 'format' => 'date-time',
1169 ],
1170 'createdAt' => [
1171 'type' => 'string',
1172 'description' => esc_html__('Event creation date and time', 'give'),
1173 'format' => 'date-time',
1174 ],
1175 'updatedAt' => [
1176 'type' => 'string',
1177 'description' => esc_html__('Event last update date and time', 'give'),
1178 'format' => 'date-time',
1179 ],
1180 ],
1181 ],
1182 'ticketType' => [
1183 'type' => 'object',
1184 'properties' => [
1185 'id' => [
1186 'type' => 'integer',
1187 'description' => esc_html__('Ticket type ID', 'give'),
1188 ],
1189 'eventId' => [
1190 'type' => 'integer',
1191 'description' => esc_html__('Event ID', 'give'),
1192 ],
1193 'title' => [
1194 'type' => 'string',
1195 'description' => esc_html__('Ticket type title', 'give'),
1196 ],
1197 'description' => [
1198 'type' => 'string',
1199 'description' => esc_html__('Ticket type description', 'give'),
1200 ],
1201 'price' => SchemaTypes::money()->description(esc_html__('Ticket type price', 'give'))->toArray(),
1202 'capacity' => [
1203 'type' => 'integer',
1204 'description' => esc_html__('Ticket type capacity', 'give'),
1205 ],
1206 'createdAt' => [
1207 'type' => 'string',
1208 'description' => esc_html__('Ticket type creation date and time', 'give'),
1209 'format' => 'date-time',
1210 ],
1211 'updatedAt' => [
1212 'type' => 'string',
1213 'description' => esc_html__('Ticket type last update date and time', 'give'),
1214 'format' => 'date-time',
1215 ],
1216 ],
1217 ],
1218 ],
1219 ],
1220 ],
1221 'anyOf' => [
1222 [
1223 // 1) type = renewal -> require subscriptionId
1224 [
1225 'properties' => [
1226 'type' => [
1227 'enum' => ['renewal'],
1228 ],
1229 ],
1230 'required' => ['subscriptionId'],
1231 ],
1232
1233 // 2) type = single -> require donorId, amount, gatewayId, mode, formId, firstName, email
1234 [
1235 'properties' => [
1236 'type' => [
1237 'enum' => ['single'],
1238 ],
1239 ],
1240 'required' => ['donorId', 'amount', 'gatewayId', 'mode', 'formId', 'firstName', 'email'],
1241 ],
1242
1243 // 3) type = subscription -> require donorId, amount, gatewayId, mode, formId, firstName, email, subscriptionId
1244 [
1245 'properties' => [
1246 'type' => [
1247 'enum' => ['subscription'],
1248 ],
1249 ],
1250 'required' => ['donorId', 'amount', 'gatewayId', 'mode', 'formId', 'firstName', 'email', 'subscriptionId'],
1251 ],
1252 ],
1253 ],
1254 ],
1255 ];
1256
1257 return $this->add_additional_fields_schema($schema);
1258 }
1259
1260 /**
1261 * Gets all available honorific prefixes.
1262 *
1263 * Fetches the user-configured honorific prefixes from settings and merges them
1264 * with a hardcoded 'anonymous' prefix. The 'anonymous' prefix is required
1265 * when requests with anonymousDonations=redact are present.
1266 *
1267 * @return array<string> An array of honorific prefixes.
1268 */
1269 private function get_honorific_prefixes(): array {
1270 $prefixes = (array) give_get_option( 'title_prefixes', array_values( give_get_default_title_prefixes() ) );
1271
1272 return array_merge( $prefixes, ['anonymous', null] );
1273 }
1274 }
1275