| @@ -102,64 +102,43 @@ | ||
| 102 | 102 | if ( empty( $payment['subscription_id'] ) ) { |
| 103 | 103 | wp_send_json_error( [ 'message' => esc_html__( 'Subscription ID not found.', 'sureforms' ) ] ); |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | - // Cancel the subscription. | |
| 107 | - $cancel_result = $this->cancel_subscription( $payment['subscription_id'] ); | |
| 108 | - if ( ! $cancel_result ) { | |
| 109 | - wp_send_json_error( [ 'message' => esc_html__( 'Subscription cancellation failed.', 'sureforms' ) ] ); | |
| 106 | + // Cancel via the gateway-agnostic filter so the payment's OWN gateway (Stripe or | |
| 107 | + // PayPal) performs the cancellation. Previously this called the Stripe API directly, | |
| 108 | + // which failed for PayPal subscriptions cancelled from the admin screen. Both gateways | |
| 109 | + // hook 'srfm_process_subscription_cancellation' (Stripe + PayPal), matching the | |
| 110 | + // frontend cancellation path. | |
| 111 | + $cancel_result = apply_filters( | |
| 112 | + 'srfm_process_subscription_cancellation', | |
| 113 | + [ | |
| 114 | + 'success' => false, | |
| 115 | + 'message' => __( 'Cancellation is not supported for this payment gateway.', 'sureforms' ), | |
| 116 | + ], | |
| 117 | + $payment | |
| 118 | + ); | |
| 119 | + | |
| 120 | + if ( empty( $cancel_result['success'] ) ) { | |
| 121 | + wp_send_json_error( | |
| 122 | + [ | |
| 123 | + 'message' => ! empty( $cancel_result['message'] ) && is_string( $cancel_result['message'] ) | |
| 124 | + ? esc_html( $cancel_result['message'] ) | |
| 125 | + : esc_html__( 'Subscription cancellation failed.', 'sureforms' ), | |
| 126 | + ] | |
| 127 | + ); | |
| 110 | 128 | } |
| 111 | 129 | |
| 112 | - // Get current logs and add cancel log entry. | |
| 113 | - $current_logs = Helper::get_array_value( $payment['log'] ); | |
| 114 | - | |
| 115 | - // Build log messages array. | |
| 116 | - $log_messages = [ | |
| 117 | - sprintf( | |
| 118 | - /* translators: %s: Stripe subscription ID */ | |
| 119 | - __( 'Subscription ID: %s', 'sureforms' ), | |
| 120 | - $payment['subscription_id'] | |
| 121 | - ), | |
| 122 | - sprintf( | |
| 123 | - /* translators: %s: payment gateway name */ | |
| 124 | - __( 'Payment Gateway: %s', 'sureforms' ), | |
| 125 | - 'Stripe' | |
| 126 | - ), | |
| 127 | - sprintf( | |
| 128 | - /* translators: %s: subscription status */ | |
| 129 | - __( 'Subscription Status: %s', 'sureforms' ), | |
| 130 | - __( 'Canceled', 'sureforms' ) | |
| 131 | - ), | |
| 132 | - sprintf( | |
| 133 | - /* translators: %s: user display name */ | |
| 134 | - __( 'Canceled by: %s', 'sureforms' ), | |
| 135 | - wp_get_current_user()->display_name | |
| 136 | - ), | |
| 137 | - __( 'Note: The subscription has been permanently canceled. The customer will no longer be charged and will lose access to subscription benefits.', 'sureforms' ), | |
| 138 | - ]; | |
| 139 | - | |
| 140 | - // Create new log entry. | |
| 141 | - $new_log = [ | |
| 142 | - 'title' => __( 'Subscription Canceled', 'sureforms' ), | |
| 143 | - 'created_at' => current_time( 'mysql' ), | |
| 144 | - 'messages' => $log_messages, | |
| 145 | - ]; | |
| 146 | - $current_logs[] = $new_log; | |
| 147 | - | |
| 148 | - // Update database status to canceled (following WPForms pattern). | |
| 149 | - $updated = Payments::update( | |
| 150 | - $payment_id, | |
| 130 | + // The gateway callback (Stripe/PayPal) is the single source of truth: it has already | |
| 131 | + // cancelled at the gateway AND persisted subscription_status + the "Subscription Canceled" | |
| 132 | + // activity log. Just report success here — mirroring the frontend cancel path — so we don't | |
| 133 | + // write the DB a second time or append a duplicate log entry. | |
| 134 | + wp_send_json_success( | |
| 151 | 135 | [ |
| 152 | - 'subscription_status' => 'canceled', | |
| 153 | - 'status' => 'canceled', | |
| 154 | - 'log' => $current_logs, | |
| 136 | + 'message' => ! empty( $cancel_result['message'] ) && is_string( $cancel_result['message'] ) | |
| 137 | + ? esc_html( $cancel_result['message'] ) | |
| 138 | + : esc_html__( 'Subscription cancelled successfully.', 'sureforms' ), | |
| 155 | 139 | ] |
| 156 | 140 | ); |
| 157 | - if ( ! $updated ) { | |
| 158 | - wp_send_json_error( [ 'message' => esc_html__( 'Failed to update subscription status in database.', 'sureforms' ) ] ); | |
| 159 | - } | |
| 160 | - | |
| 161 | - wp_send_json_success( [ 'message' => esc_html__( 'Subscription canceled successfully!', 'sureforms' ) ] ); | |
| 162 | 141 | } |
| 163 | 142 | |
| 164 | 143 | /** |
| 165 | 144 | * Process Stripe subscription cancellation via filter system. |
| @@ -172,10 +151,12 @@ | ||
| 172 | 151 | * @param array<string,mixed> $payment Payment record from database. |
| 173 | 152 | * @return array<string,mixed> Result with success status and message. |
| 174 | 153 | */ |
| 175 | 154 | public function process_stripe_subscription_cancellation( $result, $payment ) { |
| 176 | - // Only process Stripe payments. | |
| 177 | - if ( empty( $payment['gateway'] ) || 'stripe' !== $payment['gateway'] ) { | |
| 155 | + // Process Stripe payments. Stripe is the only gateway in the free plugin, and the | |
| 156 | + // `gateway` column defaults to '' for legacy/imported rows — so an empty gateway is | |
| 157 | + // treated as Stripe. Only an explicitly different gateway (e.g. 'paypal') is skipped. | |
| 158 | + if ( ! empty( $payment['gateway'] ) && 'stripe' !== $payment['gateway'] ) { | |
| 178 | 159 | return $result; |
| 179 | 160 | } |
| 180 | 161 | |
| 181 | 162 | if ( empty( $payment['subscription_id'] ) || ! is_string( $payment['subscription_id'] ) ) { |
| @@ -227,13 +208,14 @@ | ||
| 227 | 208 | 'messages' => $log_messages, |
| 228 | 209 | ]; |
| 229 | 210 | |
| 230 | 211 | $payment_id = isset( $payment['id'] ) && is_numeric( $payment['id'] ) ? absint( $payment['id'] ) : 0; |
| 212 | + // Preserve the transaction `status` so the admin Refund option stays enabled | |
| 213 | + // after the customer cancels from the My Account page. | |
| 231 | 214 | Payments::update( |
| 232 | 215 | $payment_id, |
| 233 | 216 | [ |
| 234 | 217 | 'subscription_status' => 'canceled', |
| 235 | - 'status' => 'canceled', | |
| 236 | 218 | 'log' => $current_logs, |
| 237 | 219 | ] |
| 238 | 220 | ); |
| 239 | 221 | |
| @@ -850,9 +832,11 @@ | ||
| 850 | 832 | } |
| 851 | 833 | |
| 852 | 834 | // Step 3: Verify subscription payment status. |
| 853 | 835 | // Note: 'active' status is used for subscription records, while 'succeeded' is used for one-time payments. |
| 854 | - $refundable_statuses = [ 'active', 'succeeded', 'partially_refunded' ]; | |
| 836 | + // 'canceled' is accepted because the initial charge on a canceled subscription is still refundable | |
| 837 | + // (and historical rows persisted with `status='canceled'` should remain refundable). | |
| 838 | + $refundable_statuses = [ 'active', 'succeeded', 'partially_refunded', 'canceled' ]; | |
| 855 | 839 | if ( empty( $payment['status'] ) || ! in_array( $payment['status'], $refundable_statuses, true ) ) { |
| 856 | 840 | return [ |
| 857 | 841 | 'success' => false, |
| 858 | 842 | 'message' => __( 'Only active, succeeded, or partially refunded subscription payments can be refunded.', 'sureforms' ), |
| @@ -1031,9 +1015,9 @@ | ||
| 1031 | 1015 | if ( is_string( $charge_id ) && '' !== $charge_id ) { |
| 1032 | 1016 | return $this->create_refund_by_charge( $payment, $charge_id, $refund_amount, $refund_notes ); |
| 1033 | 1017 | } |
| 1034 | 1018 | |
| 1035 | - throw new \Exception( __( 'Unable to determine the appropriate refund method for this subscription payment.', 'sureforms' ) ); | |
| 1019 | + throw new \Exception( esc_html__( 'Unable to determine the appropriate refund method for this subscription payment.', 'sureforms' ) ); | |
| 1036 | 1020 | } |
| 1037 | 1021 | |
| 1038 | 1022 | /** |
| 1039 | 1023 | * Create refund using charge ID |