menu . ' | ' . $payments_string, self::payments_menu_title( $payments_string ), 'frm_view_entries', 'formidable-payments', $menu_route ); } /** * @since 6.11.1 * * @param string $payments_string * * @return string */ private static function payments_menu_title( $payments_string ) { ob_start(); echo esc_html( $payments_string ); FrmAppHelper::show_pill_text(); return ob_get_clean(); } /** * @return void */ public static function route() { $action = isset( $_REQUEST['frm_action'] ) ? 'frm_action' : 'action'; $action = FrmAppHelper::get_param( $action, '', 'get', 'sanitize_title' ); $type = FrmAppHelper::get_param( 'type', '', 'get', 'sanitize_title' ); $class_name = $type === 'subscriptions' ? 'FrmTransLiteSubscriptionsController' : 'FrmTransLitePaymentsController'; if ( method_exists( $class_name, $action ) ) { $class_name::$action(); return; } FrmTransLiteListsController::route( $action ); } /** * @param object $payment * * @return void */ public static function load_sidebar_actions( $payment ) { FrmTransLiteActionsController::actions_js(); $date_format = __( 'M j, Y @ G:i', 'formidable' ); $created_at = FrmAppHelper::get_localized_date( $date_format, $payment->created_at ); include FrmTransLiteAppHelper::plugin_path() . '/views/payments/sidebar_actions.php'; } /** * Echo a receipt link. * * @param object $payment * * @return void */ public static function show_receipt_link( $payment ) { $link = esc_html( $payment->receipt_id ); $paysys = $payment->paysys; if ( $payment->receipt_id !== 'None' && self::should_filter_receipt_link( $paysys ) ) { /** * Filter a receipt link for a specific gateway. * For example, Stripe uses frm_pay_stripe_receipt. * * @param string $link */ $link = apply_filters( 'frm_pay_' . $paysys . '_receipt', $link ); } FrmAppHelper::kses_echo( $link, array( 'a' ) ); } /** * @param string $paysys * * @return bool */ private static function should_filter_receipt_link( $paysys ) { $allowed_types = array( 'stripe', 'authnet_aim' ); return in_array( $paysys, $allowed_types, true ); } /** * Echo a refund link. * * @param object $payment * * @return void */ public static function show_refund_link( $payment ) { $link = self::refund_link( $payment ); FrmTransLiteAppHelper::echo_confirmation_link( $link ); } /** * Show a link to a payment entry (unless it is deleted). * * @param object $payment * * @return void */ public static function show_entry_link( $payment ) { $entry = FrmDb::get_col( 'frm_items', array( 'id' => $payment->item_id ) ); if ( ! $entry ) { // translators: %d: Entry ID. echo esc_html( sprintf( __( '%d (Deleted)', 'formidable' ), $payment->item_id ) ); return; } // phpcs:disable Generic.WhiteSpace.ScopeIndent ?> item_id ); ?> status === 'refunded' ) { $link = esc_html__( 'Refunded', 'formidable' ); } else { $confirm = __( 'Are you sure you want to refund that payment?', 'formidable' ); $link = admin_url( 'admin-ajax.php?action=frm_trans_refund&payment_id=' . $payment->id . '&nonce=' . wp_create_nonce( 'frm_trans_ajax' ) ); $link = ''; $link .= esc_html__( 'Refund', 'formidable' ); $link .= ''; } $paysys = $payment->paysys; if ( self::should_filter_refund_link( $paysys ) ) { /** * Filter the refund link for a specific gateway. * For example, Stripe uses frm_pay_stripe_refund_link. * * @param string $link * @param object $payment */ return apply_filters( 'frm_pay_' . $paysys . '_refund_link', $link, $payment ); } return $link; } /** * @param string $paysys * * @return bool */ private static function should_filter_refund_link( $paysys ) { $allowed_types = array( 'stripe', 'authnet_aim' ); return in_array( $paysys, $allowed_types, true ); } /** * Process the ajax request to refund a payment. * * @return void */ public static function refund_payment() { FrmAppHelper::permission_check( 'frm_edit_entries' ); check_ajax_referer( 'frm_trans_ajax', 'nonce' ); $payment_id = FrmAppHelper::get_param( 'payment_id', '', 'get', 'absint' ); if ( ! $payment_id ) { wp_die( esc_html__( 'Oops! No payment was selected for refund.', 'formidable' ) ); } $payment = ( new FrmTransLitePayment() )->get_one( $payment_id ); if ( ! $payment ) { wp_die( esc_html__( 'Oops! That payment does not exist.', 'formidable' ) ); } $refunded = false; $reason = ''; $debug_id = ''; $paysys = $payment->paysys; switch ( $paysys ) { case 'stripe': $refunded = FrmStrpLiteAppHelper::call_stripe_helper_class( 'refund_payment', $payment->receipt_id ); break; case 'square': $refunded = FrmSquareLiteConnectHelper::refund_payment( $payment->receipt_id ); break; case 'paypal': $response = FrmPayPalLiteConnectHelper::refund_payment( $payment->receipt_id ); // Check for structured error response with message and debug_id if ( is_object( $response ) && isset( $response->message ) && isset( $response->debug_id ) ) { $refunded = false; $reason = $response->message; $debug_id = $response->debug_id; } elseif ( false === $response ) { $refunded = false; $reason = self::get_paypal_refund_reason(); $debug_id = FrmPayPalLiteConnectHelper::get_latest_debug_id_from_paypal_api(); } elseif ( is_object( $response ) && isset( $response->refund_error ) ) { // Handle mock error responses from PayPal API $refunded = false; $reason = $response->message ?? ''; $debug_id = $response->debug_id ?? ''; } else { $refunded = true; } break; default: $refunded = false; break; }//end switch if ( $refunded ) { self::change_payment_status( $payment, 'refunded' ); $message = __( 'Refunded', 'formidable' ); // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found } else { // If the reason is already a complete error message, use it directly // instead of wrapping it redundantly in "Refund Failed (...)" if ( $reason && ! preg_match( '/^[A-Z_]+$/', $reason ) ) { $message = $reason; } else { $message = __( 'Refund Failed', 'formidable' ); if ( $reason ) { $message .= ' (' . $reason . ')'; } } } if ( $debug_id ) { $message .= '

Debug ID: ' . esc_html( $debug_id ); } wp_die( sprintf( '
%2$s
', $refunded ? 'frm_updated_message' : 'frm_error_style', wp_kses_post( $message ) ) ); } /** * Get a human-friendly reason from the latest PayPal refund error. * * Handles both uppercase issue codes (e.g. REFUND_FAILED_INSUFFICIENT_FUNDS) * and human-friendly description strings from the PayPal API. * Strips the {{debug_id:...}} token if present. * * @since 6.31 * * @return string */ private static function get_paypal_refund_reason() { $error = FrmPayPalLiteConnectHelper::get_latest_error_from_paypal_api(); if ( ! $error ) { return ''; } $error = preg_replace( '/\{\{debug_id:[^}]+\}\}/', '', $error ); $error = trim( $error ); if ( preg_match( '/^[A-Z_]+$/', $error ) ) { return self::convert_uppercase_underscores_to_ucwords( $error, array( 'REFUND_FAILED_', 'REFUND_' ) ); } return $error; } /** * @param string $error The uppercase underscored string to convert. * @param array $prefixes_to_strip Prefixes to remove before converting. * * @return string */ private static function convert_uppercase_underscores_to_ucwords( $error, $prefixes_to_strip = array() ) { if ( ! preg_match( '/^[A-Z_]+$/', $error ) ) { return ''; } $reason = str_replace( $prefixes_to_strip, '', $error ); return ucwords( strtolower( str_replace( '_', ' ', $reason ) ) ); } /** * Update the status of a payment. * * @param object $payment * @param string $status * * @return void */ public static function change_payment_status( $payment, $status ) { if ( $status === $payment->status ) { // The payment status has not actually changed. return; } $frm_payment = new FrmTransLitePayment(); $frm_payment->update( $payment->id, array( 'status' => $status ) ); FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) ); } /** * @since 6.31 * * @param array|string $expected_gateways * @param array|string $selected_gateways * * @return void */ public static function maybe_hide_payment_setting( $expected_gateways, $selected_gateways ) { if ( ! array_intersect( (array) $expected_gateways, (array) $selected_gateways ) ) { echo ' frm_hidden'; } } }