PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.13
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.13
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / gateways / class-wpfnl-gateway-stripe.php

class-wpfnl-gateway-stripe.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.13, at includes/core/gateways/class-wpfnl-gateway-stripe.php

1,294 lines 51.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPFunnels\Gateway;
4
5 use WC_Stripe_API;
6 use WC_Stripe_Helper;
7 use WPFunnels\Wpfnl_functions;
8
9 class Wpfnl_Stripe_payment_process {
10
11 public $key = 'stripe';
12
13 public $refund_support;
14
15 public function __construct() {
16 $this->refund_support = true;
17
18 add_filter( 'wc_stripe_force_save_source', array( $this, 'should_tokenize_stripe' ), 9999);
19 add_filter( 'wc_stripe_force_save_payment_method', array( $this, 'should_save_payment_method_for_offer' ), 9999, 2 );
20 add_filter( 'wc_stripe_3ds_source', array( $this, 'may_be_modify_3ds_param' ), 9999, 2);
21 add_action( 'wc_gateway_stripe_process_response', array( $this, 'handle_redirection' ), 9999, 2 );
22
23 add_action( 'wp_ajax_wpfunnels_stripe_sca_check', array( $this, 'check_stripe_sca' ),9999);
24 add_action( 'wp_ajax_nopriv_wpfunnels_stripe_sca_check', array( $this, 'check_stripe_sca' ),9999);
25
26 add_action( 'wpfunnels/child_order_created_' . $this->key, array( $this, 'add_capture_meta_to_child_order' ), 9999, 3 );
27
28 add_action( 'wpfunnels/subscription_created', array( $this, 'add_offer_subscription_meta' ), 9999, 3 );
29
30 add_action( 'woocommerce_checkout_after_order_review', array( $this, 'add_stripe_hidden_field' ), 99 );
31
32 // Support for Stripe Express Checkout Element (Apple Pay / Google Pay) which uses the WC Store API.
33 add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( $this, 'save_funnel_meta_for_express_checkout' ), 10, 2 );
34
35 // Force setup_future_usage on checkout PaymentIntents for funnel orders so the Express Checkout
36 // PaymentMethod (pm_xxx) gets attached to the Stripe customer. Without this, guests using
37 // Google Pay / Apple Pay produce a single-use pm_ that cannot be reused for offer charges.
38 // wc_stripe_force_save_payment_method is ignored for guests (is_user_logged_in guard in WC Stripe),
39 // so we must inject directly into the PI request instead.
40 add_filter( 'wc_stripe_generate_create_intent_request', array( $this, 'force_save_pm_for_funnel_order' ), 10, 2 );
41
42 // Stripe Checkout Sessions (WC Stripe 10.x hosted payment page) do not fire
43 // wc_stripe_generate_create_intent_request, so setup_future_usage is never set
44 // and the resulting PM becomes permanently single-use. Disabling the feature on
45 // funnel checkout pages forces WC Stripe to use the standard UPE inline flow
46 // where our force_save_pm_for_funnel_order hook can run.
47 add_filter( 'wc_stripe_is_checkout_sessions_available', array( $this, 'disable_checkout_sessions_for_funnels' ) );
48
49 }
50
51
52 /**
53 * wpfnl_stripe_tokenization
54 * If required then tokenize to save source of payment
55 *
56 * @param bool $save_source force save source.
57 *
58 */
59 public function should_tokenize_stripe( $save_source )
60 {
61 $checkout_id = Wpfnl_functions::get_checkout_id_from_post_data();
62 // Get checkout id if not found in post data.
63 $checkout_id = !$checkout_id ? get_the_ID() : $checkout_id;
64 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $checkout_id );
65
66 if ( $checkout_id && $funnel_id ) {
67
68 if ( Wpfnl_functions::is_offer_exists_in_funnel($funnel_id) ) {
69 $save_source = true;
70 }
71 }
72
73 return $save_source;
74 }
75
76
77 /**
78 * Force save payment method for funnel orders with offers.
79 * Handles the newer wc_stripe_force_save_payment_method filter used by the UPE gateway.
80 * This is needed for Stripe Express Checkout (Apple Pay/Google Pay) which uses the Store API
81 * and doesn't go through the classic $_POST checkout flow.
82 *
83 * @param bool $force_save Whether to force save the payment method.
84 * @param string|int $order_id The WooCommerce order ID (may be empty during intent creation).
85 * @return bool
86 */
87 public function should_save_payment_method_for_offer( $force_save, $order_id ) {
88 if ( $force_save ) {
89 return $force_save;
90 }
91
92 // For Store API / ECE: funnel meta is already saved on the order by
93 // save_funnel_meta_for_express_checkout before payment processing runs.
94 if ( $order_id ) {
95 $order = wc_get_order( $order_id );
96 if ( $order ) {
97 $funnel_id = Wpfnl_functions::get_funnel_id_from_order( $order );
98 if ( $funnel_id && Wpfnl_functions::is_offer_exists_in_funnel( $funnel_id ) ) {
99 return true;
100 }
101 }
102 }
103
104 return $force_save;
105 }
106
107
108 /**
109 * Save WPFunnels funnel metadata to the order when Stripe Express Checkout
110 * (Apple Pay / Google Pay) processes via the WooCommerce Store API.
111 *
112 * Classic checkout sends _wpfunnels_checkout_id via $_POST, but the Stripe
113 * Express Checkout Element submits to /wp-json/wc/store/v1/checkout (Blocks
114 * Store API) where $_POST is not populated. We recover the checkout step ID
115 * from the HTTP Referer header instead.
116 *
117 * @param \WC_Order $order The WooCommerce order being processed.
118 * @param \WP_REST_Request $request The REST API request.
119 * @return void
120 */
121 public function save_funnel_meta_for_express_checkout( $order, $request ) {
122 // Only handle Stripe payments (including express methods like Apple Pay and Google Pay).
123 $payment_method = $request->get_param( 'payment_method' );
124
125 // Check if this is a Stripe payment method:
126 // - 'stripe' (main gateway)
127 // - 'stripe_*' (split UPE gateways)
128 // - 'google_pay', 'apple_pay', 'link' (express checkout methods)
129 $is_stripe_payment = (
130 'stripe' === $payment_method ||
131 0 === strpos( $payment_method, 'stripe_' ) ||
132 in_array( $payment_method, [ 'google_pay', 'apple_pay', 'link' ], true )
133 );
134
135 if ( ! $is_stripe_payment ) {
136 return;
137 }
138
139 // Skip if funnel metadata was already saved (classic checkout path).
140 if ( $order->get_meta( '_wpfunnels_funnel_id' ) ) {
141 return;
142 }
143
144 // Determine the checkout page from the HTTP Referer.
145 $referer = $request->get_header( 'referer' );
146 if ( ! $referer ) {
147 $referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
148 }
149
150 if ( ! $referer ) {
151 return;
152 }
153
154 // Strip query string and get the slug from the URL.
155 $referer_path = strtok( $referer, '?' );
156 $page_id = url_to_postid( $referer_path );
157
158 // If url_to_postid fails (returns 0), try to find the page by slug.
159 // This is needed for custom post types with custom rewrite rules.
160 if ( ! $page_id ) {
161
162 // Extract the slug from the URL (last segment of the path).
163 $parsed_url = wp_parse_url( $referer_path );
164 $path = isset( $parsed_url['path'] ) ? trim( $parsed_url['path'], '/' ) : '';
165
166 if ( $path ) {
167 $path_parts = explode( '/', $path );
168 $slug = end( $path_parts );
169
170
171 // Try to find a wpfunnel_steps post with this slug.
172 $args = array(
173 'name' => $slug,
174 'post_type' => 'wpfunnel_steps',
175 'post_status' => 'publish',
176 'posts_per_page' => 1,
177 );
178
179 $posts = get_posts( $args );
180
181 if ( ! empty( $posts ) ) {
182 $page_id = $posts[0]->ID;
183 }
184 }
185 }
186
187 if ( ! $page_id ) {
188 return;
189 }
190
191 // Confirm this is a WPFunnels checkout step.
192 $is_checkout_step = Wpfnl_functions::check_if_this_is_step_type_by_id( $page_id, 'checkout' );
193 if ( ! $is_checkout_step ) {
194 return;
195 }
196
197 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $page_id );
198 if ( ! $funnel_id ) {
199 return;
200 }
201
202 $order->update_meta_data( '_wpfunnels_checkout_id', $page_id );
203 $order->update_meta_data( '_wpfunnels_funnel_id', $funnel_id );
204 $order->update_meta_data( '_wpfunnels_order', 'yes' );
205 $order->save();
206
207 // Mark this session as the order owner so is_valid_order_owner() passes
208 // for subsequent upsell/downsell AJAX calls in the same browser session.
209 if ( WC()->session ) {
210 $session_handler = new \WC_Session_Handler();
211 WC()->session->set( 'wpfnl_order_owner', $session_handler->generate_customer_id() );
212 }
213 }
214
215 /**
216 * Force setup_future_usage = 'off_session' on the checkout PaymentIntent for
217 * WPFunnels funnel orders that use Stripe Express Checkout (Google Pay / Apple Pay).
218 *
219 * WC Stripe 10.x uses "confirmation tokens" for Express Checkout. In that flow
220 * setup_future_usage is only added when has_subscription = true. For regular
221 * funnel checkouts (guest or logged-in) the PM would otherwise be single-use
222 * and cannot be reused for the subsequent offer/upsell charge.
223 *
224 * WC_Stripe_Helper::should_force_save_payment_method() has an is_user_logged_in()
225 * hard-guard meaning the wc_stripe_force_save_payment_method filter never fires
226 * for guests. Injecting directly into the PI request via this filter is the only
227 * reliable approach.
228 *
229 * @param array $request The Stripe PaymentIntent request parameters.
230 * @param WC_Order $order The WooCommerce order.
231 * @return array
232 */
233 /**
234 * Disable Stripe Checkout Sessions (WC Stripe 10.x hosted payment page) for funnel
235 * checkout steps. The sessions flow bypasses wc_stripe_generate_create_intent_request,
236 * so setup_future_usage is never injected and guest PMs become permanently single-use.
237 * Returning false here forces the standard UPE inline flow where our hook can run.
238 *
239 * @param bool $available
240 * @return bool
241 */
242 public function disable_checkout_sessions_for_funnels( $available ) {
243 if ( ! $available ) {
244 return $available;
245 }
246 $checkout_id = Wpfnl_functions::get_checkout_id_from_post_data();
247 $checkout_id = $checkout_id ?: get_the_ID();
248 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $checkout_id );
249 if ( $funnel_id && Wpfnl_functions::is_offer_exists_in_funnel( $funnel_id ) ) {
250 return false;
251 }
252 return $available;
253 }
254
255 public function force_save_pm_for_funnel_order( $request, $order ) {
256 // Only apply to payment creation requests (must have either a payment_method or confirmation_token).
257 $has_pm = ! empty( $request['payment_method'] );
258 $has_ct = ! empty( $request['confirmation_token'] );
259 if ( ! $has_pm && ! $has_ct ) {
260 return $request;
261 }
262
263 // Identify the order object — $order may be null in some call paths.
264 if ( ! $order instanceof \WC_Order ) {
265 return $request;
266 }
267
268 $funnel_id = Wpfnl_functions::get_funnel_id_from_order( $order );
269 if ( ! $funnel_id ) {
270 return $request;
271 }
272
273 if ( ! Wpfnl_functions::is_offer_exists_in_funnel( $funnel_id ) ) {
274 return $request;
275 }
276
277 if ( $has_ct ) {
278 // Confirmation token flow (ECE with Optimised Checkout Element):
279 // setup_future_usage must be set inside payment_method_options.
280 $pm_type = 'card';
281 if ( ! empty( $request['payment_method_types'] ) && is_array( $request['payment_method_types'] ) ) {
282 $pm_type = reset( $request['payment_method_types'] );
283 }
284 if ( ! isset( $request['payment_method_options'] ) ) {
285 $request['payment_method_options'] = [];
286 }
287 if ( empty( $request['payment_method_options'][ $pm_type ]['setup_future_usage'] ) ) {
288 $request['payment_method_options'][ $pm_type ]['setup_future_usage'] = 'off_session';
289 }
290 } else {
291 // Payment method flow (ECE with classic pm_ or saved token):
292 // setup_future_usage is set at the top level of the PI request.
293 if ( empty( $request['setup_future_usage'] ) ) {
294 $request['setup_future_usage'] = 'off_session';
295 }
296 }
297
298 return $request;
299 }
300
301
302 /**
303 * @param $funnel_id
304 * @param $node_found
305 * @return bool
306 */
307 public function go_to_output_1($funnel_id, $node_found) {
308 $funnel_json = get_post_meta($funnel_id, '_funnel_data', true);
309 if ($funnel_json) {
310 $node_data = $funnel_json['drawflow']['Home']['data'];
311 foreach ($node_data as $node_key => $node_value) {
312 if ($node_value['id'] == $node_found) {
313 $next_node = $node_value['outputs']['output_1']['connections'][0]['node'];
314 return $next_node;
315 }
316 }
317 return false;
318 }
319 }
320
321
322 /**
323 * @param $funnel_id
324 * @param $node_found
325 * @return bool
326 */
327 public function go_to_output_2($funnel_id, $node_found) {
328 $funnel_json = get_post_meta($funnel_id, '_funnel_data', true);
329 if ($funnel_json) {
330 $node_data = $funnel_json['drawflow']['Home']['data'];
331 foreach ($node_data as $node_key => $node_value) {
332 if ($node_value['id'] == $node_found) {
333 $next_node = $node_value['outputs']['output_2']['connections'][0]['node'];
334 return $next_node;
335 }
336 }
337 return false;
338 }
339 }
340
341
342 /**
343 * save 3ds source data for offers
344 *
345 * @param $post_data
346 * @param $order
347 * @return mixed
348 */
349 public function may_be_modify_3ds_param( $post_data, $order ) {
350 if ( $order && Wpfnl_functions::check_if_offer_exists($order) ) {
351 $order->update_meta_data( '_wpfunnels_stripe_source_id', $post_data['three_d_secure']['card'] );
352 $order->save();
353 }
354 return $post_data;
355 }
356
357
358 /**
359 * Redirection to order received url
360 *
361 * @param $response
362 * @param $order
363 */
364 public function handle_redirection( $response, $order ) {
365 if ( 1 === did_action( 'wpfunnels/offer_funnel_started' ) && 1 === did_action( 'wc_gateway_stripe_process_redirect_payment' ) ) {
366 $order_received_url = $order->get_checkout_order_received_url();
367 wp_safe_redirect( $order_received_url );
368 exit();
369 }
370 }
371
372 /**
373 * wpfnl_stripe_maybe_hide_save_payment
374 */
375 public function wpfnl_stripe_maybe_hide_save_payment( $is_show ) {
376 return $is_show;
377 }
378
379
380 /**
381 * wpfnl_stripe_verify_sca
382 * Verify if payment type is SCA or not
383 *
384 * @throws \WC_Stripe_Exception
385 */
386 public function check_stripe_sca() {
387 $security = isset( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
388 if ( ! wp_verify_nonce( $security, 'wpfnl_stripe_sca_check_nonce' ) ) {
389 return;
390 }
391
392 global $woocommerce;
393
394 $step_id = isset( $_POST['step_id'] ) ? intval( $_POST['step_id'] ) : 0;
395 $order_id = isset( $_POST['order_id'] ) ? intval($_POST['order_id']) : 0;
396 $offer_type = isset( $_POST['offer_type'] ) ? sanitize_text_field( wp_unslash( $_POST['offer_type'] ) ) : '';
397 $offer_action = isset( $_POST['offer_action'] ) ? sanitize_text_field( wp_unslash( $_POST['offer_action'] ) ) : '';
398 $product_id = isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : '';
399
400 if( !empty($_POST['attr']) && Wpfnl_functions::is_wc_active() ){
401 $variation_id = (new \WC_Product_Data_Store_CPT())->find_matching_product_variation(
402 new \WC_Product($product_id),
403 $_POST['attr']
404 );
405 if( $variation_id ){
406 $product_id = $variation_id;
407 }
408 }
409
410 $order = wc_get_order( $order_id );
411
412 // Get offer product data - use offer class method if available, otherwise use pro functions
413 if ( class_exists( '\WPFunnels\Offer\Wpfnl_Offer' ) ) {
414 $offer_product = \WPFunnels\Offer\Wpfnl_Offer::get_offer_product_data( $step_id, $product_id, 0, $order_id );
415 } elseif ( class_exists( '\WPFunnelsPro\Wpfnl_Pro_functions' ) ) {
416 $offer_product = \WPFunnelsPro\Wpfnl_Pro_functions::get_offer_product_data( $step_id, $product_id, 0, $order_id );
417 } else {
418 wp_send_json(array(
419 'result' => 'fail',
420 'message' => __('Offer functionality not available', 'wpfnl'),
421 ));
422 return;
423 }
424
425 if ( isset($offer_product['price']) && (floatval(0) === floatval( $offer_product['price'] ) || '' === trim($offer_product['price'])) ) {
426 wp_send_json(array(
427 'result' => 'fail',
428 'message' => __('Product price is less than 0', 'wpfnl-pro'),
429 ));
430 } else {
431 $gateways = $woocommerce->payment_gateways->payment_gateways();
432 $gateway = $gateways['stripe'];
433 if ( $gateway ) {
434 $order_source = $gateway->prepare_order_source( $order );
435
436 $main_settings = get_option( 'woocommerce_stripe_settings' );
437 $testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] );
438 $publishable_key = $testmode
439 ? ( ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '' )
440 : ( ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '' );
441
442 $offer_settings = Wpfnl_functions::get_offer_settings();
443
444 // Express Checkout (pm_xxx PaymentMethod): no client-side 3DS step needed.
445 // Return without intent_secret so JS routes to wpfunnels_process_offer directly,
446 // where process_payment() handles the full charge server-side.
447 if ( ! empty( $order_source->source ) && 0 === strpos( $order_source->source, 'pm_' ) ) {
448 wp_send_json( array( 'result' => 'success' ) );
449 return;
450 }
451
452 $is_3ds = isset( $order_source->source_object->card->three_d_secure ) ? $order_source->source_object->card->three_d_secure : false;
453 $_3ds_array = [ 'optional', 'not_supported' ];
454
455 // check if 3ds is active or not
456 // is_string guard: $is_3ds is false when source is empty/missing — without it,
457 // !in_array(false, $_3ds_array) = true and create_intent fires with no payment method.
458 if ( is_string( $is_3ds ) && ! empty( $order_source->source ) && ! in_array( $is_3ds, $_3ds_array ) ) {
459
460 $intent = $this->create_intent( $order, $order_source, $offer_product );
461
462 // Confirm the intent after locking the order to make sure webhooks will not interfere.
463 if ( empty( $intent->error ) ) {
464 $intent = $this->confirm_stripe_intent( $intent, $order, $order_source );
465
466 }
467
468 if ( ! empty( $intent->error ) ) {
469 wp_send_json( array(
470 'result' => 'fail',
471 'message' => isset( $intent->error->message ) ? $intent->error->message : __( 'Stripe payment failed.', 'wpfnl' ),
472 ) );
473 return;
474 }
475
476 // Use get_latest_charge_from_intent() to support both old charges->data
477 // and new latest_charge (required since Stripe API 2022-11-15).
478 $response = $gateway->get_latest_charge_from_intent( $intent );
479 if ( ! empty( $response->balance_transaction ) ) {
480 $order->update_meta_data( '_stripe_balance_transaction_' . $step_id, $response->balance_transaction );
481 if ( isset( $offer_settings['offer_orders'] ) && 'main-order' === $offer_settings['offer_orders'] ) {
482 self::update_stripe_fees( $order, $response->balance_transaction );
483 $this->store_offer_transaction( $order, $response, $offer_product );
484 }
485 }
486 if ( $order ) {
487 $order->update_meta_data( '_stripe_intent_id_' . $step_id, $intent->id );
488 $order->save();
489 }
490
491 wp_send_json( array(
492 'result' => 'success',
493 'redirect' => $gateway->get_return_url( $order ),
494 'intent_secret' => $intent->client_secret,
495 'stripe_pk' => $publishable_key,
496 ) );
497 }
498 }
499
500 wp_send_json( array( 'result' => 'success' ) );
501 }
502 }
503
504
505 /**
506 * Confirms an intent if it is the `requires_confirmation` state.
507 *
508 * @param object $intent The intent to confirm.
509 * @param WC_Order $order The order that the intent is associated with.
510 * @param object $prepared_source The source that is being charged.
511 * @return object Either an error or the updated intent.
512 *
513 * @since 2.2.3
514 */
515 public function confirm_stripe_intent( $intent, $order, $prepared_source ) {
516 if ( 'requires_confirmation' !== $intent->status ) {
517 return $intent;
518 }
519
520 // Try to confirm the intent & capture the charge (if 3DS is not required).
521 $confirm_request = WC_Stripe_Helper::add_payment_method_to_request_array( $prepared_source->source, [] );
522
523 $level3_data = $this->get_level3_data_from_order( $order );
524 $confirmed_intent = WC_Stripe_API::request_with_level3_data(
525 $confirm_request,
526 "payment_intents/$intent->id/confirm",
527 $level3_data,
528 $order
529 );
530
531
532 if ( ! empty( $confirmed_intent->error ) ) {
533 return $confirmed_intent;
534 }
535
536 return $confirmed_intent;
537 }
538
539
540
541 /**
542 * Create the level 3 data array to send to Stripe when making a purchase.
543 *
544 * @param WC_Order $order The order that is being paid for.
545 * @return array The level 3 data to send to Stripe.
546 *
547 * @since 2.2.3
548 */
549 public function get_level3_data_from_order( $order ) {
550 // Get the order items. Don't need their keys, only their values.
551 // Order item IDs are used as keys in the original order items array.
552 $order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) );
553 $currency = $order->get_currency();
554
555 $stripe_line_items = array_map(
556 function( $item ) use ( $currency ) {
557 if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
558 $product_id = $item->get_variation_id()
559 ? $item->get_variation_id()
560 : $item->get_product_id();
561 $subtotal = $item->get_subtotal();
562 } else {
563 $product_id = substr( sanitize_title( $item->get_name() ), 0, 12 );
564 $subtotal = $item->get_total();
565 }
566 $product_description = substr( $item->get_name(), 0, 26 );
567 $quantity = $item->get_quantity();
568 $unit_cost = WC_Stripe_Helper::get_stripe_amount( ( $subtotal / $quantity ), $currency );
569 $tax_amount = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
570 $discount_amount = WC_Stripe_Helper::get_stripe_amount( $subtotal - $item->get_total(), $currency );
571
572 return (object) [
573 'product_code' => (string) $product_id, // Up to 12 characters that uniquely identify the product.
574 'product_description' => $product_description, // Up to 26 characters long describing the product.
575 'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
576 'quantity' => $quantity, // The number of items of this type sold, as a non-negative integer.
577 'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
578 'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
579 ];
580 },
581 $order_items
582 );
583
584 $level3_data = [
585 'merchant_reference' => $order->get_id(), // An alphanumeric string of up to characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.
586 'shipping_amount' => WC_Stripe_Helper::get_stripe_amount( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), $currency ), // The shipping cost, in cents, as a non-negative integer.
587 'line_items' => $stripe_line_items,
588 ];
589
590 // The customer’s U.S. shipping ZIP code.
591 $shipping_address_zip = $order->get_shipping_postcode();
592 if ( $this->is_valid_us_zip_code( $shipping_address_zip ) ) {
593 $level3_data['shipping_address_zip'] = $shipping_address_zip;
594 }
595
596 // The merchant’s U.S. shipping ZIP code.
597 $store_postcode = get_option( 'woocommerce_store_postcode' );
598 if ( $this->is_valid_us_zip_code( $store_postcode ) ) {
599 $level3_data['shipping_from_zip'] = $store_postcode;
600 }
601
602 return $level3_data;
603 }
604
605
606 /**
607 * Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions.
608 *
609 * @param string $zip The ZIP code to verify.
610 * @return boolean
611 *
612 * @since 2.2.3
613 */
614 public function is_valid_us_zip_code( $zip ) {
615 return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip );
616 }
617
618
619 /**
620 * @param $order
621 * @param $order_source
622 * @param $product
623 * @return array|\stdClass
624 * @throws \WC_Stripe_Exception
625 */
626 public function create_intent($order, $order_source, $product)
627 {
628 // The request for a charge contains metadata for the intent.
629 $full_request = $this->generate_payment_request($order, $order_source, $product);
630 $request = [
631
632 'amount' => \WC_Stripe_Helper::get_stripe_amount($product['price']),
633 'currency' => strtolower($order->get_currency()),
634 'description' => $full_request['description'],
635 'metadata' => $full_request['metadata'],
636 'statement_descriptor' => \WC_Stripe_Helper::clean_statement_descriptor($full_request['statement_descriptor']),
637 'capture_method' => (isset($full_request['capture']) && 'true' === $full_request['capture']) ? 'automatic' : 'manual',
638 'payment_method_types' => [
639 'card',
640 ],
641 ];
642
643 $request = \WC_Stripe_Helper::add_payment_method_to_request_array( $order_source->source, $request );
644
645 if ($order_source->customer) {
646 $request['customer'] = $order_source->customer;
647 }
648
649 if (!empty($full_request['statement_descriptor_suffix'])) {
650 $request['statement_descriptor_suffix'] = $full_request['statement_descriptor_suffix'];
651 }
652
653 // Express Checkout Element (Apple Pay / Google Pay) stores a PaymentMethod (pm_xxx).
654 // Confirm immediately (on-session: the customer is present on the offer page).
655 // Force capture_method=automatic so the confirmed intent settles immediately.
656 if ( 0 === strpos( $order_source->source, 'pm_' ) ) {
657 $request['confirm'] = 'true';
658 $request['capture_method'] = 'automatic';
659
660 $pm_object = \WC_Stripe_API::get_payment_method( $order_source->source );
661 if ( ! empty( $pm_object->error ) ) {
662 return $pm_object;
663 }
664
665 // Always resolve the actual PM type so Stripe doesn't reject with
666 // payment_intent_incompatible_payment_method (e.g. 'link' vs 'card').
667 if ( ! empty( $pm_object->type ) ) {
668 $request['payment_method_types'] = [ $pm_object->type ];
669 }
670
671 if ( ! empty( $order_source->customer ) ) {
672 if ( empty( $pm_object->customer ) ) {
673 // PM is not yet attached — attach it now before creating the offer intent.
674 $attach_result = \WC_Stripe_API::attach_payment_method_to_customer( $order_source->customer, $order_source->source );
675 if ( ! empty( $attach_result->error ) ) {
676 return $attach_result;
677 }
678 }
679
680 // PM is attached (either already was or we just attached it): charge off-session.
681 $request['off_session'] = 'true';
682 }
683 }
684
685 // Create an intent that awaits an action.
686 $intent = \WC_Stripe_API::request( $request, 'payment_intents' );
687
688 if ( ! empty( $intent->error ) ) {
689 return $intent;
690 }
691
692 $order_id = $order->get_id();
693
694 // Get step_id from POST data (for AJAX calls) or from product data (for direct calls).
695 $step_id = filter_input(INPUT_POST, 'step_id', FILTER_VALIDATE_INT);
696 if ( ! $step_id && isset( $product['step_id'] ) ) {
697 $step_id = intval( $product['step_id'] );
698 }
699
700 // Save the intent ID to the order.
701 if ( $step_id ) {
702 update_post_meta($order_id, '_stripe_intent_id_' . $step_id, $intent->id);
703 }
704
705 return $intent;
706 }
707
708
709
710
711 /**
712 * Retrieves intent from Stripe API by intent id.
713 *
714 * @param string $intent_type Either 'payment_intents' or 'setup_intents'.
715 * @param string $intent_id Intent id.
716 * @return object|bool Either the intent object or `false`.
717 * @throws Exception Throws exception for unknown $intent_type.
718 */
719 public function get_intent( $intent_type, $intent_id ) {
720 if ( ! in_array( $intent_type, [ 'payment_intents', 'setup_intents' ], true ) ) {
721 throw new \Exception( "Failed to get intent of type $intent_type. Type is not allowed" );
722 }
723
724 $response = \WC_Stripe_API::request( [], "$intent_type/$intent_id?expand[]=payment_method", 'GET' );
725
726 if ( $response && isset( $response->{ 'error' } ) ) {
727 return false;
728 }
729
730 return $response;
731 }
732
733
734 /**
735 * check if token is present in the order
736 *
737 * @param $order
738 */
739 private function has_token( $order ) {
740 if( false === is_a( $order, 'WC_Order' ) ){
741 return false;
742 }
743 $token = $order->get_meta('_wpfunnels_stripe_source_id');
744 if ( empty( $token ) ) {
745 $token = $order->get_meta('_stripe_source_id');
746 }
747 if ( ! empty( $token ) ) {
748 return true;
749 }
750 return false;
751 }
752
753 /**
754 * Fetch the PaymentMethod ID from the checkout PaymentIntent when _stripe_source_id
755 * was not saved (e.g. guest orders blocked by WC Stripe's is_user_logged_in() guard).
756 *
757 * @param \WC_Order $order
758 * @return string pm_xxx or empty string
759 */
760 private function recover_pm_from_intent( \WC_Order $order ) {
761 $intent_id = $order->get_meta( '_stripe_intent_id' );
762
763 // Stripe Checkout flow (hosted page) stores a session ID instead of a direct PI ID.
764 // Retrieve the session to get the underlying PaymentIntent.
765 if ( ! $intent_id || 0 !== strpos( $intent_id, 'pi_' ) ) {
766 $session_id = $order->get_meta( '_stripe_checkout_session_id' );
767 if ( $session_id && 0 === strpos( $session_id, 'cs_' ) ) {
768 $session = WC_Stripe_API::retrieve( 'checkout/sessions/' . $session_id );
769 if ( ! empty( $session->payment_intent ) && is_string( $session->payment_intent ) ) {
770 $intent_id = $session->payment_intent;
771 }
772 }
773 }
774
775 if ( ! $intent_id || 0 !== strpos( $intent_id, 'pi_' ) ) {
776 return '';
777 }
778
779 $intent = WC_Stripe_API::retrieve( 'payment_intents/' . $intent_id );
780 if ( empty( $intent->payment_method ) || ! is_string( $intent->payment_method ) ) {
781 return '';
782 }
783
784 return $intent->payment_method;
785 }
786
787 /**
788 * Ensure a pm_xxx PaymentMethod is attached to a Stripe Customer so it can be reused
789 * off-session for upsell charges. Creates a customer from order billing info if none exists.
790 * Called unconditionally before create_intent() — handles retries where _stripe_source_id
791 * was already saved but no customer was created yet.
792 *
793 * @param \WC_Order $order
794 */
795 private function ensure_pm_customer( \WC_Order $order ) {
796 $pm_id = $order->get_meta( '_stripe_source_id' );
797 if ( ! $pm_id || 0 !== strpos( $pm_id, 'pm_' ) ) {
798 return;
799 }
800
801 $customer_id = $order->get_meta( '_stripe_customer_id' );
802 if ( ! $customer_id ) {
803 $customer = WC_Stripe_API::request(
804 [
805 'email' => $order->get_billing_email(),
806 'name' => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
807 'metadata' => [ 'wc_order_id' => $order->get_id() ],
808 ],
809 'customers'
810 );
811 if ( ! empty( $customer->id ) ) {
812 $customer_id = $customer->id;
813 $order->update_meta_data( '_stripe_customer_id', $customer_id );
814 }
815 }
816
817 if ( $customer_id ) {
818 $pm_obj = WC_Stripe_API::retrieve( 'payment_methods/' . $pm_id );
819 if ( empty( $pm_obj->customer ) ) {
820 WC_Stripe_API::attach_payment_method_to_customer( $customer_id, $pm_id );
821 }
822 }
823 }
824
825 /**
826 * process the offer payment
827 *
828 * @param $order
829 * @param $offer_product
830 * @return bool
831 * @throws \WC_Stripe_Exception
832 */
833 public function process_payment( $order, $offer_product ) {
834 $result = array(
835 'is_success' => false,
836 'message' => ''
837 );
838 if ( ! $this->has_token( $order ) ) {
839 $pm_id = $this->recover_pm_from_intent( $order );
840 if ( $pm_id ) {
841 $order->update_meta_data( '_stripe_source_id', $pm_id );
842 } else {
843 return $result;
844 }
845 }
846 // Always ensure a Customer exists and the PM is attached before charging off-session.
847 // Required even on retries where _stripe_source_id is already set but no customer exists.
848 $this->ensure_pm_customer( $order );
849 $order->save();
850 global $woocommerce;
851 $gateways = $woocommerce->payment_gateways->payment_gateways();
852 $gateway = $gateways['stripe'];
853 $order_source = $gateway->prepare_order_source( $order );
854
855
856
857 // Express Checkout Element (Apple Pay / Google Pay) stores a PaymentMethod (pm_xxx).
858 // These must be charged using Payment Intents, not the legacy Charges API.
859 if ( 0 === strpos( $order_source->source, 'pm_' ) ) {
860 $intent = $this->create_intent( $order, $order_source, $offer_product );
861
862 if ( ! empty( $intent->error ) ) {
863 $result['message'] = $intent->error->message;
864 return $result;
865 }
866
867 // Payment Intents are confirmed immediately when created with confirm=true.
868 // Check if the charge was successful.
869 // Use get_latest_charge_from_intent() to support both old charges->data
870 // and new latest_charge (required since Stripe API 2022-11-15).
871 $response = $gateway->get_latest_charge_from_intent( $intent );
872
873 if ( ! $response || empty( $response->id ) ) {
874 $result['message'] = __( 'Payment intent created but no charge found', 'wpfnl' );
875 return $result;
876 }
877
878 if ( ! empty( $response->balance_transaction ) ) {
879 $this->update_stripe_payout_details( $order, $response );
880 }
881
882 $result['is_success'] = true;
883 $this->store_offer_transaction( $order, $response, $offer_product );
884
885 return $result;
886 }
887
888 // Legacy flow: Use Charges API for Sources (src_xxx) and cards (card_xxx).
889 $response = \WC_Stripe_API::request( $this->generate_payment_request( $order, $order_source, $offer_product ) );
890
891 if ( ! is_wp_error( $response ) ) {
892 if ( ! empty( $response->error ) ) {
893 $result['message'] = $response->error->message;
894 } else {
895 $result['is_success'] = true;
896 $this->update_stripe_payout_details( $order, $response );
897 $this->store_offer_transaction( $order, $response, $offer_product );
898 }
899 }
900 return $result;
901 }
902
903 /**
904 * generate payment request post data
905 *
906 * @param $order
907 * @param $order_source
908 * @param $product
909 * @return mixed|void
910 */
911 public function generate_payment_request($order, $order_source, $product) {
912
913 $settings = get_option( 'woocommerce_stripe_settings', [] );
914 $is_short_statement_descriptor_enabled = ! empty( $settings['is_short_statement_descriptor_enabled'] ) && 'yes' === $settings['is_short_statement_descriptor_enabled'];
915 $capture = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
916 $post_data = [];
917
918 if ( \WC_Stripe_Helper::payment_method_allows_manual_capture( $order->get_payment_method() ) ) {
919 $post_data['capture'] = $capture ? 'true' : 'false';
920 if ( $is_short_statement_descriptor_enabled ) {
921 $post_data['statement_descriptor_suffix'] = WC_Stripe_Helper::get_dynamic_statement_descriptor_suffix( $order );
922 }
923 }
924
925 $post_data['currency'] = strtolower($order ? $order->get_currency() : get_woocommerce_currency());
926 $post_data['amount'] = \WC_Stripe_Helper::get_stripe_amount($product['price'], $post_data['currency']);
927 /* translators: %1s site name */
928 $post_data['description'] = sprintf(__('%1$s - Order %2$s - One Time offer', 'wpfnl'), wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES), $order->get_order_number());
929
930 /* translators: %1s order number */
931 $post_data['statement_descriptor'] = apply_filters( 'wpfunnels/stripe_descriptor_text_modifiaction', sprintf(__('Order %1$s-OTO', 'wpfnl'), $order->get_order_number()), $order );
932
933 $billing_first_name = $order->get_billing_first_name();
934 $billing_last_name = $order->get_billing_last_name();
935 $billing_email = $order->get_billing_email();
936
937 if (!empty($billing_email) && apply_filters('wc_stripe_send_stripe_receipt', false)) {
938 $post_data['receipt_email'] = $billing_email;
939 }
940
941 $metadata = [
942 __('customer_name', 'wpfnl') => sanitize_text_field($billing_first_name) . ' ' . sanitize_text_field($billing_last_name),
943 __('customer_email', 'wpfnl') => sanitize_email($billing_email),
944 'order_id' => apply_filters( 'wpfunnels/stripe_descriptor_text_modifiaction', sprintf(__('Order %1$s-OTO', 'wpfnl'), $order->get_order_number()), $order ) . '_' . $product['id'],
945 ];
946
947 $post_data['expand[]'] = 'balance_transaction';
948 $post_data['metadata'] = apply_filters('wc_stripe_payment_metadata', $metadata, $order, $order_source);
949
950 if ($order_source->customer) {
951 $post_data['customer'] = $order_source->customer;
952 }
953
954 if ($order_source->source) {
955 $source_3ds = $order->get_meta('_wpfunnels_stripe_source_id', true);
956
957 $post_data['source'] = ('' !== $source_3ds) ? $source_3ds : $order_source->source;
958 }
959 $post_data['source'] = $order_source->source;
960 return $post_data;
961 }
962
963
964 /**
965 * @param $order
966 * @param $response
967 */
968 public function update_stripe_payout_details($order, $response)
969 {
970 $fee = !empty($response->balance_transaction->fee) ? \WC_Stripe_Helper::format_balance_fee($response->balance_transaction, 'fee') : 0;
971 $net = !empty($response->balance_transaction->net) ? \WC_Stripe_Helper::format_balance_fee($response->balance_transaction, 'net') : 0;
972
973 // WC Stripe 10.x moved fee/net helpers from WC_Stripe_Helper to WC_Stripe_Order_Helper.
974 if ( class_exists( 'WC_Stripe_Order_Helper' ) ) {
975 $order_helper = \WC_Stripe_Order_Helper::get_instance();
976 $fee = (float) $fee + (float) $order_helper->get_stripe_fee( $order );
977 $net = (float) $net + (float) $order_helper->get_stripe_net( $order );
978 $order_helper->update_stripe_fee( $order, $fee );
979 $order_helper->update_stripe_net( $order, $net );
980 } else {
981 $fee = (float) $fee + (float) \WC_Stripe_Helper::get_stripe_fee( $order );
982 $net = (float) $net + (float) \WC_Stripe_Helper::get_stripe_net( $order );
983 \WC_Stripe_Helper::update_stripe_fee( $order, $fee );
984 \WC_Stripe_Helper::update_stripe_net( $order, $net );
985 }
986 }
987
988
989 /**
990 * Updates Stripe fees/net.
991 * e.g usage would be after a refund.
992 *
993 * @since 4.0.0
994 * @version 4.0.6
995 * @param object $order The order object
996 * @param int $balance_transaction_id
997 */
998 public static function update_stripe_fees( $order, $balance_transaction_id ) {
999 $balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
1000
1001 if ( empty( $balance_transaction->error ) ) {
1002 if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
1003 // Fees and Net needs to both come from Stripe to be accurate as the returned
1004 // values are in the local currency of the Stripe account, not from WC.
1005 $fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
1006 $net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
1007
1008 // WC Stripe 10.x moved fee/net helpers from WC_Stripe_Helper to WC_Stripe_Order_Helper.
1009 if ( class_exists( 'WC_Stripe_Order_Helper' ) ) {
1010 $order_helper = WC_Stripe_Order_Helper::get_instance();
1011 $fee_current = $order_helper->get_stripe_fee( $order );
1012 $net_current = $order_helper->get_stripe_net( $order );
1013 $fee = (float) $fee_current + (float) $fee_refund;
1014 $net = (float) $net_current + (float) $net_refund;
1015 $order_helper->update_stripe_fee( $order, $fee );
1016 $order_helper->update_stripe_net( $order, $net );
1017 } else {
1018 $fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
1019 $net_current = WC_Stripe_Helper::get_stripe_net( $order );
1020 $fee = (float) $fee_current + (float) $fee_refund;
1021 $net = (float) $net_current + (float) $net_refund;
1022 WC_Stripe_Helper::update_stripe_fee( $order, $fee );
1023 WC_Stripe_Helper::update_stripe_net( $order, $net );
1024 }
1025
1026 $currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
1027 WC_Stripe_Helper::update_stripe_currency( $order, $currency );
1028
1029 if ( is_callable( [ $order, 'save' ] ) ) {
1030 $order->save();
1031 }
1032 }
1033 }
1034 }
1035
1036
1037 /**
1038 * @param $order
1039 * @param $response
1040 * @param $product
1041 */
1042 public function store_offer_transaction( $order, $response, $product )
1043 {
1044 $order->update_meta_data('_wpfunnels_offer_txn_resp_' . $product['step_id'], $response->id);
1045 $order->save();
1046 }
1047
1048
1049 /**
1050 * create child order reference with the parent order
1051 *
1052 * @param $parent_order
1053 * @param $product_data
1054 * @param string $type
1055 * @return bool|\WC_Order|\WP_Error
1056 * @throws \WC_Data_Exception
1057 */
1058 public function create_child_order( $parent_order, $product_data, $type = 'upsell' )
1059 {
1060 $order = false;
1061
1062 if (!empty($parent_order)) {
1063 $parent_order_id = $parent_order->get_id();
1064 $parent_order_billing = $parent_order->get_address('billing');
1065 $funnel_id = $parent_order->get_meta('_wpfunnels_funnel_id');
1066
1067 if (!empty($parent_order_billing['email'])) {
1068 $customer_id = $parent_order->get_customer_id();
1069
1070 $order = wc_create_order(
1071 [
1072 'customer_id' => $customer_id,
1073 'status' => 'wc-pending',
1074 'parent' => $parent_order_id,
1075 ]
1076 );
1077
1078 /* Set Order type */
1079 $order->update_meta_data('_wpfunnels_offer', 'yes' );
1080 $order->update_meta_data('_wpfunnels_offer_type', $type );
1081 $order->update_meta_data('_wpfunnels_parent_funnel_id', $funnel_id);
1082 $order->update_meta_data('_wpfunnels_offer_step_id', $product_data['step_id'] );
1083 $order->update_meta_data('_wpfunnels_offer_parent_id', $parent_order_id );
1084
1085 $item_id = $order->add_product(wc_get_product($product_data['id']), $product_data['qty'], $product_data['args']);
1086
1087 if( $item_id ){
1088 wc_add_order_item_meta( $item_id, "_wpfunnels_{$type}", 'yes' );
1089 wc_add_order_item_meta( $item_id, '_wpfunnels_step_id', $product_data['step_id']);
1090 }
1091
1092 $order->set_address($parent_order->get_address('billing'), 'billing');
1093 $order->set_address($parent_order->get_address('shipping'), 'shipping');
1094
1095 // Set shipping data.
1096 $order->set_payment_method($parent_order->get_payment_method());
1097 $order->set_payment_method_title($parent_order->get_payment_method_title());
1098
1099 if (!wc_tax_enabled()) {
1100 // Reports won't track orders fix.
1101 $order->set_shipping_tax(0);
1102 $order->set_cart_tax(0);
1103 }
1104
1105 $order->calculate_totals();
1106
1107 $offer_orders_meta = $parent_order->get_meta('_wpfunnels_offer_child_orders');
1108
1109 if (!is_array($offer_orders_meta)) {
1110 $offer_orders_meta = [];
1111 }
1112
1113 $offer_orders_meta[$order->get_id()] = ['type' => $type];
1114
1115 $parent_order->update_meta_data('_wpfunnels_offer_child_orders', $offer_orders_meta);
1116
1117 // Save the order.
1118 $parent_order->save();
1119 }
1120 }
1121
1122 if ($order) {
1123 $transaction_id = $parent_order->get_transaction_id();
1124
1125 $this->payment_complete($order, $transaction_id);
1126
1127 $order->set_transaction_id($transaction_id);
1128 $order->save();
1129
1130 $transaction_id_note = '';
1131
1132 if (!empty($transaction_id)) {
1133 $transaction_id_note = sprintf(' (Transaction ID: %s)', $transaction_id);
1134 }
1135
1136 $order->add_order_note('Offer Accepted | ' . $type . ' | Step ID - ' . $product_data['step_id'] . ' | ' . $transaction_id_note);
1137 return $order;
1138 }
1139 return false;
1140 }
1141
1142
1143 /**
1144 * payment_complete
1145 * Complete the payment
1146 *
1147 * @param WC_Order $order Parent order detail
1148 * @param String $transaction_id Transaction id.
1149 *
1150 */
1151
1152 public function payment_complete($order, $transaction_id = '')
1153 {
1154 $payment_method = $order->get_payment_method();
1155
1156 if ('cod' === $payment_method) {
1157 $order->set_status('processing');
1158 wc_reduce_stock_levels($order);
1159 } elseif ('bacs' === $payment_method) {
1160 $order->set_status('on-hold');
1161 wc_reduce_stock_levels($order);
1162 } else {
1163 $order->payment_complete($transaction_id);
1164 }
1165 }
1166
1167
1168 /**
1169 * Get WooCommerce payment geteways.
1170 *
1171 * @return array
1172 */
1173 public function get_wc_gateway() {
1174
1175 global $woocommerce;
1176 $gateways = $woocommerce->payment_gateways->payment_gateways();
1177
1178 return $gateways[ $this->key ];
1179 }
1180
1181
1182 /**
1183 * process refund offer
1184 *
1185 * @param $order
1186 * @param $data
1187 * @return bool
1188 * @throws \WC_Stripe_Exception
1189 */
1190 public function process_refund_offer( $order, $data ) {
1191
1192 $transaction_id = $data['transaction_id'];
1193 $amount = $data['amount'];
1194 $currency = $order->get_currency( $order );
1195
1196 $request = array();
1197 $response_id = false;
1198
1199 if ( ! is_null( $amount ) && class_exists( 'WC_Stripe_Helper' ) ) {
1200 $request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $currency );
1201 }
1202 if ( ! is_null( $amount ) && class_exists( 'WC_Stripe_API' ) ) {
1203 $request['charge'] = $transaction_id;
1204 $response = WC_Stripe_API::request( $request, 'refunds' );
1205 if ( ! empty( $response->error ) || ! $response ) {
1206 $response_id = false;
1207 } else {
1208 $this->get_wc_gateway()->update_fees( $order, $response->balance_transaction );
1209 $response_id = isset( $response->id ) ? $response->id : true;
1210 }
1211 }
1212 return $response_id;
1213 }
1214
1215
1216 /**
1217 * add required meta for refund
1218 *
1219 * @param $parent_order
1220 * @param $child_order
1221 * @param $transaction_id
1222 */
1223 public function add_capture_meta_to_child_order( $parent_order, $child_order, $transaction_id ) {
1224 $child_order->update_meta_data("_stripe_charge_captured", "yes");
1225
1226 $stripe_source_id = $parent_order->get_meta( '_stripe_source_id', true );
1227 $stripe_customer_id = $parent_order->get_meta( '_stripe_customer_id', true );
1228 if (function_exists("wcs_order_contains_subscription") && wcs_order_contains_subscription($child_order)) {
1229 $subscription = wcs_get_subscriptions_for_order($child_order->get_id());
1230 if (!empty($subscription)) {
1231 foreach ( $subscription as $sub ) {
1232 $sub->update_meta_data( '_stripe_source_id', $stripe_source_id );
1233 $sub->update_meta_data( '_stripe_customer_id', $stripe_customer_id );
1234 $sub->save();
1235 }
1236 }
1237 }
1238 }
1239
1240
1241 /**
1242 * add subscription offer meta to order
1243 *
1244 * @param $subscription
1245 * @param $offer_product
1246 * @param $order
1247 */
1248 public function add_offer_subscription_meta( $subscription, $offer_product, $order ) {
1249 if ( 'stripe' === $order->get_payment_method() ) {
1250 // Use CRUD methods instead of update_post_meta().
1251 $stripe_source_id = $order->get_meta( '_stripe_source_id', true );
1252 $stripe_customer_id = $order->get_meta( '_stripe_customer_id', true );
1253
1254 if ( $stripe_source_id ) {
1255 $subscription->update_meta_data( '_stripe_source_id', $stripe_source_id );
1256 }
1257
1258 if ( $stripe_customer_id ) {
1259 $subscription->update_meta_data( '_stripe_customer_id', $stripe_customer_id );
1260 }
1261
1262 // Save the subscription after updating meta.
1263 $subscription->save();
1264 }
1265 }
1266
1267
1268 /**
1269 * Add hidden field for stripe
1270 *
1271 * @return void
1272 * @since 2.2.3
1273 */
1274 public function add_stripe_hidden_field(){
1275 // Get funnel checkout ID from post data.
1276 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
1277
1278 // Get checkout id if not found in post data.
1279 $checkout_id = !$checkout_id ? get_the_ID() : $checkout_id;
1280
1281 // Get funnel ID from checkout ID.
1282 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $checkout_id );
1283
1284 // Check if checkout ID and funnel ID exists.
1285 if ( $checkout_id && $funnel_id ) {
1286 // Check if offer exists in funnel.
1287 if ( Wpfnl_functions::is_offer_exists_in_funnel($funnel_id) ) {
1288 // Add hidden field for stripe.
1289 echo '<input type="hidden" name="wc-stripe-new-payment-method" value="1">';
1290 }
1291 }
1292 }
1293 }
1294