Uname:Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64

403WebShell
403Webshell
Server IP : 146.59.209.152  /  Your IP : 216.73.216.152
Web Server : Apache
System : Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64
User : monpetu ( 144298)
PHP Version : 7.4.33
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/monpetu/www/ipprint/wp-content/plugins/sureforms/inc/payments/stripe/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/monpetu/www/ipprint/wp-content/plugins/sureforms/inc/payments/stripe/admin-stripe-handler.php
<?php
/**
 * Admin Stripe Handler for SureForms
 *
 * Handles admin-related Stripe operations including refunds for payments and subscriptions.
 *
 * @package SureForms
 * @since 2.0.0
 */

namespace SRFM\Inc\Payments\Stripe;

use SRFM\Inc\Database\Tables\Payments;
use SRFM\Inc\Helper;
use SRFM\Inc\Traits\Get_Instance;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Admin Stripe Handler class.
 *
 * Manages admin operations for Stripe payments including refunds, cancellations,
 * and payment management for both one-time and subscription payments.
 *
 * @since 2.0.0
 */
class Admin_Stripe_Handler {
	use Get_Instance;

	/**
	 * Payment mode.
	 *
	 * @var string
	 * @since 2.0.0
	 */
	private string $payment_mode = 'test';

	/**
	 * Constructor
	 */
	public function __construct() {
		// AJAX handlers for admin refund operations.
		add_action( 'wp_ajax_srfm_stripe_cancel_subscription', [ $this, 'ajax_cancel_subscription' ] );
		add_action( 'wp_ajax_srfm_stripe_pause_subscription', [ $this, 'ajax_pause_subscription' ] );
		// Hook into unified refund filter system.
		add_filter( 'srfm_process_transaction_refund', [ $this, 'process_stripe_refund' ], 10, 2 );
		// Hook into unified subscription cancellation filter system.
		add_filter( 'srfm_process_subscription_cancellation', [ $this, 'process_stripe_subscription_cancellation' ], 10, 2 );
		// Admin notices.
		add_action( 'admin_notices', [ $this, 'webhook_configuration_notice' ] );
	}

	/**
	 * AJAX handler for subscription cancellation (following WPForms pattern)
	 *
	 * @since 2.0.0
	 */
	/**
	 * AJAX handler for subscription cancellation (following WPForms pattern)
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function ajax_cancel_subscription() {
		// Security checks.
		if ( ! isset( $_POST['payment_id'] ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Missing payment ID.', 'sureforms' ) ] );
		}

		// Verify nonce.
		if (
			! wp_verify_nonce(
				sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ),
				'srfm_payment_admin_nonce'
			)
		) {
			wp_send_json_error( __( 'Invalid nonce.', 'sureforms' ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to perform this action.', 'sureforms' ) ] );
		}

		$payment_id = absint( $_POST['payment_id'] );

		// Get payment record.
		$payment = Payments::get( $payment_id );

		$this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';
		if ( ! $payment ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'sureforms' ) ] );
		}

		// Validate it's a subscription payment.
		if ( empty( $payment['type'] ) || 'subscription' !== $payment['type'] ) {
			wp_send_json_error( [ 'message' => esc_html__( 'This is not a subscription payment.', 'sureforms' ) ] );
		}

		if ( empty( $payment['subscription_id'] ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Subscription ID not found.', 'sureforms' ) ] );
		}

		// Cancel via the gateway-agnostic filter so the payment's OWN gateway (Stripe or
		// PayPal) performs the cancellation. Previously this called the Stripe API directly,
		// which failed for PayPal subscriptions cancelled from the admin screen. Both gateways
		// hook 'srfm_process_subscription_cancellation' (Stripe + PayPal), matching the
		// frontend cancellation path.
		$cancel_result = apply_filters(
			'srfm_process_subscription_cancellation',
			[
				'success' => false,
				'message' => __( 'Cancellation is not supported for this payment gateway.', 'sureforms' ),
			],
			$payment
		);

		if ( empty( $cancel_result['success'] ) ) {
			wp_send_json_error(
				[
					'message' => ! empty( $cancel_result['message'] ) && is_string( $cancel_result['message'] )
						? esc_html( $cancel_result['message'] )
						: esc_html__( 'Subscription cancellation failed.', 'sureforms' ),
				]
			);
		}

		// The gateway callback (Stripe/PayPal) is the single source of truth: it has already
		// cancelled at the gateway AND persisted subscription_status + the "Subscription Canceled"
		// activity log. Just report success here — mirroring the frontend cancel path — so we don't
		// write the DB a second time or append a duplicate log entry.
		wp_send_json_success(
			[
				'message' => ! empty( $cancel_result['message'] ) && is_string( $cancel_result['message'] )
					? esc_html( $cancel_result['message'] )
					: esc_html__( 'Subscription cancelled successfully.', 'sureforms' ),
			]
		);
	}

	/**
	 * Process Stripe subscription cancellation via filter system.
	 *
	 * Filter callback for 'srfm_process_subscription_cancellation'.
	 * Used by both admin and frontend to cancel Stripe subscriptions.
	 *
	 * @since 2.8.0
	 * @param array<string,mixed> $result  Default result array.
	 * @param array<string,mixed> $payment Payment record from database.
	 * @return array<string,mixed> Result with success status and message.
	 */
	public function process_stripe_subscription_cancellation( $result, $payment ) {
		// Process Stripe payments. Stripe is the only gateway in the free plugin, and the
		// `gateway` column defaults to '' for legacy/imported rows — so an empty gateway is
		// treated as Stripe. Only an explicitly different gateway (e.g. 'paypal') is skipped.
		if ( ! empty( $payment['gateway'] ) && 'stripe' !== $payment['gateway'] ) {
			return $result;
		}

		if ( empty( $payment['subscription_id'] ) || ! is_string( $payment['subscription_id'] ) ) {
			return [
				'success' => false,
				'message' => __( 'Subscription ID not found.', 'sureforms' ),
			];
		}

		$subscription_id    = $payment['subscription_id'];
		$this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';

		$cancel_result = $this->cancel_subscription( $subscription_id );
		if ( ! $cancel_result ) {
			return [
				'success' => false,
				'message' => __( 'Subscription cancellation failed.', 'sureforms' ),
			];
		}

		// Build log entry.
		$current_logs = Helper::get_array_value( $payment['log'] );
		$log_messages = [
			sprintf(
				/* translators: %s: Stripe subscription ID */
				__( 'Subscription ID: %s', 'sureforms' ),
				$subscription_id
			),
			sprintf(
				/* translators: %s: payment gateway name */
				__( 'Payment Gateway: %s', 'sureforms' ),
				'Stripe'
			),
			sprintf(
				/* translators: %s: subscription status */
				__( 'Subscription Status: %s', 'sureforms' ),
				__( 'Canceled', 'sureforms' )
			),
			sprintf(
				/* translators: %s: user display name */
				__( 'Canceled by: %s', 'sureforms' ),
				wp_get_current_user()->display_name
			),
		];

		$current_logs[] = [
			'title'      => __( 'Subscription Canceled', 'sureforms' ),
			'created_at' => current_time( 'mysql' ),
			'messages'   => $log_messages,
		];

		$payment_id = isset( $payment['id'] ) && is_numeric( $payment['id'] ) ? absint( $payment['id'] ) : 0;
		// Preserve the transaction `status` so the admin Refund option stays enabled
		// after the customer cancels from the My Account page.
		Payments::update(
			$payment_id,
			[
				'subscription_status' => 'canceled',
				'log'                 => $current_logs,
			]
		);

		return [
			'success' => true,
			'message' => __( 'Subscription cancelled successfully.', 'sureforms' ),
		];
	}

	/**
	 * Process Stripe payment refund via filter system.
	 *
	 * Filter callback for 'srfm_process_transaction_refund' that handles Stripe refunds.
	 * Only processes refunds for payments with gateway = 'stripe'.
	 *
	 * @since 2.0.0
	 * @param array<string,mixed> $refund_result Default refund result.
	 * @param array<string,mixed> $refund_args {
	 *     Refund arguments from admin handler.
	 *
	 *     @type array  $payment        Full payment record from database.
	 *     @type int    $payment_id     Payment record ID.
	 *     @type string $transaction_id Transaction/charge ID from Stripe.
	 *     @type int    $refund_amount  Refund amount in smallest currency unit (cents for USD).
	 *     @type string $refund_notes   Optional refund notes/reason.
	 *     @type string $gateway        Payment gateway identifier.
	 * }
	 * @return array<string,mixed> Refund result with success status and message.
	 */
	public function process_stripe_refund( $refund_result, $refund_args ) {
		// Only process if this is a Stripe payment.
		if ( empty( $refund_args['gateway'] ) || 'stripe' !== $refund_args['gateway'] ) {
			return $refund_result;
		}

		// Extract arguments.
		$payment        = isset( $refund_args['payment'] ) && is_array( $refund_args['payment'] ) ? $refund_args['payment'] : [];
		$payment_id     = isset( $refund_args['payment_id'] ) && is_numeric( $refund_args['payment_id'] ) ? intval( $refund_args['payment_id'] ) : 0;
		$transaction_id = isset( $refund_args['transaction_id'] ) && is_string( $refund_args['transaction_id'] ) ? $refund_args['transaction_id'] : '';
		$refund_amount  = isset( $refund_args['refund_amount'] ) && is_numeric( $refund_args['refund_amount'] ) ? intval( $refund_args['refund_amount'] ) : 0;
		$refund_notes   = isset( $refund_args['refund_notes'] ) && is_string( $refund_args['refund_notes'] ) ? $refund_args['refund_notes'] : '';

		// Validate required data.
		if ( empty( $payment ) || empty( $payment_id ) || empty( $transaction_id ) || $refund_amount <= 0 ) {
			return [
				'success' => false,
				'message' => __( 'Invalid refund parameters.', 'sureforms' ),
				'data'    => [],
			];
		}

		try {
			$this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';

			// Detect subscription payments and route to specialized handler (following WPForms pattern).
			if ( isset( $payment['type'], $payment['subscription_id'] ) && ! empty( $payment['type'] ) && ! empty( $payment['subscription_id'] ) ) {
				return $this->refund_subscription_payment_via_filter( $payment, $refund_amount, $refund_notes );
			}

			// Verify payment status (for one-time payments).
			if ( isset( $payment['status'] ) && 'succeeded' !== $payment['status'] && 'partially_refunded' !== $payment['status'] ) {
				return [
					'success' => false,
					'message' => __( 'Only succeeded or partially refunded payments can be refunded.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Verify transaction ID matches.
			if ( isset( $payment['transaction_id'] ) && $transaction_id !== $payment['transaction_id'] ) {
				return [
					'success' => false,
					'message' => __( 'Transaction ID mismatch.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Create refund using Stripe API directly.
			$stripe_refund_data = [
				'amount'   => $refund_amount,
				'metadata' => [
					'source'      => 'SureForms',
					'payment_id'  => $payment_id,
					'refunded_at' => time(),
					'refunded_by' => get_current_user_id(),
				],
			];

			// Add refund notes/reason to Stripe API request if provided.
			if ( ! empty( $refund_notes && is_string( $refund_notes ) ) ) {
				// Add to metadata for detailed notes.
				$stripe_refund_data['metadata']['refund_notes'] = esc_html( $refund_notes );
				// Set reason as requested_by_customer (Stripe accepts: duplicate, fraudulent, requested_by_customer).
				$stripe_refund_data['reason'] = 'requested_by_customer';
			}

			// Determine if we're refunding by charge ID or payment intent ID.
			if ( is_string( $transaction_id ) && strpos( $transaction_id, 'ch_' ) === 0 ) {
				$stripe_refund_data['charge'] = $transaction_id;
			} elseif ( is_string( $transaction_id ) && strpos( $transaction_id, 'pi_' ) === 0 ) {
				$stripe_refund_data['payment_intent'] = $transaction_id;
			} else {
				return [
					'success' => false,
					'message' => __( 'Invalid transaction ID format for refund.', 'sureforms' ),
					'data'    => [],
				];
			}

			$refund_response = Stripe_Helper::stripe_api_request( 'refunds', 'POST', $stripe_refund_data, '', [ 'mode' => $this->payment_mode ] );

			if ( ! $refund_response['success'] ) {
				$error_message = $refund_response['error']['message'] ?? __( 'Failed to process refund through Stripe API.', 'sureforms' );
				return [
					'success' => false,
					'message' => $error_message,
					'data'    => [],
				];
			}

			$refund   = $refund_response['data'];
			$currency = isset( $payment['currency'] ) && is_string( $payment['currency'] ) ? $payment['currency'] : 'USD';
			// Store refund data and update payment status/log.
			$refund_stored = $this->update_refund_data( $payment_id, $refund, $refund_amount, $currency, null, $refund_notes );
			if ( ! $refund_stored ) {
				return [
					'success' => false,
					'message' => __( 'Failed to update payment record after refund.', 'sureforms' ),
					'data'    => [],
				];
			}

			return [
				'success' => true,
				'message' => __( 'Payment refunded successfully.', 'sureforms' ),
				'data'    => [
					'refund_id' => is_array( $refund ) && isset( $refund['id'] ) ? $refund['id'] : '',
					'status'    => is_array( $refund ) && isset( $refund['status'] ) ? $refund['status'] : 'processed',
				],
			];

		} catch ( \Exception $e ) {
			return [
				'success' => false,
				'message' => __( 'Failed to process refund. Please try again.', 'sureforms' ),
				'data'    => [],
			];
		}
	}

	/**
	 * Cancel subscription (following WPForms pattern)
	 *
	 * @param string $subscription_id Subscription ID.
	 * @since 2.0.0
	 * @return bool Success status.
	 */
	public function cancel_subscription( $subscription_id ) {
		try {
			// Retrieve the subscription using direct Stripe API.
			$subscription_response = Stripe_Helper::stripe_api_request( 'subscriptions', 'GET', [], $subscription_id, [ 'mode' => $this->payment_mode ] );

			if ( ! $subscription_response['success'] ) {
				return false;
			}

			$subscription = $subscription_response['data'];

			// If subscription is valid, check the status. If status is not 'active', return true early.
			if ( isset( $subscription['status'] ) && ! in_array( $subscription['status'], [ 'active', 'trialing' ], true ) ) {
				return true;
			}

			$updated_metadata = array_merge(
				isset( $subscription['metadata'] ) && is_array( $subscription['metadata'] ) ? $subscription['metadata'] : [],
				[
					'canceled_by' => 'sureforms_dashboard',
				]
			);

			Stripe_Helper::stripe_api_request(
				'subscriptions',
				'POST',
				[
					'metadata' => $updated_metadata,
				],
				$subscription_id,
				[ 'mode' => $this->payment_mode ]
			);

			// Cancel the subscription.
			$cancelled_subscription_response = Stripe_Helper::stripe_api_request(
				'subscriptions',
				'DELETE',
				[],
				$subscription_id,
				[ 'mode' => $this->payment_mode ]
			);

			if ( ! $cancelled_subscription_response['success'] ) {
				return false;
			}

			return true;

		} catch ( \Exception $e ) {
			return false;
		}
	}

	/**
	 * AJAX handler for subscription pause
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function ajax_pause_subscription() {
		// Security checks.
		if ( ! isset( $_POST['payment_id'] ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Missing payment ID.', 'sureforms' ) ] );
		}

		// Verify nonce.
		if (
			! wp_verify_nonce(
				sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ),
				'srfm_payment_admin_nonce'
			)
		) {
			wp_send_json_error( __( 'Invalid nonce.', 'sureforms' ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to perform this action.', 'sureforms' ) ] );
		}

		$payment_id = absint( $_POST['payment_id'] );

		// Get payment record.
		$payment = Payments::get( $payment_id );
		if ( ! $payment ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'sureforms' ) ] );
		}

		$this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';

		// Validate it's a subscription payment.
		if ( empty( $payment['type'] ) || 'subscription' !== $payment['type'] ) {
			wp_send_json_error( [ 'message' => esc_html__( 'This is not a subscription payment.', 'sureforms' ) ] );
		}

		if ( empty( $payment['subscription_id'] ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Subscription ID not found.', 'sureforms' ) ] );
		}

		// Pause the subscription.
		$pause_result = $this->pause_subscription( $payment['subscription_id'] );
		if ( ! $pause_result ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Subscription pause failed.', 'sureforms' ) ] );
		}

		// Get current logs and add pause log entry.
		$current_logs = Helper::get_array_value( $payment['log'] );

		// Build log messages array.
		$log_messages = [
			sprintf(
				/* translators: %s: Stripe subscription ID */
				__( 'Subscription ID: %s', 'sureforms' ),
				$payment['subscription_id']
			),
			sprintf(
				/* translators: %s: payment gateway name */
				__( 'Payment Gateway: %s', 'sureforms' ),
				'Stripe'
			),
			sprintf(
				/* translators: %s: subscription status */
				__( 'Subscription Status: %s', 'sureforms' ),
				__( 'Paused', 'sureforms' )
			),
			sprintf(
				/* translators: %s: user display name */
				__( 'Paused by: %s', 'sureforms' ),
				wp_get_current_user()->display_name
			),
			__( 'Note: The subscription billing has been paused. No charges will occur until the subscription is resumed.', 'sureforms' ),
		];

		// Create new log entry.
		$new_log        = [
			'title'      => __( 'Subscription Paused', 'sureforms' ),
			'created_at' => current_time( 'mysql' ),
			'messages'   => $log_messages,
		];
		$current_logs[] = $new_log;

		// Update database status to paused with log.
		$updated = Payments::update(
			$payment_id,
			[
				'subscription_status' => 'paused',
				'log'                 => $current_logs,
			]
		);
		if ( ! $updated ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Failed to update subscription status in database.', 'sureforms' ) ] );
		}

		wp_send_json_success( [ 'message' => esc_html__( 'Subscription paused successfully!', 'sureforms' ) ] );
	}

	/**
	 * Pause subscription
	 *
	 * @param string $subscription_id Subscription ID.
	 * @since 2.0.0
	 * @return bool Success status.
	 */
	public function pause_subscription( $subscription_id ) {
		try {
			// Retrieve subscription using direct Stripe API.
			$subscription_response = Stripe_Helper::stripe_api_request( 'subscriptions', 'GET', [], $subscription_id, [ 'mode' => $this->payment_mode ] );

			if ( ! $subscription_response['success'] ) {
				return false;
			}

			$subscription = $subscription_response['data'];

			$updated_metadata = array_merge(
				isset( $subscription['metadata'] ) && is_array( $subscription['metadata'] ) ? $subscription['metadata'] : [],
				[
					'paused_by' => 'sureforms_dashboard',
				]
			);

			// Pause the subscription using pause_collection.
			$paused_subscription_response = Stripe_Helper::stripe_api_request(
				'subscriptions',
				'POST',
				[
					'pause_collection' => [
						'behavior' => 'void',
					],
					'metadata'         => $updated_metadata,
				],
				$subscription_id,
				[ 'mode' => $this->payment_mode ]
			);

			if ( ! $paused_subscription_response['success'] ) {
				return false;
			}

			return true;

		} catch ( \Exception $e ) {
			return false;
		}
	}

	/**
	 * Update refund data in payment_data column and log
	 *
	 * @param int                      $payment_id Payment record ID.
	 * @param array<string,mixed>      $refund_response Refund response from Stripe.
	 * @param int                      $refund_amount Refund amount in cents.
	 * @param string                   $currency Currency code.
	 * @param array<string,mixed>|null $payment Payment record data.
	 * @param string                   $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return bool True if successful, false otherwise.
	 */
	public function update_refund_data(
		$payment_id,
		$refund_response,
		$refund_amount,
		$currency,
		$payment = null,
		$refund_notes = ''
	) {
		if ( empty( $payment_id ) || empty( $refund_response ) ) {
			return false;
		}

		// Get payment record if not provided.
		$payment = Payments::get( $payment_id );
		if ( ! $payment ) {
			return false;
		}

		$check_if_refund_already_exists = $this->check_if_refund_already_exists( $payment, $refund_response );
		if ( $check_if_refund_already_exists ) {
			return true;
		}

		// Prepare refund data for payment_data column.
		$refund_data = [
			'refund_id'      => is_string( $refund_response['id'] ) ? sanitize_text_field( $refund_response['id'] ) : '',
			'amount'         => absint( $refund_amount ),
			'currency'       => sanitize_text_field( strtoupper( $currency ) ),
			'status'         => is_string( $refund_response['status'] ) ? sanitize_text_field( $refund_response['status'] ) : 'processed',
			'created'        => time(),
			'reason'         => is_string( $refund_response['reason'] ) ? sanitize_text_field( $refund_response['reason'] ) : 'requested_by_customer',
			'description'    => is_string( $refund_response['description'] ) ? sanitize_text_field( $refund_response['description'] ) : '',
			'receipt_number' => is_string( $refund_response['receipt_number'] ) ? sanitize_text_field( $refund_response['receipt_number'] ) : '',
			'refunded_by'    => is_string( wp_get_current_user()->display_name ) ? sanitize_text_field( wp_get_current_user()->display_name ) : 'System',
			'refunded_at'    => gmdate( 'Y-m-d H:i:s' ),
		];

		// Validate refund amount to prevent over-refunding.
		$original_amount    = floatval( $payment['total_amount'] );
		$existing_refunds   = floatval( $payment['refunded_amount'] ); // Use column directly.
		$new_refund_amount  = Stripe_Helper::amount_from_stripe_format( $refund_amount, $currency );
		$total_after_refund = $existing_refunds + $new_refund_amount;

		if ( $total_after_refund > $original_amount ) {
			return false;
		}

		// Add refund data to payment_data column (for audit trail).
		Payments::add_refund_to_payment_data( $payment_id, $refund_data );

		// Update the refunded_amount column.
		$refund_amount_result = Payments::add_refund_amount( $payment_id, $new_refund_amount );

		// Calculate appropriate payment status.
		$payment_status = 'succeeded'; // Default to current status.
		if ( $total_after_refund >= $original_amount ) {
			$payment_status = 'refunded'; // Fully refunded.
		} elseif ( $total_after_refund > 0 ) {
			$payment_status = 'partially_refunded'; // Partially refunded.
		}

		// Update payment status and log.
		$current_logs = Helper::get_array_value( $payment['log'] );
		$refund_type  = $total_after_refund >= $original_amount ? __( 'Full', 'sureforms' ) : __( 'Partial', 'sureforms' );

		// Build log messages array.
		$log_messages = [
			sprintf(
				/* translators: %s: refund ID */
				__( 'Refund ID: %s', 'sureforms' ),
				is_string( $refund_response['id'] ) ? $refund_response['id'] : 'N/A'
			),
			sprintf(
				/* translators: %s: payment gateway name (e.g., Stripe) */
				__( 'Payment Gateway: %s', 'sureforms' ),
				'Stripe'
			),
			sprintf(
				/* translators: 1: refund amount, 2: currency */
				__( 'Refund Amount: %1$s %2$s', 'sureforms' ),
				number_format( Stripe_Helper::amount_from_stripe_format( $refund_amount, $currency ), 2 ),
				strtoupper( $currency )
			),
			sprintf(
				/* translators: 1: total refunded, 2: currency, 3: original total, 4: currency */
				__( 'Total Refunded: %1$s %2$s of %3$s %4$s', 'sureforms' ),
				number_format( $total_after_refund, 2 ),
				strtoupper( $currency ),
				number_format( $original_amount, 2 ),
				strtoupper( $currency )
			),
			sprintf(
				/* translators: %s: status (e.g., succeeded, processed) */
				__( 'Refund Status: %s', 'sureforms' ),
				is_string( $refund_response['status'] ) ? $refund_response['status'] : 'processed'
			),
			sprintf(
				/* translators: %s: payment status (e.g., succeeded, refunded, partially_refunded) */
				__( 'Payment Status: %s', 'sureforms' ),
				ucfirst( str_replace( '_', ' ', $payment_status ) )
			),
			sprintf(
				/* translators: %s: user display name */
				__( 'Refunded by: %s', 'sureforms' ),
				wp_get_current_user()->display_name
			),
		];

		// Add refund notes to log if provided.
		if ( ! empty( $refund_notes && is_string( $refund_notes ) ) ) {
			$log_messages[] = sprintf(
				/* translators: %s: refund notes */
				__( 'Refund Notes: %s', 'sureforms' ),
				esc_html( $refund_notes )
			);
		}

		/* translators: %s: refund type (Full or Partial) */
		$new_log        = [
			'title'      => sprintf(
				/* translators: %s: refund type (Full or Partial) */
				__( '%s Payment Refund', 'sureforms' ),
				$refund_type
			),
			'created_at' => current_time( 'mysql' ),
			'messages'   => $log_messages,
		];
		$current_logs[] = $new_log;

		$update_data = [
			'status' => $payment_status,
			'log'    => $current_logs,
		];

		// Update payment record with status and log.
		$payment_update_result = Payments::update( $payment_id, $update_data );

		if ( false === $refund_amount_result ) {
			return false;
		}

		if ( false === $payment_update_result ) {
			return false;
		}

		return true;
	}

	/**
	 * Display admin notice for webhook configuration issues.
	 *
	 * Shows a warning notice when webhooks are not properly configured.
	 * The notice will automatically disappear when a new Stripe request comes in.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function webhook_configuration_notice() {
		// Only show on admin pages.
		if ( ! is_admin() ) {
			return;
		}

		// Only show to users with manage_options capability.
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		// Check if Stripe is connected.
		if ( ! Stripe_Helper::is_stripe_connected() ) {
			return;
		}

		// Check if webhooks are configured.
		if ( Stripe_Helper::is_webhook_configured() ) {
			return;
		}

		// Display the notice.
		?>
		<div class="notice notice-error is-dismissible">
			<p>
				<?php
				printf(
					/* translators: %1$s: Payment settings link */
					esc_html__(
						'Webhooks keep SureForms in sync with Stripe by automatically updating payment and subscription data. Please %1$s Webhook.',
						'sureforms'
					),
					sprintf(
						'<a href="%s">%s</a>',
						esc_url( Stripe_Helper::get_stripe_settings_url() ),
						esc_html__( 'configure', 'sureforms' )
					)
				);
				?>
			</p>
		</div>
		<?php
	}

	/**
	 * Refund subscription payment via filter system.
	 *
	 * IMPORTANT: This method refunds the INITIAL/FIRST charge of a subscription only.
	 * The transaction_id field contains the charge ID from the first subscription payment.
	 * Subsequent renewal charges are NOT refunded by this method and should be refunded
	 * individually through their own payment records.
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param int                 $refund_amount Refund amount in cents.
	 * @param string              $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return array<string,mixed> Refund result with success status and message.
	 * @throws \Exception If unable to determine the appropriate refund method.
	 */
	private function refund_subscription_payment_via_filter( $payment, $refund_amount, $refund_notes = '' ) {
		try {
			// Step 1: Validate input parameters.
			if ( empty( $payment ) || ! is_array( $payment ) || $refund_amount <= 0 ) {
				return [
					'success' => false,
					'message' => __( 'Invalid refund parameters provided.', 'sureforms' ),
					'data'    => [],
				];
			}

			$payment_id     = isset( $payment['id'] ) && is_numeric( $payment['id'] ) ? intval( $payment['id'] ) : 0;
			$transaction_id = isset( $payment['transaction_id'] ) && is_string( $payment['transaction_id'] ) ? $payment['transaction_id'] : '';
			$currency       = is_string( $payment['currency'] ) ? $payment['currency'] : 'USD';

			// Step 2: Verify this is a subscription-related payment.
			$is_subscription_payment = $this->is_subscription_related_payment( $payment );
			if ( ! $is_subscription_payment ) {
				return [
					'success' => false,
					'message' => __( 'This payment is not related to a subscription.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Step 3: Verify subscription payment status.
			// Note: 'active' status is used for subscription records, while 'succeeded' is used for one-time payments.
			// 'canceled' is accepted because the initial charge on a canceled subscription is still refundable
			// (and historical rows persisted with `status='canceled'` should remain refundable).
			$refundable_statuses = [ 'active', 'succeeded', 'partially_refunded', 'canceled' ];
			if ( empty( $payment['status'] ) || ! in_array( $payment['status'], $refundable_statuses, true ) ) {
				return [
					'success' => false,
					'message' => __( 'Only active, succeeded, or partially refunded subscription payments can be refunded.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Step 4: Validate refund amount limits.
			$validation_result = $this->validate_subscription_refund_amount( $payment, $refund_amount );
			if ( ! $validation_result['valid'] ) {
				return [
					'success' => false,
					'message' => $validation_result['message'],
					'data'    => [],
				];
			}

			// Step 5: Validate Stripe connection.
			if ( ! Stripe_Helper::is_stripe_connected() ) {
				return [
					'success' => false,
					'message' => __( 'Stripe is not connected.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Step 6: Create refund using appropriate method based on transaction ID type.
			$refund = $this->create_subscription_refund( $payment, $transaction_id, $refund_amount, $refund_notes );

			if ( ! $refund || empty( $refund['id'] ) ) {
				return [
					'success' => false,
					'message' => __( 'Stripe refund creation failed. Please check your Stripe dashboard for more details.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Step 7: Update database with refund information.
			$refund_stored = $this->update_subscription_refund_data( $payment_id, $refund, $refund_amount, $currency, $refund_notes );

			if ( ! $refund_stored ) {
				return [
					'success' => false,
					'message' => __( 'Refund was processed by Stripe but failed to update local records. Please check your payment records manually.', 'sureforms' ),
					'data'    => [],
				];
			}

			// Step 8: Success response.
			return [
				'success' => true,
				'message' => __( 'Subscription payment refunded successfully.', 'sureforms' ),
				'data'    => [
					'refund_id'     => isset( $refund['id'] ) && is_string( $refund['id'] ) ? $refund['id'] : '',
					'status'        => isset( $refund['status'] ) && is_string( $refund['status'] ) ? $refund['status'] : '',
					'type'          => 'subscription_refund',
					'charge_id'     => isset( $refund['charge'] ) && is_string( $refund['charge'] ) ? $refund['charge'] : '',
					'refund_amount' => number_format( $refund_amount / 100, 2 ),
					'currency'      => strtoupper( $currency ),
				],
			];

		} catch ( \Exception $e ) {
			// Provide more specific error messages based on error type.
			$error_message = $this->get_user_friendly_refund_error( $e->getMessage() );
			return [
				'success' => false,
				'message' => $error_message,
				'data'    => [],
			];
		}
	}

	/**
	 * Check if payment is subscription-related
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @since 2.0.0
	 * @return bool True if payment is subscription-related, false otherwise.
	 */
	private function is_subscription_related_payment( $payment ) {
		// Check if it's a main subscription record.
		if ( ! empty( $payment['type'] ) && 'renewal' === $payment['type'] ) {
			return true;
		}

		// Check if it's a subscription billing cycle payment (has subscription_id).
		if ( ! empty( $payment['subscription_id'] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Validate subscription refund amount
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param int                 $refund_amount Refund amount in cents.
	 * @since 2.0.0
	 * @return array{valid: bool, message: string} Validation result with 'valid' boolean and 'message' string.
	 */
	private function validate_subscription_refund_amount( $payment, $refund_amount ) {
		$currency = isset( $payment['currency'] ) && is_string( $payment['currency'] ) ? $payment['currency'] : 'USD';

		$total_amount = isset( $payment['total_amount'] ) && is_string( $payment['total_amount'] ) ? floatval( $payment['total_amount'] ) : 0;
		$total_amount = Stripe_Helper::amount_to_stripe_format( $total_amount, $currency );

		$refunded_amount = isset( $payment['refunded_amount'] ) && is_string( $payment['refunded_amount'] ) ? floatval( $payment['refunded_amount'] ) : 0;
		$refunded_amount = Stripe_Helper::amount_to_stripe_format( $refunded_amount, $currency );

		$available_for_refund = $total_amount - $refunded_amount;

		if ( $refund_amount > $available_for_refund ) {
			return [
				'valid'   => false,
				'message' => sprintf(
					/* translators: 1: Maximum refundable amount (numeric), 2: Currency code (e.g. USD) */
					__( 'Refund amount exceeds available amount. Maximum refundable: %1$s %2$s', 'sureforms' ),
					number_format( $available_for_refund / 100, 2 ),
					isset( $payment['currency'] ) && is_string( $payment['currency'] ) ? strtoupper( $payment['currency'] ) : 'USD'
				),
			];
		}

		if ( $refund_amount <= 0 ) {
			return [
				'valid'   => false,
				'message' => __( 'Refund amount must be greater than zero.', 'sureforms' ),
			];
		}

		// Stripe minimum refund amount (usually $0.50 for most currencies).
		if ( $refund_amount < 50 ) {
			return [
				'valid'   => false,
				'message' => __( 'Refund amount must be at least $0.50.', 'sureforms' ),
			];
		}

		return [
			'valid'   => true,
			'message' => '',
		];
	}

	/**
	 * Create refund for subscription payment using the most appropriate method
	 *
	 * For subscriptions, the transaction_id field contains the charge ID from the FIRST/INITIAL payment.
	 * This ensures refunds are processed against the initial charge only, not any subsequent renewal charges.
	 * Subsequent renewal charges should be refunded individually through their own payment records.
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param string              $transaction_id Transaction ID (charge ID from first payment for subscriptions).
	 * @param int                 $refund_amount Refund amount in cents.
	 * @param string              $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return array<string,mixed>|false Refund data or false on failure.
	 * @throws \Exception If unable to determine the appropriate refund method.
	 */
	private function create_subscription_refund( $payment, $transaction_id, $refund_amount, $refund_notes = '' ) {
		// Method 1: Use charge ID directly (default for subscriptions - contains first payment charge).
		// For subscription payments, transaction_id contains the charge ID from the initial payment.
		if ( is_string( $transaction_id ) && strpos( $transaction_id, 'ch_' ) === 0 ) {
			return $this->create_refund_by_charge( $payment, $transaction_id, $refund_amount, $refund_notes );
		}

		// Method 2: Use payment intent ID if provided (fallback for legacy data).
		if ( is_string( $transaction_id ) && strpos( $transaction_id, 'pi_' ) === 0 ) {
			return $this->create_refund_by_payment_intent( $payment, $transaction_id, $refund_amount, $refund_notes );
		}

		// Method 3: Try to find charge ID in payment data (fallback for edge cases).
		$charge_id = $this->get_charge_id_from_payment( $payment );
		if ( is_string( $charge_id ) && '' !== $charge_id ) {
			return $this->create_refund_by_charge( $payment, $charge_id, $refund_amount, $refund_notes );
		}

		throw new \Exception( esc_html__( 'Unable to determine the appropriate refund method for this subscription payment.', 'sureforms' ) );
	}

	/**
	 * Create refund using charge ID
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param string              $charge_id Stripe charge ID.
	 * @param int                 $refund_amount Refund amount in cents.
	 * @param string              $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return array<string,mixed>|false Refund data or false on failure.
	 */
	private function create_refund_by_charge( $payment, $charge_id, $refund_amount, $refund_notes = '' ) {
		$metadata = [
			'refunded_by'     => 'sureforms_dashboard',
			'subscription_id' => $payment['subscription_id'] ?? '',
			'source'          => 'SureForms',
			'payment_id'      => $payment['id'] ?? '',
			'refunded_at'     => time(),
			'refund_type'     => 'subscription_billing',
			'refund_method'   => 'charge_refund',
		];

		// Add refund notes to metadata if provided.
		if ( ! empty( $refund_notes ) ) {
			$metadata['refund_notes'] = $refund_notes;
		}

		$refund_response = Stripe_Helper::stripe_api_request(
			'refunds',
			'POST',
			[
				'charge'   => $charge_id,
				'amount'   => $refund_amount,
				'reason'   => 'requested_by_customer',
				'metadata' => $metadata,
			],
			'',
			[ 'mode' => $this->payment_mode ]
		);

		return $refund_response['success'] ? $refund_response['data'] : false;
	}

	/**
	 * Create refund using payment intent ID
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param string              $payment_intent_id Stripe payment intent ID.
	 * @param int                 $refund_amount Refund amount in cents.
	 * @param string              $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return array<string,mixed>|false Refund data or false on failure.
	 */
	private function create_refund_by_payment_intent( $payment, $payment_intent_id, $refund_amount, $refund_notes = '' ) {
		$metadata = [
			'refunded_by'     => 'sureforms_dashboard',
			'subscription_id' => $payment['subscription_id'] ?? '',
			'source'          => 'SureForms',
			'payment_id'      => $payment['id'] ?? '',
			'refunded_at'     => time(),
			'refund_type'     => 'subscription_billing',
			'refund_method'   => 'payment_intent_refund',
		];

		// Add refund notes to metadata if provided.
		if ( ! empty( $refund_notes ) ) {
			$metadata['refund_notes'] = $refund_notes;
		}

		$refund_response = Stripe_Helper::stripe_api_request(
			'refunds',
			'POST',
			[
				'payment_intent' => $payment_intent_id,
				'amount'         => $refund_amount,
				'reason'         => 'requested_by_customer',
				'metadata'       => $metadata,
			],
			'',
			[ 'mode' => $this->payment_mode ]
		);

		return $refund_response['success'] ? $refund_response['data'] : false;
	}

	/**
	 * Update subscription refund data in database
	 *
	 * @param int                 $payment_id Payment record ID.
	 * @param array<string,mixed> $refund_response Refund response from Stripe.
	 * @param int                 $refund_amount Refund amount in cents.
	 * @param string              $currency Currency code.
	 * @param string              $refund_notes Refund notes.
	 * @since 2.0.0
	 * @return bool True if successful, false otherwise.
	 */
	private function update_subscription_refund_data(
		int $payment_id,
		array $refund_response,
		int $refund_amount,
		string $currency,
		?string $refund_notes = null
	) {
		if ( empty( $payment_id ) || empty( $refund_response ) ) {
			return false;
		}

		// Get payment record.
		$payment = Payments::get( $payment_id );
		if ( ! $payment ) {
			return false;
		}

		// Prepare refund data for payment_data column.
		$refund_data = [
			'refund_id'      => is_string( $refund_response['id'] ) ? sanitize_text_field( $refund_response['id'] ) : '',
			'amount'         => absint( $refund_amount ),
			'currency'       => is_string( $currency ) ? sanitize_text_field( strtoupper( $currency ) ) : 'USD',
			'status'         => is_string( $refund_response['status'] ) ? sanitize_text_field( $refund_response['status'] ) : 'processed',
			'created'        => time(),
			'reason'         => is_string( $refund_response['reason'] ) ? sanitize_text_field( $refund_response['reason'] ) : 'requested_by_customer',
			'description'    => is_string( $refund_response['description'] ) ? sanitize_text_field( $refund_response['description'] ) : '',
			'receipt_number' => is_string( $refund_response['receipt_number'] ) ? sanitize_text_field( $refund_response['receipt_number'] ) : '',
			'refunded_by'    => is_string( wp_get_current_user()->display_name ) ? sanitize_text_field( wp_get_current_user()->display_name ) : 'System',
			'refunded_at'    => gmdate( 'Y-m-d H:i:s' ),
			'type'           => 'subscription_refund',
		];

		// Validate refund amount to prevent over-refunding.
		$original_amount    = floatval( $payment['total_amount'] );
		$existing_refunds   = floatval( $payment['refunded_amount'] ?? 0 ); // Use column directly.
		$new_refund_amount  = Stripe_Helper::amount_from_stripe_format( $refund_amount, $currency );
		$total_after_refund = $existing_refunds + $new_refund_amount;

		if ( $total_after_refund > $original_amount ) {
			return false;
		}

		// Add refund data to payment_data column (for audit trail).
		$payment_data_result = Payments::add_refund_to_payment_data( $payment_id, $refund_data );
		if ( ! $payment_data_result ) {
			return false;
		}

		// Update the refunded_amount column.
		$refund_amount_result = Payments::add_refund_amount( $payment_id, $new_refund_amount );
		if ( ! $refund_amount_result ) {
			return false;
		}

		// Determine new payment status.
		$total_amount   = (float) $payment['total_amount'];
		$total_refunded = Payments::get_refunded_amount( $payment_id );
		$payment_status = $total_refunded >= $total_amount ? 'refunded' : 'partially_refunded';

		// Prepare comprehensive log entry.
		$current_logs       = Helper::get_array_value( $payment['log'] );
		$original_amount    = $total_amount;
		$total_after_refund = $total_refunded;
		$refund_type        = $total_after_refund >= $original_amount ? __( 'Full', 'sureforms' ) : __( 'Partial', 'sureforms' );

		// Build log messages array.
		$log_messages = [
			sprintf(
				/* translators: %s: refund ID */
				__( 'Refund ID: %s', 'sureforms' ),
				is_string( $refund_response['id'] ) ? $refund_response['id'] : 'N/A'
			),
			sprintf(
				/* translators: %s: payment gateway */
				__( 'Payment Gateway: %s', 'sureforms' ),
				'Stripe'
			),
			sprintf(
				/* translators: 1: refund amount, 2: currency code */
				__( 'Refund Amount: %1$s %2$s', 'sureforms' ),
				number_format( Stripe_Helper::amount_from_stripe_format( $refund_amount, $currency ), 2 ),
				strtoupper( $currency )
			),
			sprintf(
				/* translators: 1: total refunded, 2: currency, 3: original amount, 4: currency */
				__( 'Total Refunded: %1$s %2$s of %3$s %4$s', 'sureforms' ),
				number_format( $total_after_refund, 2 ),
				strtoupper( $currency ),
				number_format( $original_amount, 2 ),
				strtoupper( $currency )
			),
			sprintf(
				/* translators: %s: refund status */
				__( 'Refund Status: %s', 'sureforms' ),
				is_string( $refund_response['status'] ) ? $refund_response['status'] : 'processed'
			),
			sprintf(
				/* translators: %s: payment status */
				__( 'Payment Status: %s', 'sureforms' ),
				ucfirst( str_replace( '_', ' ', $payment_status ) )
			),
			sprintf(
				/* translators: %s: refunded by user */
				__( 'Refunded by: %s', 'sureforms' ),
				wp_get_current_user()->display_name
			),
		];

		// Add refund notes to log if provided.
		if ( ! empty( $refund_notes ) ) {
			$log_messages[] = sprintf(
				/* translators: %s: refund notes */
				__( 'Refund Notes: %s', 'sureforms' ),
				$refund_notes
			);
		}

		$new_log        = [
			'title'      => sprintf(
				/* translators: %s: refund type (Full/Partial) */
				__( '%s Subscription Payment Refund', 'sureforms' ),
				$refund_type
			),
			'created_at' => current_time( 'mysql' ),
			'messages'   => $log_messages,
		];
		$current_logs[] = $new_log;

		$update_data = [
			'status' => $payment_status,
			'log'    => $current_logs,
		];

		// Update payment record with status and log.
		$payment_update_result = Payments::update( $payment_id, $update_data );

		if ( ! $payment_update_result ) {
			return false;
		}

		return true;
	}

	/**
	 * Convert technical error messages to user-friendly ones
	 *
	 * @param string $technical_error Technical error message.
	 * @since 2.0.0
	 * @return string User-friendly error message.
	 */
	private function get_user_friendly_refund_error( $technical_error ) {
		$error_patterns = [
			'/charge.*already.*refunded/i'                 => __( 'This payment has already been fully refunded.', 'sureforms' ),
			'/charge.*not.*found/i'                        => __( 'The payment could not be found in Stripe.', 'sureforms' ),
			'/amount.*exceeds/i'                           => __( 'The refund amount exceeds the available refundable amount.', 'sureforms' ),
			'/payment.*intent.*not.*found/i'               => __( 'The payment for this subscription could not be found.', 'sureforms' ),
			'/subscription.*not.*found/i'                  => __( 'The subscription could not be found in Stripe.', 'sureforms' ),
			'/no.*successful.*payments/i'                  => __( 'This subscription has no successful payments to refund.', 'sureforms' ),
			'/invalid.*payment.*method/i'                  => __( 'The payment method for this subscription is invalid.', 'sureforms' ),
			'/insufficient.*permissions/i'                 => __( 'Insufficient permissions to process refunds.', 'sureforms' ),
			'/rate.*limit/i'                               => __( 'Too many requests. Please try again in a moment.', 'sureforms' ),
			'/network.*error|connection.*failed|timeout/i' => __( 'Network error. Please check your connection and try again.', 'sureforms' ),
		];

		foreach ( $error_patterns as $pattern => $friendly_message ) {
			if ( preg_match( $pattern, $technical_error ) ) {
				return $friendly_message;
			}
		}

		// Default fallback message.
		// translators: %s: technical error message returned from Stripe.
		return sprintf( __( 'Subscription refund failed: %s', 'sureforms' ), $technical_error );
	}

	/**
	 * Check if refund already exists for this payment
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @param array<string,mixed> $refund_response Refund response from Stripe.
	 * @since 2.0.0
	 * @return bool True if refund already exists, false otherwise.
	 */
	private function check_if_refund_already_exists( $payment, $refund_response ) {
		if ( empty( $payment['payment_data'] ) || empty( $refund_response['id'] ) ) {
			return false;
		}

		$payment_data = Helper::get_array_value( $payment['payment_data'] );
		if ( empty( $payment_data['refunds'] ) ) {
			return false;
		}

		$refund_id = $refund_response['id'];

		// O(1) lookup using refund ID as array key.
		return isset( $payment_data['refunds'][ $refund_id ] );
	}

	/**
	 * Get charge ID from payment data
	 *
	 * @param array<string,mixed> $payment Payment record.
	 * @since 2.0.0
	 * @return string|null Charge ID or null if not found.
	 */
	private function get_charge_id_from_payment( $payment ) {
		// Check if transaction_id is already a charge ID.
		if ( ! empty( $payment['transaction_id'] ) && is_string( $payment['transaction_id'] ) && strpos( $payment['transaction_id'], 'ch_' ) === 0 ) {
			return $payment['transaction_id'];
		}

		// Look in payment_data for charge_id.
		if ( empty( $payment['payment_data'] ) ) {
			return null;
		}

		$payment_data = Helper::get_array_value( $payment['payment_data'] );
		if ( empty( $payment_data ) ) {
			return null;
		}

		// Look for charge ID in various places in payment_data.
		$charge_keys = [
			'charge_id',
			'charge',
			'invoice_charge_id',
		];

		foreach ( $charge_keys as $key ) {
			$charge_id = $this->get_nested_value( $payment_data, $key );
			if ( ! empty( $charge_id ) && is_string( $charge_id ) && strpos( $charge_id, 'ch_' ) === 0 ) {
				return $charge_id;
			}
		}

		return null;
	}

	/**
	 * Get nested value from array using dot notation
	 *
	 * @param array<string,mixed> $array Array to search.
	 * @param string              $key Dot-separated key path.
	 * @since 2.0.0
	 * @return mixed Value or null if not found.
	 */
	private function get_nested_value( $array, $key ) {
		$keys  = explode( '.', $key );
		$value = $array;

		foreach ( $keys as $k ) {
			if ( ! is_array( $value ) || ! isset( $value[ $k ] ) ) {
				return null;
			}
			$value = $value[ $k ];
		}

		return $value;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit