PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.1
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
sureforms / inc / payments / front-end.php

front-end.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.1, at inc/payments/front-end.php

1,658 lines 65.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureForms Payments Frontend Class.
4 *
5 * @package sureforms
6 * @since 2.0.0
7 */
8
9 namespace SRFM\Inc\Payments;
10
11 use SRFM\Inc\Database\Tables\Payments;
12 use SRFM\Inc\Field_Validation;
13 use SRFM\Inc\Payments\Stripe\Stripe_Helper;
14 use SRFM\Inc\Submit_Token;
15 use SRFM\Inc\Traits\Get_Instance;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * SureForms Payments Frontend Class.
23 *
24 * @since 2.0.0
25 */
26 class Front_End {
27 use Get_Instance;
28
29 /**
30 * Stores payment entries for later linking with form submissions.
31 *
32 * @var array
33 * @since 2.0.0
34 */
35 private $stripe_payment_entries = [];
36
37 /**
38 * Constructor.
39 *
40 * @since 2.0.0
41 */
42 public function __construct() {
43 add_action( 'wp_ajax_srfm_create_payment_intent', [ $this, 'create_payment_intent' ] );
44 add_action( 'wp_ajax_nopriv_srfm_create_payment_intent', [ $this, 'create_payment_intent' ] );
45 add_action( 'wp_ajax_srfm_create_subscription_intent', [ $this, 'create_subscription_intent' ] );
46 add_action( 'wp_ajax_nopriv_srfm_create_subscription_intent', [ $this, 'create_subscription_intent' ] ); // For non-logged-in users.
47 add_filter( 'srfm_form_submit_data', [ $this, 'validate_payment_fields' ], 5, 1 );
48 add_action( 'srfm_form_submit', [ $this, 'update_payment_entry_id_form_submit' ], 10, 1 );
49 add_filter( 'srfm_show_options_values', [ $this, 'show_options_values' ], 10, 2 );
50 add_filter( 'srfm_all_data_field_row', [ $this, 'skip_payment_fields_from_all_data' ], 10, 2 );
51 add_filter( 'srfm_map_slug_to_submission_data_should_skip', [ $this, 'skip_payment_fields_from_submission_data' ], 10, 2 );
52 add_filter( 'srfm_should_skip_field_from_sample_data', [ $this, 'skip_payment_fields_from_sample_data' ], 10, 2 );
53 }
54
55 /**
56 * Show options values
57 *
58 * @param bool $default_value Default value.
59 * @param bool $value Value.
60 * @since 2.0.0
61 * @return bool
62 */
63 public function show_options_values( $default_value, $value ) {
64 return $value ? true : $default_value;
65 }
66 /**
67 * Create payment intent
68 *
69 * @throws \Exception When Stripe configuration is invalid.
70 * @since 2.0.0
71 * @return void
72 */
73 public function create_payment_intent() {
74 // Verify submit token.
75 $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
76 $form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
77 if ( ! Submit_Token::verify( $token, $form_id ) ) {
78 wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
79 }
80
81 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified via Submit_Token::verify() above.
82 $amount = intval( $_POST['amount'] ?? 0 );
83 $currency = sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'usd' ) );
84 $description = sanitize_text_field( wp_unslash( $_POST['description'] ?? 'SureForms Payment' ) );
85 $block_id = sanitize_text_field( wp_unslash( $_POST['block_id'] ?? '' ) );
86 $customer_email = sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) );
87 $customer_name = sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) );
88 $form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
89 // phpcs:enable WordPress.Security.NonceVerification.Missing
90
91 if ( $amount <= 0 ) {
92 wp_send_json_error( __( 'Invalid payment amount.', 'sureforms' ) );
93 }
94
95 $amount_processed_with_currency = Stripe_Helper::amount_from_stripe_format( $amount, $currency );
96 // Validate payment amount against stored form configuration.
97 if ( $form_id <= 0 || empty( $block_id ) ) {
98 wp_send_json_error( __( 'Invalid form configuration.', 'sureforms' ) );
99 }
100
101 // BOTH MODE: pass 'one-time' so the validator uses the correct per-type amount config.
102 $validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id, 'one-time' );
103 if ( ! $validation_result['valid'] ) {
104 wp_send_json_error( $validation_result['message'] );
105 }
106
107 // Validate customer email (required for one-time payments).
108 if ( empty( $customer_email ) || ! is_email( $customer_email ) ) {
109 wp_send_json_error( __( 'Valid customer email is required for payments.', 'sureforms' ) );
110 }
111
112 try {
113 // Validate Stripe connection.
114 if ( ! Stripe_Helper::is_stripe_connected() ) {
115 throw new \Exception( __( 'Stripe is not connected.', 'sureforms' ) );
116 }
117
118 $secret_key = Stripe_Helper::get_stripe_secret_key();
119
120 if ( empty( $secret_key ) ) {
121 throw new \Exception( __( 'Stripe secret key not found.', 'sureforms' ) );
122 }
123
124 // Create or get customer ID for logged-in users.
125 $customer_id = null;
126 if ( is_user_logged_in() ) {
127 $customer_id = $this->get_or_create_stripe_customer(
128 [
129 'email' => $customer_email,
130 'name' => $customer_name,
131 ]
132 );
133 }
134
135 $license_key = Stripe_Helper::get_license_key();
136
137 // Create payment intent with confirm: true for immediate processing.
138 $payment_intent_data = [
139 'secret_key' => $secret_key,
140 'amount' => $amount,
141 'currency' => strtolower( $currency ),
142 'description' => $description,
143 'confirm' => false, // Will be confirmed by frontend.
144 'receipt_email' => $customer_email,
145 'license_key' => $license_key,
146 // One-time payments use manual capture; methods that don't support it (Bacs, Link, Cash App, BNPL) make
147 // Stripe reject the deferred Elements session in live mode, and an automatic-payment-methods intent can't
148 // be confirmed by the card-scoped client Element. Pin to card so the client Element, this payload, and the
149 // middleware intent all agree (Apple/Google Pay are still surfaced through 'card').
150 'payment_method_types' => [ 'card' ],
151 'metadata' => [
152 'source' => 'SureForms',
153 'block_id' => $block_id,
154 'original_amount' => $amount,
155 'receipt_email' => $customer_email,
156 'customer_name' => $customer_name,
157 ],
158 ];
159
160 // Add customer ID to payment intent data if user is logged in.
161 if ( ! empty( $customer_id ) ) {
162 $payment_intent_data['customer'] = $customer_id;
163 }
164
165 $payment_intent_data = apply_filters(
166 'srfm_create_payment_intent_data',
167 $payment_intent_data,
168 $customer_id
169 );
170
171 $payment_intent_data = wp_json_encode( $payment_intent_data );
172 $payment_intent_data = is_string( $payment_intent_data ) ? $payment_intent_data : '';
173 $payment_intent_data = base64_encode( $payment_intent_data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
174
175 $payment_intent = wp_remote_post(
176 Stripe_Helper::middle_ware_base_url() . 'payment-intent/create',
177 [
178 'body' => $payment_intent_data,
179 'headers' => [
180 'Content-Type' => 'application/json',
181 ],
182 ]
183 );
184
185 if ( is_wp_error( $payment_intent ) ) {
186 throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
187 }
188
189 $payment_intent = json_decode( wp_remote_retrieve_body( $payment_intent ), true );
190 $payment_intent = is_array( $payment_intent ) ? $payment_intent : [];
191
192 // Check if we have an error from Stripe API (verify both status and code).
193 if ( isset( $payment_intent['status'] ) && 'error' === $payment_intent['status'] && isset( $payment_intent['code'] ) && ! empty( $payment_intent['code'] ) ) {
194 // Handle amount_too_small error with custom message.
195 if ( 'amount_too_small' === $payment_intent['code'] ) {
196 // Format the amount for display.
197 $currency_symbol = Stripe_Helper::get_currency_symbol( $currency );
198 $display_amount = $amount_processed_with_currency;
199 $formatted_amount = $currency_symbol . number_format( $display_amount, 2 );
200
201 throw new \Exception(
202 sprintf(
203 /* translators: %s: formatted payment amount */
204 __( 'The payment amount (%s) is below the minimum allowed. Stripe only processes amounts above 50¢.', 'sureforms' ),
205 $formatted_amount
206 )
207 );
208 }
209
210 // For other error codes, use the message from Stripe API if available.
211 if ( isset( $payment_intent['message'] ) && ! empty( $payment_intent['message'] ) ) {
212 throw new \Exception( $payment_intent['message'] );
213 }
214
215 // Fallback if we have error code but no message.
216 throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
217 }
218
219 if ( ! isset( $payment_intent['client_secret'] ) || empty( $payment_intent['client_secret'] ) || ! isset( $payment_intent['id'] ) || empty( $payment_intent['id'] ) ) {
220 throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
221 }
222
223 // Store payment intent metadata in transient for verification.
224 // active_type binds this intent to the one-time flow so a tampered
225 // submission cannot replay it through the subscription submit path.
226 Payment_Helper::store_payment_intent_metadata(
227 $block_id,
228 $payment_intent['id'],
229 [
230 'form_id' => $form_id,
231 'block_id' => $block_id,
232 'amount' => $amount_processed_with_currency,
233 'currency' => strtolower( $currency ),
234 'active_type' => 'one-time',
235 ]
236 );
237
238 wp_send_json_success(
239 [
240 'client_secret' => $payment_intent['client_secret'],
241 'payment_intent_id' => $payment_intent['id'],
242 'customer_id' => $customer_id,
243 ]
244 );
245 } catch ( \Exception $e ) {
246 $error_message = $e->getMessage();
247 $error_message = empty( $error_message ) ? Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) : $error_message;
248 wp_send_json_error( $error_message );
249 }
250 }
251
252 /**
253 * Create subscription intent with improved error handling from simple-stripe-subscriptions
254 *
255 * @throws \Exception When Stripe configuration is invalid.
256 * @since 2.0.0
257 * @return void
258 */
259 public function create_subscription_intent() {
260 // Verify submit token.
261 $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
262 $form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
263 if ( ! Submit_Token::verify( $token, $form_id ) ) {
264 wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
265 }
266
267 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified via Submit_Token::verify() above.
268
269 // Validate required fields like simple-stripe-subscriptions.
270 $required_fields = [ 'amount', 'currency', 'description', 'block_id', 'interval', 'plan_name' ];
271 foreach ( $required_fields as $field ) {
272 if ( empty( $_POST[ $field ] ) ) {
273 /* translators: %s: Field name */
274 wp_send_json_error( sprintf( __( 'Missing required field: %s', 'sureforms' ), $field ) );
275 }
276 }
277
278 $amount = intval( $_POST['amount'] ?? 0 );
279 $currency = sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'usd' ) );
280 $description = sanitize_text_field( wp_unslash( $_POST['description'] ?? 'SureForms Subscription' ) );
281 $block_id = sanitize_text_field( wp_unslash( $_POST['block_id'] ?? '' ) );
282
283 $subscription_interval = sanitize_text_field( wp_unslash( $_POST['interval'] ?? 'month' ) );
284 $plan_name = sanitize_text_field( wp_unslash( $_POST['plan_name'] ?? 'Subscription Plan' ) );
285 $customer_email = sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) );
286 $customer_name = sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) );
287 $form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
288
289 // phpcs:enable WordPress.Security.NonceVerification.Missing
290
291 // Validate customer email (required for all subscriptions).
292 if ( empty( $customer_email ) || ! is_email( $customer_email ) ) {
293 wp_send_json_error( __( 'Valid customer email is required for subscriptions.', 'sureforms' ) );
294 }
295
296 // Validate customer name (required for subscriptions).
297 if ( empty( $customer_name ) ) {
298 wp_send_json_error( __( 'Customer name is required for subscriptions.', 'sureforms' ) );
299 }
300
301 $amount_processed_with_currency = Stripe_Helper::amount_from_stripe_format( $amount, $currency );
302 // Validate payment amount against stored form configuration.
303 if ( $form_id <= 0 || empty( $block_id ) ) {
304 wp_send_json_error( __( 'Invalid form configuration.', 'sureforms' ) );
305 }
306
307 // BOTH MODE: pass 'subscription' so the validator uses the correct per-type amount config.
308 $validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id, 'subscription' );
309 if ( ! $validation_result['valid'] ) {
310 wp_send_json_error( $validation_result['message'] );
311 }
312
313 // Validate amount like simple-stripe-subscriptions.
314 if ( $amount <= 0 ) {
315 wp_send_json_error( __( 'Amount must be greater than 0', 'sureforms' ) );
316 }
317
318 // Validate interval like simple-stripe-subscriptions.
319 // BOTH MODE: 'quarter' is a valid editor option but was missing from the allow-list,
320 // causing Quarterly subscriptions to be rejected at submit time.
321 $valid_intervals = [ 'day', 'week', 'month', 'quarter', 'year' ];
322 if ( ! in_array( $subscription_interval, $valid_intervals, true ) ) {
323 wp_send_json_error( __( 'Invalid billing interval', 'sureforms' ) );
324 }
325
326 // Reject when the submitted interval does not match what the admin saved in
327 // the form's stored block config. Admin picks a single interval in the editor;
328 // the end user has no chooser. So a divergence here is always tampering — the
329 // data attribute the server itself rendered has been altered before submit.
330 $stored_block_config = Field_Validation::get_or_migrate_block_config_for_legacy_form( $form_id );
331 if ( is_array( $stored_block_config ) && isset( $stored_block_config[ $block_id ] ) && is_array( $stored_block_config[ $block_id ] ) ) {
332 $stored_interval = $stored_block_config[ $block_id ]['subscription_interval'] ?? '';
333 if ( ! empty( $stored_interval ) && $stored_interval !== $subscription_interval ) {
334 wp_send_json_error( __( 'Billing interval does not match the form configuration.', 'sureforms' ) );
335 }
336 }
337
338 try {
339 // Validate Stripe connection.
340 if ( ! Stripe_Helper::is_stripe_connected() ) {
341 throw new \Exception( __( 'Stripe is not connected.', 'sureforms' ) );
342 }
343
344 $secret_key = Stripe_Helper::get_stripe_secret_key();
345
346 if ( empty( $secret_key ) ) {
347 throw new \Exception( __( 'Stripe secret key not found.', 'sureforms' ) );
348 }
349
350 // Get or create Stripe customer for subscriptions.
351 $customer_id = $this->get_or_create_stripe_customer(
352 [
353 'email' => $customer_email,
354 'name' => $customer_name,
355 ]
356 );
357 if ( ! $customer_id ) {
358 throw new \Exception( __( 'Failed to create customer for subscription.', 'sureforms' ) );
359 }
360
361 $license_key = Stripe_Helper::get_license_key();
362 // Prepare subscription data for middleware.
363 $subscription_data = apply_filters(
364 'srfm_create_subscription_data',
365 [
366 'secret_key' => $secret_key,
367 'customer_id' => $customer_id,
368 'amount' => $amount,
369 'currency' => strtolower( $currency ),
370 'description' => $description,
371 'interval' => $subscription_interval,
372 'license_key' => $license_key,
373 'block_id' => $block_id,
374 'plan_name' => $plan_name,
375 'metadata' => [
376 'source' => 'SureForms',
377 'block_id' => $block_id,
378 'original_amount' => $amount,
379 'billing_interval' => $subscription_interval,
380 ],
381 ]
382 );
383
384 $endpoint = Stripe_Helper::middle_ware_base_url() . 'subscription/create';
385
386 $subscription_data_body = wp_json_encode( $subscription_data );
387 $subscription_data_body = is_string( $subscription_data_body ) ? $subscription_data_body : '';
388 $subscription_data_body = base64_encode( $subscription_data_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
389
390 if ( empty( $subscription_data_body ) ) {
391 throw new \Exception( __( 'Failed to create subscription through middleware.', 'sureforms' ) );
392 }
393
394 // Call middleware subscription creation endpoint.
395 $subscription_response = wp_remote_post(
396 $endpoint,
397 [
398 'body' => $subscription_data_body,
399 'headers' => [
400 'Content-Type' => 'application/json',
401 ],
402 'timeout' => 60, // Subscription creation can take longer.
403 ]
404 );
405
406 if ( is_wp_error( $subscription_response ) ) {
407 throw new \Exception( __( 'Failed to create subscription through middleware.', 'sureforms' ) );
408 }
409
410 $response_body = wp_remote_retrieve_body( $subscription_response );
411 if ( empty( $response_body ) ) {
412 throw new \Exception( __( 'Empty response from subscription creation.', 'sureforms' ) );
413 }
414
415 $subscription = json_decode( $response_body, true );
416 if ( json_last_error() !== JSON_ERROR_NONE ) {
417 throw new \Exception( __( 'Invalid JSON response from subscription creation.', 'sureforms' ) );
418 }
419
420 if ( ! is_array( $subscription ) ) {
421 wp_send_json_error( __( 'Invalid subscription data.', 'sureforms' ) );
422 }
423
424 if ( 'error' === $subscription['status'] ) {
425 wp_send_json_error( isset( $subscription['message'] ) && ! empty( $subscription['message'] ) ? $subscription['message'] : __( 'Invalid subscription data.', 'sureforms' ) );
426 }
427
428 $payment_intent_id = isset( $subscription['setup_intent']['id'] ) && ! empty( $subscription['setup_intent']['id'] ) ? $subscription['setup_intent']['id'] : '';
429 $subscription_id = isset( $subscription['subscription_data']['id'] ) && ! empty( $subscription['subscription_data']['id'] ) ? $subscription['subscription_data']['id'] : '';
430 $client_secret = isset( $subscription['client_secret'] ) && ! empty( $subscription['client_secret'] ) ? $subscription['client_secret'] : '';
431 if ( empty( $client_secret ) || empty( $subscription_id ) || empty( $payment_intent_id ) ) {
432 throw new \Exception( __( 'Failed to create subscription.', 'sureforms' ) );
433 }
434
435 // Store subscription metadata in transient for verification.
436 // active_type binds this intent to the subscription flow so a tampered
437 // submission cannot replay it through the one-time submit path.
438 Payment_Helper::store_payment_intent_metadata(
439 $block_id,
440 $payment_intent_id,
441 [
442 'form_id' => $form_id,
443 'block_id' => $block_id,
444 'amount' => $amount_processed_with_currency,
445 'currency' => strtolower( $currency ),
446 'subscription_id' => $subscription_id,
447 'active_type' => 'subscription',
448 ]
449 );
450
451 $response = [
452 'type' => 'subscription',
453 'client_secret' => $client_secret,
454 'subscription_id' => $subscription_id,
455 'customer_id' => $customer_id,
456 'payment_intent_id' => $payment_intent_id,
457 'amount' => Stripe_Helper::amount_from_stripe_format( $amount, $currency ),
458 'interval' => $subscription_interval,
459 ];
460
461 wp_send_json_success( $response );
462
463 } catch ( \Exception $e ) {
464 /* translators: %s: Error message */
465 wp_send_json_error( sprintf( __( 'Unexpected error: %s', 'sureforms' ), $e->getMessage() ) );
466 }
467 }
468
469 /**
470 * Validate payment fields before form submission
471 *
472 * @param array<mixed> $form_data Form data.
473 * @since 2.0.0
474 * @return array<mixed>
475 */
476 public function validate_payment_fields( $form_data ) {
477 // Check if form data is valid.
478 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
479 return $form_data;
480 }
481
482 $payment_response = [];
483
484 // Loop through form data to find payment fields.
485 foreach ( $form_data as $field_name => $field_value ) {
486 // Check if field name contains "-lbl-" pattern.
487 if ( strpos( $field_name, '-lbl-' ) === false ) {
488 continue;
489 }
490
491 // Split field name by "-lbl-" delimiter.
492 $name_parts = explode( '-lbl-', $field_name );
493
494 // Check if we have the expected parts.
495 if ( count( $name_parts ) < 2 ) {
496 continue;
497 }
498
499 // Check if the first part starts with "srfm-payment-".
500 if ( ! ( strpos( $name_parts[0], 'srfm-payment-' ) === 0 ) ) {
501 continue;
502 }
503
504 // Value will be in the form of the json string.
505 $payment_value = json_decode( $field_value, true );
506
507 if ( empty( $payment_value ) || ! is_array( $payment_value ) ) {
508 continue;
509 }
510
511 // Extract payment ID - this will be the payment intent ID for one-time payments,
512 // or the payment method ID (result.setupIntent.payment_method) for subscriptions.
513 $payment_id = ! empty( $payment_value['paymentId'] ) ? $payment_value['paymentId'] : '';
514 $setup_intent = ! empty( $payment_value['setupIntent'] ) ? $payment_value['setupIntent'] : '';
515
516 // introduced during the paypal implementation and in the other payment methods, we use the transactionId to verify the payment.
517 $transaction_id = ! empty( $payment_value['transactionId'] ) ? $payment_value['transactionId'] : '';
518
519 if ( empty( $payment_id ) && empty( $setup_intent ) && empty( $transaction_id ) ) {
520 continue;
521 }
522
523 $block_id = ! empty( $payment_value['blockId'] ) ? $payment_value['blockId'] : '';
524 $payment_type = ! empty( $payment_value['paymentType'] ) ? $payment_value['paymentType'] : '';
525
526 $payment_method = ! empty( $payment_value['paymentMethod'] ) ? $payment_value['paymentMethod'] : 'stripe';
527
528 if ( empty( $block_id ) || empty( $payment_type ) ) {
529 continue;
530 }
531
532 if ( 'stripe' === $payment_method ) {
533 $payment_response = $this->verify_stripe_payment( $payment_value, $payment_id, $block_id, $form_data, $payment_type );
534 } else {
535 $payment_response = apply_filters(
536 'srfm_verify_payment_value',
537 [
538 'payment_value' => $payment_value,
539 'class' => $this,
540 'block_id' => $block_id,
541 'form_data' => $form_data,
542 ]
543 );
544 }
545
546 if ( ! empty( $payment_response ) && isset( $payment_response['payment_id'] ) ) {
547 // Modify the form data with the payment ID.
548 $form_data[ $field_name ] = $payment_response['payment_id'];
549 }
550 }
551
552 if ( ! empty( $payment_response ) && isset( $payment_response['error'] ) ) {
553 $form_data = array_merge( $form_data, $payment_response );
554 }
555
556 return $form_data;
557 }
558
559 /**
560 * Verify Stripe payment
561 *
562 * @param array<mixed> $payment_value Payment value.
563 * @param string $payment_id Payment ID.
564 * @param string $block_id Block ID.
565 * @param array<mixed> $form_data Form data.
566 * @param string $payment_type Payment type.
567 * @since 2.0.0
568 * @return array<mixed> Payment response.
569 */
570 public function verify_stripe_payment( $payment_value, $payment_id, $block_id, $form_data, $payment_type ) {
571 if ( 'stripe-subscription' === $payment_type ) {
572
573 /**
574 * For subscription payments, we receive the following data structure:
575 * - paymentMethod: Stripe payment method ID (e.g., "pm_1S82ZkHqS7N4oFQhruGV67u1")
576 * - setupIntent: Stripe setup intent ID (e.g., "seti_1S82ZkHqS7N4oFQhPa4LYPYg")
577 * - subscriptionId: Stripe subscription ID (e.g., "sub_1S82ZiHqS7N4oFQhPGhm2eNR")
578 * - customerId: Stripe customer ID (e.g., "cus_T4Apjla33GlYAk")
579 * - blockId: Form block identifier (e.g., "be920796")
580 * - paymentType: Payment type identifier ("stripe-subscription")
581 * - status: Payment status ("succeeded")
582 */
583 $payment_response = $this->verify_stripe_subscription_intent_and_save( $payment_value, $block_id, $form_data );
584 } else {
585 $payment_response = $this->verify_stripe_payment_intent_and_save( $payment_value, $payment_id, $block_id, $form_data );
586 }
587
588 return ! empty( $payment_response ) && is_array( $payment_response ) ? $payment_response : [];
589 }
590
591 /**
592 * Simplified subscription verification using simple-stripe-subscriptions approach
593 *
594 * @param array<mixed> $subscription_value Subscription data from frontend.
595 * @param string $block_id Block ID.
596 * @param array<mixed> $form_data Form data.
597 * @since 2.0.0
598 * @return void|array<mixed> True if subscription is verified and saved successfully.
599 */
600 public function verify_stripe_subscription_intent_and_save( $subscription_value, $block_id, $form_data ) {
601 $subscription_id = ! empty( $subscription_value['subscriptionId'] ) && is_string( $subscription_value['subscriptionId'] ) ? $subscription_value['subscriptionId'] : '';
602
603 if ( empty( $subscription_id ) ) {
604 return [
605 'error' => __( 'Subscription ID not found.', 'sureforms' ),
606 ];
607 }
608
609 $customer_id = ! empty( $subscription_value['customerId'] ) ? $subscription_value['customerId'] : '';
610 $setup_intent_id = ! empty( $subscription_value['setupIntent'] ) && is_string( $subscription_value['setupIntent'] ) ? $subscription_value['setupIntent'] : '';
611
612 // Verify payment intent with comprehensive validation including form data.
613 // BOTH MODE: pass 'subscription' so per-type amount config is used for verification.
614 $verification_result = Payment_Helper::verify_payment_intent( $block_id, $setup_intent_id, $form_data, 'subscription' );
615
616 if ( false === $verification_result['valid'] ) {
617 return [
618 'error' => $verification_result['message'],
619 ];
620 }
621
622 if ( empty( $customer_id ) ) {
623 return [
624 'error' => __( 'Customer ID not found for the payment.', 'sureforms' ),
625 ];
626 }
627
628 try {
629 // Get payment mode and secret key.
630 $payment_mode = Stripe_Helper::get_stripe_mode();
631 $secret_key = Stripe_Helper::get_stripe_secret_key();
632
633 if ( empty( $secret_key ) ) {
634 return [
635 'error' => __( 'Stripe secret key not found.', 'sureforms' ),
636 ];
637 }
638
639 // Update subscription with payment method from setup intent if available.
640 $paid_invoice = [];
641 if ( ! empty( $setup_intent_id ) ) {
642 try {
643 $setup_intent_response = Stripe_Helper::stripe_api_request(
644 'setup_intents',
645 'GET',
646 [],
647 $setup_intent_id
648 );
649
650 if ( ! $setup_intent_response['success'] ) {
651 return [
652 'error' => $setup_intent_response['error']['message'] ?? __( 'Failed to retrieve setup intent.', 'sureforms' ),
653 ];
654 }
655
656 $setup_intent = $setup_intent_response['data'];
657
658 if ( ( isset( $setup_intent['payment_method'] ) && ! empty( $setup_intent['payment_method'] ) && is_string( $setup_intent['payment_method'] ) ) ) {
659
660 // Prepare subscription update data.
661 $subscription_update_data = [
662 'default_payment_method' => $setup_intent['payment_method'],
663 'collection_method' => 'charge_automatically',
664 ];
665
666 // Override interval + billing cycles with the values stored in the
667 // form's block config. These come from the data attributes the
668 // server itself rendered, so they cannot legitimately diverge from
669 // the admin's saved subscriptionPlan. Trusting the submitted values
670 // would let an attacker DevTools-flip cancel_at to 'ongoing'.
671 $form_id_for_config = isset( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? intval( $form_data['form-id'] ) : 0;
672 if ( $form_id_for_config > 0 && ! empty( $block_id ) ) {
673 $stored_block_config = Field_Validation::get_or_migrate_block_config_for_legacy_form( $form_id_for_config );
674 if ( is_array( $stored_block_config ) && isset( $stored_block_config[ $block_id ] ) && is_array( $stored_block_config[ $block_id ] ) ) {
675 $stored_payment_config = $stored_block_config[ $block_id ];
676 if ( isset( $stored_payment_config['subscription_interval'] ) ) {
677 $subscription_value['subscriptionInterval'] = $stored_payment_config['subscription_interval'];
678 }
679 if ( isset( $stored_payment_config['subscription_billing_cycles'] ) ) {
680 $subscription_value['subscriptionBillingCycles'] = $stored_payment_config['subscription_billing_cycles'];
681 }
682 }
683 }
684
685 // Calculate cancel_at timestamp based on billing cycles and interval.
686 $cancel_at = $this->prepare_cancel_at( $subscription_value );
687 if ( ! empty( $cancel_at ) ) {
688 $subscription_update_data['cancel_at'] = $cancel_at;
689 }
690
691 $subscription_update_response = Stripe_Helper::stripe_api_request(
692 'subscriptions',
693 'POST',
694 $subscription_update_data,
695 $subscription_id
696 );
697
698 if ( ! $subscription_update_response['success'] ) {
699 return [
700 'error' => $subscription_update_response['error']['message'] ?? __( 'Failed to update subscription.', 'sureforms' ),
701 ];
702 }
703
704 $subscription_update = $subscription_update_response['data'];
705
706 if ( empty( $subscription_update['latest_invoice'] ) ) {
707 return [
708 'error' => __( 'Latest invoice not found on subscription.', 'sureforms' ),
709 ];
710 }
711
712 $invoice_response = Stripe_Helper::stripe_api_request(
713 'invoices',
714 'GET',
715 [],
716 $subscription_update['latest_invoice']
717 );
718
719 if ( ! $invoice_response['success'] ) {
720 return [
721 'error' => $invoice_response['error']['message'] ?? __( 'Failed to retrieve invoice.', 'sureforms' ),
722 ];
723 }
724
725 $invoice = $invoice_response['data'];
726
727 // Ensure invoice auto-advance is enabled for recurring payments.
728 // This tells Stripe to automatically finalize and charge future invoices.
729 if ( empty( $invoice['auto_advance'] ) && ! empty( $invoice['id'] ) && is_string( $invoice['id'] ) ) {
730 Stripe_Helper::stripe_api_request(
731 'invoices',
732 'POST',
733 [ 'auto_advance' => true ],
734 $invoice['id']
735 );
736 }
737
738 // Extract payment intent from the invoice.
739 $payment_intent_id = isset( $invoice['payment_intent'] ) && ! empty( $invoice['payment_intent'] ) && is_string( $invoice['payment_intent'] ) ? $invoice['payment_intent'] : '';
740
741 if ( empty( $payment_intent_id ) ) {
742 return [
743 'error' => __( 'Payment intent not found on invoice.', 'sureforms' ),
744 ];
745 }
746
747 // Confirm the payment intent with payment method.
748 // This completes the payment and activates the subscription.
749 $paid_invoice_response = Stripe_Helper::stripe_api_request(
750 'payment_intents',
751 'POST',
752 [ 'payment_method' => $setup_intent['payment_method'] ],
753 $payment_intent_id . '/confirm'
754 );
755
756 if ( ! $paid_invoice_response['success'] ) {
757 return [
758 'error' => $paid_invoice_response['error']['message'] ?? __( 'Failed to confirm payment.', 'sureforms' ),
759 ];
760 }
761
762 $paid_invoice = $paid_invoice_response['data'];
763
764 // Get the subscription.
765 $subscription_response = Stripe_Helper::stripe_api_request(
766 'subscriptions',
767 'GET',
768 [],
769 $subscription_id
770 );
771
772 if ( ! $subscription_response['success'] ) {
773 return [
774 'error' => $subscription_response['error']['message'] ?? __( 'Failed to retrieve subscription.', 'sureforms' ),
775 ];
776 }
777
778 $subscription = $subscription_response['data'];
779 }
780 } catch ( \Exception $e ) {
781 return [
782 'error' => $e->getMessage(),
783 ];
784 }
785 }
786
787 if ( empty( $subscription ) ) {
788 return [
789 'error' => __( 'Subscription not found for the payment.', 'sureforms' ),
790 ];
791 }
792
793 // Use simple-stripe-subscriptions validation logic - check if subscription is in good state.
794 $is_subscription_active = in_array( $subscription['status'], [ 'active', 'trialing' ], true );
795 $final_status = $is_subscription_active ? 'active' : 'failed';
796
797 $amount = isset( $paid_invoice['amount'] ) && ! empty( $paid_invoice['amount'] ) ? $paid_invoice['amount'] : 0;
798 $currency = isset( $paid_invoice['currency'] ) && ! empty( $paid_invoice['currency'] ) ? $paid_invoice['currency'] : 'usd';
799 $form_id = isset( $form_data['form-id'] ) && ! empty( $form_data['form-id'] ) ? $form_data['form-id'] : 0;
800 $subscription_status = isset( $subscription['status'] ) && ! empty( $subscription['status'] ) && is_string( $subscription['status'] ) ? $subscription['status'] : '';
801
802 // Defense-in-depth: re-validate the amount Stripe actually invoiced against the form's
803 // server-side configuration. The recurring price is the invoiced amount, so an
804 // underpayment here would otherwise repeat every billing cycle.
805 $charged_amount = Stripe_Helper::amount_from_stripe_format( is_numeric( $amount ) ? (int) $amount : 0, is_string( $currency ) ? $currency : 'usd' );
806 $charge_validation = Payment_Helper::validate_amount_against_config( $block_id, is_numeric( $form_id ) ? (int) $form_id : 0, $form_data, $charged_amount, 'subscription' );
807 if ( false === $charge_validation['valid'] ) {
808 return [
809 'error' => $charge_validation['message'],
810 ];
811 }
812
813 $invoice_status = isset( $paid_invoice['status'] ) && ! empty( $paid_invoice['status'] ) && is_string( $paid_invoice['status'] ) ? $paid_invoice['status'] : '';
814
815 // Extract customer data.
816 $customer_data = $this->extract_customer_data( $subscription_value );
817
818 // Extract charge ID from the first payment intent for refund purposes.
819 // For subscriptions, we store the charge ID in transaction_id so refunds can be processed.
820 $charge_id = '';
821 if ( ! empty( $paid_invoice['latest_charge'] ) && is_string( $paid_invoice['latest_charge'] ) ) {
822 $charge_id = $paid_invoice['latest_charge'];
823 } elseif ( ! empty( $paid_invoice['charges']['data'][0]['id'] ) && is_string( $paid_invoice['charges']['data'][0]['id'] ) ) {
824 $charge_id = $paid_invoice['charges']['data'][0]['id'];
825 }
826
827 // Use charge ID as transaction_id if available, otherwise fall back to subscription ID.
828 $transaction_id = ! empty( $charge_id ) ? $charge_id : $subscription_id;
829
830 // Send payment data to middleware for analytics.
831 if ( ! empty( $charge_id ) ) {
832 Stripe_Helper::intersect_payment( $charge_id, $secret_key, '', 'SureForms' );
833 }
834
835 // Prepare minimal subscription data for database.
836 $entry_data = [
837 'form_id' => $form_id,
838 'block_id' => $block_id,
839 'status' => $final_status,
840 'total_amount' => Stripe_Helper::amount_from_stripe_format( $amount, $currency ),
841 'currency' => $currency,
842 'entry_id' => 0,
843 'gateway' => 'stripe',
844 'type' => 'subscription',
845 'mode' => $payment_mode,
846 'transaction_id' => $transaction_id,
847 'customer_id' => $customer_id,
848 'subscription_id' => $subscription_id,
849 'subscription_status' => $subscription_status,
850 'srfm_txn_id' => '', // Will be updated after getting payment entry ID.
851 'customer_email' => $customer_data['email'],
852 'customer_name' => $customer_data['name'],
853 'payment_data' => [
854 'initial_invoice' => $paid_invoice,
855 'subscription' => $subscription,
856 'payment_value' => $subscription_value,
857 ],
858 ];
859
860 // Get user ID if logged in.
861 $user_id = get_current_user_id();
862 $user_info = $user_id > 0
863 /* translators: %d: User ID */
864 ? sprintf( __( 'User ID: %d', 'sureforms' ), $user_id )
865 /* translators: Message for guest user in payment logs */
866 : __( 'Guest User', 'sureforms' );
867
868 // If invoice is not paid then we need to set the status in the subscription log and return error.
869 $paid_invoice_log = '';
870 if ( 'paid' !== $invoice_status ) {
871 /* translators: %s: Invoice status */
872 $paid_invoice_log = sprintf( __( 'Invoice Status: %s', 'sureforms' ), $invoice_status );
873 }
874
875 // Add simple log entry.
876 $entry_data['log'] = [
877 [
878 /* translators: Title for subscription verification log */
879 'title' => __( 'Subscription Verification', 'sureforms' ),
880 'created_at' => current_time( 'mysql' ),
881 'messages' => [
882 /* translators: %s: Subscription ID */
883 sprintf( __( 'Subscription ID: %s', 'sureforms' ), $subscription_id ),
884 /* translators: %s: Payment Gateway */
885 sprintf( __( 'Payment Gateway: %s', 'sureforms' ), 'Stripe' ),
886 /* translators: %s: Payment Intent ID */
887 sprintf( __( 'Payment Intent ID: %s', 'sureforms' ), $setup_intent_id ),
888 /* translators: %s: Charge ID */
889 sprintf( __( 'Charge ID: %s', 'sureforms' ), ! empty( $charge_id ) ? $charge_id : 'N/A' ),
890 /* translators: %s: Subscription Status */
891 sprintf( __( 'Subscription Status: %s', 'sureforms' ), $subscription_status ),
892 /* translators: %s: Customer ID */
893 sprintf( __( 'Customer ID: %s', 'sureforms' ), $customer_id ),
894 /* translators: 1: Amount, 2: Currency */
895 sprintf( __( 'Amount: %1$s %2$s', 'sureforms' ), number_format( Stripe_Helper::amount_from_stripe_format( $amount, $currency ), 2 ), strtoupper( $currency ) ),
896 $user_info,
897 /* translators: %s: Payment mode (e.g. Live or Test) */
898 sprintf( __( 'Mode: %s', 'sureforms' ), ucfirst( $payment_mode ) ),
899 $paid_invoice_log,
900 ],
901 ],
902 ];
903
904 // Save to database.
905 $payment_entry_id = Payments::add( $entry_data );
906
907 if ( $payment_entry_id ) {
908 // Generate unique payment ID using the auto-increment ID and update the entry.
909 $unique_payment_id = Stripe_Helper::generate_unique_payment_id( $payment_entry_id );
910 // For initial subscription, set parent_subscription_id to itself (it's the parent).
911 Payments::update(
912 $payment_entry_id,
913 [
914 'srfm_txn_id' => $unique_payment_id,
915 'parent_subscription_id' => $payment_entry_id,
916 ]
917 );
918
919 // Store in static array for later entry linking.
920 $this->stripe_payment_entries[] = [
921 'payment_id' => $transaction_id,
922 'block_id' => $block_id,
923 'form_id' => $form_id,
924 ];
925
926 return [
927 'payment_id' => $payment_entry_id,
928 ];
929 }
930 } catch ( \Exception $e ) {
931 return [
932 'error' => empty( $e->getMessage() ) ? __( 'Failed to verify subscription.', 'sureforms' ) : $e->getMessage(),
933 ];
934 }
935 }
936
937 /**
938 * Prepare cancel_at timestamp for subscription based on billing cycles and interval.
939 *
940 * @param array<string,mixed> $input_value Array containing subscriptionBillingCycles and subscriptionInterval.
941 * @since 2.0.0
942 * @return int|false|null Unix timestamp for cancel_at, or null if not applicable.
943 */
944 public function prepare_cancel_at( $input_value ) {
945 $subscription_billing_cycles = ! empty( $input_value['subscriptionBillingCycles'] ) ? $input_value['subscriptionBillingCycles'] : 0;
946 $subscription_interval = ! empty( $input_value['subscriptionInterval'] ) ? $input_value['subscriptionInterval'] : '';
947
948 // Return null if billing cycles is 0, empty, or equals 'ongoing'.
949 if ( empty( $subscription_billing_cycles ) || 'ongoing' === $subscription_billing_cycles || ! is_numeric( $subscription_billing_cycles ) ) {
950 return null;
951 }
952
953 // Convert billing cycles to integer.
954 $billing_cycles = (int) $subscription_billing_cycles;
955
956 // Return null if billing cycles is less than or equal to 0.
957 if ( $billing_cycles <= 0 ) {
958 return null;
959 }
960
961 // Calculate cancel_at timestamp based on interval.
962 $current_time = time();
963 $cancel_at = null;
964
965 switch ( $subscription_interval ) {
966 case 'day':
967 // Add days: cycles * 1 day.
968 $cancel_at = strtotime( "+{$billing_cycles} days", $current_time );
969 break;
970
971 case 'week':
972 // Add weeks: cycles * 7 days.
973 $cancel_at = strtotime( "+{$billing_cycles} weeks", $current_time );
974 break;
975
976 case 'month':
977 // Add months: cycles * 1 month.
978 $cancel_at = strtotime( "+{$billing_cycles} months", $current_time );
979 break;
980
981 case 'quarter':
982 // Add quarters: cycles * 3 months.
983 $total_months = $billing_cycles * 3;
984 $cancel_at = strtotime( "+{$total_months} months", $current_time );
985 break;
986
987 case 'year':
988 // Add years: cycles * 1 year.
989 $cancel_at = strtotime( "+{$billing_cycles} years", $current_time );
990 break;
991
992 default:
993 // Invalid interval, return null.
994 return null;
995 }
996
997 return $cancel_at;
998 }
999
1000 /**
1001 * Handle form submit and update payment entries with entry_id
1002 *
1003 * This function is called after a form submission to link the created entry
1004 * with any associated Stripe payment records. It matches payment entries
1005 * by form_id and updates them with the newly created entry_id.
1006 *
1007 * @param array<string,mixed> $form_submit_response The form submission response containing entry_id and form_id.
1008 * @since 2.0.0
1009 * @return void
1010 */
1011 public function update_payment_entry_id_form_submit( $form_submit_response ) {
1012 // Check if entry_id exists in the form_submit_response.
1013 if ( ! empty( $form_submit_response['entry_id'] ) && ! empty( $this->stripe_payment_entries ) ) {
1014 $entry_id = is_numeric( $form_submit_response['entry_id'] ) ? intval( $form_submit_response['entry_id'] ) : 0;
1015
1016 // Loop through stored payment entries to update with entry_id.
1017 foreach ( $this->stripe_payment_entries as $stripe_payment_entry ) {
1018 if ( ! empty( $stripe_payment_entry['payment_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) ) {
1019 // Check if form_id matches.
1020 $stored_form_id = isset( $stripe_payment_entry['form_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) && is_numeric( $stripe_payment_entry['form_id'] ) ? intval( $stripe_payment_entry['form_id'] ) : 0;
1021 $response_form_id = isset( $form_submit_response['form_id'] ) && ! empty( $form_submit_response['form_id'] ) && is_numeric( $form_submit_response['form_id'] ) ? intval( $form_submit_response['form_id'] ) : 0;
1022
1023 $payment_id = is_string( $stripe_payment_entry['payment_id'] ) ? sanitize_text_field( $stripe_payment_entry['payment_id'] ) : '';
1024
1025 if ( ! empty( $stored_form_id ) && $stored_form_id === $response_form_id ) {
1026 // Update the payment entry with the entry_id.
1027 $this->update_payment_entry_id( $payment_id, $entry_id );
1028 }
1029 } elseif ( ! empty( $stripe_payment_entry['subscription_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) ) {
1030 // Check if form_id matches for subscription-based payment.
1031 $stored_form_id = isset( $stripe_payment_entry['form_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) && is_numeric( $stripe_payment_entry['form_id'] ) ? intval( $stripe_payment_entry['form_id'] ) : 0;
1032 $response_form_id = isset( $form_submit_response['form_id'] ) && ! empty( $form_submit_response['form_id'] ) && is_numeric( $form_submit_response['form_id'] ) ? intval( $form_submit_response['form_id'] ) : 0;
1033
1034 $subscription_id = is_string( $stripe_payment_entry['subscription_id'] ) ? sanitize_text_field( $stripe_payment_entry['subscription_id'] ) : '';
1035
1036 if ( ! empty( $stored_form_id ) && $stored_form_id === $response_form_id ) {
1037 // Update the payment entry with the entry_id using subscription_id.
1038 $this->update_payment_entry_id_by_subscription_id( $subscription_id, $entry_id );
1039 }
1040 }
1041 }
1042 }
1043 }
1044
1045 /**
1046 * Add payment entry for later linking with form submission.
1047 *
1048 * Allows payment gateways (Stripe, PayPal, etc.) to register their entries
1049 * for linking with form submissions. The entries are stored in memory and
1050 * linked when the form is successfully submitted.
1051 *
1052 * @param array<string,mixed> $entry Payment entry containing payment_id, block_id, and form_id.
1053 * @since 2.0.0
1054 * @return void
1055 */
1056 public function add_payment_entry_for_linking( $entry ) {
1057 if ( ! empty( $entry ) && is_array( $entry ) ) {
1058 $this->stripe_payment_entries[] = $entry;
1059 }
1060 }
1061
1062 /**
1063 * Filter callback to determine if a payment field should be included in all data output.
1064 *
1065 * Excludes payment-related fields (like Stripe payment blocks) from being
1066 * rendered in submission summaries, emails, exports, etc., as these fields
1067 * serve as backend tracking data instead of user input.
1068 *
1069 * @since 2.0.0
1070 *
1071 * @param bool $should_add_field_row Whether this row should be output.
1072 * @param array<string | mixed> $args Args describing the field row. Should contain 'block_name'.
1073 * @return bool False for payment blocks; otherwise, original filter value.
1074 */
1075 public function skip_payment_fields_from_all_data( $should_add_field_row, $args ) {
1076 // Check if the block is a payment block by inspecting the block name.
1077 $block_name = isset( $args['block_name'] ) && is_string( $args['block_name'] ) ? $args['block_name'] : '';
1078 if ( 'srfm-payment' === $block_name ) {
1079 return false;
1080 }
1081 return $should_add_field_row;
1082 }
1083
1084 /**
1085 * Skip payment fields from submission data.
1086 *
1087 * This function checks if a field is a payment field by validating its key prefix.
1088 * Payment fields have keys that start with 'srfm-payment-' and should be skipped
1089 * from certain data operations.
1090 *
1091 * @param bool $default_value The default skip value.
1092 * @param array<mixed> $args Field arguments containing 'key', 'slug', and 'value'.
1093 * @since 2.0.0
1094 * @return bool True if the field should be skipped (is a payment field), false otherwise.
1095 */
1096 public function skip_payment_fields_from_submission_data( $default_value, $args ) {
1097 // Validate that args is an array and has the 'key' parameter.
1098 if ( ! is_array( $args ) || ! isset( $args['key'] ) || ! is_string( $args['key'] ) ) {
1099 return $default_value;
1100 }
1101
1102 // Check if the key starts with 'srfm-payment-' to identify payment fields.
1103 if ( 0 === strpos( $args['key'], 'srfm-payment-' ) ) {
1104 return true;
1105 }
1106
1107 return $default_value;
1108 }
1109
1110 /**
1111 * Skip payment fields from sample data.
1112 *
1113 * This function determines if a field associated with a "srfm/payment" block
1114 * should be skipped when processing sample data. If the provided arguments
1115 * specify a block with the name 'srfm/payment', the function returns true to
1116 * indicate that the field should be skipped. Otherwise, it returns the given
1117 * default value.
1118 *
1119 * @param bool $default_value The default skip value.
1120 * @param array<mixed> $args Field arguments containing at least 'block_name'.
1121 * @since 2.0.0
1122 * @return bool True if the field should be skipped (is a payment block), false otherwise.
1123 */
1124 public function skip_payment_fields_from_sample_data( $default_value, $args ) {
1125 if ( ! is_array( $args ) || ! isset( $args['block_name'] ) || ! is_string( $args['block_name'] ) ) {
1126 return $default_value;
1127 }
1128
1129 if ( 'srfm/payment' === $args['block_name'] ) {
1130 return true;
1131 }
1132
1133 return $default_value;
1134 }
1135
1136 /**
1137 * Verify payment intent status
1138 *
1139 * @param array<mixed> $payment_value Payment value.
1140 * @param string $payment_id Payment ID.
1141 * @param string $block_id Block ID.
1142 * @param array<mixed> $form_data Form data.
1143 *
1144 * @since 2.0.0
1145 * @return void|array<mixed>
1146 */
1147 private function verify_stripe_payment_intent_and_save( $payment_value, $payment_id, $block_id, $form_data ) {
1148 try {
1149 $payment_mode = Stripe_Helper::get_stripe_mode();
1150 $secret_key = Stripe_Helper::get_stripe_secret_key();
1151
1152 if ( empty( $secret_key ) ) {
1153 return [
1154 'error' => __( 'Stripe secret key not found.', 'sureforms' ),
1155 ];
1156 }
1157
1158 // Verify payment intent with comprehensive validation including form data.
1159 // BOTH MODE: pass 'one-time' so per-type amount config is used for verification.
1160 $verification_result = Payment_Helper::verify_payment_intent( $block_id, $payment_id, $form_data, 'one-time' );
1161
1162 if ( false === $verification_result['valid'] ) {
1163 return [
1164 'error' => $verification_result['message'],
1165 ];
1166 }
1167
1168 $get_stripe_account_id = Stripe_Helper::get_stripe_account_id();
1169
1170 // Retrieve confirmed payment intent status.
1171 $retrieve_body = apply_filters(
1172 'srfm_retrieve_payment_intent_data',
1173 [
1174 'secret_key' => $secret_key,
1175 'payment_intent_id' => $payment_id,
1176 'stripe_account_id' => $get_stripe_account_id,
1177 'plugin_name' => 'SureForms',
1178 ]
1179 );
1180
1181 $retrieve_body = wp_json_encode( $retrieve_body );
1182 $retrieve_body = is_string( $retrieve_body ) ? $retrieve_body : '';
1183 $retrieve_body = base64_encode( $retrieve_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1184
1185 if ( empty( $retrieve_body ) ) {
1186 return [
1187 'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
1188 ];
1189 }
1190
1191 // Call middleware retrieve endpoint to get confirmed payment intent.
1192 $retrieve_response = wp_remote_post(
1193 Stripe_Helper::middle_ware_base_url() . 'payment-intent/capture',
1194 [
1195 'timeout' => 60,
1196 'body' => $retrieve_body,
1197 'headers' => [
1198 'Content-Type' => 'application/json',
1199 ],
1200 ]
1201 );
1202
1203 if ( is_wp_error( $retrieve_response ) ) {
1204 return [
1205 'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
1206 ];
1207 }
1208
1209 $confirmed_payment_intent = json_decode( wp_remote_retrieve_body( $retrieve_response ), true );
1210
1211 if ( empty( $confirmed_payment_intent ) && ! is_array( $confirmed_payment_intent ) ) {
1212 return [
1213 'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
1214 ];
1215 }
1216
1217 // Strict type validation and array check to resolve phpstan errors.
1218 if ( is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) && 'error' === $confirmed_payment_intent['status'] ) {
1219 return [
1220 'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
1221 ];
1222 }
1223
1224 // Check if payment was actually confirmed successfully, safely.
1225 $confirmed_status = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) ? (string) $confirmed_payment_intent['status'] : '';
1226 if ( ! in_array( $confirmed_status, [ 'succeeded', 'requires_capture' ], true ) ) {
1227 return [
1228 'error' => __( 'Payment was not confirmed successfully.', 'sureforms' ),
1229 ];
1230 }
1231
1232 $entry_data = [];
1233
1234 $form_id = isset( $form_data['form-id'] ) && ! empty( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? intval( $form_data['form-id'] ) : 0;
1235 $confirm_payment_status = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) && ! empty( $confirmed_payment_intent['status'] ) ? (string) $confirmed_payment_intent['status'] : '';
1236 $confirm_payment_amount = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['amount'] ) && ! empty( $confirmed_payment_intent['amount'] ) ? intval( $confirmed_payment_intent['amount'] ) : 0;
1237 $confirm_payment_currency = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['currency'] ) && ! empty( $confirmed_payment_intent['currency'] ) ? (string) $confirmed_payment_intent['currency'] : 'usd';
1238 $confirm_payment_id = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['id'] ) && ! empty( $confirmed_payment_intent['id'] ) ? (string) $confirmed_payment_intent['id'] : '';
1239
1240 // Defense-in-depth: re-validate the amount Stripe actually charged against the form's
1241 // server-side configuration — not only the amount recorded when the intent was created.
1242 $charged_amount = Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency );
1243 $charge_validation = Payment_Helper::validate_amount_against_config( $block_id, $form_id, $form_data, $charged_amount, 'one-time' );
1244 if ( false === $charge_validation['valid'] ) {
1245 return [
1246 'error' => $charge_validation['message'],
1247 ];
1248 }
1249
1250 // Extract customer data.
1251 $customer_data = $this->extract_customer_data( $payment_value );
1252
1253 // update payment status and save to the payment entries table.
1254 $entry_data['form_id'] = $form_id;
1255 $entry_data['block_id'] = $block_id;
1256 $entry_data['status'] = $confirm_payment_status;
1257 $entry_data['total_amount'] = Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency );
1258 $entry_data['currency'] = $confirm_payment_currency;
1259 $entry_data['entry_id'] = 0;
1260 $entry_data['gateway'] = 'stripe';
1261 $entry_data['type'] = 'payment';
1262 $entry_data['mode'] = $payment_mode;
1263 $entry_data['transaction_id'] = $confirm_payment_id;
1264 $entry_data['srfm_txn_id'] = ''; // Will be updated after getting payment entry ID.
1265 $entry_data['customer_email'] = $customer_data['email'];
1266 $entry_data['customer_name'] = $customer_data['name'];
1267 $entry_data['customer_id'] = $customer_data['customer_id'];
1268 $entry_data['payment_data'] = [
1269 'payment_value' => $payment_value,
1270 ];
1271
1272 // Get user ID if logged in.
1273 $user_id = get_current_user_id();
1274 /* translators: %d: User ID */
1275 $user_info = $user_id > 0 ? sprintf( __( 'User ID: %d', 'sureforms' ), $user_id ) : __( 'Guest User', 'sureforms' );
1276
1277 // Add initial log entry for audit trail.
1278 $entry_data['log'] = [
1279 [
1280 'title' => __( 'Payment Verification', 'sureforms' ),
1281 'created_at' => current_time( 'mysql' ),
1282 'messages' => [
1283 /* translators: %s: Stripe transaction ID */
1284 sprintf( __( 'Transaction ID: %s', 'sureforms' ), $confirm_payment_id ),
1285 /* translators: %s: Payment gateway name. */
1286 sprintf( __( 'Payment Gateway: %s', 'sureforms' ), 'Stripe' ),
1287 /* translators: %1$s: amount, %2$s: currency. */
1288 sprintf( __( 'Amount: %1$s %2$s', 'sureforms' ), number_format( Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency ), 2 ), strtoupper( $confirm_payment_currency ) ),
1289 /* translators: %s: payment status */
1290 sprintf( __( 'Status: %s', 'sureforms' ), ucfirst( str_replace( '_', ' ', $confirm_payment_status ) ) ),
1291 $user_info,
1292 /* translators: %s: payment mode */
1293 sprintf( __( 'Mode: %s', 'sureforms' ), ucfirst( $payment_mode ) ),
1294 ],
1295 ],
1296 ];
1297
1298 $get_payment_entry_id = Payments::add( $entry_data );
1299
1300 if ( $get_payment_entry_id ) {
1301 // Generate unique payment ID using the auto-increment ID and update the entry.
1302 $unique_payment_id = Stripe_Helper::generate_unique_payment_id( $get_payment_entry_id );
1303 Payments::update( $get_payment_entry_id, [ 'srfm_txn_id' => $unique_payment_id ] );
1304
1305 $add_in_static_value = [
1306 'payment_id' => $confirm_payment_id,
1307 'block_id' => $block_id,
1308 'form_id' => $form_id,
1309 ];
1310
1311 $this->stripe_payment_entries[] = $add_in_static_value;
1312
1313 // Clean up transient after successful verification to prevent reuse.
1314 Payment_Helper::delete_payment_intent_metadata( $block_id, $payment_id );
1315
1316 return [
1317 'payment_id' => $get_payment_entry_id,
1318 ];
1319 }
1320 } catch ( \Exception $e ) {
1321 return [
1322 'error' => $e->getMessage(),
1323 ];
1324 }
1325 }
1326
1327 /**
1328 * Get or create Stripe customer
1329 *
1330 * @param array<string,string> $customer_data Customer data containing 'email' and 'name' from POST.
1331 * @since 2.0.0
1332 * @return string|false Customer ID on success, false on failure.
1333 */
1334 private function get_or_create_stripe_customer( $customer_data = [] ) {
1335 $current_user = wp_get_current_user();
1336
1337 if ( $current_user->ID > 0 ) {
1338 // Logged-in user - check for existing customer ID in user meta.
1339 $customer_id = get_user_meta( $current_user->ID, 'srfm_stripe_customer_id', true );
1340
1341 if ( ! empty( $customer_id ) && is_string( $customer_id ) && $this->verify_stripe_customer( $customer_id ) ) {
1342 return $customer_id;
1343 }
1344
1345 // Create new customer for logged-in user.
1346 return $this->create_stripe_customer_for_user( $current_user, $customer_data );
1347 }
1348
1349 // Non-logged-in user - create temporary customer.
1350 return $this->create_stripe_customer_for_guest( $customer_data );
1351 }
1352
1353 /**
1354 * Create Stripe customer for logged-in user
1355 *
1356 * @param \WP_User $user WordPress user object.
1357 * @param array<string,string> $post_customer_data Customer data from POST containing 'email' and 'name'.
1358 * @since 2.0.0
1359 * @return string|false Customer ID on success, false on failure.
1360 * @throws \Exception When Stripe API request fails.
1361 */
1362 private function create_stripe_customer_for_user( $user, $post_customer_data = [] ) {
1363 try {
1364 // Use POST email if provided, else use logged-in user email.
1365 $customer_email = ! empty( $post_customer_data['email'] ) ? $post_customer_data['email'] : $user->user_email;
1366
1367 // Use POST name if provided, else use logged-in user name.
1368 $customer_name = ! empty( $post_customer_data['name'] ) ? $post_customer_data['name'] : ( trim( $user->first_name . ' ' . $user->last_name ) );
1369 $customer_name = ! empty( $customer_name ) ? $customer_name : $user->display_name;
1370
1371 // Build description with provided email and name.
1372 $description_parts = [];
1373 if ( ! empty( $customer_email ) ) {
1374 $description_parts[] = $customer_email;
1375 }
1376 if ( ! empty( $customer_name ) ) {
1377 $description_parts[] = $customer_name;
1378 }
1379 $description = ! empty( $description_parts ) ? implode( ', ', $description_parts ) : sprintf( 'WordPress User ID: %d', $user->ID );
1380
1381 $customer_data = [
1382 'email' => $customer_email,
1383 'name' => $customer_name,
1384 'description' => $description,
1385 'metadata' => [
1386 'source' => 'SureForms',
1387 'wp_user_id' => $user->ID,
1388 'wp_username' => $user->user_login,
1389 'wp_user_email' => $user->user_email,
1390 ],
1391 ];
1392
1393 $customer_response = Stripe_Helper::stripe_api_request( 'customers', 'POST', $customer_data );
1394
1395 if ( ! $customer_response['success'] || empty( $customer_response['data']['id'] ) ) {
1396 throw new \Exception( __( 'Failed to create Stripe customer.', 'sureforms' ) );
1397 }
1398
1399 $customer = $customer_response['data'];
1400
1401 // Save customer ID to user meta for future use.
1402 update_user_meta( $user->ID, 'srfm_stripe_customer_id', $customer['id'] );
1403
1404 return $customer['id'];
1405
1406 } catch ( \Exception $e ) {
1407 return false;
1408 }
1409 }
1410
1411 /**
1412 * Create Stripe customer for guest user
1413 *
1414 * @param array<string,string> $post_customer_data Customer data from POST containing 'email' and 'name'.
1415 * @since 2.0.0
1416 * @return string|false Customer ID on success, false on failure.
1417 * @throws \Exception When Stripe API request fails.
1418 */
1419 private function create_stripe_customer_for_guest( $post_customer_data = [] ) {
1420 try {
1421 // Use email and name from POST data.
1422 $customer_email = ! empty( $post_customer_data['email'] ) ? sanitize_email( $post_customer_data['email'] ) : '';
1423 $customer_name = ! empty( $post_customer_data['name'] ) ? sanitize_text_field( $post_customer_data['name'] ) : '';
1424
1425 // Build description with provided email and name.
1426 $description_parts = [];
1427 if ( ! empty( $customer_email ) ) {
1428 $description_parts[] = $customer_email;
1429 }
1430 if ( ! empty( $customer_name ) ) {
1431 $description_parts[] = $customer_name;
1432 }
1433 $description = ! empty( $description_parts ) ? implode( ', ', $description_parts ) : 'Guest User - SureForms Subscription';
1434
1435 $customer_data = [
1436 'description' => $description,
1437 'metadata' => [
1438 'source' => 'SureForms',
1439 'user_type' => 'guest',
1440 'created_at' => current_time( 'mysql' ),
1441 'ip_address' => $this->get_user_ip(),
1442 ],
1443 ];
1444
1445 // Add email if available from POST data.
1446 if ( ! empty( $customer_email ) ) {
1447 $customer_data['email'] = $customer_email;
1448 $customer_data['metadata']['form_email'] = $customer_email;
1449 }
1450
1451 // Add name if available from POST data.
1452 if ( ! empty( $customer_name ) ) {
1453 $customer_data['name'] = $customer_name;
1454 $customer_data['metadata']['form_name'] = $customer_name;
1455 }
1456
1457 $customer_response = Stripe_Helper::stripe_api_request( 'customers', 'POST', $customer_data );
1458
1459 if ( ! $customer_response['success'] || empty( $customer_response['data']['id'] ) ) {
1460 throw new \Exception( __( 'Failed to create Stripe guest customer.', 'sureforms' ) );
1461 }
1462
1463 $customer = $customer_response['data'];
1464
1465 return $customer['id'];
1466
1467 } catch ( \Exception $e ) {
1468 return false;
1469 }
1470 }
1471
1472 /**
1473 * Verify Stripe customer exists
1474 *
1475 * @param string $customer_id Stripe customer ID.
1476 * @since 2.0.0
1477 * @return bool True if customer exists, false otherwise.
1478 */
1479 private function verify_stripe_customer( $customer_id ) {
1480 try {
1481 $customer_response = Stripe_Helper::stripe_api_request( 'customers', 'GET', [], $customer_id );
1482
1483 if ( ! $customer_response['success'] ) {
1484 return false;
1485 }
1486
1487 $customer = $customer_response['data'] ?? [];
1488 /**
1489 * Stripe API returns customer object with the following structure:
1490 * {
1491 * "id": "cus_Syq4hfWO9S5XC2",
1492 * "object": "customer",
1493 * "deleted": true // Present and true only if customer is deleted
1494 * }
1495 *
1496 * When a customer is deleted, the 'deleted' property is set to true.
1497 * Active customers do not have this property or it's set to false.
1498 */
1499
1500 $is_deleted_customer = isset( $customer['deleted'] ) && true === $customer['deleted'];
1501
1502 return ! empty( $customer['id'] ) && false === $is_deleted_customer;
1503 } catch ( \Exception $e ) {
1504 return false;
1505 }
1506 }
1507
1508 /**
1509 * Get user IP address
1510 *
1511 * @since 2.0.0
1512 * @return string User IP address.
1513 */
1514 private function get_user_ip() {
1515 // Check for various IP address headers.
1516 $ip_keys = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR' ];
1517
1518 foreach ( $ip_keys as $key ) {
1519 if ( ! empty( $_SERVER[ $key ] ) ) {
1520 $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
1521 // Handle comma-separated IPs (from proxies).
1522 if ( strpos( $ip, ',' ) !== false ) {
1523 $ip = trim( explode( ',', $ip )[0] );
1524 }
1525 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
1526 return $ip;
1527 }
1528 }
1529 }
1530
1531 return '127.0.0.1'; // Fallback.
1532 }
1533
1534 /**
1535 * Update payment entry with entry_id
1536 *
1537 * @param string $payment_id Payment intent ID.
1538 * @param int $entry_id Entry ID to update.
1539 * @since 2.0.0
1540 * @return bool True if payment entry updated, false otherwise.
1541 */
1542 private function update_payment_entry_id( $payment_id, $entry_id ) {
1543 // Find the payment entry by transaction_id.
1544 $payment_entries = Payments::get_instance()->get_results(
1545 [ 'transaction_id' => $payment_id ],
1546 'id'
1547 );
1548
1549 if ( ! empty( $payment_entries ) && is_array( $payment_entries ) && isset( $payment_entries[0] ) && is_array( $payment_entries[0] ) && isset( $payment_entries[0]['id'] ) ) {
1550 $payment_entry_id = intval( $payment_entries[0]['id'] );
1551
1552 // Update the payment entry with entry_id using Payments class.
1553 $updated = Payments::update( $payment_entry_id, [ 'entry_id' => $entry_id ] );
1554
1555 if ( $updated ) {
1556 $this->maybe_fire_payment_completed( $payment_entry_id );
1557 return true;
1558 }
1559
1560 return false;
1561 }
1562
1563 return false;
1564 }
1565
1566 /**
1567 * Fire the `srfm_payment_completed` action for a freshly linked payment.
1568 *
1569 * Called right after a payment row is linked to its form entry, so `entry_id`
1570 * (and therefore the submitting user) is resolvable. Gated on the `succeeded`
1571 * status so consumers never grant access for pending, failed or refunded
1572 * payments.
1573 *
1574 * @param int $payment_entry_id Primary key of the linked `sureforms_payments` row.
1575 * @since 2.12.0
1576 * @return void
1577 */
1578 private function maybe_fire_payment_completed( $payment_entry_id ) {
1579 $payment = Payments::get( $payment_entry_id );
1580 if ( ! is_array( $payment ) ) {
1581 return;
1582 }
1583
1584 $status = ! empty( $payment['status'] ) && is_string( $payment['status'] ) ? $payment['status'] : '';
1585 if ( 'succeeded' !== $status ) {
1586 return;
1587 }
1588
1589 /**
1590 * Fires when a SureForms payment reaches the `succeeded` state and has been
1591 * linked to its form entry — a one-time payment, or the initial charge of a
1592 * subscription.
1593 *
1594 * @param array<string, mixed> $payment Payment record (a `sureforms_payments` row).
1595 * @param array<string, mixed> $context Resolved context: form_id, entry_id,
1596 * user_id (0 for guests), customer_email,
1597 * type, gateway, mode.
1598 * @since 2.12.0
1599 */
1600 do_action( 'srfm_payment_completed', $payment, Payment_Helper::build_payment_context( $payment ) );
1601 }
1602
1603 /**
1604 * Update payment entry with entry_id by subscription_id.
1605 *
1606 * Similar to update_payment_entry_id but looks up payment records by subscription_id
1607 * instead of transaction_id. This is useful for subscription payments (PayPal, Stripe)
1608 * where the subscription_id is available before the transaction_id.
1609 *
1610 * @param string $subscription_id The subscription ID from payment gateway.
1611 * @param int $entry_id The form entry ID to link with payment.
1612 * @since 2.4.0
1613 * @return bool True if payment entry updated, false otherwise.
1614 */
1615 private function update_payment_entry_id_by_subscription_id( $subscription_id, $entry_id ) {
1616 // Find the payment entry by subscription_id.
1617 $payment_entries = Payments::get_instance()->get_results(
1618 [ 'subscription_id' => $subscription_id ],
1619 'id'
1620 );
1621
1622 if ( ! empty( $payment_entries ) && is_array( $payment_entries ) && isset( $payment_entries[0] ) && is_array( $payment_entries[0] ) && isset( $payment_entries[0]['id'] ) ) {
1623 $payment_entry_id = intval( $payment_entries[0]['id'] );
1624
1625 // Update the payment entry with entry_id using Payments class.
1626 $updated = Payments::update( $payment_entry_id, [ 'entry_id' => $entry_id ] );
1627
1628 if ( $updated ) {
1629 $this->maybe_fire_payment_completed( $payment_entry_id );
1630 return true;
1631 }
1632
1633 return false;
1634 }
1635
1636 return false;
1637 }
1638
1639 /**
1640 * Extract customer name and email from form data
1641 *
1642 * Uses the payment block's customerNameField and customerEmailField attributes
1643 * to find the corresponding field slugs, then extracts the values from form data.
1644 *
1645 * @param array<string,mixed> $input_value Input value.
1646 * @since 2.0.0
1647 * @return array{name: string, email: string, customer_id: string} Customer data array.
1648 */
1649 private function extract_customer_data( $input_value ) {
1650 $email = ! empty( $input_value['email'] ) && is_string( $input_value['email'] ) ? sanitize_email( $input_value['email'] ) : '';
1651 return [
1652 'name' => ! empty( $input_value['name'] ) && is_string( $input_value['name'] ) ? sanitize_text_field( $input_value['name'] ) : '',
1653 'email' => $email,
1654 'customer_id' => ! empty( $input_value['customerId'] ) && is_string( $input_value['customerId'] ) ? sanitize_text_field( $input_value['customerId'] ) : '',
1655 ];
1656 }
1657 }
1658