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 / database / tables / donations.php

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

1,574 lines 41.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureDonation Database Donations Table Class.
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Database\Tables;
9
10 use SureDonation\Inc\Database\Base;
11 use SureDonation\Inc\Traits\Get_Instance;
12
13 // Exit if accessed directly.
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * SureDonation Database Donations Table Class.
18 *
19 * @since 0.0.1
20 */
21 class Donations extends Base {
22 use Get_Instance;
23
24 /**
25 * Table suffix.
26 *
27 * @var string
28 * @since 0.0.1
29 */
30 protected $table_suffix = 'donations';
31
32 /**
33 * Table version.
34 *
35 * @var int
36 * @since 0.0.1
37 */
38 protected $table_version = 1;
39
40 /**
41 * Valid payment statuses.
42 *
43 * @var array<string>
44 * @since 0.0.1
45 */
46 private static $valid_statuses = [
47 'pending',
48 'processing',
49 'completed',
50 'failed',
51 'refunded',
52 'partially_refunded',
53 'cancelled',
54 'suspicious',
55 ];
56
57 /**
58 * Valid order columns.
59 *
60 * @var array<string>
61 * @since 0.0.1
62 */
63 private static $valid_order_columns = [
64 'id',
65 'campaign_id',
66 'amount',
67 'created_at',
68 'updated_at',
69 'payment_status',
70 'donor_name',
71 'donor_email',
72 ];
73
74 /**
75 * {@inheritDoc}
76 */
77 public function get_schema() {
78 return [
79 'id' => [
80 'type' => 'number',
81 ],
82 'campaign_id' => [
83 'type' => 'number',
84 ],
85 'donor_id' => [
86 'type' => 'number',
87 'default' => 0,
88 ],
89 'form_id' => [
90 'type' => 'number',
91 'default' => 0,
92 ],
93 'amount' => [
94 'type' => 'string',
95 'default' => '0.00000000',
96 ],
97 'fees_covered' => [
98 'type' => 'string',
99 'default' => '0.00000000',
100 ],
101 'refunded_amount' => [
102 'type' => 'string',
103 'default' => '0.00000000',
104 ],
105 'currency' => [
106 'type' => 'string',
107 'default' => 'USD',
108 ],
109 'transaction_id' => [
110 'type' => 'string',
111 'default' => '',
112 ],
113 'customer_id' => [
114 'type' => 'string',
115 'default' => '',
116 ],
117 'gateway' => [
118 'type' => 'string',
119 'default' => 'stripe',
120 ],
121 'payment_status' => [
122 'type' => 'string',
123 'default' => 'pending',
124 ],
125 'payment_mode' => [
126 'type' => 'string',
127 'default' => 'test',
128 ],
129 'donor_name' => [
130 'type' => 'string',
131 'default' => '',
132 ],
133 'donor_email' => [
134 'type' => 'string',
135 'default' => '',
136 ],
137 'donor_phone' => [
138 'type' => 'string',
139 'default' => '',
140 ],
141 'is_anonymous' => [
142 'type' => 'boolean',
143 'default' => false,
144 ],
145 'donation_type' => [
146 'type' => 'string',
147 'default' => 'one-time',
148 ],
149 'donor_comment' => [
150 'type' => 'string',
151 'default' => '',
152 ],
153 'receipt_sent' => [
154 'type' => 'boolean',
155 'default' => false,
156 ],
157 'receipt_pdf_url' => [
158 'type' => 'string',
159 'default' => '',
160 ],
161 'donation_data' => [
162 'type' => 'array',
163 'default' => [],
164 ],
165 'log' => [
166 'type' => 'array',
167 'default' => [],
168 ],
169 'ip_address' => [
170 'type' => 'string',
171 'default' => '',
172 ],
173 'user_agent' => [
174 'type' => 'string',
175 'default' => '',
176 ],
177 'referer_url' => [
178 'type' => 'string',
179 'default' => '',
180 ],
181 'created_at' => [
182 'type' => 'datetime',
183 ],
184 'updated_at' => [
185 'type' => 'datetime',
186 ],
187 ];
188 }
189
190 /**
191 * {@inheritDoc}
192 */
193 public function get_columns_definition() {
194 return [
195 'id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY',
196 'campaign_id BIGINT(20) UNSIGNED NOT NULL',
197 'donor_id BIGINT(20) UNSIGNED NULL',
198 'form_id BIGINT(20) UNSIGNED NULL',
199 'amount DECIMAL(26,8) NOT NULL',
200 'fees_covered DECIMAL(26,8) NOT NULL DEFAULT 0',
201 'refunded_amount DECIMAL(26,8) NOT NULL DEFAULT 0',
202 'currency VARCHAR(10) NOT NULL',
203 'transaction_id VARCHAR(255) NOT NULL',
204 'customer_id VARCHAR(50) NOT NULL',
205 'gateway VARCHAR(20) NOT NULL',
206 'payment_status VARCHAR(50) NOT NULL',
207 'payment_mode VARCHAR(20) NOT NULL',
208 'donor_name VARCHAR(255) NOT NULL',
209 'donor_email VARCHAR(255) NOT NULL',
210 'donor_phone VARCHAR(50) NOT NULL',
211 'is_anonymous TINYINT(1) NOT NULL DEFAULT 0',
212 'donation_type VARCHAR(30) NOT NULL',
213 'donor_comment TEXT',
214 'receipt_sent TINYINT(1) NOT NULL DEFAULT 0',
215 'receipt_pdf_url VARCHAR(255) NOT NULL',
216 'donation_data LONGTEXT',
217 'log LONGTEXT',
218 'ip_address VARCHAR(45) NOT NULL',
219 'user_agent TEXT',
220 'referer_url TEXT',
221 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',
222 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
223 'INDEX idx_campaign (campaign_id)',
224 'INDEX idx_donor (donor_id)',
225 'INDEX idx_status (payment_status)',
226 'INDEX idx_email (donor_email)',
227 'INDEX idx_created (created_at)',
228 'INDEX idx_form (form_id)',
229 ];
230 }
231
232 /**
233 * Add a new donation record.
234 *
235 * @param array<mixed> $data Donation data to insert.
236 * @return int|false The donation ID on success, false on error.
237 * @since 0.0.1
238 */
239 public static function add( $data ) {
240 if ( empty( $data['campaign_id'] ) ) {
241 return false;
242 }
243
244 $instance = self::get_instance();
245
246 // Set created_at if not provided.
247 if ( ! isset( $data['created_at'] ) ) {
248 $data['created_at'] = current_time( 'mysql' );
249 }
250
251 return $instance->use_insert( $data );
252 }
253
254 /**
255 * Update a donation record.
256 *
257 * @param int $donation_id Donation ID to update.
258 * @param array<string,mixed> $data Data to update.
259 * @return int|false Number of rows updated or false on error.
260 * @since 0.0.1
261 */
262 public static function update( $donation_id, $data = [] ) {
263 if ( empty( $donation_id ) ) {
264 return false;
265 }
266
267 // Set updated_at.
268 $data['updated_at'] = current_time( 'mysql' );
269
270 return self::get_instance()->use_update( $data, [ 'id' => absint( $donation_id ) ] );
271 }
272
273 /**
274 * Get a single donation by ID.
275 *
276 * @param int $donation_id Donation ID.
277 * @return array<mixed>|null Donation data or null if not found.
278 * @since 0.0.1
279 */
280 public static function get( $donation_id ) {
281 if ( empty( $donation_id ) ) {
282 return null;
283 }
284
285 $instance = self::get_instance();
286 global $wpdb;
287
288 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
289 $result = $wpdb->get_row(
290 $wpdb->prepare(
291 'SELECT * FROM %i WHERE id = %d',
292 $instance->get_tablename(),
293 absint( $donation_id )
294 ),
295 ARRAY_A
296 );
297
298 if ( ! $result ) {
299 return null;
300 }
301
302 return $instance->decode_by_datatype( $result );
303 }
304
305 /**
306 * Get all donations with pagination.
307 *
308 * @param int $limit Number of records to return.
309 * @param int $offset Offset for pagination.
310 * @param string $orderby Column to order by.
311 * @param string $order Order direction (ASC or DESC).
312 * @return array<mixed> Array of donations.
313 * @since 0.0.1
314 */
315 public static function get_all( $limit = 10, $offset = 0, $orderby = 'created_at', $order = 'DESC' ) {
316 $instance = self::get_instance();
317 global $wpdb;
318 $table = $instance->get_tablename();
319
320 // Validate orderby column.
321 if ( ! in_array( $orderby, self::$valid_order_columns, true ) ) {
322 $orderby = 'created_at';
323 }
324
325 // Validate order direction.
326 $order = strtoupper( $order );
327 if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
328 $order = 'DESC';
329 }
330
331 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Data changes frequently, caching would show stale results.
332 $results = 'ASC' === $order
333 ? $wpdb->get_results(
334 $wpdb->prepare(
335 'SELECT * FROM %i ORDER BY %i ASC LIMIT %d, %d',
336 $table,
337 $orderby,
338 absint( $offset ),
339 absint( $limit )
340 ),
341 ARRAY_A
342 )
343 : $wpdb->get_results(
344 $wpdb->prepare(
345 'SELECT * FROM %i ORDER BY %i DESC LIMIT %d, %d',
346 $table,
347 $orderby,
348 absint( $offset ),
349 absint( $limit )
350 ),
351 ARRAY_A
352 );
353 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
354
355 if ( ! $results || ! is_array( $results ) ) {
356 return [];
357 }
358
359 return array_map( [ $instance, 'decode_by_datatype' ], $results );
360 }
361
362 /**
363 * Get donations for admin listing with optional filters.
364 *
365 * @param string $status Payment status filter ('all' for no filter).
366 * @param int $campaign_id Campaign ID filter (0 for no filter).
367 * @param string $search Search term for donor_name, donor_email, or transaction_id.
368 * @param int $limit Number of records to return.
369 * @param int $offset Offset for pagination.
370 * @param string $orderby Column to order by.
371 * @param string $order Order direction (ASC or DESC).
372 * @return array<mixed> Array of donations.
373 * @since 0.0.1
374 */
375 public static function get_admin_list( $status = 'all', $campaign_id = 0, $search = '', $limit = 10, $offset = 0, $orderby = 'created_at', $order = 'DESC' ) {
376 $instance = self::get_instance();
377 global $wpdb;
378 $table = $instance->get_tablename();
379
380 // Validate orderby column.
381 if ( ! in_array( $orderby, self::$valid_order_columns, true ) ) {
382 $orderby = 'created_at';
383 }
384
385 // Validate order direction.
386 $order = strtoupper( $order );
387 if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
388 $order = 'DESC';
389 }
390
391 // Build query based on filters.
392 $has_status = 'all' !== $status;
393 $has_campaign = $campaign_id > 0;
394 $has_search = ! empty( $search );
395 $is_asc = 'ASC' === $order;
396
397 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Data changes frequently, caching would show stale results.
398
399 // All three filters.
400 if ( $has_status && $has_campaign && $has_search ) {
401 $search_term = '%' . $wpdb->esc_like( sanitize_text_field( $search ) ) . '%';
402 $results = $is_asc
403 ? $wpdb->get_results(
404 $wpdb->prepare(
405 'SELECT * FROM %i WHERE payment_status = %s AND campaign_id = %d AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i ASC LIMIT %d, %d',
406 $table,
407 sanitize_text_field( $status ),
408 absint( $campaign_id ),
409 $search_term,
410 $search_term,
411 $search_term,
412 $orderby,
413 absint( $offset ),
414 absint( $limit )
415 ),
416 ARRAY_A
417 )
418 : $wpdb->get_results(
419 $wpdb->prepare(
420 'SELECT * FROM %i WHERE payment_status = %s AND campaign_id = %d AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i DESC LIMIT %d, %d',
421 $table,
422 sanitize_text_field( $status ),
423 absint( $campaign_id ),
424 $search_term,
425 $search_term,
426 $search_term,
427 $orderby,
428 absint( $offset ),
429 absint( $limit )
430 ),
431 ARRAY_A
432 );
433 } elseif ( $has_status && $has_campaign ) {
434 $results = $is_asc
435 ? $wpdb->get_results(
436 $wpdb->prepare(
437 'SELECT * FROM %i WHERE payment_status = %s AND campaign_id = %d ORDER BY %i ASC LIMIT %d, %d',
438 $table,
439 sanitize_text_field( $status ),
440 absint( $campaign_id ),
441 $orderby,
442 absint( $offset ),
443 absint( $limit )
444 ),
445 ARRAY_A
446 )
447 : $wpdb->get_results(
448 $wpdb->prepare(
449 'SELECT * FROM %i WHERE payment_status = %s AND campaign_id = %d ORDER BY %i DESC LIMIT %d, %d',
450 $table,
451 sanitize_text_field( $status ),
452 absint( $campaign_id ),
453 $orderby,
454 absint( $offset ),
455 absint( $limit )
456 ),
457 ARRAY_A
458 );
459 } elseif ( $has_status && $has_search ) {
460 $search_term = '%' . $wpdb->esc_like( sanitize_text_field( $search ) ) . '%';
461 $results = $is_asc
462 ? $wpdb->get_results(
463 $wpdb->prepare(
464 'SELECT * FROM %i WHERE payment_status = %s AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i ASC LIMIT %d, %d',
465 $table,
466 sanitize_text_field( $status ),
467 $search_term,
468 $search_term,
469 $search_term,
470 $orderby,
471 absint( $offset ),
472 absint( $limit )
473 ),
474 ARRAY_A
475 )
476 : $wpdb->get_results(
477 $wpdb->prepare(
478 'SELECT * FROM %i WHERE payment_status = %s AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i DESC LIMIT %d, %d',
479 $table,
480 sanitize_text_field( $status ),
481 $search_term,
482 $search_term,
483 $search_term,
484 $orderby,
485 absint( $offset ),
486 absint( $limit )
487 ),
488 ARRAY_A
489 );
490 } elseif ( $has_campaign && $has_search ) {
491 $search_term = '%' . $wpdb->esc_like( sanitize_text_field( $search ) ) . '%';
492 $results = $is_asc
493 ? $wpdb->get_results(
494 $wpdb->prepare(
495 'SELECT * FROM %i WHERE campaign_id = %d AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i ASC LIMIT %d, %d',
496 $table,
497 absint( $campaign_id ),
498 $search_term,
499 $search_term,
500 $search_term,
501 $orderby,
502 absint( $offset ),
503 absint( $limit )
504 ),
505 ARRAY_A
506 )
507 : $wpdb->get_results(
508 $wpdb->prepare(
509 'SELECT * FROM %i WHERE campaign_id = %d AND (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i DESC LIMIT %d, %d',
510 $table,
511 absint( $campaign_id ),
512 $search_term,
513 $search_term,
514 $search_term,
515 $orderby,
516 absint( $offset ),
517 absint( $limit )
518 ),
519 ARRAY_A
520 );
521 } elseif ( $has_status ) {
522 $results = $is_asc
523 ? $wpdb->get_results(
524 $wpdb->prepare(
525 'SELECT * FROM %i WHERE payment_status = %s ORDER BY %i ASC LIMIT %d, %d',
526 $table,
527 sanitize_text_field( $status ),
528 $orderby,
529 absint( $offset ),
530 absint( $limit )
531 ),
532 ARRAY_A
533 )
534 : $wpdb->get_results(
535 $wpdb->prepare(
536 'SELECT * FROM %i WHERE payment_status = %s ORDER BY %i DESC LIMIT %d, %d',
537 $table,
538 sanitize_text_field( $status ),
539 $orderby,
540 absint( $offset ),
541 absint( $limit )
542 ),
543 ARRAY_A
544 );
545 } elseif ( $has_campaign ) {
546 $results = $is_asc
547 ? $wpdb->get_results(
548 $wpdb->prepare(
549 'SELECT * FROM %i WHERE campaign_id = %d ORDER BY %i ASC LIMIT %d, %d',
550 $table,
551 absint( $campaign_id ),
552 $orderby,
553 absint( $offset ),
554 absint( $limit )
555 ),
556 ARRAY_A
557 )
558 : $wpdb->get_results(
559 $wpdb->prepare(
560 'SELECT * FROM %i WHERE campaign_id = %d ORDER BY %i DESC LIMIT %d, %d',
561 $table,
562 absint( $campaign_id ),
563 $orderby,
564 absint( $offset ),
565 absint( $limit )
566 ),
567 ARRAY_A
568 );
569 } elseif ( $has_search ) {
570 $search_term = '%' . $wpdb->esc_like( sanitize_text_field( $search ) ) . '%';
571 $results = $is_asc
572 ? $wpdb->get_results(
573 $wpdb->prepare(
574 'SELECT * FROM %i WHERE (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i ASC LIMIT %d, %d',
575 $table,
576 $search_term,
577 $search_term,
578 $search_term,
579 $orderby,
580 absint( $offset ),
581 absint( $limit )
582 ),
583 ARRAY_A
584 )
585 : $wpdb->get_results(
586 $wpdb->prepare(
587 'SELECT * FROM %i WHERE (donor_name LIKE %s OR donor_email LIKE %s OR transaction_id LIKE %s) ORDER BY %i DESC LIMIT %d, %d',
588 $table,
589 $search_term,
590 $search_term,
591 $search_term,
592 $orderby,
593 absint( $offset ),
594 absint( $limit )
595 ),
596 ARRAY_A
597 );
598 } else {
599 $results = $is_asc
600 ? $wpdb->get_results(
601 $wpdb->prepare(
602 'SELECT * FROM %i ORDER BY %i ASC LIMIT %d, %d',
603 $table,
604 $orderby,
605 absint( $offset ),
606 absint( $limit )
607 ),
608 ARRAY_A
609 )
610 : $wpdb->get_results(
611 $wpdb->prepare(
612 'SELECT * FROM %i ORDER BY %i DESC LIMIT %d, %d',
613 $table,
614 $orderby,
615 absint( $offset ),
616 absint( $limit )
617 ),
618 ARRAY_A
619 );
620 }
621
622 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
623
624 if ( ! $results || ! is_array( $results ) ) {
625 return [];
626 }
627
628 return array_map( [ $instance, 'decode_by_datatype' ], $results );
629 }
630
631 /**
632 * Get donations by status with pagination.
633 *
634 * @param string $status Payment status.
635 * @param int $limit Number of records to return.
636 * @param int $offset Offset for pagination.
637 * @param string $orderby Column to order by.
638 * @param string $order Order direction (ASC or DESC).
639 * @return array<mixed> Array of donations.
640 * @since 0.0.1
641 */
642 public static function get_by_status( $status, $limit = 10, $offset = 0, $orderby = 'created_at', $order = 'DESC' ) {
643 $instance = self::get_instance();
644 global $wpdb;
645 $table = $instance->get_tablename();
646
647 // Validate orderby column.
648 if ( ! in_array( $orderby, self::$valid_order_columns, true ) ) {
649 $orderby = 'created_at';
650 }
651
652 // Validate order direction.
653 $order = strtoupper( $order );
654 if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
655 $order = 'DESC';
656 }
657
658 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Data changes frequently, caching would show stale results.
659 $results = 'ASC' === $order
660 ? $wpdb->get_results(
661 $wpdb->prepare(
662 'SELECT * FROM %i WHERE payment_status = %s ORDER BY %i ASC LIMIT %d, %d',
663 $table,
664 sanitize_text_field( $status ),
665 $orderby,
666 absint( $offset ),
667 absint( $limit )
668 ),
669 ARRAY_A
670 )
671 : $wpdb->get_results(
672 $wpdb->prepare(
673 'SELECT * FROM %i WHERE payment_status = %s ORDER BY %i DESC LIMIT %d, %d',
674 $table,
675 sanitize_text_field( $status ),
676 $orderby,
677 absint( $offset ),
678 absint( $limit )
679 ),
680 ARRAY_A
681 );
682 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
683
684 if ( ! $results || ! is_array( $results ) ) {
685 return [];
686 }
687
688 return array_map( [ $instance, 'decode_by_datatype' ], $results );
689 }
690
691 /**
692 * Get donations by campaign ID with pagination.
693 *
694 * @param int $campaign_id Campaign ID.
695 * @param int $limit Number of records to return.
696 * @param int $offset Offset for pagination.
697 * @param string $orderby Column to order by.
698 * @param string $order Order direction (ASC or DESC).
699 * @return array<mixed> Array of donations.
700 * @since 0.0.1
701 */
702 public static function get_by_campaign_id( $campaign_id, $limit = 100, $offset = 0, $orderby = 'created_at', $order = 'DESC' ) {
703 if ( empty( $campaign_id ) ) {
704 return [];
705 }
706
707 $instance = self::get_instance();
708 global $wpdb;
709 $table = $instance->get_tablename();
710
711 // Validate orderby column.
712 if ( ! in_array( $orderby, self::$valid_order_columns, true ) ) {
713 $orderby = 'created_at';
714 }
715
716 // Validate order direction.
717 $order = strtoupper( $order );
718 if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
719 $order = 'DESC';
720 }
721
722 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Data changes frequently, caching would show stale results.
723 $results = 'ASC' === $order
724 ? $wpdb->get_results(
725 $wpdb->prepare(
726 'SELECT * FROM %i WHERE campaign_id = %d ORDER BY %i ASC LIMIT %d, %d',
727 $table,
728 absint( $campaign_id ),
729 $orderby,
730 absint( $offset ),
731 absint( $limit )
732 ),
733 ARRAY_A
734 )
735 : $wpdb->get_results(
736 $wpdb->prepare(
737 'SELECT * FROM %i WHERE campaign_id = %d ORDER BY %i DESC LIMIT %d, %d',
738 $table,
739 absint( $campaign_id ),
740 $orderby,
741 absint( $offset ),
742 absint( $limit )
743 ),
744 ARRAY_A
745 );
746 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
747
748 if ( ! $results || ! is_array( $results ) ) {
749 return [];
750 }
751
752 return array_map( [ $instance, 'decode_by_datatype' ], $results );
753 }
754
755 /**
756 * Delete a donation.
757 *
758 * @param int $donation_id Donation ID.
759 * @return int|false Number of rows deleted or false on error.
760 * @since 0.0.1
761 */
762 public static function delete( $donation_id ) {
763 if ( empty( $donation_id ) ) {
764 return false;
765 }
766
767 return self::get_instance()->use_delete( [ 'id' => absint( $donation_id ) ] );
768 }
769
770 /**
771 * Get donations by donor email.
772 *
773 * @param string $email Donor email.
774 * @return array<mixed> Array of donations.
775 * @since 0.0.1
776 */
777 public static function get_by_donor_email( $email ) {
778 if ( empty( $email ) ) {
779 return [];
780 }
781
782 $instance = self::get_instance();
783 global $wpdb;
784
785 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
786 $results = $wpdb->get_results(
787 $wpdb->prepare(
788 'SELECT * FROM %i WHERE donor_email = %s ORDER BY created_at DESC',
789 $instance->get_tablename(),
790 sanitize_email( $email )
791 ),
792 ARRAY_A
793 );
794
795 if ( ! $results || ! is_array( $results ) ) {
796 return [];
797 }
798
799 return array_map( [ $instance, 'decode_by_datatype' ], $results );
800 }
801
802 /**
803 * Get donation by transaction ID.
804 *
805 * @param string $transaction_id Transaction ID.
806 * @return array<string, mixed>|null Donation data or null if not found.
807 * @since 0.0.1
808 */
809 public static function get_by_transaction_id( $transaction_id ) {
810 if ( empty( $transaction_id ) ) {
811 return null;
812 }
813
814 $instance = self::get_instance();
815 global $wpdb;
816
817 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
818 $result = $wpdb->get_row(
819 $wpdb->prepare(
820 'SELECT * FROM %i WHERE transaction_id = %s LIMIT 1',
821 $instance->get_tablename(),
822 sanitize_text_field( $transaction_id )
823 ),
824 ARRAY_A
825 );
826
827 if ( ! $result ) {
828 return null;
829 }
830
831 return $instance->decode_by_datatype( $result );
832 }
833
834 /**
835 * Get total donations count.
836 *
837 * @return int Total count.
838 * @since 0.0.1
839 */
840 public static function count_all() {
841 $instance = self::get_instance();
842 global $wpdb;
843
844 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
845 $count = $wpdb->get_var(
846 $wpdb->prepare(
847 'SELECT COUNT(*) FROM %i',
848 $instance->get_tablename()
849 )
850 );
851
852 return is_numeric( $count ) ? (int) $count : 0;
853 }
854
855 /**
856 * Get total donations count by status.
857 *
858 * @param string $status Payment status.
859 * @return int Total count.
860 * @since 0.0.1
861 */
862 public static function count_by_status( $status ) {
863 $instance = self::get_instance();
864 global $wpdb;
865
866 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
867 $count = $wpdb->get_var(
868 $wpdb->prepare(
869 'SELECT COUNT(*) FROM %i WHERE payment_status = %s',
870 $instance->get_tablename(),
871 sanitize_text_field( $status )
872 )
873 );
874
875 return is_numeric( $count ) ? (int) $count : 0;
876 }
877
878 /**
879 * Get total donations count by campaign.
880 *
881 * @param int $campaign_id Campaign ID.
882 * @return int Total count.
883 * @since 0.0.1
884 */
885 public static function count_by_campaign( $campaign_id ) {
886 $instance = self::get_instance();
887 global $wpdb;
888
889 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
890 $count = $wpdb->get_var(
891 $wpdb->prepare(
892 'SELECT COUNT(*) FROM %i WHERE campaign_id = %d',
893 $instance->get_tablename(),
894 absint( $campaign_id )
895 )
896 );
897
898 return is_numeric( $count ) ? (int) $count : 0;
899 }
900
901 /**
902 * Get total donations count by status and campaign.
903 *
904 * @param string $status Payment status ('all' for no filter).
905 * @param int $campaign_id Optional campaign ID (0 for no filter).
906 * @return int Total count.
907 * @since 0.0.1
908 */
909 public static function get_total_donations_by_status( $status = 'all', $campaign_id = 0 ) {
910 $instance = self::get_instance();
911 global $wpdb;
912
913 // Both filters.
914 if ( 'all' !== $status && $campaign_id > 0 ) {
915 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
916 $count = $wpdb->get_var(
917 $wpdb->prepare(
918 'SELECT COUNT(*) FROM %i WHERE payment_status = %s AND campaign_id = %d',
919 $instance->get_tablename(),
920 sanitize_text_field( $status ),
921 absint( $campaign_id )
922 )
923 );
924 return is_numeric( $count ) ? (int) $count : 0;
925 }
926
927 // Status filter only.
928 if ( 'all' !== $status ) {
929 return self::count_by_status( $status );
930 }
931
932 // Campaign filter only.
933 if ( $campaign_id > 0 ) {
934 return self::count_by_campaign( $campaign_id );
935 }
936
937 // No filters.
938 return self::count_all();
939 }
940
941 /**
942 * Get campaign statistics.
943 *
944 * @param int $campaign_id Campaign ID.
945 * @return array<string,mixed> Campaign statistics.
946 * @since 0.0.1
947 */
948 public static function get_campaign_stats( $campaign_id ) {
949 $instance = self::get_instance();
950 global $wpdb;
951
952 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
953 $stats = $wpdb->get_row(
954 $wpdb->prepare(
955 "SELECT
956 COUNT(*) as donation_count,
957 COALESCE(SUM(amount - refunded_amount), 0) as total_raised,
958 COUNT(DISTINCT donor_email) as unique_donors,
959 COALESCE(AVG(amount - refunded_amount), 0) as average_donation,
960 COALESCE(MAX(amount - refunded_amount), 0) as largest_donation
961 FROM %i
962 WHERE campaign_id = %d AND payment_status IN ('completed', 'partially_refunded')",
963 $instance->get_tablename(),
964 absint( $campaign_id )
965 ),
966 ARRAY_A
967 );
968
969 return $stats ? $stats : [
970 'donation_count' => 0,
971 'total_raised' => 0,
972 'unique_donors' => 0,
973 'average_donation' => 0,
974 'largest_donation' => 0,
975 ];
976 }
977
978 /**
979 * Get global dashboard statistics.
980 *
981 * @return array{total_donations: string, total_raised: string, unique_donors: string, average_donation: string, largest_donation: string} Dashboard statistics.
982 * @since 0.0.1
983 */
984 public static function get_dashboard_stats() {
985 $instance = self::get_instance();
986 global $wpdb;
987
988 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
989 $stats = $wpdb->get_row(
990 $wpdb->prepare(
991 "SELECT
992 COUNT(*) as total_donations,
993 COALESCE(SUM(amount - refunded_amount), 0) as total_raised,
994 COUNT(DISTINCT donor_email) as unique_donors,
995 COALESCE(AVG(amount - refunded_amount), 0) as average_donation,
996 COALESCE(MAX(amount - refunded_amount), 0) as largest_donation
997 FROM %i
998 WHERE payment_status IN ('completed', 'partially_refunded')",
999 $instance->get_tablename()
1000 ),
1001 ARRAY_A
1002 );
1003
1004 return $stats ? $stats : [
1005 'total_donations' => 0,
1006 'total_raised' => 0,
1007 'unique_donors' => 0,
1008 'average_donation' => 0,
1009 'largest_donation' => 0,
1010 ];
1011 }
1012
1013 /**
1014 * Get recent donations globally (all campaigns).
1015 *
1016 * @param int $limit Number of donations to retrieve.
1017 * @return array<int, array<string, mixed>> Array of recent donations.
1018 * @since 0.0.1
1019 */
1020 public static function get_recent_donations_global( $limit = 5 ) {
1021 $instance = self::get_instance();
1022 global $wpdb;
1023
1024 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1025 $results = $wpdb->get_results(
1026 $wpdb->prepare(
1027 "SELECT * FROM %i WHERE payment_status IN ('completed', 'partially_refunded') ORDER BY created_at DESC LIMIT %d",
1028 $instance->get_tablename(),
1029 absint( $limit )
1030 ),
1031 ARRAY_A
1032 );
1033
1034 if ( ! $results || ! is_array( $results ) ) {
1035 return [];
1036 }
1037
1038 return array_map( [ $instance, 'decode_by_datatype' ], $results );
1039 }
1040
1041 /**
1042 * Get top campaigns by donations.
1043 *
1044 * @param int $limit Number of campaigns to retrieve.
1045 * @return array<int, array{campaign_id: string, donation_count: string, total_raised: string, unique_donors: string}> Array of top campaigns with stats.
1046 * @since 0.0.1
1047 */
1048 public static function get_top_campaigns( $limit = 5 ) {
1049 $instance = self::get_instance();
1050 global $wpdb;
1051
1052 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1053 $results = $wpdb->get_results(
1054 $wpdb->prepare(
1055 "SELECT
1056 campaign_id,
1057 COUNT(*) as donation_count,
1058 COALESCE(SUM(amount - refunded_amount), 0) as total_raised,
1059 COUNT(DISTINCT donor_email) as unique_donors
1060 FROM %i
1061 WHERE payment_status IN ('completed', 'partially_refunded')
1062 GROUP BY campaign_id
1063 ORDER BY total_raised DESC
1064 LIMIT %d",
1065 $instance->get_tablename(),
1066 absint( $limit )
1067 ),
1068 ARRAY_A
1069 );
1070
1071 return $results ? $results : [];
1072 }
1073
1074 /**
1075 * Get donation trends over time.
1076 *
1077 * @param string $after Start date (ISO format).
1078 * @param string $before End date (ISO format).
1079 * @param string $group Grouping: 'day', 'week', or 'month'.
1080 * @return array<int, array{period: string, donation_count: string, total_amount: string}> Array of donation trends.
1081 * @since 0.0.1
1082 */
1083 public static function get_donation_trends( $after = '', $before = '', $group = 'day' ) {
1084 $instance = self::get_instance();
1085 global $wpdb;
1086
1087 // Default to last 30 days if no dates provided.
1088 if ( empty( $after ) ) {
1089 $after = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
1090 }
1091 if ( empty( $before ) ) {
1092 $before = gmdate( 'Y-m-d' );
1093 }
1094
1095 // Determine date format based on grouping.
1096 switch ( $group ) {
1097 case 'month':
1098 $date_format = '%Y-%m-01';
1099 break;
1100 case 'week':
1101 $date_format = '%x-%v'; // ISO year-week.
1102 break;
1103 case 'day':
1104 default:
1105 $date_format = '%Y-%m-%d';
1106 break;
1107 }
1108
1109 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1110 $results = $wpdb->get_results(
1111 $wpdb->prepare(
1112 "SELECT
1113 DATE_FORMAT(created_at, %s) as period,
1114 COUNT(*) as donation_count,
1115 COALESCE(SUM(amount - refunded_amount), 0) as total_amount
1116 FROM %i
1117 WHERE payment_status IN ('completed', 'partially_refunded')
1118 AND DATE(created_at) >= %s
1119 AND DATE(created_at) <= %s
1120 GROUP BY period
1121 ORDER BY period ASC",
1122 $date_format,
1123 $instance->get_tablename(),
1124 $after,
1125 $before
1126 ),
1127 ARRAY_A
1128 );
1129
1130 return $results ? $results : [];
1131 }
1132
1133 /**
1134 * Get recent donations for a campaign.
1135 *
1136 * @param int $campaign_id Campaign ID.
1137 * @param int $limit Number of donations to retrieve.
1138 * @return array<mixed> Array of recent donations.
1139 * @since 0.0.1
1140 */
1141 public static function get_recent_donations( $campaign_id, $limit = 5 ) {
1142 $instance = self::get_instance();
1143 global $wpdb;
1144
1145 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1146 $results = $wpdb->get_results(
1147 $wpdb->prepare(
1148 "SELECT * FROM %i WHERE campaign_id = %d AND payment_status IN ('completed', 'partially_refunded') ORDER BY created_at DESC LIMIT %d",
1149 $instance->get_tablename(),
1150 absint( $campaign_id ),
1151 absint( $limit )
1152 ),
1153 ARRAY_A
1154 );
1155
1156 if ( ! $results || ! is_array( $results ) ) {
1157 return [];
1158 }
1159
1160 return array_map( [ $instance, 'decode_by_datatype' ], $results );
1161 }
1162
1163 /**
1164 * Update donation status.
1165 *
1166 * @param int $donation_id Donation ID.
1167 * @param string $status New status.
1168 * @return int|false Number of rows updated or false on error.
1169 * @since 0.0.1
1170 */
1171 public static function update_status( $donation_id, $status ) {
1172 if ( empty( $donation_id ) || ! in_array( $status, self::$valid_statuses, true ) ) {
1173 return false;
1174 }
1175
1176 return self::update( $donation_id, [ 'payment_status' => $status ] );
1177 }
1178
1179 /**
1180 * Get valid payment statuses.
1181 *
1182 * @return array<string> Valid statuses.
1183 * @since 0.0.1
1184 */
1185 public static function get_valid_statuses() {
1186 return self::$valid_statuses;
1187 }
1188
1189 /**
1190 * Add a log entry to a donation.
1191 *
1192 * @param int $donation_id Donation ID.
1193 * @param string $action Action type (e.g., 'status_change', 'refund', 'webhook').
1194 * @param string $message Log message.
1195 * @param array<string, mixed> $data Optional additional data.
1196 * @return int|false Number of rows updated or false on error.
1197 * @since 0.0.1
1198 */
1199 public static function add_log( $donation_id, $action, $message, $data = [] ) {
1200 if ( empty( $donation_id ) ) {
1201 return false;
1202 }
1203
1204 $donation = self::get( $donation_id );
1205 if ( ! $donation ) {
1206 return false;
1207 }
1208
1209 // Get existing log or initialize empty array.
1210 // Note: decode_by_datatype() already decodes JSON to array, so check for array first.
1211 $log_data = $donation['log'] ?? [];
1212 if ( is_array( $log_data ) ) {
1213 $log = $log_data;
1214 } elseif ( is_string( $log_data ) && ! empty( $log_data ) ) {
1215 $log = json_decode( $log_data, true );
1216 if ( ! is_array( $log ) ) {
1217 $log = [];
1218 }
1219 } else {
1220 $log = [];
1221 }
1222
1223 // Add new log entry.
1224 $log[] = [
1225 'action' => sanitize_text_field( $action ),
1226 'message' => sanitize_text_field( $message ),
1227 'data' => $data,
1228 'timestamp' => current_time( 'mysql' ),
1229 ];
1230
1231 return self::update( $donation_id, [ 'log' => $log ] );
1232 }
1233
1234 /**
1235 * Get log entries for a donation.
1236 *
1237 * @param int $donation_id Donation ID.
1238 * @return array<int, array<string, mixed>> Log entries.
1239 * @since 0.0.1
1240 */
1241 public static function get_log( $donation_id ) {
1242 if ( empty( $donation_id ) ) {
1243 return [];
1244 }
1245
1246 $donation = self::get( $donation_id );
1247 if ( ! $donation || empty( $donation['log'] ) ) {
1248 return [];
1249 }
1250
1251 // Note: decode_by_datatype() already decodes JSON to array, so check for array first.
1252 $log_data = $donation['log'];
1253 if ( is_array( $log_data ) ) {
1254 return $log_data;
1255 }
1256
1257 if ( is_string( $log_data ) ) {
1258 $log = json_decode( $log_data, true );
1259 return is_array( $log ) ? $log : [];
1260 }
1261
1262 return [];
1263 }
1264
1265 /**
1266 * Add refund data to donation_data for audit trail and duplicate prevention.
1267 *
1268 * Stores each refund with its ID as the key for O(1) lookups.
1269 *
1270 * @param int $donation_id Donation ID.
1271 * @param array<string, mixed> $refund_data Refund data to store.
1272 * @return bool True on success, false on failure.
1273 * @since 0.0.1
1274 */
1275 public static function add_refund_to_donation_data( $donation_id, $refund_data ) {
1276 $refund_id = $refund_data['refund_id'] ?? '';
1277
1278 if ( empty( $refund_id ) || empty( $donation_id ) ) {
1279 return false;
1280 }
1281
1282 $donation = self::get( $donation_id );
1283 if ( ! $donation ) {
1284 return false;
1285 }
1286
1287 // Get existing donation_data.
1288 $donation_data = $donation['donation_data'] ?? [];
1289 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1290 $donation_data = json_decode( $donation_data, true );
1291 }
1292 if ( ! is_array( $donation_data ) ) {
1293 $donation_data = [];
1294 }
1295
1296 // Initialize refunds array if not exists.
1297 if ( ! isset( $donation_data['refunds'] ) || ! is_array( $donation_data['refunds'] ) ) {
1298 $donation_data['refunds'] = [];
1299 }
1300
1301 // Store with refund ID as key for O(1) lookup (duplicate prevention).
1302 $donation_data['refunds'][ $refund_id ] = $refund_data;
1303
1304 // Update donation_data in database.
1305 $result = self::update( $donation_id, [ 'donation_data' => $donation_data ] );
1306
1307 return false !== $result;
1308 }
1309
1310 /**
1311 * Check if a refund already exists in the donation data.
1312 *
1313 * This prevents duplicate processing of the same refund.
1314 *
1315 * @param int $donation_id Donation ID.
1316 * @param string $refund_id Refund ID to check.
1317 * @return bool True if refund already exists, false otherwise.
1318 * @since 0.0.1
1319 */
1320 public static function check_refund_exists( $donation_id, $refund_id ) {
1321 if ( empty( $donation_id ) || empty( $refund_id ) ) {
1322 return false;
1323 }
1324
1325 $donation = self::get( $donation_id );
1326 if ( ! $donation ) {
1327 return false;
1328 }
1329
1330 // Get donation_data and parse if needed.
1331 $donation_data = $donation['donation_data'] ?? [];
1332 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1333 $donation_data = json_decode( $donation_data, true );
1334 }
1335 if ( ! is_array( $donation_data ) ) {
1336 return false;
1337 }
1338
1339 // Check if refunds array exists and contains this refund ID.
1340 if ( empty( $donation_data['refunds'] ) || ! is_array( $donation_data['refunds'] ) ) {
1341 return false;
1342 }
1343
1344 // O(1) lookup using refund ID as array key.
1345 return isset( $donation_data['refunds'][ $refund_id ] );
1346 }
1347
1348 /**
1349 * Add a note to a donation.
1350 *
1351 * @param int $donation_id Donation ID.
1352 * @param string $note_content Note content.
1353 * @param int $author_id Author user ID.
1354 * @return array{success: bool, note_id: string|null} Result with success status and note ID.
1355 * @since 0.0.1
1356 */
1357 public static function add_note( $donation_id, $note_content, $author_id = 0 ) {
1358 $result = [
1359 'success' => false,
1360 'note_id' => null,
1361 ];
1362
1363 if ( empty( $donation_id ) || empty( $note_content ) ) {
1364 return $result;
1365 }
1366
1367 $donation = self::get( $donation_id );
1368 if ( ! $donation ) {
1369 return $result;
1370 }
1371
1372 // Get existing donation_data.
1373 $donation_data = $donation['donation_data'] ?? [];
1374 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1375 $donation_data = json_decode( $donation_data, true );
1376 }
1377 if ( ! is_array( $donation_data ) ) {
1378 $donation_data = [];
1379 }
1380
1381 // Initialize notes array if not exists.
1382 if ( ! isset( $donation_data['notes'] ) || ! is_array( $donation_data['notes'] ) ) {
1383 $donation_data['notes'] = [];
1384 }
1385
1386 // Generate unique note ID.
1387 $note_id = uniqid( 'note_', true );
1388
1389 // Get author info.
1390 $author_name = __( 'System', 'suredonation' );
1391 if ( $author_id > 0 ) {
1392 $user = get_userdata( $author_id );
1393 if ( $user ) {
1394 $author_name = $user->display_name;
1395 }
1396 }
1397
1398 // Add new note.
1399 $donation_data['notes'][ $note_id ] = [
1400 'id' => $note_id,
1401 'content' => wp_kses_post( $note_content ),
1402 'author_id' => $author_id,
1403 'author_name' => $author_name,
1404 'created_at' => current_time( 'mysql' ),
1405 ];
1406
1407 // Update donation_data in database.
1408 $update_result = self::update( $donation_id, [ 'donation_data' => $donation_data ] );
1409
1410 if ( false !== $update_result ) {
1411 $result['success'] = true;
1412 $result['note_id'] = $note_id;
1413 }
1414
1415 return $result;
1416 }
1417
1418 /**
1419 * Get notes for a donation with pagination.
1420 *
1421 * @param int $donation_id Donation ID.
1422 * @param int $page Current page (1-indexed).
1423 * @param int $per_page Notes per page.
1424 * @return array{notes: array<int, array<string, mixed>>, total: int, total_pages: int} Paginated notes.
1425 * @since 0.0.1
1426 */
1427 public static function get_notes( $donation_id, $page = 1, $per_page = 3 ) {
1428 $result = [
1429 'notes' => [],
1430 'total' => 0,
1431 'total_pages' => 0,
1432 ];
1433
1434 if ( empty( $donation_id ) ) {
1435 return $result;
1436 }
1437
1438 $donation = self::get( $donation_id );
1439 if ( ! $donation ) {
1440 return $result;
1441 }
1442
1443 // Get donation_data and parse if needed.
1444 $donation_data = $donation['donation_data'] ?? [];
1445 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1446 $donation_data = json_decode( $donation_data, true );
1447 }
1448 if ( ! is_array( $donation_data ) ) {
1449 return $result;
1450 }
1451
1452 // Get notes array.
1453 if ( empty( $donation_data['notes'] ) || ! is_array( $donation_data['notes'] ) ) {
1454 return $result;
1455 }
1456
1457 // Convert to array values and sort by created_at (newest first).
1458 $all_notes = array_values( $donation_data['notes'] );
1459 usort(
1460 $all_notes,
1461 static function ( $a, $b ) {
1462 return strtotime( $b['created_at'] ?? '0' ) - strtotime( $a['created_at'] ?? '0' );
1463 }
1464 );
1465
1466 $total = count( $all_notes );
1467 $total_pages = (int) ceil( $total / $per_page );
1468 $offset = ( $page - 1 ) * $per_page;
1469
1470 // Get paginated notes.
1471 $notes = array_slice( $all_notes, $offset, $per_page );
1472
1473 return [
1474 'notes' => $notes,
1475 'total' => $total,
1476 'total_pages' => $total_pages,
1477 ];
1478 }
1479
1480 /**
1481 * Delete a note from a donation.
1482 *
1483 * @param int $donation_id Donation ID.
1484 * @param string $note_id Note ID to delete.
1485 * @return bool True on success, false on failure.
1486 * @since 0.0.1
1487 */
1488 public static function delete_note( $donation_id, $note_id ) {
1489 if ( empty( $donation_id ) || empty( $note_id ) ) {
1490 return false;
1491 }
1492
1493 $donation = self::get( $donation_id );
1494 if ( ! $donation ) {
1495 return false;
1496 }
1497
1498 // Get donation_data and parse if needed.
1499 $donation_data = $donation['donation_data'] ?? [];
1500 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1501 $donation_data = json_decode( $donation_data, true );
1502 }
1503 if ( ! is_array( $donation_data ) ) {
1504 return false;
1505 }
1506
1507 // Check if note exists.
1508 if ( empty( $donation_data['notes'] ) || ! isset( $donation_data['notes'][ $note_id ] ) ) {
1509 return false;
1510 }
1511
1512 // Remove the note.
1513 unset( $donation_data['notes'][ $note_id ] );
1514
1515 // Update donation_data in database.
1516 $result = self::update( $donation_id, [ 'donation_data' => $donation_data ] );
1517
1518 return false !== $result;
1519 }
1520
1521 /**
1522 * Remove a refund from donation_data.
1523 *
1524 * Used when a refund is canceled.
1525 *
1526 * @param int $donation_id Donation ID.
1527 * @param string $refund_id Refund ID to remove.
1528 * @return array{removed: bool, refund_data: array<string, mixed>|null} Result with removed status and refund data.
1529 * @since 0.0.1
1530 */
1531 public static function remove_refund_from_donation_data( $donation_id, $refund_id ) {
1532 $result = [
1533 'removed' => false,
1534 'refund_data' => null,
1535 ];
1536
1537 if ( empty( $donation_id ) || empty( $refund_id ) ) {
1538 return $result;
1539 }
1540
1541 $donation = self::get( $donation_id );
1542 if ( ! $donation ) {
1543 return $result;
1544 }
1545
1546 // Get donation_data and parse if needed.
1547 $donation_data = $donation['donation_data'] ?? [];
1548 if ( is_string( $donation_data ) && ! empty( $donation_data ) ) {
1549 $donation_data = json_decode( $donation_data, true );
1550 }
1551 if ( ! is_array( $donation_data ) ) {
1552 return $result;
1553 }
1554
1555 // Check if refund exists.
1556 if ( empty( $donation_data['refunds'] ) || ! isset( $donation_data['refunds'][ $refund_id ] ) ) {
1557 return $result;
1558 }
1559
1560 // Store the refund data before removing.
1561 $result['refund_data'] = $donation_data['refunds'][ $refund_id ];
1562
1563 // Remove the refund.
1564 unset( $donation_data['refunds'][ $refund_id ] );
1565
1566 // Update donation_data in database.
1567 $update_result = self::update( $donation_id, [ 'donation_data' => $donation_data ] );
1568
1569 $result['removed'] = false !== $update_result;
1570
1571 return $result;
1572 }
1573 }
1574