PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.7.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.7.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / payments / admin / admin-handler.php
admin-handler.php
1,479 lines 47.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Payment Operations Handler Class.
4 *
5 * @package sureforms.
6 */
7
8 namespace SRFM\Inc\Payments\Admin;
9
10 use SRFM\Inc\Database\Tables\Payments;
11 use SRFM\Inc\Helper;
12 use SRFM\Inc\Payments\Payment_Helper;
13 use SRFM\Inc\Traits\Get_Instance;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Admin Payment Operations handler class.
21 *
22 * @since 2.0.0
23 */
24 class Admin_Handler {
25 use Get_Instance;
26
27 /**
28 * Class constructor.
29 *
30 * @since 2.0.0
31 * @return void
32 */
33 public function __construct() {
34 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
35 add_action( 'wp_ajax_srfm_fetch_payments_transactions', [ $this, 'fetch_payments' ] );
36 add_action( 'wp_ajax_srfm_fetch_single_payment', [ $this, 'fetch_single_payment' ] );
37 add_action( 'wp_ajax_srfm_fetch_subscription', [ $this, 'fetch_subscription' ] );
38 add_action( 'wp_ajax_srfm_fetch_forms_list', [ $this, 'fetch_forms_list' ] );
39 add_action( 'wp_ajax_srfm_add_payment_note', [ $this, 'ajax_add_note' ] );
40 add_action( 'wp_ajax_srfm_delete_payment_note', [ $this, 'ajax_delete_note' ] );
41 add_action( 'wp_ajax_srfm_delete_payment_log', [ $this, 'ajax_delete_log' ] );
42 add_action( 'wp_ajax_srfm_bulk_delete_payments', [ $this, 'ajax_bulk_delete_payments' ] );
43 add_action( 'wp_ajax_srfm_refund_payment', [ $this, 'ajax_refund_payment' ] );
44 }
45
46 /**
47 * Enqueue Admin Scripts for Payment Operations.
48 *
49 * @since 2.0.0
50 * @return void
51 */
52 public function enqueue_scripts() {
53 $current_screen = get_current_screen();
54
55 /**
56 * List of the handles in which we need to add translation compatibility.
57 */
58 $script_translations_handlers = [];
59
60 // Check if we're on payment related pages.
61 if ( isset( $current_screen->id ) &&
62 ( strpos( $current_screen->id, 'sureforms_payments' ) !== false || strpos( $current_screen->id, 'sureforms_payments_react' ) !== false ) ) {
63 // Enqueue payment specific scripts.
64 wp_enqueue_script( SRFM_SLUG . '-payments', SRFM_URL . 'assets/build/payments.js', [], SRFM_VER, true );
65 wp_enqueue_style( SRFM_SLUG . '-payments', SRFM_URL . 'assets/build/payments.css', [], SRFM_VER );
66
67 // Localize script with payment admin data.
68 wp_localize_script(
69 SRFM_SLUG . '-payments',
70 SRFM_SLUG . '_payment_admin',
71 [
72 'ajax_url' => admin_url( 'admin-ajax.php' ),
73 'srfm_payment_admin_nonce' => wp_create_nonce( 'srfm_payment_admin_nonce' ),
74 'zeroDecimalCurrencies' => Payment_Helper::get_zero_decimal_currencies(),
75 'currenciesData' => Payment_Helper::get_all_currencies_data(),
76 ]
77 );
78
79 $script_translations_handlers[] = SRFM_SLUG . '-payments';
80 }
81
82 // Register script translations if needed.
83 if ( ! empty( $script_translations_handlers ) ) {
84 foreach ( $script_translations_handlers as $script_handle ) {
85 Helper::register_script_translations( $script_handle );
86 }
87 }
88 }
89
90 /**
91 * AJAX handler for fetching payments data.
92 *
93 * @since 2.0.0
94 * @return void
95 */
96 public function fetch_payments() {
97 // Verify nonce for security.
98 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
99 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
100 }
101
102 // Check user capabilities.
103 if ( ! current_user_can( 'manage_options' ) ) {
104 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
105 }
106
107 try {
108 // Sanitize input parameters.
109 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
110 $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';
111 $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
112 $payment_mode = isset( $_POST['payment_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['payment_mode'] ) ) : '';
113 $date_from = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : '';
114 $date_to = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : '';
115 $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
116 $per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 20;
117
118 // Validate pagination parameters.
119 $page = max( 1, $page );
120 $per_page = max( 1, min( 100, $per_page ) ); // Limit to 100 records per page.
121 $offset = ( $page - 1 ) * $per_page;
122
123 // Validate date format if provided.
124 if ( ! empty( $date_from ) && ! $this->validate_date( $date_from ) ) {
125 wp_send_json_error( [ 'message' => __( 'Invalid date format for date_from.', 'sureforms' ) ] );
126 }
127
128 if ( ! empty( $date_to ) && ! $this->validate_date( $date_to ) ) {
129 wp_send_json_error( [ 'message' => __( 'Invalid date format for date_to.', 'sureforms' ) ] );
130 }
131
132 // Get total count for pagination.
133 $total_count = $this->get_payments_count( $search, $status, $date_from, $date_to, $form_id, $payment_mode );
134
135 if ( 0 === $total_count && empty( $search ) && empty( $status ) && empty( $date_from ) && empty( $date_to ) && empty( $form_id ) && empty( $payment_mode ) ) {
136 wp_send_json_success(
137 [
138 'payments' => [],
139 'total' => 0,
140 'transactions_is_empty' => 'with_no_filter',
141 ]
142 );
143 }
144
145 if ( 0 === $total_count && ( ! empty( $search ) || ! empty( $status ) || ! empty( $date_from ) || ! empty( $date_to ) || ! empty( $form_id ) || ! empty( $payment_mode ) ) ) {
146 wp_send_json_success(
147 [
148 'payments' => [],
149 'total' => 0,
150 'transactions_is_empty' => 'with_filter',
151 ]
152 );
153 }
154
155 // Get payments data from database.
156 $payments = $this->get_payments_data( $search, $status, $date_from, $date_to, $per_page, $offset, $form_id, $payment_mode );
157
158 wp_send_json_success(
159 [
160 'payments' => $payments,
161 'total' => $total_count,
162 'page' => $page,
163 'per_page' => $per_page,
164 'total_pages' => ceil( $total_count / $per_page ),
165 'transactions_is_empty' => false,
166 ]
167 );
168
169 } catch ( \Exception $e ) {
170 wp_send_json_error( [ 'message' => __( 'Unable to load payments. Please refresh the page.', 'sureforms' ) ] );
171 }
172 }
173
174 /**
175 * AJAX handler for fetching single payment data.
176 *
177 * @since 2.0.0
178 * @return void
179 */
180 public function fetch_single_payment() {
181 // Verify nonce for security.
182 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
183 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
184 }
185
186 // Check user capabilities.
187 if ( ! current_user_can( 'manage_options' ) ) {
188 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
189 }
190
191 // Validate payment ID.
192 $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
193 if ( empty( $payment_id ) ) {
194 wp_send_json_error( [ 'message' => __( 'Payment ID is required.', 'sureforms' ) ] );
195 }
196
197 try {
198 // Get single payment from database.
199 $payment = Payments::get( $payment_id );
200
201 if ( ! $payment ) {
202 wp_send_json_error( [ 'message' => __( 'Payment not found.', 'sureforms' ) ] );
203 }
204
205 // Transform payment data for frontend.
206 $payment_data = $this->transform_payment_for_frontend( $payment );
207
208 wp_send_json_success( $payment_data );
209
210 } catch ( \Exception $e ) {
211 wp_send_json_error( [ 'message' => __( 'Unable to load payment details. Please try again.', 'sureforms' ) ] );
212 }
213 }
214
215 /**
216 * AJAX handler for fetching subscription data with billing history.
217 *
218 * @since 2.0.0
219 * @return void
220 */
221 public function fetch_subscription() {
222 // Verify nonce for security.
223 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
224 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
225 }
226
227 // Check user capabilities.
228 if ( ! current_user_can( 'manage_options' ) ) {
229 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
230 }
231
232 // Validate subscription ID - could be main subscription record ID or subscription_id.
233 $subscription_id = isset( $_POST['subscription_id'] ) ? sanitize_text_field( wp_unslash( $_POST['subscription_id'] ) ) : '';
234 if ( empty( $subscription_id ) ) {
235 wp_send_json_error( [ 'message' => __( 'Subscription ID is required.', 'sureforms' ) ] );
236 }
237
238 try {
239 // Check if the ID is a payment record ID first.
240 if ( is_numeric( $subscription_id ) ) {
241 $payment_record = Payments::get( absint( $subscription_id ) );
242 if ( $payment_record && 'subscription' === ( $payment_record['type'] ?? '' ) ) {
243 // This is a main subscription record ID, get the Stripe subscription ID.
244 $stripe_subscription_id = $payment_record['subscription_id'] ?? '';
245 if ( empty( $stripe_subscription_id ) ) {
246 wp_send_json_error( [ 'message' => __( 'Stripe subscription ID not found in payment record.', 'sureforms' ) ] );
247 }
248 $main_subscription = $payment_record;
249 } else {
250 wp_send_json_error( [ 'message' => __( 'Invalid subscription record.', 'sureforms' ) ] );
251 }
252 } else {
253 // This should be a Stripe subscription ID, get the main subscription record.
254 $main_subscription = Payments::get_main_subscription_record( $subscription_id );
255 if ( ! $main_subscription ) {
256 wp_send_json_error( [ 'message' => __( 'Subscription not found.', 'sureforms' ) ] );
257 }
258 $stripe_subscription_id = $subscription_id;
259 }
260
261 // Get all related billing transactions for this subscription.
262 $billing_payments = Payments::get_subscription_related_payments( $stripe_subscription_id );
263
264 // Transform main subscription data for frontend.
265 $subscription_data = $this->transform_payment_for_frontend( $main_subscription );
266
267 // Transform billing payments for frontend.
268 $billing_data = [];
269 foreach ( $billing_payments as $payment ) {
270 $billing_data[] = $this->transform_payment_for_frontend( $payment );
271 }
272
273 // Add subscription-specific fields.
274 $subscription_data['stripe_subscription_id'] = $stripe_subscription_id;
275 $subscription_data['interval'] = $this->get_subscription_interval( $main_subscription );
276 $subscription_data['next_payment_date'] = $this->get_next_payment_date( $main_subscription );
277 $subscription_data['amount_per_cycle'] = $subscription_data['total_amount']; // Use total_amount as cycle amount.
278
279 // Combine data.
280 $response_data = [
281 'subscription' => $subscription_data,
282 'payments' => $billing_data,
283 ];
284
285 /**
286 * Filter subscription details response data.
287 *
288 * Allows payment gateways (PayPal, Stripe, etc.) to add gateway-specific
289 * subscription fields to the response data sent to the frontend.
290 *
291 * @since 2.0.0
292 * @param array $response_data Response data containing subscription and payments.
293 * @param array $args Additional arguments including the main subscription record.
294 */
295 $response_data = apply_filters( 'srfm_subscription_details_response', $response_data, [ 'subscription' => $main_subscription ] );
296
297 wp_send_json_success( $response_data );
298
299 } catch ( \Exception $e ) {
300 wp_send_json_error( [ 'message' => __( 'Unable to load subscription details. Please try again.', 'sureforms' ) ] );
301 }
302 }
303
304 /**
305 * AJAX handler for fetching forms list.
306 *
307 * @since 2.0.0
308 * @return void
309 */
310 public function fetch_forms_list() {
311 // Verify nonce for security.
312 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
313 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
314 }
315
316 // Check user capabilities.
317 if ( ! current_user_can( 'manage_options' ) ) {
318 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
319 }
320
321 try {
322 // Get all published forms.
323 $all_forms = \SRFM\Inc\Helper::get_sureforms();
324
325 // Get form IDs that have payments.
326 $forms_with_payments = \SRFM\Inc\Database\Tables\Payments::get_all_forms_with_payments();
327
328 // Filter forms to only include those with payments.
329 $forms_list = [];
330 foreach ( $all_forms as $form_id => $form_title ) {
331 // Only include forms that have payment records.
332 if ( in_array( (int) $form_id, $forms_with_payments, true ) ) {
333 $forms_list[] = [
334 'id' => $form_id,
335 /* translators: %d: Form ID */
336 'title' => ! empty( $form_title ) ? $form_title : sprintf( __( 'Form - #%d', 'sureforms' ), $form_id ),
337 ];
338 }
339 }
340
341 wp_send_json_success( [ 'forms' => $forms_list ] );
342
343 } catch ( \Exception $e ) {
344 wp_send_json_error( [ 'message' => __( 'Unable to load forms. Please try again.', 'sureforms' ) ] );
345 }
346 }
347
348 /**
349 * AJAX handler for adding a note to payment.
350 *
351 * @since 2.0.0
352 * @return void
353 */
354 public function ajax_add_note() {
355 // Verify nonce for security.
356 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
357 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
358 }
359
360 // Check user capabilities.
361 if ( ! current_user_can( 'manage_options' ) ) {
362 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
363 }
364
365 // Validate and sanitize inputs.
366 $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
367 $note_text = isset( $_POST['note_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['note_text'] ) ) : '';
368
369 if ( empty( $payment_id ) ) {
370 wp_send_json_error( [ 'message' => __( 'Payment ID is required.', 'sureforms' ) ] );
371 }
372
373 if ( empty( trim( $note_text ) ) ) {
374 wp_send_json_error( [ 'message' => __( 'Note text cannot be empty.', 'sureforms' ) ] );
375 }
376
377 try {
378 // Add the note.
379 $updated_notes = $this->add_payment_note( $payment_id, $note_text );
380
381 if ( false === $updated_notes ) {
382 wp_send_json_error( [ 'message' => __( 'Failed to add note.', 'sureforms' ) ] );
383 }
384
385 wp_send_json_success( [ 'notes' => $this->get_formatted_notes( $updated_notes ) ] );
386
387 } catch ( \Exception $e ) {
388 wp_send_json_error( [ 'message' => __( 'Unable to add note. Please try again.', 'sureforms' ) ] );
389 }
390 }
391
392 /**
393 * AJAX handler for deleting a note from payment.
394 *
395 * @since 2.0.0
396 * @return void
397 */
398 public function ajax_delete_note() {
399 // Verify nonce for security.
400 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
401 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
402 }
403
404 // Check user capabilities.
405 if ( ! current_user_can( 'manage_options' ) ) {
406 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
407 }
408
409 // Validate and sanitize inputs.
410 $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
411 $note_index = isset( $_POST['note_index'] ) ? absint( $_POST['note_index'] ) : -1;
412
413 if ( empty( $payment_id ) ) {
414 wp_send_json_error( [ 'message' => __( 'Payment ID is required.', 'sureforms' ) ] );
415 }
416
417 if ( $note_index < 0 ) {
418 wp_send_json_error( [ 'message' => __( 'Invalid note index.', 'sureforms' ) ] );
419 }
420
421 try {
422 // Delete the note.
423 $updated_notes = $this->delete_payment_note( $payment_id, $note_index );
424
425 if ( false === $updated_notes ) {
426 wp_send_json_error( [ 'message' => __( 'Failed to delete note.', 'sureforms' ) ] );
427 }
428
429 wp_send_json_success( [ 'notes' => $this->get_formatted_notes( $updated_notes ) ] );
430
431 } catch ( \Exception $e ) {
432 wp_send_json_error( [ 'message' => __( 'Unable to delete note. Please try again.', 'sureforms' ) ] );
433 }
434 }
435
436 /**
437 * AJAX handler for deleting a log entry from payment.
438 *
439 * @since 2.0.0
440 * @return void
441 */
442 public function ajax_delete_log() {
443 // Verify nonce for security.
444 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
445 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
446 }
447
448 // Check user capabilities.
449 if ( ! current_user_can( 'manage_options' ) ) {
450 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
451 }
452
453 // Validate and sanitize inputs.
454 $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
455 $log_index = isset( $_POST['log_index'] ) ? absint( $_POST['log_index'] ) : -1;
456
457 if ( empty( $payment_id ) ) {
458 wp_send_json_error( [ 'message' => __( 'Payment ID is required.', 'sureforms' ) ] );
459 }
460
461 if ( $log_index < 0 ) {
462 wp_send_json_error( [ 'message' => __( 'Invalid log index.', 'sureforms' ) ] );
463 }
464
465 try {
466 // Delete the log entry.
467 $updated_logs = $this->delete_payment_log( $payment_id, $log_index );
468
469 if ( false === $updated_logs ) {
470 wp_send_json_error( [ 'message' => __( 'Failed to delete log entry.', 'sureforms' ) ] );
471 }
472
473 wp_send_json_success( [ 'logs' => $updated_logs ] );
474
475 } catch ( \Exception $e ) {
476 wp_send_json_error( [ 'message' => __( 'Unable to delete log. Please try again.', 'sureforms' ) ] );
477 }
478 }
479
480 /**
481 * AJAX handler for bulk deleting payments.
482 *
483 * @since 2.0.0
484 * @return void
485 */
486 public function ajax_bulk_delete_payments() {
487 // Verify nonce for security.
488 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'srfm_payment_admin_nonce' ) ) {
489 wp_send_json_error( [ 'message' => __( 'Security verification failed.', 'sureforms' ) ] );
490 }
491
492 // Check user capabilities.
493 if ( ! current_user_can( 'manage_options' ) ) {
494 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'sureforms' ) ] );
495 }
496
497 // Get and validate payment IDs.
498 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below after JSON decode.
499 $payment_ids_raw = isset( $_POST['payment_ids'] ) ? wp_unslash( $_POST['payment_ids'] ) : [];
500
501 // Handle JSON string or array format.
502 if ( is_string( $payment_ids_raw ) ) {
503 // Decode JSON string.
504 $payment_ids = json_decode( $payment_ids_raw, true );
505
506 // Check if JSON decode was successful.
507 if ( json_last_error() !== JSON_ERROR_NONE ) {
508 wp_send_json_error( [ 'message' => __( 'Invalid JSON format for payment IDs.', 'sureforms' ) ] );
509 }
510 } else {
511 $payment_ids = $payment_ids_raw;
512 }
513
514 // Ensure it's an array.
515 if ( ! is_array( $payment_ids ) ) {
516 wp_send_json_error( [ 'message' => __( 'Invalid payment IDs format.', 'sureforms' ) ] );
517 }
518
519 // Sanitize: Convert to integers and remove invalid values.
520 // This handles both string numbers ("169") and actual integers.
521 $payment_ids = array_map( 'absint', $payment_ids );
522 $payment_ids = array_filter(
523 $payment_ids,
524 static function ( $id ) {
525 return $id > 0;
526 }
527 );
528
529 // Re-index array to ensure sequential keys.
530 $payment_ids = array_values( $payment_ids );
531
532 // Check if array is empty after sanitization.
533 if ( empty( $payment_ids ) ) {
534 wp_send_json_error( [ 'message' => __( 'No valid payment IDs provided.', 'sureforms' ) ] );
535 }
536
537 // Limit bulk operations to prevent timeout (max 100 at once).
538 if ( count( $payment_ids ) > 100 ) {
539 wp_send_json_error(
540 [
541 'message' => __( 'Cannot delete more than 100 payments at once. Select fewer payments.', 'sureforms' ),
542 ]
543 );
544 }
545
546 try {
547 $deleted_count = 0;
548 $failed_ids = [];
549
550 // Delete each payment with proper error handling.
551 foreach ( $payment_ids as $payment_id ) {
552 // Verify payment exists before attempting delete.
553 $payment = Payments::get( $payment_id );
554
555 if ( ! $payment ) {
556 $failed_ids[] = $payment_id;
557 continue;
558 }
559
560 // Attempt deletion.
561 $result = Payments::delete( $payment_id );
562
563 if ( $result ) {
564 $deleted_count++;
565 } else {
566 $failed_ids[] = $payment_id;
567 }
568 }
569
570 // Prepare response message.
571 if ( count( $payment_ids ) === $deleted_count ) {
572 // All deleted successfully.
573 wp_send_json_success(
574 [
575 'message' => sprintf(
576 /* translators: %d: number of payments deleted */
577 _n(
578 '%d payment deleted successfully.',
579 '%d payments deleted successfully.',
580 $deleted_count,
581 'sureforms'
582 ),
583 $deleted_count
584 ),
585 'deleted_count' => $deleted_count,
586 ]
587 );
588 } elseif ( $deleted_count > 0 ) {
589 // Partial success.
590 wp_send_json_success(
591 [
592 'message' => sprintf(
593 /* translators: 1: number deleted, 2: number failed */
594 __( '%1$d payment(s) deleted successfully. %2$d failed.', 'sureforms' ),
595 $deleted_count,
596 count( $failed_ids )
597 ),
598 'deleted_count' => $deleted_count,
599 'failed_count' => count( $failed_ids ),
600 'partial' => true,
601 ]
602 );
603 } else {
604 // All failed.
605 wp_send_json_error(
606 [
607 'message' => __( 'Failed to delete payments. Please try again.', 'sureforms' ),
608 'failed_count' => count( $failed_ids ),
609 ]
610 );
611 }
612 } catch ( \Exception $e ) {
613 wp_send_json_error(
614 [
615 'message' => __( 'Unable to delete payments. Please try again.', 'sureforms' ),
616 ]
617 );
618 }
619 }
620
621 /**
622 * AJAX handler for payment refund.
623 *
624 * Gateway-agnostic refund handler that routes refund requests to the appropriate
625 * payment gateway (Stripe, PayPal, etc.) using a filter system.
626 *
627 * @since 2.0.0
628 * @return void
629 */
630 public function ajax_refund_payment() {
631 // Verify nonce for security.
632 if (
633 ! wp_verify_nonce(
634 sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ),
635 'srfm_payment_admin_nonce'
636 )
637 ) {
638 wp_send_json_error( __( 'Invalid nonce.', 'sureforms' ) );
639 }
640
641 // Check if user has permission to refund payments.
642 if ( ! current_user_can( 'manage_options' ) ) {
643 wp_send_json_error( __( 'Insufficient permissions.', 'sureforms' ) );
644 }
645
646 // Get and validate input parameters.
647 $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
648 $transaction_id = isset( $_POST['transaction_id'] ) ? sanitize_text_field( wp_unslash( $_POST['transaction_id'] ) ) : '';
649 $refund_amount = isset( $_POST['refund_amount'] ) ? absint( $_POST['refund_amount'] ) : 0;
650 $refund_notes = isset( $_POST['refund_notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['refund_notes'] ) ) : '';
651
652 // Validate required parameters.
653 if ( empty( $payment_id ) || empty( $transaction_id ) || $refund_amount <= 0 ) {
654 wp_send_json_error( __( 'Invalid payment data.', 'sureforms' ) );
655 }
656
657 try {
658 // Get payment from database.
659 $payment = Payments::get( $payment_id );
660 if ( ! $payment ) {
661 wp_send_json_error( __( 'Payment not found.', 'sureforms' ) );
662 }
663
664 // Get payment gateway (stripe, paypal, etc.).
665 $gateway = isset( $payment['gateway'] ) && is_string( $payment['gateway'] ) ? $payment['gateway'] : '';
666
667 if ( empty( $gateway ) ) {
668 wp_send_json_error( __( 'Payment gateway not found.', 'sureforms' ) );
669 }
670
671 /**
672 * Filter: srfm_process_transaction_refund
673 *
674 * Allows payment gateways to process refunds for their transactions.
675 * Gateway handlers should hook into this filter and check if the gateway matches theirs.
676 *
677 * @since 2.0.0
678 *
679 * @param array<string,mixed> $refund_result {
680 * Refund result array. Gateway handlers should return success/error status.
681 *
682 * @type bool $success Whether the refund was successful.
683 * @type string $message Success or error message.
684 * @type array $data Optional. Additional data like refund_id, status, etc.
685 * }
686 * @param array<string,mixed> $refund_args {
687 * Arguments passed to gateway refund handlers.
688 *
689 * @type array $payment Full payment record from database.
690 * @type int $payment_id Payment record ID.
691 * @type string $transaction_id Transaction/charge ID from gateway.
692 * @type int $refund_amount Refund amount in smallest currency unit (cents for USD).
693 * @type string $refund_notes Optional refund notes/reason.
694 * @type string $gateway Payment gateway identifier (stripe, paypal, etc.).
695 * }
696 */
697 $refund_result = apply_filters(
698 'srfm_process_transaction_refund',
699 [
700 'success' => false,
701 'message' => sprintf(
702 /* translators: %s: payment gateway name */
703 __( 'Refund processing is not supported for %s gateway.', 'sureforms' ),
704 ucfirst( $gateway )
705 ),
706 'data' => [],
707 ],
708 [
709 'payment' => $payment,
710 'payment_id' => $payment_id,
711 'transaction_id' => $transaction_id,
712 'refund_amount' => $refund_amount,
713 'refund_notes' => $refund_notes,
714 'gateway' => $gateway,
715 ]
716 );
717
718 // Check if refund was successful.
719 if ( ! empty( $refund_result['success'] ) && true === $refund_result['success'] ) {
720 $result_data = isset( $refund_result['data'] ) && is_array( $refund_result['data'] ) ? $refund_result['data'] : [];
721 wp_send_json_success(
722 [
723 'message' => ! empty( $refund_result['message'] ) ? $refund_result['message'] : __( 'Payment refunded successfully.', 'sureforms' ),
724 'refund_id' => ! empty( $result_data['refund_id'] ) ? $result_data['refund_id'] : '',
725 'status' => ! empty( $result_data['status'] ) ? $result_data['status'] : 'processed',
726 ]
727 );
728 } else {
729 // Refund failed - return error message.
730 wp_send_json_error(
731 ! empty( $refund_result['message'] )
732 ? $refund_result['message']
733 : __( 'Failed to process refund. Please try again.', 'sureforms' )
734 );
735 }
736 } catch ( \Exception $e ) {
737 wp_send_json_error( __( 'Unable to process refund. Please try again.', 'sureforms' ) );
738 }
739 }
740
741 /**
742 * Get payments data based on filters.
743 *
744 * @param string $search Search term.
745 * @param string $status Payment status filter.
746 * @param string $date_from Start date filter.
747 * @param string $date_to End date filter.
748 * @param int $limit Number of records to return.
749 * @param int $offset Number of records to skip.
750 * @param int $form_id Form ID filter.
751 * @param string $payment_mode Payment mode filter (test/live).
752 * @since 2.0.0
753 * @return array Filtered payments data.
754 */
755 private function get_payments_data( $search = '', $status = '', $date_from = '', $date_to = '', $limit = 20, $offset = 0, $form_id = 0, $payment_mode = '' ) {
756 // Build WHERE conditions for database query.
757 $where_conditions = [];
758
759 // Add search filter - search in form names, customer data, etc.
760 if ( ! empty( $search ) ) {
761 global $wpdb;
762 $search_term = '%' . $wpdb->esc_like( $search ) . '%';
763 $where_conditions[] = [
764 [
765 'key' => 'id',
766 'compare' => 'IN',
767 'value' => $this->get_payment_ids_by_search( $search_term ),
768 ],
769 ];
770 }
771
772 // Add status filter - map frontend status to database status.
773 if ( ! empty( $status ) ) {
774 $db_status = $this->map_frontend_status_to_db( $status );
775 if ( $db_status ) {
776 $where_conditions[] = [
777 [
778 'key' => 'status',
779 'compare' => '=',
780 'value' => $db_status,
781 ],
782 ];
783 }
784 }
785
786 // Add form_id filter.
787 if ( ! empty( $form_id ) ) {
788 $where_conditions[] = [
789 [
790 'key' => 'form_id',
791 'compare' => '=',
792 'value' => $form_id,
793 ],
794 ];
795 }
796
797 // Add payment mode filter (test/live).
798 if ( ! empty( $payment_mode ) && in_array( $payment_mode, [ 'test', 'live' ], true ) ) {
799 $where_conditions[] = [
800 [
801 'key' => 'mode',
802 'compare' => '=',
803 'value' => $payment_mode,
804 ],
805 ];
806 }
807
808 // Add date range filter.
809 if ( ! empty( $date_from ) || ! empty( $date_to ) ) {
810 if ( ! empty( $date_from ) && ! empty( $date_to ) ) {
811 $where_conditions[] = [
812 [
813 'key' => 'created_at',
814 'compare' => '>=',
815 'value' => $date_from . ' 00:00:00',
816 ],
817 [
818 'key' => 'created_at',
819 'compare' => '<=',
820 'value' => $date_to . ' 23:59:59',
821 ],
822 ];
823 } elseif ( ! empty( $date_from ) ) {
824 $where_conditions[] = [
825 [
826 'key' => 'created_at',
827 'compare' => '>=',
828 'value' => $date_from . ' 00:00:00',
829 ],
830 ];
831 } elseif ( ! empty( $date_to ) ) {
832 $where_conditions[] = [
833 [
834 'key' => 'created_at',
835 'compare' => '<=',
836 'value' => $date_to . ' 23:59:59',
837 ],
838 ];
839 }
840 }
841
842 // Get payments from database using the main payments method.
843 // Sorted by created_at in descending order (newest first).
844 $args = [
845 'where' => $where_conditions,
846 'limit' => $limit,
847 'offset' => $offset,
848 'orderby' => 'created_at',
849 'order' => 'DESC',
850 ];
851
852 $db_payments = Payments::get_all_main_payments( $args, true );
853
854 // Transform database records to frontend format.
855 $formatted_payments = [];
856 foreach ( $db_payments as $payment ) {
857 $formatted_payments[] = $this->transform_payment_for_frontend( $payment );
858 }
859
860 return $formatted_payments;
861 }
862
863 /**
864 * Get payment IDs that match search criteria.
865 *
866 * @param string $search_term Search term with wildcards.
867 * @since 2.0.0
868 * @return array Array of payment IDs.
869 */
870 private function get_payment_ids_by_search( $search_term ) {
871 global $wpdb;
872
873 // Get payments table name.
874 $payments_table = Payments::get_instance()->get_tablename();
875
876 if ( empty( $payments_table ) || ! is_string( $payments_table ) ) {
877 return [ 0 ];
878 }
879
880 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
881 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query for search, table name is validated and cannot be parameterized with prepare().
882 $results = $wpdb->get_col(
883 $wpdb->prepare(
884 "SELECT DISTINCT id FROM {$payments_table}
885 WHERE id LIKE %s
886 OR customer_name LIKE %s
887 OR customer_email LIKE %s
888 OR transaction_id LIKE %s
889 OR srfm_txn_id LIKE %s",
890 $search_term,
891 $search_term,
892 $search_term,
893 $search_term,
894 $search_term
895 )
896 );
897 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
898
899 // If no results found, return array with 0 to prevent empty IN clause.
900 return ! empty( $results ) ? array_map( 'absint', $results ) : [ 0 ];
901 }
902
903 /**
904 * Map frontend status to database status.
905 *
906 * @param string $frontend_status Status from frontend.
907 * @since 2.0.0
908 * @return string|false Database status or false if invalid.
909 */
910 private function map_frontend_status_to_db( $frontend_status ) {
911 $status_mapping = [
912 'succeeded' => 'succeeded',
913 'partially_refunded' => 'partially_refunded',
914 'pending' => 'pending',
915 'failed' => 'failed',
916 'refunded' => 'refunded',
917 'cancelled' => 'canceled',
918 ];
919
920 return $status_mapping[ $frontend_status ] ?? false;
921 }
922
923 /**
924 * Map database status to frontend status.
925 *
926 * @param string $db_status Status from database.
927 * @since 2.0.0
928 * @return string Frontend status.
929 */
930 private function map_db_status_to_frontend( $db_status ) {
931 $status_mapping = [
932 'succeeded' => 'paid',
933 'pending' => 'pending',
934 'failed' => 'failed',
935 'refunded' => 'refunded',
936 'partially_refunded' => 'refunded',
937 'canceled' => 'canceled',
938 'requires_action' => 'pending',
939 'requires_payment_method' => 'pending',
940 'processing' => 'pending',
941 ];
942
943 return $status_mapping[ $db_status ] ?? $db_status;
944 }
945
946 /**
947 * Transform database payment record to frontend format.
948 *
949 * @param array<mixed> $payment Database payment record.
950 * @since 2.0.0
951 * @return array Transformed payment data.
952 */
953 private function transform_payment_for_frontend( $payment ) {
954 static $form_titles = []; // Cache for form titles.
955
956 // Get form title with caching using WordPress built-in function.
957 $form_id = isset( $payment['form_id'] ) && ! empty( $payment['form_id'] ) && is_numeric( $payment['form_id'] ) ? intval( $payment['form_id'] ) : 0;
958 if ( is_numeric( $form_id ) && ! isset( $form_titles[ $form_id ] ) ) {
959 $form_title = get_the_title( intval( $form_id ) );
960 $form_titles[ $form_id ] = ! empty( $form_title ) ? html_entity_decode( $form_title, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) : __( 'Unknown Form', 'sureforms' );
961 }
962 $form_title = isset( $form_titles[ $form_id ] ) && ! empty( $form_titles[ $form_id ] ) ? $form_titles[ $form_id ] : __( 'Unknown Form', 'sureforms' );
963 $form_permalink = isset( $form_titles[ $form_id ] ) && ! empty( $form_titles[ $form_id ] ) ? get_permalink( intval( $form_id ) ) : '';
964 $form_url = ! empty( $form_permalink ) && is_string( $form_permalink ) ? html_entity_decode( $form_permalink ) : '';
965
966 // Get customer name - for now use customer_id, in real implementation.
967 // You would get customer data from entries or payment_data.
968 $customer_name = ! empty( $payment['customer_name'] ) ? $payment['customer_name'] : __( 'N/A', 'sureforms' );
969 $customer_email = ! empty( $payment['customer_email'] ) ? $payment['customer_email'] : __( 'N/A', 'sureforms' );
970 $notes = Payments::get_extra_value( $payment['id'], 'notes', [] );
971 $notes = ! empty( $notes ) && is_array( $notes ) ? $notes : [];
972
973 // Determine payment type display label.
974 if ( 'subscription' === $payment['type'] ) {
975 $payment_type = __( 'Subscription', 'sureforms' );
976 } elseif ( 'renewal' === $payment['type'] ) {
977 $payment_type = __( 'Renewal', 'sureforms' );
978 } else {
979 $payment_type = __( 'One Time', 'sureforms' );
980 }
981
982 $payment_front_end_data = [
983 // All original payment_data fields.
984 'id' => $payment['id'],
985 'form_id' => $payment['form_id'],
986 'block_id' => $payment['block_id'] ?? '',
987 'status' => $payment['status'],
988 'total_amount' => $payment['total_amount'],
989 'refunded_amount' => $payment['refunded_amount'] ?? '0.00000000',
990 'currency' => $payment['currency'],
991 'entry_id' => $payment['entry_id'] ?? '',
992 'gateway' => $payment['gateway'],
993 'type' => $payment['type'],
994 'mode' => $payment['mode'] ?? '',
995 'transaction_id' => $payment['transaction_id'] ?? '',
996 'customer_id' => $payment['customer_id'] ?? '',
997 'subscription_id' => $payment['subscription_id'] ?? '',
998 'subscription_status' => $payment['subscription_status'] ?? '',
999 'parent_subscription_id' => $payment['parent_subscription_id'] ?? '0',
1000 'payment_data' => $payment['payment_data'] ?? '{}',
1001 'extra' => $payment['extra'] ?? '[]',
1002 'log' => $payment['log'] ?? '[]',
1003 'created_at' => $payment['created_at'],
1004 'updated_at' => $payment['updated_at'],
1005 'srfm_txn_id' => $payment['srfm_txn_id'],
1006
1007 // Additional frontend fields.
1008 'form_title' => $form_title,
1009 'form_url' => $form_url,
1010 'form' => $form_title, // Keep for backward compatibility.
1011 'customer_name' => $customer_name,
1012 'customer_email' => $customer_email,
1013 'amount' => floatval( $payment['total_amount'] ),
1014 'frontend_status' => $this->map_db_status_to_frontend( $payment['status'] ),
1015 'datetime' => $payment['created_at'], // Keep for backward compatibility.
1016 'payment_type' => $payment_type,
1017 'notes' => $this->get_formatted_notes( $notes ),
1018 'logs' => $this->get_formatted_logs( $payment['log'] ),
1019 ];
1020
1021 return apply_filters( 'srfm_payment_admin_data', $payment_front_end_data, $payment );
1022 }
1023
1024 /**
1025 * Get total count of payments with filters.
1026 *
1027 * @param string $search Search term.
1028 * @param string $status Payment status filter.
1029 * @param string $date_from Start date filter.
1030 * @param string $date_to End date filter.
1031 * @param int $form_id Form ID filter.
1032 * @param string $payment_mode Payment mode filter (test/live).
1033 * @since 2.0.0
1034 * @return int Total count.
1035 */
1036 private function get_payments_count( $search = '', $status = '', $date_from = '', $date_to = '', $form_id = 0, $payment_mode = '' ) {
1037 // Build WHERE conditions similar to get_payments_data.
1038 $where_conditions = [];
1039
1040 if ( ! empty( $search ) ) {
1041 global $wpdb;
1042 $search_term = '%' . $wpdb->esc_like( $search ) . '%';
1043 $where_conditions[] = [
1044 [
1045 'key' => 'id',
1046 'compare' => 'IN',
1047 'value' => $this->get_payment_ids_by_search( $search_term ),
1048 ],
1049 ];
1050 }
1051
1052 if ( ! empty( $status ) ) {
1053 $db_status = $this->map_frontend_status_to_db( $status );
1054 if ( $db_status ) {
1055 $where_conditions[] = [
1056 [
1057 'key' => 'status',
1058 'compare' => '=',
1059 'value' => $db_status,
1060 ],
1061 ];
1062 }
1063 }
1064
1065 // Add form_id filter.
1066 if ( ! empty( $form_id ) ) {
1067 $where_conditions[] = [
1068 [
1069 'key' => 'form_id',
1070 'compare' => '=',
1071 'value' => $form_id,
1072 ],
1073 ];
1074 }
1075
1076 // Add payment mode filter (test/live).
1077 if ( ! empty( $payment_mode ) && in_array( $payment_mode, [ 'test', 'live' ], true ) ) {
1078 $where_conditions[] = [
1079 [
1080 'key' => 'mode',
1081 'compare' => '=',
1082 'value' => $payment_mode,
1083 ],
1084 ];
1085 }
1086
1087 if ( ! empty( $date_from ) || ! empty( $date_to ) ) {
1088 if ( ! empty( $date_from ) && ! empty( $date_to ) ) {
1089 $where_conditions[] = [
1090 [
1091 'key' => 'created_at',
1092 'compare' => '>=',
1093 'value' => $date_from . ' 00:00:00',
1094 ],
1095 [
1096 'key' => 'created_at',
1097 'compare' => '<=',
1098 'value' => $date_to . ' 23:59:59',
1099 ],
1100 ];
1101 } elseif ( ! empty( $date_from ) ) {
1102 $where_conditions[] = [
1103 [
1104 'key' => 'created_at',
1105 'compare' => '>=',
1106 'value' => $date_from . ' 00:00:00',
1107 ],
1108 ];
1109 } elseif ( ! empty( $date_to ) ) {
1110 $where_conditions[] = [
1111 [
1112 'key' => 'created_at',
1113 'compare' => '<=',
1114 'value' => $date_to . ' 23:59:59',
1115 ],
1116 ];
1117 }
1118 }
1119
1120 return Payments::get_total_main_payments_by_status( 'all', 0, $where_conditions );
1121 }
1122
1123 /**
1124 * Get subscription billing interval from payment data.
1125 *
1126 * @param array<mixed> $subscription_record Main subscription payment record.
1127 * @since 2.0.0
1128 * @return string Billing interval.
1129 */
1130 private function get_subscription_interval( $subscription_record ) {
1131 // Try to get interval from payment_data.
1132 if ( ! empty( $subscription_record['payment_data'] ) ) {
1133 $payment_data = \SRFM\Inc\Helper::get_array_value( $subscription_record['payment_data'] );
1134
1135 // Check various possible locations for interval data.
1136 $interval_paths = [
1137 'subscription.items.data.0.price.recurring.interval',
1138 'subscription.plan.interval',
1139 'price.recurring.interval',
1140 'plan.interval',
1141 'interval',
1142 ];
1143
1144 foreach ( $interval_paths as $path ) {
1145 $interval = $this->get_nested_array_value( $payment_data, $path );
1146 if ( ! empty( $interval ) && is_string( $interval ) ) {
1147 return ucfirst( $interval ); // month -> Month, year -> Year.
1148 }
1149 }
1150 }
1151
1152 return __( 'Unknown', 'sureforms' );
1153 }
1154
1155 /**
1156 * Get next payment date from subscription data.
1157 *
1158 * @param array<mixed> $subscription_record Main subscription payment record.
1159 * @since 2.0.0
1160 * @return string|null Next payment date or null.
1161 */
1162 private function get_next_payment_date( $subscription_record ) {
1163 // Try to get next payment date from payment_data.
1164 if ( ! empty( $subscription_record['payment_data'] ) ) {
1165 $payment_data = \SRFM\Inc\Helper::get_array_value( $subscription_record['payment_data'] );
1166
1167 // Check various possible locations for next payment date.
1168 $date_paths = [
1169 'subscription.current_period_end',
1170 'current_period_end',
1171 'next_payment_attempt',
1172 ];
1173
1174 foreach ( $date_paths as $path ) {
1175 $timestamp = $this->get_nested_array_value( $payment_data, $path );
1176 if ( ! empty( $timestamp ) && is_numeric( $timestamp ) ) {
1177 $timestamp_int = intval( $timestamp );
1178 return gmdate( 'Y-m-d H:i:s', $timestamp_int );
1179 }
1180 }
1181 }
1182
1183 return null;
1184 }
1185
1186 /**
1187 * Get nested value from array using dot notation.
1188 *
1189 * @param array<mixed> $array Array to search.
1190 * @param string $path Dot-separated path.
1191 * @since 2.0.0
1192 * @return mixed Value or null if not found.
1193 */
1194 private function get_nested_array_value( $array, $path ) {
1195 $keys = explode( '.', $path );
1196 $current = $array;
1197
1198 foreach ( $keys as $key ) {
1199 if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) {
1200 return null;
1201 }
1202 $current = $current[ $key ];
1203 }
1204
1205 return $current;
1206 }
1207
1208 /**
1209 * Validate date format (YYYY-MM-DD).
1210 *
1211 * @param string $date Date string to validate.
1212 * @since 2.0.0
1213 * @return bool True if valid, false otherwise.
1214 */
1215 private function validate_date( $date ) {
1216 $parsed_date = \DateTime::createFromFormat( 'Y-m-d', $date );
1217 return $parsed_date && $parsed_date->format( 'Y-m-d' ) === $date;
1218 }
1219
1220 /**
1221 * Add a note to payment.
1222 *
1223 * @param int $payment_id Payment ID.
1224 * @param string $note_text Note text to add.
1225 * @since 2.0.0
1226 * @return array|false Updated notes array or false on failure.
1227 */
1228 private function add_payment_note( $payment_id, $note_text ) {
1229 if ( empty( $payment_id ) || empty( trim( $note_text ) ) ) {
1230 return false;
1231 }
1232
1233 // Verify payment exists.
1234 $payment = Payments::get( $payment_id );
1235 if ( ! $payment ) {
1236 return false;
1237 }
1238
1239 // Get current notes from extra data.
1240 $notes = Payments::get_extra_value( $payment_id, 'notes', [] );
1241
1242 // Ensure notes is an array.
1243 if ( ! is_array( $notes ) ) {
1244 $notes = [];
1245 }
1246
1247 // Create new note with metadata.
1248 $new_note = [
1249 'text' => trim( $note_text ),
1250 'created_at' => current_time( 'mysql' ),
1251 'created_by' => get_current_user_id(),
1252 ];
1253
1254 // Add new note to the beginning of the array (most recent first).
1255 array_unshift( $notes, $new_note );
1256
1257 // Update extra data with new notes array.
1258 $result = Payments::update_extra_key( $payment_id, 'notes', $notes );
1259
1260 if ( false === $result ) {
1261 return false;
1262 }
1263
1264 return $notes;
1265 }
1266
1267 /**
1268 * Delete a note from payment by index.
1269 *
1270 * @param int $payment_id Payment ID.
1271 * @param int $note_index Index of note to delete.
1272 * @since 2.0.0
1273 * @return array|false Updated notes array or false on failure.
1274 */
1275 private function delete_payment_note( $payment_id, $note_index ) {
1276 if ( empty( $payment_id ) || $note_index < 0 ) {
1277 return false;
1278 }
1279
1280 // Verify payment exists.
1281 $payment = Payments::get( $payment_id );
1282 if ( ! $payment ) {
1283 return false;
1284 }
1285
1286 // Get current notes.
1287 $notes = Payments::get_extra_value( $payment_id, 'notes', [] );
1288
1289 // Ensure notes is an array.
1290 if ( ! is_array( $notes ) ) {
1291 return false;
1292 }
1293
1294 // Check if note index exists.
1295 if ( ! isset( $notes[ $note_index ] ) ) {
1296 return false;
1297 }
1298
1299 // Remove note at specified index.
1300 array_splice( $notes, $note_index, 1 );
1301
1302 // Re-index array to prevent gaps.
1303 $notes = array_values( $notes );
1304
1305 // Update extra data with modified notes array.
1306 $result = Payments::update_extra_key( $payment_id, 'notes', $notes );
1307
1308 if ( false === $result ) {
1309 return false;
1310 }
1311
1312 return $notes;
1313 }
1314
1315 /**
1316 * Delete a log entry from payment by index.
1317 *
1318 * @param int $payment_id Payment ID.
1319 * @param int $log_index Index of log entry to delete.
1320 * @since 2.0.0
1321 * @return array|false Updated formatted logs array or false on failure.
1322 */
1323 private function delete_payment_log( $payment_id, $log_index ) {
1324 if ( empty( $payment_id ) || $log_index < 0 ) {
1325 return false;
1326 }
1327
1328 // Verify payment exists.
1329 $payment = Payments::get( $payment_id );
1330 if ( ! $payment ) {
1331 return false;
1332 }
1333
1334 // Get current logs from log column.
1335 $logs_data = $payment['log'] ?? '[]';
1336 $logs = json_decode( $logs_data, true ) ?? [];
1337
1338 // Ensure logs is an array.
1339 if ( ! is_array( $logs ) ) {
1340 return false;
1341 }
1342
1343 // Check if log index exists.
1344 if ( ! isset( $logs[ $log_index ] ) ) {
1345 return false;
1346 }
1347
1348 // Remove log at specified index.
1349 array_splice( $logs, $log_index, 1 );
1350
1351 // Re-index array to prevent gaps.
1352 $logs = array_values( $logs );
1353
1354 // Update log column with modified logs array.
1355 $result = Payments::update(
1356 $payment_id,
1357 [
1358 'log' => wp_json_encode( $logs ),
1359 ]
1360 );
1361
1362 if ( false === $result ) {
1363 return false;
1364 }
1365
1366 $encoded_logs = wp_json_encode( $logs );
1367 $encoded_logs = is_string( $encoded_logs ) ? $encoded_logs : '';
1368
1369 // Return formatted logs for frontend.
1370 return $this->get_formatted_logs( $encoded_logs );
1371 }
1372
1373 /**
1374 * Get formatted logs from log data.
1375 *
1376 * @param string $log_data JSON encoded log data.
1377 * @since 2.0.0
1378 * @return array Formatted logs array.
1379 */
1380 private function get_formatted_logs( $log_data ) {
1381 if ( empty( $log_data ) ) {
1382 return [];
1383 }
1384
1385 // If already an array, use it directly.
1386 if ( is_array( $log_data ) ) {
1387 $logs = $log_data;
1388 } else {
1389 return [];
1390 }
1391
1392 if ( ! is_array( $logs ) ) {
1393 return [];
1394 }
1395
1396 $formatted_logs = [];
1397
1398 foreach ( $logs as $log ) {
1399 // Handle both object and array formats.
1400 if ( is_object( $log ) ) {
1401 $formatted_logs[] = [
1402 'title' => $log->title ?? '',
1403 'created_at' => $log->created_at ?? 0,
1404 'messages' => $log->messages ?? [],
1405 ];
1406 } elseif ( is_array( $log ) ) {
1407 $formatted_logs[] = [
1408 'title' => $log['title'] ?? '',
1409 'created_at' => $log['created_at'] ?? 0,
1410 'messages' => $log['messages'] ?? [],
1411 ];
1412 }
1413 }
1414
1415 return $formatted_logs;
1416 }
1417
1418 /**
1419 * Get formatted notes from notes data.
1420 *
1421 * @param array<mixed> $notes_data Notes array from extra data.
1422 * @since 2.0.0
1423 * @return array Formatted notes array.
1424 */
1425 private function get_formatted_notes( $notes_data ) {
1426 if ( empty( $notes_data ) ) {
1427 return [];
1428 }
1429
1430 // If not an array, return empty.
1431 if ( ! is_array( $notes_data ) ) {
1432 return [];
1433 }
1434
1435 $formatted_notes = [];
1436
1437 foreach ( $notes_data as $note ) {
1438 // Handle both object and array formats.
1439 if ( is_object( $note ) ) {
1440 $formatted_notes[] = [
1441 'text' => $note->text ?? '',
1442 'created_at' => $note->created_at ?? '',
1443 'created_by' => $note->created_by ?? 0,
1444 'created_by_user_name' => isset( $note->created_by ) && is_numeric( $note->created_by ) ? $this->get_user_detail_by_id( $note->created_by ) : __( 'Guest User', 'sureforms' ),
1445 ];
1446 } elseif ( is_array( $note ) ) {
1447 $formatted_notes[] = [
1448 'text' => $note['text'] ?? '',
1449 'created_at' => $note['created_at'] ?? '',
1450 'created_by' => $note['created_by'] ?? 0,
1451 'created_by_user_name' => isset( $note['created_by'] ) && is_numeric( $note['created_by'] ) ? $this->get_user_detail_by_id( $note['created_by'] ) : __( 'Guest User', 'sureforms' ),
1452 ];
1453 }
1454 }
1455
1456 return $formatted_notes;
1457 }
1458
1459 /**
1460 * Get user detail by ID.
1461 *
1462 * @param mixed $user_id User ID.
1463 * @since 2.0.0
1464 * @return string User name or 'Guest User' if user not found.
1465 */
1466 private function get_user_detail_by_id( $user_id ) {
1467 if ( empty( $user_id ) || ! is_numeric( $user_id ) || $user_id <= 0 ) {
1468 return __( 'Guest User', 'sureforms' );
1469 }
1470 $user_id = is_numeric( $user_id ) ? intval( $user_id ) : 0;
1471 if ( $user_id <= 0 ) {
1472 return __( 'Guest User', 'sureforms' );
1473 }
1474 $user = get_userdata( $user_id );
1475 $user_name = $user ? $user->user_login : '';
1476 return is_string( $user_name ) ? $user_name : __( 'Guest User', 'sureforms' );
1477 }
1478 }
1479