PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.4
GiveWP – Donation Plugin and Fundraising Platform v4.16.4
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 / API / REST / V3 / Routes / Donations / DonationController.php

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

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