PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
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.13.3, at includes/core/gateways/class-wpfnl-gateway-stripe.php

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