PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.9
GiveWP – Donation Plugin and Fundraising Platform v4.16.9
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 / Subscriptions / SubscriptionController.php

SubscriptionController.php in GiveWP – Donation Plugin and Fundraising Platform 4.16.9, at src/API/REST/V3/Routes/Subscriptions/SubscriptionController.php

730 lines 24.7 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\Subscriptions;
4
5 use Exception;
6 use Give\API\REST\V3\Routes\Donors\ValueObjects\DonorAnonymousMode;
7 use Give\API\REST\V3\Routes\Subscriptions\Actions\GetSubscriptionCollectionParams;
8 use Give\API\REST\V3\Routes\Subscriptions\Actions\GetSubscriptionItemSchema;
9 use Give\API\REST\V3\Routes\Subscriptions\Actions\GetSubscriptionSharedParamsForGetMethods;
10 use Give\API\REST\V3\Routes\Subscriptions\DataTransferObjects\SubscriptionCreateData;
11 use Give\API\REST\V3\Routes\Subscriptions\Exceptions\SubscriptionValidationException;
12 use Give\API\REST\V3\Routes\Subscriptions\Fields\SubscriptionFields;
13 use Give\API\REST\V3\Routes\Subscriptions\Permissions\SubscriptionPermissions;
14 use Give\API\REST\V3\Routes\Subscriptions\ValueObjects\SubscriptionRoute;
15 use Give\API\REST\V3\Support\CURIE;
16 use Give\API\REST\V3\Support\Headers;
17 use Give\API\REST\V3\Support\Item;
18 use Give\Subscriptions\Models\Subscription;
19 use Give\Subscriptions\SubscriptionQuery;
20 use Give\Subscriptions\ValueObjects\SubscriptionStatus;
21 use Give\Subscriptions\ViewModels\SubscriptionViewModel;
22 use WP_Error;
23 use WP_REST_Controller;
24 use WP_REST_Request;
25 use WP_REST_Response;
26 use WP_REST_Server;
27
28 /**
29 * The methods using snake case like register_routes() are present in the base class,
30 * and the methods using camel case like deleteItems() are available only on this class.
31 *
32 * @since 4.8.0
33 */
34 class SubscriptionController extends WP_REST_Controller
35 {
36 /**
37 * @var string
38 */
39 protected $namespace;
40
41 /**
42 * @var string
43 */
44 protected $rest_base;
45
46 /**
47 * @since 4.8.0
48 */
49 public function __construct()
50 {
51 $this->namespace = SubscriptionRoute::NAMESPACE;
52 $this->rest_base = SubscriptionRoute::BASE;
53 }
54
55 /**
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.8.0
58 */
59 public function register_routes()
60 {
61 register_rest_route($this->namespace, '/' . $this->rest_base, [
62 [
63 'methods' => WP_REST_Server::READABLE,
64 'callback' => [$this, 'get_items'],
65 'permission_callback' => [$this, 'get_items_permissions_check'],
66 'args' => array_merge($this->get_collection_params(), give(GetSubscriptionSharedParamsForGetMethods::class)()),
67 ],
68 [
69 'methods' => WP_REST_Server::CREATABLE,
70 'callback' => [$this, 'create_item'],
71 'permission_callback' => [$this, 'create_item_permissions_check'],
72 'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::CREATABLE),
73 ],
74 [
75 'methods' => WP_REST_Server::DELETABLE,
76 'callback' => [$this, 'deleteItems'],
77 'permission_callback' => [$this, 'delete_items_permissions_check'],
78 'args' => [
79 'ids' => [
80 'description' => __('Array of subscription IDs to delete', 'give'),
81 'type' => 'array',
82 'items' => [
83 'type' => 'integer',
84 ],
85 'required' => true,
86 ],
87 'force' => [
88 'description' => __('Whether to permanently delete (force=true) or move to trash (force=false, default).', 'give'),
89 'type' => 'boolean',
90 'default' => false,
91 ],
92 ],
93 ],
94 'schema' => [$this, 'get_public_item_schema'],
95 ]);
96
97 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
98 [
99 'methods' => WP_REST_Server::READABLE,
100 'callback' => [$this, 'get_item'],
101 'permission_callback' => [$this, 'get_item_permissions_check'],
102 'args' => array_merge([
103 'id' => [
104 'description' => __('The subscription ID.', 'give'),
105 'type' => 'integer',
106 'required' => true,
107 ],
108 '_embed' => [
109 'description' => __(
110 '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.',
111 'give'
112 ),
113 'type' => [
114 'string',
115 'boolean',
116 ],
117 'default' => false,
118 ],
119 ], give(GetSubscriptionSharedParamsForGetMethods::class)()),
120 ],
121 [
122 'methods' => WP_REST_Server::EDITABLE,
123 'callback' => [$this, 'update_item'],
124 'permission_callback' => [$this, 'update_item_permissions_check'],
125 'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::EDITABLE),
126 ],
127 [
128 'methods' => WP_REST_Server::DELETABLE,
129 'callback' => [$this, 'delete_item'],
130 'permission_callback' => [$this, 'delete_item_permissions_check'],
131 'args' => [
132 'id' => [
133 'description' => __('The subscription ID.', 'give'),
134 'type' => 'integer',
135 'required' => true,
136 ],
137 'force' => [
138 'description' => __('Whether to permanently delete (force=true) or move to trash (force=false, default).', 'give'),
139 'type' => 'boolean',
140 'default' => false,
141 ],
142 ],
143 ],
144 'schema' => [$this, 'get_public_item_schema'],
145 ]);
146
147 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/cancel', [
148 [
149 'methods' => WP_REST_Server::EDITABLE,
150 'callback' => [$this, 'cancel_item'],
151 'permission_callback' => [$this, 'cancel_item_permissions_check'],
152 'args' => [
153 'id' => [
154 'type' => 'integer',
155 'required' => true,
156 ],
157 'trash' => [
158 'type' => 'boolean',
159 'default' => false,
160 'description' => __('Whether to also move the subscription to trash (trash=true) instead of just canceling it.', 'give'),
161 ],
162 ],
163 'schema' => [$this, 'get_public_item_schema'],
164 ],
165 ]);
166 }
167
168 /**
169 * Get subscriptions.
170 *
171 * @since 4.8.0
172 *
173 * @param WP_REST_Request $request Full data about the request.
174 *
175 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
176 *
177 * @throws Exception
178 */
179 public function get_items($request)
180 {
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 $includeSensitiveData = $request->get_param('includeSensitiveData');
188 $donorAnonymousMode = new DonorAnonymousMode($request->get_param('anonymousDonors'));
189
190 $query = new SubscriptionQuery();
191 $query->whereMode($mode);
192
193 if ($campaignId = $request->get_param('campaignId')) {
194 $query->whereCampaignId($campaignId);
195 }
196
197 if ($donorAnonymousMode->isExcluded()) {
198 $query->excludeAnonymousDonors();
199 }
200
201 if ($donorId = $request->get_param('donorId')) {
202 $query->whereDonorId($donorId);
203 }
204
205 if (!in_array('any', (array) $status, true)) {
206 $query->whereStatus((array)$status);
207 }
208
209 if (in_array($sortColumn, ['firstName', 'lastName'], true)) {
210 $query->selectDonorNames();
211 }
212
213 $totalQuery = $query->clone();
214 $query->limit($perPage)->offset(($page - 1) * $perPage)->orderBy($sortColumn, $sortDirection);
215
216 $subscriptions = $query->getAll() ?? [];
217
218 $subscriptions = array_map(function ($subscription) use ($donorAnonymousMode, $includeSensitiveData, $request) {
219 $item = (new SubscriptionViewModel($subscription))
220 ->anonymousMode($donorAnonymousMode)
221 ->includeSensitiveData($includeSensitiveData)
222 ->exports();
223
224 return $this->prepare_response_for_collection(
225 $this->prepare_item_for_response($item, $request)
226 );
227 }, $subscriptions);
228
229 $totalSubscriptions = empty($subscriptions) ? 0 : $totalQuery->count();
230 $response = rest_ensure_response($subscriptions);
231 $response = Headers::addPagination($response, $request, $totalSubscriptions, $perPage, $this->rest_base);
232
233 return $response;
234 }
235
236 /**
237 * Get a subscription.
238 *
239 * @since 4.16.3 Return 404 for anonymous donors unless explicitly included.
240 * @since 4.8.0
241 *
242 * @param WP_REST_Request $request Full data about the request.
243 *
244 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
245 *
246 * @throws Exception
247 */
248 public function get_item($request)
249 {
250 $subscription = Subscription::find($request->get_param('id'));
251 $donorAnonymousMode = new DonorAnonymousMode($request->get_param('anonymousDonors'));
252
253 // Hide anonymous donors unless explicitly included, matching the collection and donor endpoints.
254 if (
255 !$subscription
256 || ($subscription->donor && $subscription->donor->isAnonymous() && $donorAnonymousMode->isExcluded())
257 ) {
258 return new WP_Error('subscription_not_found', __('Subscription not found', 'give'), ['status' => 404]);
259 }
260
261 $includeSensitiveData = $request->get_param('includeSensitiveData');
262
263 $item = (new SubscriptionViewModel($subscription))
264 ->anonymousMode($donorAnonymousMode)
265 ->includeSensitiveData($includeSensitiveData)
266 ->exports();
267
268 $response = $this->prepare_item_for_response($item, $request);
269
270 return rest_ensure_response($response);
271 }
272
273 /**
274 * Create a subscription.
275 *
276 * @since 4.8.0
277 *
278 * @param WP_REST_Request $request Full data about the request.
279 *
280 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
281 *
282 * @throws Exception
283 */
284 public function create_item($request)
285 {
286 try {
287 $data = SubscriptionCreateData::fromRequest($request);
288 $subscription = $data->createSubscription();
289
290 $fieldsUpdate = $this->update_additional_fields_for_object($subscription, $request);
291
292 if (is_wp_error($fieldsUpdate)) {
293 return $fieldsUpdate;
294 }
295 } catch (SubscriptionValidationException $e) {
296 return new WP_REST_Response([
297 'message' => $e->getMessage(),
298 'error' => $e->getErrorCode()
299 ], $e->getStatusCode());
300 } catch (Exception $e) {
301 return new WP_REST_Response([
302 'message' => sprintf(__('Failed to create subscription: %s', 'give'), $e->getMessage()),
303 'error' => 'internal_server_error'
304 ], 500);
305 }
306
307 $item = (new SubscriptionViewModel($subscription))
308 ->includeSensitiveData(true)
309 ->exports();
310
311 $response = $this->prepare_item_for_response($item, $request);
312 $response->set_status(201);
313
314 return rest_ensure_response($response);
315 }
316
317 /**
318 * Update a subscription.
319 *
320 * @since 4.11.0 Exclude gatewaySubscriptionId from non-editable fields
321 * @since 4.8.0
322 *
323 * @param WP_REST_Request $request Full data about the request.
324 *
325 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
326 *
327 * @throws Exception
328 */
329 public function update_item($request)
330 {
331 $subscription = Subscription::find($request->get_param('id'));
332
333 if (!$subscription) {
334 return new WP_REST_Response(__('Subscription not found', 'give'), 404);
335 }
336
337 $nonEditableFields = [
338 'id',
339 'createdAt',
340 'mode',
341 'gatewayId',
342 ];
343
344 foreach ($request->get_params() as $key => $value) {
345 if (!in_array($key, $nonEditableFields, true)) {
346 if (in_array($key, $subscription::propertyKeys(), true)) {
347 try {
348 $processedValue = SubscriptionFields::processValue($key, $value);
349 if ($subscription->isPropertyTypeValid($key, $processedValue)) {
350 $subscription->$key = $processedValue;
351 }
352 } catch (Exception $e) {
353 continue;
354 }
355 }
356 }
357 }
358
359 if ($subscription->isDirty()) {
360 $subscription->save();
361 }
362
363 $fieldsUpdate = $this->update_additional_fields_for_object($subscription, $request);
364
365 if (is_wp_error($fieldsUpdate)) {
366 return $fieldsUpdate;
367 }
368
369 $item = (new SubscriptionViewModel($subscription))->includeSensitiveData(true)->exports();
370
371 $response = $this->prepare_item_for_response($item, $request);
372
373 return rest_ensure_response($response);
374 }
375
376 /**
377 * Delete a subscription.
378 *
379 * @since 4.8.0
380 *
381 * @param WP_REST_Request $request Full data about the request.
382 *
383 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
384 *
385 * @throws Exception
386 */
387 public function delete_item($request): WP_REST_Response
388 {
389 $subscription = Subscription::find($request->get_param('id'));
390 $force = $request->get_param('force');
391
392 if (!$subscription) {
393 return new WP_REST_Response(['message' => __('Subscription not found', 'give')], 404);
394 }
395
396 $item = (new SubscriptionViewModel($subscription))->exports();
397
398 if ($force) { // Permanently delete the subscription
399 $deleted = $subscription->delete();
400
401 if (!$deleted) {
402 return new WP_REST_Response(['message' => __('Failed to delete subscription', 'give')], 500);
403 }
404 } else { // Move the subscription to trash (soft delete)
405 $trashed = $subscription->trash();
406
407 if (!$trashed) {
408 return new WP_REST_Response(['message' => __('Failed to trash subscription', 'give')], 500);
409 }
410 }
411
412 return new WP_REST_Response(['deleted' => true, 'previous' => $item], 200);
413 }
414
415 /**
416 * Delete multiple subscriptions.
417 *
418 * @since 4.8.0
419 *
420 * @param WP_REST_Request $request Full data about the request.
421 *
422 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
423 *
424 * @throws Exception
425 */
426 public function deleteItems($request): WP_REST_Response
427 {
428 $ids = $request->get_param('ids');
429 $force = $request->get_param('force');
430 $deleted = [];
431 $errors = [];
432
433 foreach ($ids as $id) {
434 $subscription = Subscription::find($id);
435
436 if (!$subscription) {
437 $errors[] = ['id' => $id, 'message' => __('Subscription not found', 'give')];
438
439 continue;
440 }
441
442 $item = (new SubscriptionViewModel($subscription))->exports();
443
444 if ($force) {
445 if ($subscription->delete()) {
446 $deleted[] = ['id' => $id, 'previous' => $item];
447 } else {
448 $errors[] = ['id' => $id, 'message' => __('Failed to delete subscription', 'give')];
449 }
450 } else {
451 $trashed = $subscription->trash();
452
453 if ($trashed) {
454 $deleted[] = ['id' => $id, 'previous' => $item];
455 } else {
456 $errors[] = ['id' => $id, 'message' => __('Failed to trash subscription', 'give')];
457 }
458 }
459 }
460
461 return new WP_REST_Response([
462 'deleted' => $deleted,
463 'errors' => $errors,
464 'total_requested' => count($ids),
465 'total_deleted' => count($deleted),
466 'total_errors' => count($errors),
467 ], 200);
468 }
469
470 /**
471 * Cancel a subscription.
472 *
473 * @since 4.8.0
474 */
475 public function cancel_item($request)
476 {
477 $subscription = Subscription::find($request->get_param('id'));
478
479 if (!$subscription) {
480 return new WP_REST_Response(__('Subscription not found', 'give'), 404);
481 }
482
483 try {
484 if (give()->gateways->hasPaymentGateway($subscription->gatewayId)) {
485 $subscription->cancel(true);
486 } else {
487 $subscription->status = SubscriptionStatus::CANCELLED();
488 $subscription->save();
489 }
490
491 $trash = $request->get_param('trash');
492
493 if ($trash) {
494 $subscription->trash();
495 }
496
497 $item = (new SubscriptionViewModel($subscription))->includeSensitiveData(true)->exports();
498 $response = $this->prepare_item_for_response($item, $request);
499
500 return rest_ensure_response($response);
501 } catch (Exception $e) {
502 return new WP_REST_Response(__('Failed to cancel subscription', 'give'), 500);
503 }
504 }
505
506 /**
507 * @since 4.8.0
508 */
509 public function getSortColumn(string $sortColumn): string
510 {
511 $sortColumnsMap = [
512 'id' => 'id',
513 'createdAt' => 'created',
514 'renewsAt' => 'expiration',
515 'status' => 'status',
516 'amount' => 'recurring_amount',
517 'feeAmountRecovered' => 'recurring_fee_amount',
518 'donorId' => 'customer_id',
519 'firstName' => 'firstName',
520 'lastName' => 'lastName',
521 ];
522
523 return $sortColumnsMap[$sortColumn] ?? 'id';
524 }
525
526 /**
527 * @since 4.8.0
528 */
529 public function get_collection_params(): array
530 {
531 $params = parent::get_collection_params();
532
533 $params['page']['default'] = 1;
534 $params['per_page']['default'] = 30;
535
536 // Remove default parameters not being used
537 unset($params['context']);
538 unset($params['search']);
539
540 $params += give(GetSubscriptionCollectionParams::class)();
541
542 return $params;
543 }
544
545 /**
546 * @since 4.13.0 added anonymousDonors and includeSensitiveData to embeddable links
547 * @since 4.10.0 added embeddable links for campaign and form
548 * @since 4.8.0
549 *
550 * @param mixed $item WordPress representation of the item.
551 * @param WP_REST_Request $request Request object.
552 *
553 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
554 */
555 public function prepare_item_for_response($item, $request)
556 {
557 try {
558 $subscriptionId = $request->get_param('id') ?? $item['id'] ?? null;
559
560 if ($subscriptionId && $subscription = Subscription::find($subscriptionId)) {
561 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $subscription->id));
562
563 $links = [
564 'self' => ['href' => $self_url]
565 ];
566
567 if (!empty($item['donorId'])) {
568 $donor_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'donors', $item['donorId']));
569 $donor_url = add_query_arg([
570 'mode' => $request->get_param('mode'),
571 'anonymousDonors' => $request->get_param('anonymousDonors'),
572 'includeSensitiveData' => $request->get_param('includeSensitiveData'),
573 ], $donor_url);
574
575 $links[CURIE::relationUrl('donor')] = [
576 'href' => $donor_url,
577 'embeddable' => true,
578 ];
579 }
580
581 if (!empty($item['donationFormId'])) {
582 $form_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'forms', $item['donationFormId']));
583 $form_url = add_query_arg([
584 'mode' => $subscription->mode->getValue(),
585 ], $form_url);
586
587 $links[CURIE::relationUrl('form')] = [
588 'href' => $form_url,
589 'embeddable' => true,
590 ];
591 }
592
593 if (!empty($item['campaignId'])) {
594 $campaign_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'campaigns', $item['campaignId']));
595 $campaign_url = add_query_arg([
596 'mode' => $subscription->mode->getValue(),
597 ], $campaign_url);
598
599 $links[CURIE::relationUrl('campaign')] = [
600 'href' => $campaign_url,
601 'embeddable' => true,
602 ];
603 }
604
605 $donations_url = rest_url(sprintf('%s/%s', $this->namespace, 'donations'));
606 $donations_url = add_query_arg([
607 'mode' => $subscription->mode->getValue(),
608 'subscriptionId' => $subscription->id,
609 'anonymousDonations' => $request->get_param('anonymousDonors'),
610 'includeSensitiveData' => $request->get_param('includeSensitiveData'),
611 ], $donations_url);
612
613 $links[CURIE::relationUrl('donations')] = [
614 'href' => $donations_url,
615 'embeddable' => true,
616 ];
617 } else {
618 $links = [];
619 }
620
621 $response = new WP_REST_Response(Item::formatDatesForResponse($item, ['createdAt', 'renewsAt']));
622 if (!empty($links)) {
623 $response->add_links($links);
624 }
625
626 $response->data = $this->add_additional_fields_to_object($response->data, $request);
627
628 return $response;
629 } catch (Exception $e) {
630 return new WP_Error(
631 'prepare_item_for_response_error',
632 sprintf(
633 __('Error while preparing subscription for response: %s', 'give'),
634 $e->getMessage()
635 ),
636 ['status' => 400]
637 );
638 }
639 }
640
641 /**
642 * @since 4.8.0
643 *
644 * @param WP_REST_Request $request
645 *
646 * @return true|WP_Error
647 */
648 public function get_items_permissions_check($request)
649 {
650 return SubscriptionPermissions::validationForGetMethods($request);
651 }
652
653 /**
654 * @since 4.8.0
655 *
656 * @param WP_REST_Request $request
657 *
658 * @return true|WP_Error
659 */
660 public function get_item_permissions_check($request)
661 {
662 return SubscriptionPermissions::validationForGetMethods($request);
663 }
664
665 /**
666 * @since 4.8.0
667 *
668 * @param WP_REST_Request $request
669 *
670 * @return true|WP_Error
671 */
672 public function update_item_permissions_check($request)
673 {
674 return SubscriptionPermissions::validationForUpdateMethod($request);
675 }
676
677 /**
678 * @since 4.8.0
679 *
680 * @param WP_REST_Request $request
681 *
682 * @return true|WP_Error
683 */
684 public function create_item_permissions_check($request)
685 {
686 return SubscriptionPermissions::validationForUpdateMethod($request);
687 }
688
689 /**
690 * @since 4.8.0
691 *
692 * @param WP_REST_Request $request
693 *
694 * @return true|WP_Error
695 */
696 public function delete_item_permissions_check($request)
697 {
698 return SubscriptionPermissions::validationForDeleteMethods($request);
699 }
700
701 /**
702 * @since 4.8.0
703 *
704 * @param WP_REST_Request $request
705 *
706 * @return true|WP_Error
707 */
708 public function delete_items_permissions_check($request)
709 {
710 return SubscriptionPermissions::validationForDeleteMethods($request);
711 }
712
713 /**
714 * @since 4.8.0
715 */
716 public function cancel_item_permissions_check($request)
717 {
718 return SubscriptionPermissions::validationForDeleteMethods($request);
719 }
720
721 /**
722 * @since 4.8.0
723 */
724 public function get_item_schema(): array
725 {
726 $schema = give(GetSubscriptionItemSchema::class)();
727 return $this->add_additional_fields_schema($schema);
728 }
729 }
730