PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 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 All 98 releases
← All changes | inc/payments/stripe/admin-stripe-handler.php +91 -23 2.7.0 → 2.12.8 View file →
@@ -46,8 +46,10 @@
46 46 add_action( 'wp_ajax_srfm_stripe_cancel_subscription', [ $this, 'ajax_cancel_subscription' ] );
47 47 add_action( 'wp_ajax_srfm_stripe_pause_subscription', [ $this, 'ajax_pause_subscription' ] );
48 48 // Hook into unified refund filter system.
49 49 add_filter( 'srfm_process_transaction_refund', [ $this, 'process_stripe_refund' ], 10, 2 );
50 + // Hook into unified subscription cancellation filter system.
51 + add_filter( 'srfm_process_subscription_cancellation', [ $this, 'process_stripe_subscription_cancellation' ], 10, 2 );
50 52 // Admin notices.
51 53 add_action( 'admin_notices', [ $this, 'webhook_configuration_notice' ] );
52 54 }
53 55
@@ -86,9 +88,9 @@
86 88
87 89 // Get payment record.
88 90 $payment = Payments::get( $payment_id );
89 91
90 - $this->payment_mode = $payment['payment_mode'] ?? 'test';
92 + $this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';
91 93 if ( ! $payment ) {
92 94 wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'sureforms' ) ] );
93 95 }
94 96
@@ -100,23 +102,89 @@
100 102 if ( empty( $payment['subscription_id'] ) ) {
101 103 wp_send_json_error( [ 'message' => esc_html__( 'Subscription ID not found.', 'sureforms' ) ] );
102 104 }
103 105
104 - // Cancel the subscription.
105 - $cancel_result = $this->cancel_subscription( $payment['subscription_id'] );
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 + );
128 + }
129 +
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(
135 + [
136 + 'message' => ! empty( $cancel_result['message'] ) && is_string( $cancel_result['message'] )
137 + ? esc_html( $cancel_result['message'] )
138 + : esc_html__( 'Subscription cancelled successfully.', 'sureforms' ),
139 + ]
140 + );
141 + }
142 +
143 + /**
144 + * Process Stripe subscription cancellation via filter system.
145 + *
146 + * Filter callback for 'srfm_process_subscription_cancellation'.
147 + * Used by both admin and frontend to cancel Stripe subscriptions.
148 + *
149 + * @since 2.8.0
150 + * @param array<string,mixed> $result Default result array.
151 + * @param array<string,mixed> $payment Payment record from database.
152 + * @return array<string,mixed> Result with success status and message.
153 + */
154 + public function process_stripe_subscription_cancellation( $result, $payment ) {
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'] ) {
159 + return $result;
160 + }
161 +
162 + if ( empty( $payment['subscription_id'] ) || ! is_string( $payment['subscription_id'] ) ) {
163 + return [
164 + 'success' => false,
165 + 'message' => __( 'Subscription ID not found.', 'sureforms' ),
166 + ];
167 + }
168 +
169 + $subscription_id = $payment['subscription_id'];
170 + $this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';
171 +
172 + $cancel_result = $this->cancel_subscription( $subscription_id );
106 173 if ( ! $cancel_result ) {
107 - wp_send_json_error( [ 'message' => esc_html__( 'Subscription cancellation failed.', 'sureforms' ) ] );
174 + return [
175 + 'success' => false,
176 + 'message' => __( 'Subscription cancellation failed.', 'sureforms' ),
177 + ];
108 178 }
109 179
110 - // Get current logs and add cancel log entry.
180 + // Build log entry.
111 181 $current_logs = Helper::get_array_value( $payment['log'] );
112 -
113 - // Build log messages array.
114 182 $log_messages = [
115 183 sprintf(
116 184 /* translators: %s: Stripe subscription ID */
117 185 __( 'Subscription ID: %s', 'sureforms' ),
118 - $payment['subscription_id']
186 + $subscription_id
119 187 ),
120 188 sprintf(
121 189 /* translators: %s: payment gateway name */
122 190 __( 'Payment Gateway: %s', 'sureforms' ),
@@ -131,33 +199,31 @@
131 199 /* translators: %s: user display name */
132 200 __( 'Canceled by: %s', 'sureforms' ),
133 201 wp_get_current_user()->display_name
134 202 ),
135 - __( 'Note: The subscription has been permanently canceled. The customer will no longer be charged and will lose access to subscription benefits.', 'sureforms' ),
136 203 ];
137 204
138 - // Create new log entry.
139 - $new_log = [
205 + $current_logs[] = [
140 206 'title' => __( 'Subscription Canceled', 'sureforms' ),
141 207 'created_at' => current_time( 'mysql' ),
142 208 'messages' => $log_messages,
143 209 ];
144 - $current_logs[] = $new_log;
145 210
146 - // Update database status to canceled (following WPForms pattern).
147 - $updated = Payments::update(
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.
214 + Payments::update(
148 215 $payment_id,
149 216 [
150 217 'subscription_status' => 'canceled',
151 - 'status' => 'canceled',
152 218 'log' => $current_logs,
153 219 ]
154 220 );
155 - if ( ! $updated ) {
156 - wp_send_json_error( [ 'message' => esc_html__( 'Failed to update subscription status in database.', 'sureforms' ) ] );
157 - }
158 221
159 - wp_send_json_success( [ 'message' => esc_html__( 'Subscription canceled successfully!', 'sureforms' ) ] );
222 + return [
223 + 'success' => true,
224 + 'message' => __( 'Subscription cancelled successfully.', 'sureforms' ),
225 + ];
160 226 }
161 227
162 228 /**
163 229 * Process Stripe payment refund via filter system.
@@ -201,9 +267,9 @@
201 267 ];
202 268 }
203 269
204 270 try {
205 - $this->payment_mode = $payment['payment_mode'] ?? 'test';
271 + $this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';
206 272
207 273 // Detect subscription payments and route to specialized handler (following WPForms pattern).
208 274 if ( isset( $payment['type'], $payment['subscription_id'] ) && ! empty( $payment['type'] ) && ! empty( $payment['subscription_id'] ) ) {
209 275 return $this->refund_subscription_payment_via_filter( $payment, $refund_amount, $refund_notes );
@@ -393,9 +459,9 @@
393 459 if ( ! $payment ) {
394 460 wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'sureforms' ) ] );
395 461 }
396 462
397 - $this->payment_mode = $payment['payment_mode'] ?? 'test';
463 + $this->payment_mode = ! empty( $payment['mode'] ) && is_string( $payment['mode'] ) ? $payment['mode'] : 'test';
398 464
399 465 // Validate it's a subscription payment.
400 466 if ( empty( $payment['type'] ) || 'subscription' !== $payment['type'] ) {
401 467 wp_send_json_error( [ 'message' => esc_html__( 'This is not a subscription payment.', 'sureforms' ) ] );
@@ -766,9 +832,11 @@
766 832 }
767 833
768 834 // Step 3: Verify subscription payment status.
769 835 // Note: 'active' status is used for subscription records, while 'succeeded' is used for one-time payments.
770 - $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' ];
771 839 if ( empty( $payment['status'] ) || ! in_array( $payment['status'], $refundable_statuses, true ) ) {
772 840 return [
773 841 'success' => false,
774 842 'message' => __( 'Only active, succeeded, or partially refunded subscription payments can be refunded.', 'sureforms' ),
@@ -947,9 +1015,9 @@
947 1015 if ( is_string( $charge_id ) && '' !== $charge_id ) {
948 1016 return $this->create_refund_by_charge( $payment, $charge_id, $refund_amount, $refund_notes );
949 1017 }
950 1018
951 - 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' ) );
952 1020 }
953 1021
954 1022 /**
955 1023 * Create refund using charge ID