PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.15.0
GiveWP – Donation Plugin and Fundraising Platform v4.15.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / API / REST / V3 / Routes / Subscriptions / SubscriptionController.php

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

725 lines 24.4 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.8.0
240 *
241 * @param WP_REST_Request $request Full data about the request.
242 *
243 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
244 *
245 * @throws Exception
246 */
247 public function get_item($request)
248 {
249 $subscription = Subscription::find($request->get_param('id'));
250
251 if (!$subscription) {
252 return new WP_Error('subscription_not_found', __('Subscription not found', 'give'), ['status' => 404]);
253 }
254
255 $includeSensitiveData = $request->get_param('includeSensitiveData');
256 $donorAnonymousMode = new DonorAnonymousMode($request->get_param('anonymousDonors'));
257
258 $item = (new SubscriptionViewModel($subscription))
259 ->anonymousMode($donorAnonymousMode)
260 ->includeSensitiveData($includeSensitiveData)
261 ->exports();
262
263 $response = $this->prepare_item_for_response($item, $request);
264
265 return rest_ensure_response($response);
266 }
267
268 /**
269 * Create a subscription.
270 *
271 * @since 4.8.0
272 *
273 * @param WP_REST_Request $request Full data about the request.
274 *
275 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
276 *
277 * @throws Exception
278 */
279 public function create_item($request)
280 {
281 try {
282 $data = SubscriptionCreateData::fromRequest($request);
283 $subscription = $data->createSubscription();
284
285 $fieldsUpdate = $this->update_additional_fields_for_object($subscription, $request);
286
287 if (is_wp_error($fieldsUpdate)) {
288 return $fieldsUpdate;
289 }
290 } catch (SubscriptionValidationException $e) {
291 return new WP_REST_Response([
292 'message' => $e->getMessage(),
293 'error' => $e->getErrorCode()
294 ], $e->getStatusCode());
295 } catch (Exception $e) {
296 return new WP_REST_Response([
297 'message' => sprintf(__('Failed to create subscription: %s', 'give'), $e->getMessage()),
298 'error' => 'internal_server_error'
299 ], 500);
300 }
301
302 $item = (new SubscriptionViewModel($subscription))
303 ->includeSensitiveData(true)
304 ->exports();
305
306 $response = $this->prepare_item_for_response($item, $request);
307 $response->set_status(201);
308
309 return rest_ensure_response($response);
310 }
311
312 /**
313 * Update a subscription.
314 *
315 * @since 4.11.0 Exclude gatewaySubscriptionId from non-editable fields
316 * @since 4.8.0
317 *
318 * @param WP_REST_Request $request Full data about the request.
319 *
320 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
321 *
322 * @throws Exception
323 */
324 public function update_item($request)
325 {
326 $subscription = Subscription::find($request->get_param('id'));
327
328 if (!$subscription) {
329 return new WP_REST_Response(__('Subscription not found', 'give'), 404);
330 }
331
332 $nonEditableFields = [
333 'id',
334 'createdAt',
335 'mode',
336 'gatewayId',
337 ];
338
339 foreach ($request->get_params() as $key => $value) {
340 if (!in_array($key, $nonEditableFields, true)) {
341 if (in_array($key, $subscription::propertyKeys(), true)) {
342 try {
343 $processedValue = SubscriptionFields::processValue($key, $value);
344 if ($subscription->isPropertyTypeValid($key, $processedValue)) {
345 $subscription->$key = $processedValue;
346 }
347 } catch (Exception $e) {
348 continue;
349 }
350 }
351 }
352 }
353
354 if ($subscription->isDirty()) {
355 $subscription->save();
356 }
357
358 $fieldsUpdate = $this->update_additional_fields_for_object($subscription, $request);
359
360 if (is_wp_error($fieldsUpdate)) {
361 return $fieldsUpdate;
362 }
363
364 $item = (new SubscriptionViewModel($subscription))->includeSensitiveData(true)->exports();
365
366 $response = $this->prepare_item_for_response($item, $request);
367
368 return rest_ensure_response($response);
369 }
370
371 /**
372 * Delete a subscription.
373 *
374 * @since 4.8.0
375 *
376 * @param WP_REST_Request $request Full data about the request.
377 *
378 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
379 *
380 * @throws Exception
381 */
382 public function delete_item($request): WP_REST_Response
383 {
384 $subscription = Subscription::find($request->get_param('id'));
385 $force = $request->get_param('force');
386
387 if (!$subscription) {
388 return new WP_REST_Response(['message' => __('Subscription not found', 'give')], 404);
389 }
390
391 $item = (new SubscriptionViewModel($subscription))->exports();
392
393 if ($force) { // Permanently delete the subscription
394 $deleted = $subscription->delete();
395
396 if (!$deleted) {
397 return new WP_REST_Response(['message' => __('Failed to delete subscription', 'give')], 500);
398 }
399 } else { // Move the subscription to trash (soft delete)
400 $trashed = $subscription->trash();
401
402 if (!$trashed) {
403 return new WP_REST_Response(['message' => __('Failed to trash subscription', 'give')], 500);
404 }
405 }
406
407 return new WP_REST_Response(['deleted' => true, 'previous' => $item], 200);
408 }
409
410 /**
411 * Delete multiple subscriptions.
412 *
413 * @since 4.8.0
414 *
415 * @param WP_REST_Request $request Full data about the request.
416 *
417 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
418 *
419 * @throws Exception
420 */
421 public function deleteItems($request): WP_REST_Response
422 {
423 $ids = $request->get_param('ids');
424 $force = $request->get_param('force');
425 $deleted = [];
426 $errors = [];
427
428 foreach ($ids as $id) {
429 $subscription = Subscription::find($id);
430
431 if (!$subscription) {
432 $errors[] = ['id' => $id, 'message' => __('Subscription not found', 'give')];
433
434 continue;
435 }
436
437 $item = (new SubscriptionViewModel($subscription))->exports();
438
439 if ($force) {
440 if ($subscription->delete()) {
441 $deleted[] = ['id' => $id, 'previous' => $item];
442 } else {
443 $errors[] = ['id' => $id, 'message' => __('Failed to delete subscription', 'give')];
444 }
445 } else {
446 $trashed = $subscription->trash();
447
448 if ($trashed) {
449 $deleted[] = ['id' => $id, 'previous' => $item];
450 } else {
451 $errors[] = ['id' => $id, 'message' => __('Failed to trash subscription', 'give')];
452 }
453 }
454 }
455
456 return new WP_REST_Response([
457 'deleted' => $deleted,
458 'errors' => $errors,
459 'total_requested' => count($ids),
460 'total_deleted' => count($deleted),
461 'total_errors' => count($errors),
462 ], 200);
463 }
464
465 /**
466 * Cancel a subscription.
467 *
468 * @since 4.8.0
469 */
470 public function cancel_item($request)
471 {
472 $subscription = Subscription::find($request->get_param('id'));
473
474 if (!$subscription) {
475 return new WP_REST_Response(__('Subscription not found', 'give'), 404);
476 }
477
478 try {
479 if (give()->gateways->hasPaymentGateway($subscription->gatewayId)) {
480 $subscription->cancel(true);
481 } else {
482 $subscription->status = SubscriptionStatus::CANCELLED();
483 $subscription->save();
484 }
485
486 $trash = $request->get_param('trash');
487
488 if ($trash) {
489 $subscription->trash();
490 }
491
492 $item = (new SubscriptionViewModel($subscription))->includeSensitiveData(true)->exports();
493 $response = $this->prepare_item_for_response($item, $request);
494
495 return rest_ensure_response($response);
496 } catch (Exception $e) {
497 return new WP_REST_Response(__('Failed to cancel subscription', 'give'), 500);
498 }
499 }
500
501 /**
502 * @since 4.8.0
503 */
504 public function getSortColumn(string $sortColumn): string
505 {
506 $sortColumnsMap = [
507 'id' => 'id',
508 'createdAt' => 'created',
509 'renewsAt' => 'expiration',
510 'status' => 'status',
511 'amount' => 'recurring_amount',
512 'feeAmountRecovered' => 'recurring_fee_amount',
513 'donorId' => 'customer_id',
514 'firstName' => 'firstName',
515 'lastName' => 'lastName',
516 ];
517
518 return $sortColumnsMap[$sortColumn] ?? 'id';
519 }
520
521 /**
522 * @since 4.8.0
523 */
524 public function get_collection_params(): array
525 {
526 $params = parent::get_collection_params();
527
528 $params['page']['default'] = 1;
529 $params['per_page']['default'] = 30;
530
531 // Remove default parameters not being used
532 unset($params['context']);
533 unset($params['search']);
534
535 $params += give(GetSubscriptionCollectionParams::class)();
536
537 return $params;
538 }
539
540 /**
541 * @since 4.13.0 added anonymousDonors and includeSensitiveData to embeddable links
542 * @since 4.10.0 added embeddable links for campaign and form
543 * @since 4.8.0
544 *
545 * @param mixed $item WordPress representation of the item.
546 * @param WP_REST_Request $request Request object.
547 *
548 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
549 */
550 public function prepare_item_for_response($item, $request)
551 {
552 try {
553 $subscriptionId = $request->get_param('id') ?? $item['id'] ?? null;
554
555 if ($subscriptionId && $subscription = Subscription::find($subscriptionId)) {
556 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $subscription->id));
557
558 $links = [
559 'self' => ['href' => $self_url]
560 ];
561
562 if (!empty($item['donorId'])) {
563 $donor_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'donors', $item['donorId']));
564 $donor_url = add_query_arg([
565 'mode' => $request->get_param('mode'),
566 'anonymousDonors' => $request->get_param('anonymousDonors'),
567 'includeSensitiveData' => $request->get_param('includeSensitiveData'),
568 ], $donor_url);
569
570 $links[CURIE::relationUrl('donor')] = [
571 'href' => $donor_url,
572 'embeddable' => true,
573 ];
574 }
575
576 if (!empty($item['donationFormId'])) {
577 $form_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'forms', $item['donationFormId']));
578 $form_url = add_query_arg([
579 'mode' => $subscription->mode->getValue(),
580 ], $form_url);
581
582 $links[CURIE::relationUrl('form')] = [
583 'href' => $form_url,
584 'embeddable' => true,
585 ];
586 }
587
588 if (!empty($item['campaignId'])) {
589 $campaign_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'campaigns', $item['campaignId']));
590 $campaign_url = add_query_arg([
591 'mode' => $subscription->mode->getValue(),
592 ], $campaign_url);
593
594 $links[CURIE::relationUrl('campaign')] = [
595 'href' => $campaign_url,
596 'embeddable' => true,
597 ];
598 }
599
600 $donations_url = rest_url(sprintf('%s/%s', $this->namespace, 'donations'));
601 $donations_url = add_query_arg([
602 'mode' => $subscription->mode->getValue(),
603 'subscriptionId' => $subscription->id,
604 'anonymousDonations' => $request->get_param('anonymousDonors'),
605 'includeSensitiveData' => $request->get_param('includeSensitiveData'),
606 ], $donations_url);
607
608 $links[CURIE::relationUrl('donations')] = [
609 'href' => $donations_url,
610 'embeddable' => true,
611 ];
612 } else {
613 $links = [];
614 }
615
616 $response = new WP_REST_Response(Item::formatDatesForResponse($item, ['createdAt', 'renewsAt']));
617 if (!empty($links)) {
618 $response->add_links($links);
619 }
620
621 $response->data = $this->add_additional_fields_to_object($response->data, $request);
622
623 return $response;
624 } catch (Exception $e) {
625 return new WP_Error(
626 'prepare_item_for_response_error',
627 sprintf(
628 __('Error while preparing subscription for response: %s', 'give'),
629 $e->getMessage()
630 ),
631 ['status' => 400]
632 );
633 }
634 }
635
636 /**
637 * @since 4.8.0
638 *
639 * @param WP_REST_Request $request
640 *
641 * @return true|WP_Error
642 */
643 public function get_items_permissions_check($request)
644 {
645 return SubscriptionPermissions::validationForGetMethods($request);
646 }
647
648 /**
649 * @since 4.8.0
650 *
651 * @param WP_REST_Request $request
652 *
653 * @return true|WP_Error
654 */
655 public function get_item_permissions_check($request)
656 {
657 return SubscriptionPermissions::validationForGetMethods($request);
658 }
659
660 /**
661 * @since 4.8.0
662 *
663 * @param WP_REST_Request $request
664 *
665 * @return true|WP_Error
666 */
667 public function update_item_permissions_check($request)
668 {
669 return SubscriptionPermissions::validationForUpdateMethod($request);
670 }
671
672 /**
673 * @since 4.8.0
674 *
675 * @param WP_REST_Request $request
676 *
677 * @return true|WP_Error
678 */
679 public function create_item_permissions_check($request)
680 {
681 return SubscriptionPermissions::validationForUpdateMethod($request);
682 }
683
684 /**
685 * @since 4.8.0
686 *
687 * @param WP_REST_Request $request
688 *
689 * @return true|WP_Error
690 */
691 public function delete_item_permissions_check($request)
692 {
693 return SubscriptionPermissions::validationForDeleteMethods($request);
694 }
695
696 /**
697 * @since 4.8.0
698 *
699 * @param WP_REST_Request $request
700 *
701 * @return true|WP_Error
702 */
703 public function delete_items_permissions_check($request)
704 {
705 return SubscriptionPermissions::validationForDeleteMethods($request);
706 }
707
708 /**
709 * @since 4.8.0
710 */
711 public function cancel_item_permissions_check($request)
712 {
713 return SubscriptionPermissions::validationForDeleteMethods($request);
714 }
715
716 /**
717 * @since 4.8.0
718 */
719 public function get_item_schema(): array
720 {
721 $schema = give(GetSubscriptionItemSchema::class)();
722 return $this->add_additional_fields_schema($schema);
723 }
724 }
725