PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 0.0.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v0.0.1
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / api / donations-api.php

donations-api.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 0.0.1, at inc/api/donations-api.php

1,115 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donations REST API endpoints.
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\API;
9
10 use SureDonation\Inc\Database\Tables\Donations;
11 use SureDonation\Inc\Database\Tables\Donors;
12 use SureDonation\Inc\Helper;
13 use SureDonation\Inc\Payments\Payment_Helper;
14 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
15 use WP_Error;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_REST_Server;
19
20 // Exit if accessed directly.
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 /**
26 * Donations API class.
27 *
28 * @since 0.0.1
29 */
30 class Donations_API {
31 /**
32 * Get donation endpoints.
33 *
34 * @return array<string, mixed>
35 * @since 0.0.1
36 */
37 public function get_endpoints() {
38 return [
39 // Get donations list & create donation.
40 '/donations' => [
41 [
42 'methods' => WP_REST_Server::READABLE,
43 'callback' => [ $this, 'get_donations' ],
44 'permission_callback' => [ $this, 'check_permissions' ],
45 ],
46 [
47 'methods' => WP_REST_Server::CREATABLE,
48 'callback' => [ $this, 'create_donation' ],
49 'permission_callback' => [ $this, 'check_permissions' ],
50 'args' => $this->get_donation_args(),
51 ],
52 ],
53
54 // Get, update, delete single donation.
55 '/donations/(?P<id>\d+)' => [
56 [
57 'methods' => WP_REST_Server::READABLE,
58 'callback' => [ $this, 'get_donation' ],
59 'permission_callback' => [ $this, 'check_permissions' ],
60 'args' => [
61 'id' => [
62 'required' => true,
63 'validate_callback' => static function ( $param ) {
64 return is_numeric( $param );
65 },
66 ],
67 ],
68 ],
69 [
70 'methods' => WP_REST_Server::EDITABLE,
71 'callback' => [ $this, 'update_donation' ],
72 'permission_callback' => [ $this, 'check_permissions' ],
73 'args' => array_merge(
74 [
75 'id' => [
76 'required' => true,
77 'validate_callback' => static function ( $param ) {
78 return is_numeric( $param );
79 },
80 ],
81 ],
82 $this->get_donation_args( false )
83 ),
84 ],
85 [
86 'methods' => WP_REST_Server::DELETABLE,
87 'callback' => [ $this, 'delete_donation' ],
88 'permission_callback' => [ $this, 'check_permissions' ],
89 'args' => [
90 'id' => [
91 'required' => true,
92 'validate_callback' => static function ( $param ) {
93 return is_numeric( $param );
94 },
95 ],
96 ],
97 ],
98 ],
99
100 // Update donation status.
101 '/donations/(?P<id>\d+)/status' => [
102 'methods' => WP_REST_Server::EDITABLE,
103 'callback' => [ $this, 'update_donation_status' ],
104 'permission_callback' => [ $this, 'check_permissions' ],
105 'args' => [
106 'id' => [
107 'required' => true,
108 'validate_callback' => static function ( $param ) {
109 return is_numeric( $param );
110 },
111 ],
112 'status' => [
113 'required' => true,
114 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
115 ],
116 ],
117 ],
118
119 // Get donations by campaign.
120 '/donations/campaign/(?P<id>\d+)' => [
121 'methods' => WP_REST_Server::READABLE,
122 'callback' => [ $this, 'get_campaign_donations' ],
123 'permission_callback' => [ $this, 'check_permissions' ],
124 'args' => [
125 'id' => [
126 'required' => true,
127 'validate_callback' => static function ( $param ) {
128 return is_numeric( $param );
129 },
130 ],
131 ],
132 ],
133
134 // Bulk actions.
135 '/donations/bulk' => [
136 'methods' => WP_REST_Server::EDITABLE,
137 'callback' => [ $this, 'bulk_action' ],
138 'permission_callback' => [ $this, 'check_permissions' ],
139 'args' => [
140 'action' => [
141 'required' => true,
142 'enum' => [ 'delete', 'update_status' ],
143 ],
144 'ids' => [
145 'required' => true,
146 'validate_callback' => static function ( $param ) {
147 return is_array( $param ) && ! empty( $param );
148 },
149 ],
150 'status' => [
151 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
152 ],
153 ],
154 ],
155
156 // Refund donation payment.
157 '/donations/(?P<id>\d+)/refund' => [
158 'methods' => WP_REST_Server::CREATABLE,
159 'callback' => [ $this, 'refund_donation' ],
160 'permission_callback' => [ $this, 'check_permissions' ],
161 'args' => [
162 'id' => [
163 'required' => true,
164 'validate_callback' => static function ( $param ) {
165 return is_numeric( $param );
166 },
167 ],
168 'transaction_id' => [
169 'required' => true,
170 'sanitize_callback' => 'sanitize_text_field',
171 ],
172 'refund_amount' => [
173 'required' => true,
174 'sanitize_callback' => 'absint',
175 ],
176 'refund_type' => [
177 'required' => true,
178 'enum' => [ 'full', 'partial' ],
179 ],
180 'refund_notes' => [
181 'sanitize_callback' => 'sanitize_textarea_field',
182 ],
183 ],
184 ],
185
186 // Delete donation log entry.
187 '/donations/(?P<id>\d+)/log/(?P<log_index>\d+)' => [
188 'methods' => WP_REST_Server::DELETABLE,
189 'callback' => [ $this, 'delete_donation_log' ],
190 'permission_callback' => [ $this, 'check_permissions' ],
191 'args' => [
192 'id' => [
193 'required' => true,
194 'validate_callback' => static function ( $param ) {
195 return is_numeric( $param );
196 },
197 ],
198 'log_index' => [
199 'required' => true,
200 'validate_callback' => static function ( $param ) {
201 return is_numeric( $param ) && $param >= 0;
202 },
203 ],
204 ],
205 ],
206
207 // Get and add donation notes.
208 '/donations/(?P<id>\d+)/notes' => [
209 [
210 'methods' => WP_REST_Server::READABLE,
211 'callback' => [ $this, 'get_donation_notes' ],
212 'permission_callback' => [ $this, 'check_permissions' ],
213 'args' => [
214 'id' => [
215 'required' => true,
216 'validate_callback' => static function ( $param ) {
217 return is_numeric( $param );
218 },
219 ],
220 'page' => [
221 'default' => 1,
222 'sanitize_callback' => 'absint',
223 ],
224 'per_page' => [
225 'default' => 3,
226 'sanitize_callback' => 'absint',
227 ],
228 ],
229 ],
230 [
231 'methods' => WP_REST_Server::CREATABLE,
232 'callback' => [ $this, 'add_donation_note' ],
233 'permission_callback' => [ $this, 'check_permissions' ],
234 'args' => [
235 'id' => [
236 'required' => true,
237 'validate_callback' => static function ( $param ) {
238 return is_numeric( $param );
239 },
240 ],
241 'note' => [
242 'required' => true,
243 'sanitize_callback' => 'sanitize_textarea_field',
244 ],
245 ],
246 ],
247 ],
248
249 // Delete donation note.
250 '/donations/(?P<id>\d+)/notes/(?P<note_id>[\w.]+)' => [
251 'methods' => WP_REST_Server::DELETABLE,
252 'callback' => [ $this, 'delete_donation_note' ],
253 'permission_callback' => [ $this, 'check_permissions' ],
254 'args' => [
255 'id' => [
256 'required' => true,
257 'validate_callback' => static function ( $param ) {
258 return is_numeric( $param );
259 },
260 ],
261 'note_id' => [
262 'required' => true,
263 'sanitize_callback' => 'sanitize_text_field',
264 ],
265 ],
266 ],
267 ];
268 }
269
270 /**
271 * Get a single donation by ID.
272 *
273 * @param WP_REST_Request $request Request object.
274 * @return WP_REST_Response|WP_Error Response object.
275 * @since 0.0.1
276 */
277 public function get_donation( $request ) {
278 $donation_id = absint( $request->get_param( 'id' ) );
279
280 // Get the donation from database.
281 $donation = Donations::get( $donation_id );
282
283 if ( ! $donation ) {
284 return new WP_Error(
285 'donation_not_found',
286 __( 'Donation not found.', 'suredonation' ),
287 [ 'status' => 404 ]
288 );
289 }
290
291 // Format and return the donation data.
292 $formatted = $this->format_donation( $donation );
293
294 return new WP_REST_Response(
295 [
296 'success' => true,
297 'donation' => $formatted,
298 ],
299 200
300 );
301 }
302
303 /**
304 * Get donations list with filters, sorting, and pagination.
305 *
306 * @param WP_REST_Request $request Request object.
307 * @return WP_REST_Response|WP_Error Response object.
308 * @since 0.0.1
309 */
310 public function get_donations( $request ) {
311 $page = $request->get_param( 'page' ) ?? 1;
312 $per_page = $request->get_param( 'per_page' ) ?? 20;
313 $search = $request->get_param( 'search' ) ?? '';
314 $status = $request->get_param( 'status' ) ?? 'all';
315 $campaign = $request->get_param( 'campaign' ) ?? '';
316 $sort_by = $request->get_param( 'sort_by' ) ?? 'created_at';
317 $order = $request->get_param( 'order' ) ?? 'desc';
318
319 // Calculate pagination.
320 $limit = absint( $per_page );
321 $offset = ( absint( $page ) - 1 ) * $limit;
322
323 // Get donations from database using admin list method with filters.
324 $results = Donations::get_admin_list(
325 $status,
326 ! empty( $campaign ) ? absint( $campaign ) : 0,
327 sanitize_text_field( $search ),
328 $limit,
329 $offset,
330 $sort_by, // using whitelist validation in the method.
331 strtoupper( $order ) // using whitelist validation in the method.
332 );
333
334 // Get total count.
335 $total = Donations::get_total_donations_by_status( $status, ! empty( $campaign ) ? absint( $campaign ) : 0 );
336
337 // Format donations data.
338 $donations = [];
339 foreach ( $results as $donation ) {
340 if ( is_array( $donation ) ) {
341 $donations[] = $this->format_donation( $donation );
342 }
343 }
344
345 // Prepare response.
346 return new WP_REST_Response(
347 [
348 'donations' => $donations,
349 'pagination' => [
350 'total' => (int) $total,
351 'total_pages' => (int) ceil( $total / $per_page ),
352 'per_page' => (int) $per_page,
353 'current' => (int) $page,
354 ],
355 ]
356 );
357 }
358
359 /**
360 * Get donations for a specific campaign.
361 *
362 * @param WP_REST_Request $request Request object.
363 * @return WP_REST_Response|WP_Error Response object.
364 * @since 0.0.1
365 */
366 public function get_campaign_donations( $request ) {
367 $campaign_id = absint( $request->get_param( 'id' ) );
368 $limit = absint( $request->get_param( 'limit' ) ?? 5 );
369
370 $results = Donations::get_recent_donations( $campaign_id, $limit );
371
372 $donations = [];
373 foreach ( $results as $donation ) {
374 if ( is_array( $donation ) ) {
375 $donations[] = $this->format_donation( $donation );
376 }
377 }
378
379 return new WP_REST_Response(
380 [
381 'success' => true,
382 'donations' => $donations,
383 ],
384 200
385 );
386 }
387
388 /**
389 * Create a new donation.
390 *
391 * @param WP_REST_Request $request Request object.
392 * @return WP_REST_Response|WP_Error Response object.
393 * @since 0.0.1
394 */
395 public function create_donation( $request ) {
396 $campaign_id = $request->get_param( 'campaign_id' );
397 $donor_name = $request->get_param( 'donor_name' ) ?? '';
398 $donor_email = $request->get_param( 'donor_email' ) ?? '';
399 $donor_phone = $request->get_param( 'donor_phone' ) ?? '';
400 $amount = $request->get_param( 'amount' );
401 $fees_covered = $request->get_param( 'fees_covered' ) ?? 0;
402 $payment_status = $request->get_param( 'payment_status' ) ?? 'pending';
403 $donation_type = $request->get_param( 'donation_type' ) ?? 'one-time';
404 $is_anonymous = $request->get_param( 'is_anonymous' ) ?? false;
405 $donor_comment = $request->get_param( 'donor_comment' ) ?? '';
406 $gateway = $request->get_param( 'gateway' ) ?? 'manual';
407 $transaction_id = $request->get_param( 'transaction_id' ) ?? '';
408
409 // Get or create donor.
410 $donor_id = 0;
411 if ( ! empty( $donor_email ) ) {
412 $donor_id = Donors::get_or_create( $donor_email, $donor_name, $donor_phone );
413 }
414
415 // Create the donation in database.
416 $donation_id = Donations::add(
417 [
418 'campaign_id' => $campaign_id,
419 'donor_id' => $donor_id ? $donor_id : 0,
420 'amount' => $amount,
421 'fees_covered' => $fees_covered,
422 'currency' => Payment_Helper::get_currency(),
423 'gateway' => $gateway,
424 'payment_status' => $payment_status,
425 'payment_mode' => Payment_Helper::get_payment_mode(),
426 'donor_name' => $donor_name,
427 'donor_email' => $donor_email,
428 'donor_phone' => $donor_phone,
429 'is_anonymous' => $is_anonymous ? 1 : 0,
430 'donation_type' => $donation_type,
431 'donor_comment' => $donor_comment,
432 'transaction_id' => $transaction_id,
433 ]
434 );
435
436 if ( ! $donation_id ) {
437 return new WP_Error(
438 'create_failed',
439 __( 'Failed to create donation.', 'suredonation' ),
440 [ 'status' => 500 ]
441 );
442 }
443
444 $donation = Donations::get( $donation_id );
445
446 return new WP_REST_Response(
447 [
448 'success' => true,
449 'message' => __( 'Donation created successfully.', 'suredonation' ),
450 'donation' => is_array( $donation ) ? $this->format_donation( $donation ) : [],
451 ],
452 201
453 );
454 }
455
456 /**
457 * Update an existing donation.
458 *
459 * @param WP_REST_Request $request Request object.
460 * @return WP_REST_Response|WP_Error Response object.
461 * @since 0.0.1
462 */
463 public function update_donation( $request ) {
464 $donation_id = absint( $request->get_param( 'id' ) );
465
466 // Check if donation exists.
467 $donation = Donations::get( $donation_id );
468 if ( ! $donation ) {
469 return new WP_Error(
470 'donation_not_found',
471 __( 'Donation not found.', 'suredonation' ),
472 [ 'status' => 404 ]
473 );
474 }
475
476 // Build update data.
477 $update_data = [];
478 $fields = [
479 'campaign_id',
480 'donor_name',
481 'donor_email',
482 'donor_phone',
483 'amount',
484 'fees_covered',
485 'donation_type',
486 'is_anonymous',
487 'donor_comment',
488 'payment_status',
489 'gateway',
490 'transaction_id',
491 ];
492
493 foreach ( $fields as $field ) {
494 $value = $request->get_param( $field );
495 if ( ! is_null( $value ) ) {
496 if ( 'is_anonymous' === $field ) {
497 $update_data[ $field ] = $value ? 1 : 0;
498 } else {
499 $update_data[ $field ] = $value;
500 }
501 }
502 }
503
504 if ( ! empty( $update_data ) ) {
505 Donations::update( $donation_id, $update_data );
506 }
507
508 $updated_donation = Donations::get( $donation_id );
509
510 return new WP_REST_Response(
511 [
512 'success' => true,
513 'message' => __( 'Donation updated successfully.', 'suredonation' ),
514 'donation' => is_array( $updated_donation ) ? $this->format_donation( $updated_donation ) : [],
515 ],
516 200
517 );
518 }
519
520 /**
521 * Update donation payment status.
522 *
523 * @param WP_REST_Request $request Request object.
524 * @return WP_REST_Response|WP_Error Response object.
525 * @since 0.0.1
526 */
527 public function update_donation_status( $request ) {
528 $donation_id = absint( $request->get_param( 'id' ) );
529 $status = $request->get_param( 'status' );
530
531 $donation = Donations::get( $donation_id );
532 if ( ! $donation ) {
533 return new WP_Error(
534 'donation_not_found',
535 __( 'Donation not found.', 'suredonation' ),
536 [ 'status' => 404 ]
537 );
538 }
539
540 $old_status = $donation['payment_status'] ?? 'pending';
541 Donations::update_status( $donation_id, $status );
542
543 // If status changed to completed, update donor stats.
544 if ( 'completed' !== $old_status && 'completed' === $status ) {
545 if ( ! empty( $donation['donor_id'] ) ) {
546 Donors::record_donation( $donation['donor_id'], floatval( $donation['amount'] ) );
547 }
548 }
549
550 return new WP_REST_Response(
551 [
552 'success' => true,
553 'message' => __( 'Donation status updated successfully.', 'suredonation' ),
554 ],
555 200
556 );
557 }
558
559 /**
560 * Delete donation.
561 *
562 * @param WP_REST_Request $request Request object.
563 * @return WP_REST_Response|WP_Error Response object.
564 * @since 0.0.1
565 */
566 public function delete_donation( $request ) {
567 $donation_id = absint( $request->get_param( 'id' ) );
568
569 $result = Donations::delete( $donation_id );
570
571 if ( ! $result ) {
572 return new WP_Error(
573 'delete_failed',
574 __( 'Failed to delete donation.', 'suredonation' ),
575 [ 'status' => 500 ]
576 );
577 }
578
579 return new WP_REST_Response(
580 [
581 'success' => true,
582 'message' => __( 'Donation deleted successfully.', 'suredonation' ),
583 ],
584 200
585 );
586 }
587
588 /**
589 * Bulk action on donations.
590 *
591 * @param WP_REST_Request $request Request object.
592 * @return WP_REST_Response|WP_Error Response object.
593 * @since 0.0.1
594 */
595 public function bulk_action( $request ) {
596 $action = $request->get_param( 'action' );
597 $ids = $request->get_param( 'ids' );
598
599 $success_count = 0;
600 $error_count = 0;
601
602 foreach ( $ids as $id ) {
603 $result = false;
604
605 if ( 'delete' === $action ) {
606 $result = Donations::delete( absint( $id ) );
607 } elseif ( 'update_status' === $action ) {
608 $status = $request->get_param( 'status' );
609 if ( $status ) {
610 $result = Donations::update_status( absint( $id ), $status );
611 }
612 }
613
614 if ( $result ) {
615 ++$success_count;
616 } else {
617 ++$error_count;
618 }
619 }
620
621 return new WP_REST_Response(
622 [
623 'success' => true,
624 'message' => sprintf(
625 // translators: %1$d: success count, %2$d: error count.
626 __( 'Bulk action completed. Success: %1$d, Failed: %2$d', 'suredonation' ),
627 $success_count,
628 $error_count
629 ),
630 'success_count' => $success_count,
631 'error_count' => $error_count,
632 ],
633 200
634 );
635 }
636
637 /**
638 * Refund a donation payment.
639 *
640 * @param WP_REST_Request $request Request object.
641 * @return WP_REST_Response|WP_Error Response object.
642 * @since 0.0.1
643 */
644 public function refund_donation( $request ) {
645 $donation_id = absint( $request->get_param( 'id' ) );
646 $transaction_id = $request->get_param( 'transaction_id' );
647 $refund_amount = absint( $request->get_param( 'refund_amount' ) );
648
649 // Get the donation.
650 $donation = Donations::get( $donation_id );
651 if ( ! $donation ) {
652 return new WP_Error(
653 'donation_not_found',
654 __( 'Donation not found.', 'suredonation' ),
655 [ 'status' => 404 ]
656 );
657 }
658
659 // Verify the donation is in a refundable state.
660 $refundable_statuses = [ 'completed', 'partially_refunded' ];
661 if ( ! in_array( $donation['payment_status'], $refundable_statuses, true ) ) {
662 return new WP_Error(
663 'not_refundable',
664 __( 'Only completed or partially refunded donations can be refunded.', 'suredonation' ),
665 [ 'status' => 400 ]
666 );
667 }
668
669 // Verify transaction ID matches.
670 if ( $transaction_id !== $donation['transaction_id'] ) {
671 return new WP_Error(
672 'transaction_mismatch',
673 __( 'Transaction ID mismatch.', 'suredonation' ),
674 [ 'status' => 400 ]
675 );
676 }
677
678 // Check if Stripe is connected.
679 if ( ! Stripe_Helper::is_stripe_connected() ) {
680 return new WP_Error(
681 'stripe_not_connected',
682 __( 'Stripe is not connected. Please configure Stripe in settings.', 'suredonation' ),
683 [ 'status' => 400 ]
684 );
685 }
686
687 // Validate refund amount.
688 $currency = $donation['currency'] ?? 'USD';
689 $total_amount = $this->amount_to_stripe_format( floatval( $donation['amount'] ), $currency );
690 $refunded_amount = $this->amount_to_stripe_format( floatval( $donation['refunded_amount'] ?? 0 ), $currency );
691 $refundable = $total_amount - $refunded_amount;
692
693 if ( $refund_amount > $refundable ) {
694 return new WP_Error(
695 'exceeds_refundable',
696 sprintf(
697 /* translators: %s: maximum refundable amount */
698 __( 'Refund amount exceeds maximum refundable amount of %s.', 'suredonation' ),
699 $this->amount_from_stripe_format( $refundable, $currency )
700 ),
701 [ 'status' => 400 ]
702 );
703 }
704
705 // Process refund through Stripe.
706 $refund_result = Stripe_Helper::create_refund( $transaction_id, $refund_amount, 'requested_by_customer' );
707
708 if ( is_wp_error( $refund_result ) ) {
709 return new WP_Error(
710 'refund_failed',
711 $refund_result->get_error_message(),
712 [ 'status' => 500 ]
713 );
714 }
715
716 // Calculate new refunded amount in cents for comparison.
717 $new_refunded_in_cents = $refunded_amount + $refund_amount;
718
719 // Determine new status by comparing in cents to avoid floating point precision issues.
720 $new_status = $new_refunded_in_cents >= $total_amount ? 'refunded' : 'partially_refunded';
721
722 // Convert back to major currency unit for storage.
723 $new_refunded_amount = $this->amount_from_stripe_format( $new_refunded_in_cents, $currency );
724
725 // Store refund in donation_data FIRST (prevents webhook duplicate processing).
726 $refund_id = $refund_result['id'] ?? '';
727 if ( ! empty( $refund_id ) ) {
728 $refund_data = [
729 'refund_id' => $refund_id,
730 'amount' => absint( $refund_amount ),
731 'currency' => strtoupper( $currency ),
732 'status' => $refund_result['status'] ?? 'succeeded',
733 'created' => time(),
734 'reason' => 'requested_by_customer',
735 'refunded_by' => 'admin',
736 'refunded_at' => gmdate( 'Y-m-d H:i:s' ),
737 ];
738 Donations::add_refund_to_donation_data( $donation_id, $refund_data );
739 }
740
741 // Update donation record with new status and refunded amount.
742 Donations::update(
743 $donation_id,
744 [
745 'payment_status' => $new_status,
746 'refunded_amount' => $new_refunded_amount,
747 ]
748 );
749
750 // Determine refund type for log message.
751 $refund_type = $new_refunded_in_cents >= $total_amount
752 ? __( 'Full', 'suredonation' )
753 : __( 'Partial', 'suredonation' );
754
755 // Add log entry.
756 Donations::add_log(
757 $donation_id,
758 'refund',
759 sprintf(
760 /* translators: %s: Refund type (Full/Partial) */
761 __( '%s refund processed via admin', 'suredonation' ),
762 $refund_type
763 ),
764 [
765 'refund_id' => $refund_id,
766 'refund_amount' => $this->amount_from_stripe_format( $refund_amount, $currency ),
767 'total_refunded' => $new_refunded_amount,
768 'original_amount' => floatval( $donation['amount'] ),
769 'payment_status' => $new_status,
770 'currency' => strtoupper( $currency ),
771 ]
772 );
773
774 // Get updated donation.
775 $updated_donation = Donations::get( $donation_id );
776
777 return new WP_REST_Response(
778 [
779 'success' => true,
780 'message' => __( 'Refund processed successfully.', 'suredonation' ),
781 'refund_id' => $refund_id,
782 'status' => $refund_result['status'] ?? 'succeeded',
783 'donation' => is_array( $updated_donation ) ? $this->format_donation( $updated_donation ) : [],
784 ],
785 200
786 );
787 }
788
789 /**
790 * Check if user has permission to manage donations.
791 *
792 * @return bool True if user has permission.
793 * @since 0.0.1
794 */
795 public function check_permissions() {
796 return current_user_can( 'manage_options' );
797 }
798
799 /**
800 * Delete a log entry from a donation.
801 *
802 * @param WP_REST_Request $request Request object.
803 * @return WP_REST_Response|WP_Error Response object.
804 * @since 0.0.1
805 */
806 public function delete_donation_log( $request ) {
807 $donation_id = absint( $request->get_param( 'id' ) );
808 $log_index = absint( $request->get_param( 'log_index' ) );
809
810 // Get the donation from database.
811 $donation = Donations::get( $donation_id );
812
813 if ( ! $donation ) {
814 return new WP_Error(
815 'donation_not_found',
816 __( 'Donation not found.', 'suredonation' ),
817 [ 'status' => 404 ]
818 );
819 }
820
821 // Get current logs.
822 $logs = Donations::get_log( $donation_id );
823
824 if ( ! is_array( $logs ) || empty( $logs ) ) {
825 return new WP_Error(
826 'no_logs',
827 __( 'No logs found for this donation.', 'suredonation' ),
828 [ 'status' => 404 ]
829 );
830 }
831
832 // Check if log index exists.
833 if ( ! isset( $logs[ $log_index ] ) ) {
834 return new WP_Error(
835 'log_not_found',
836 __( 'Log entry not found.', 'suredonation' ),
837 [ 'status' => 404 ]
838 );
839 }
840
841 // Remove log at specified index.
842 array_splice( $logs, $log_index, 1 );
843
844 // Re-index array to prevent gaps.
845 $logs = array_values( $logs );
846
847 // Update log column with modified logs array.
848 $result = Donations::update( $donation_id, [ 'log' => $logs ] );
849
850 if ( false === $result ) {
851 return new WP_Error(
852 'update_failed',
853 __( 'Failed to delete log entry.', 'suredonation' ),
854 [ 'status' => 500 ]
855 );
856 }
857
858 return new WP_REST_Response(
859 [
860 'success' => true,
861 'message' => __( 'Log entry deleted successfully.', 'suredonation' ),
862 'logs' => $logs,
863 ],
864 200
865 );
866 }
867
868 /**
869 * Get notes for a donation.
870 *
871 * @param WP_REST_Request $request Request object.
872 * @return WP_REST_Response|WP_Error Response object.
873 * @since 0.0.1
874 */
875 public function get_donation_notes( $request ) {
876 $donation_id = absint( $request->get_param( 'id' ) );
877 $page = absint( $request->get_param( 'page' ) ) ?? 1;
878 $per_page = absint( $request->get_param( 'per_page' ) ) ?? 3;
879
880 // Get the donation from database.
881 $donation = Donations::get( $donation_id );
882
883 if ( ! $donation ) {
884 return new WP_Error(
885 'donation_not_found',
886 __( 'Donation not found.', 'suredonation' ),
887 [ 'status' => 404 ]
888 );
889 }
890
891 // Get paginated notes.
892 $notes_data = Donations::get_notes( $donation_id, $page, $per_page );
893
894 return new WP_REST_Response(
895 [
896 'success' => true,
897 'notes' => $notes_data['notes'],
898 'total' => $notes_data['total'],
899 'total_pages' => $notes_data['total_pages'],
900 ],
901 200
902 );
903 }
904
905 /**
906 * Add a note to a donation.
907 *
908 * @param WP_REST_Request $request Request object.
909 * @return WP_REST_Response|WP_Error Response object.
910 * @since 0.0.1
911 */
912 public function add_donation_note( $request ) {
913 $donation_id = absint( $request->get_param( 'id' ) );
914 $note = $request->get_param( 'note' );
915
916 // Get the donation from database.
917 $donation = Donations::get( $donation_id );
918
919 if ( ! $donation ) {
920 return new WP_Error(
921 'donation_not_found',
922 __( 'Donation not found.', 'suredonation' ),
923 [ 'status' => 404 ]
924 );
925 }
926
927 // Add the note.
928 $result = Donations::add_note( $donation_id, $note, get_current_user_id() );
929
930 if ( ! $result['success'] ) {
931 return new WP_Error(
932 'note_failed',
933 __( 'Failed to add note.', 'suredonation' ),
934 [ 'status' => 500 ]
935 );
936 }
937
938 return new WP_REST_Response(
939 [
940 'success' => true,
941 'message' => __( 'Note added successfully.', 'suredonation' ),
942 'note_id' => $result['note_id'],
943 ],
944 201
945 );
946 }
947
948 /**
949 * Delete a note from a donation.
950 *
951 * @param WP_REST_Request $request Request object.
952 * @return WP_REST_Response|WP_Error Response object.
953 * @since 0.0.1
954 */
955 public function delete_donation_note( $request ) {
956 $donation_id = absint( $request->get_param( 'id' ) );
957 $note_id = $request->get_param( 'note_id' );
958
959 // Get the donation from database.
960 $donation = Donations::get( $donation_id );
961
962 if ( ! $donation ) {
963 return new WP_Error(
964 'donation_not_found',
965 __( 'Donation not found.', 'suredonation' ),
966 [ 'status' => 404 ]
967 );
968 }
969
970 // Delete the note.
971 $result = Donations::delete_note( $donation_id, $note_id );
972
973 if ( ! $result ) {
974 return new WP_Error(
975 'note_not_found',
976 __( 'Note not found or could not be deleted.', 'suredonation' ),
977 [ 'status' => 404 ]
978 );
979 }
980
981 return new WP_REST_Response(
982 [
983 'success' => true,
984 'message' => __( 'Note deleted successfully.', 'suredonation' ),
985 ],
986 200
987 );
988 }
989
990 /**
991 * Get donation arguments schema.
992 *
993 * @param bool $required Whether fields are required.
994 * @return array<string, array<string, mixed>>
995 * @since 0.0.1
996 */
997 private function get_donation_args( $required = true ) {
998 return [
999 'campaign_id' => [
1000 'required' => $required,
1001 'sanitize_callback' => 'absint',
1002 ],
1003 'donor_name' => [
1004 'sanitize_callback' => 'sanitize_text_field',
1005 ],
1006 'donor_email' => [
1007 'sanitize_callback' => 'sanitize_email',
1008 ],
1009 'donor_phone' => [
1010 'sanitize_callback' => 'sanitize_text_field',
1011 ],
1012 'amount' => [
1013 'required' => $required,
1014 'sanitize_callback' => 'floatval',
1015 ],
1016 'fees_covered' => [
1017 'sanitize_callback' => 'floatval',
1018 ],
1019 'donation_type' => [
1020 'default' => 'one-time',
1021 'enum' => [ 'one-time', 'recurring' ],
1022 ],
1023 'is_anonymous' => [
1024 'sanitize_callback' => 'rest_sanitize_boolean',
1025 ],
1026 'donor_comment' => [
1027 'sanitize_callback' => 'wp_kses_post',
1028 ],
1029 'payment_status' => [
1030 'default' => 'pending',
1031 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
1032 ],
1033 'gateway' => [
1034 'sanitize_callback' => 'sanitize_text_field',
1035 ],
1036 'transaction_id' => [
1037 'sanitize_callback' => 'sanitize_text_field',
1038 ],
1039 ];
1040 }
1041
1042 /**
1043 * Convert amount to Stripe's smallest currency unit.
1044 *
1045 * @param float $amount Amount in major currency unit.
1046 * @param string $currency Currency code.
1047 * @return int Amount in smallest currency unit.
1048 * @since 0.0.1
1049 */
1050 private function amount_to_stripe_format( $amount, $currency ) {
1051 $zero_decimal = [ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' ];
1052 return in_array( strtoupper( $currency ), $zero_decimal, true )
1053 ? (int) round( $amount )
1054 : (int) round( $amount * 100 );
1055 }
1056
1057 /**
1058 * Convert amount from Stripe's smallest currency unit.
1059 *
1060 * @param int $amount Amount in smallest currency unit.
1061 * @param string $currency Currency code.
1062 * @return float Amount in major currency unit.
1063 * @since 0.0.1
1064 */
1065 private function amount_from_stripe_format( $amount, $currency ) {
1066 $zero_decimal = [ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' ];
1067 return in_array( strtoupper( $currency ), $zero_decimal, true )
1068 ? (float) $amount
1069 : (float) $amount / 100;
1070 }
1071
1072 /**
1073 * Format donation data for API response.
1074 *
1075 * @param array<string, mixed> $donation Donation data from database.
1076 * @return array<string, mixed> Formatted donation data.
1077 * @since 0.0.1
1078 */
1079 private function format_donation( $donation ) {
1080 $campaign_id = isset( $donation['campaign_id'] ) ? Helper::get_integer_value( $donation['campaign_id'] ) : 0;
1081 $donation_id = isset( $donation['id'] ) ? Helper::get_integer_value( $donation['id'] ) : 0;
1082
1083 // Get payment logs for this donation.
1084 $logs = $donation_id ? Donations::get_log( $donation_id ) : [];
1085
1086 // Get payment mode for Stripe dashboard URL.
1087 $payment_mode = $donation['payment_mode'] ?? 'test';
1088
1089 return [
1090 'id' => $donation_id,
1091 'campaign_id' => $campaign_id,
1092 'campaign_title' => $campaign_id ? wp_kses_post( get_the_title( $campaign_id ) ) : '',
1093 'donor_id' => isset( $donation['donor_id'] ) ? Helper::get_integer_value( $donation['donor_id'] ) : 0,
1094 'donor_name' => $donation['donor_name'] ?? '',
1095 'donor_email' => $donation['donor_email'] ?? '',
1096 'donor_phone' => $donation['donor_phone'] ?? '',
1097 'amount' => Helper::get_float_value( $donation['amount'] ?? 0 ),
1098 'fees_covered' => Helper::get_float_value( $donation['fees_covered'] ?? 0 ),
1099 'refunded_amount' => Helper::get_float_value( $donation['refunded_amount'] ?? 0 ),
1100 'currency' => $donation['currency'] ?? 'USD',
1101 'donation_type' => $donation['donation_type'] ?? 'one-time',
1102 'is_anonymous' => ! empty( $donation['is_anonymous'] ),
1103 'donor_comment' => $donation['donor_comment'] ?? '',
1104 'payment_status' => $donation['payment_status'] ?? 'pending',
1105 'payment_mode' => $payment_mode,
1106 'gateway' => $donation['gateway'] ?? '',
1107 'transaction_id' => $donation['transaction_id'] ?? '',
1108 'stripe_customer_id' => $donation['customer_id'] ?? '',
1109 'created_at' => $donation['created_at'] ?? '',
1110 'updated_at' => $donation['updated_at'] ?? '',
1111 'logs' => $logs,
1112 ];
1113 }
1114 }
1115