2 if ( ! defined( 'ABSPATH' ) ) {
6 // phpcs:disable WordPress.Files.FileName
9 * Abstract class that will be inherited by all payment methods.
11 * @extends WC_Payment_Gateway_CC
15 abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
17 * Displays the admin settings webhook description.
22 public function display_admin_settings_webhook_description() {
23 /* translators: 1) webhook url */
24 return sprintf( __( 'You must add the following webhook endpoint <strong style="background-color:#ddd;"> %s </strong> to your <a href="https://dashboard.stripe.com/account/webhooks" target="_blank">Stripe account settings</a>. This will enable you to receive notifications on the charge statuses.', 'woocommerce-gateway-stripe' ), WC_Stripe_Helper::get_webhook_url() );
28 * Displays the save to account checkbox.
32 public function save_payment_method_checkbox() {
34 '<p class="form-row woocommerce-SavedPaymentMethods-saveNew">
35 <input id="wc-%1$s-new-payment-method" name="wc-%1$s-new-payment-method" type="checkbox" value="true" style="width:auto;" />
36 <label for="wc-%1$s-new-payment-method" style="display:inline;">%2$s</label>
38 esc_attr( $this->id ),
39 esc_html( apply_filters( 'wc_stripe_save_to_account_text', __( 'Save payment information to my account for future purchases.', 'woocommerce-gateway-stripe' ) ) )
44 * Checks to see if request is invalid and that
45 * they are worth retrying.
50 public function is_retryable_error( $error ) {
52 'invalid_request_error' === $error->type ||
53 'idempotency_error' === $error->type ||
54 'rate_limit_error' === $error->type ||
55 'api_connection_error' === $error->type ||
56 'api_error' === $error->type
61 * Checks to see if error is of same idempotency key
62 * error due to retries with different parameters.
67 public function is_same_idempotency_error( $error ) {
70 'idempotency_error' === $error->type &&
71 preg_match( '/Keys for idempotent requests can only be used with the same parameters they were first used with./i', $error->message )
76 * Checks to see if error is of invalid request
77 * error and it is no such customer.
82 public function is_no_such_customer_error( $error ) {
85 'invalid_request_error' === $error->type &&
86 preg_match( '/No such customer/i', $error->message )
91 * Checks to see if error is of invalid request
92 * error and it is no such token.
97 public function is_no_such_token_error( $error ) {
100 'invalid_request_error' === $error->type &&
101 preg_match( '/No such token/i', $error->message )
106 * Checks to see if error is of invalid request
107 * error and it is no such source.
110 * @param array $error
112 public function is_no_such_source_error( $error ) {
115 'invalid_request_error' === $error->type &&
116 preg_match( '/No such source/i', $error->message )
121 * Checks to see if error is of invalid request
122 * error and it is no such source linked to customer.
125 * @param array $error
127 public function is_no_linked_source_error( $error ) {
130 'invalid_request_error' === $error->type &&
131 preg_match( '/does not have a linked source with ID/i', $error->message )
136 * Check to see if we need to update the idempotency
137 * key to be different from previous charge request.
140 * @param object $source_object
141 * @param object $error
144 public function need_update_idempotency_key( $source_object, $error ) {
147 1 < $this->retry_interval &&
148 ! empty( $source_object ) &&
149 'chargeable' === $source_object->status &&
150 self::is_same_idempotency_error( $error )
155 * Checks if keys are set and valid.
158 * @return bool True if the keys are set *and* valid, false otherwise (for example, if keys are empty or the secret key was pasted as publishable key).
160 public function are_keys_set() {
161 // NOTE: updates to this function should be added to are_keys_set()
162 // in includes/payment-methods/class-wc-stripe-payment-request.php
164 if ( $this->testmode ) {
165 return preg_match( '/^pk_test_/', $this->publishable_key )
166 && preg_match( '/^[rs]k_test_/', $this->secret_key );
168 return preg_match( '/^pk_live_/', $this->publishable_key )
169 && preg_match( '/^[rs]k_live_/', $this->secret_key );
174 * Check if we need to make gateways available.
178 public function is_available() {
179 if ( 'yes' === $this->enabled ) {
180 return $this->are_keys_set();
183 return parent::is_available();
187 * Checks if we need to process pre orders when
188 * pre orders is in the cart.
191 * @param int $order_id
194 public function maybe_process_pre_orders( $order_id ) {
196 WC_Stripe_Helper::is_pre_orders_exists() &&
197 $this->pre_orders->is_pre_order( $order_id ) &&
198 WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) &&
199 ! is_wc_endpoint_url( 'order-pay' )
204 * All payment icons that work with Stripe. Some icons references
208 * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
211 public function payment_icons() {
212 return apply_filters(
213 'wc_stripe_payment_icons',
215 'visa' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg" class="stripe-visa-icon stripe-icon" alt="Visa" />',
216 'amex' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg" class="stripe-amex-icon stripe-icon" alt="American Express" />',
217 'mastercard' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg" class="stripe-mastercard-icon stripe-icon" alt="Mastercard" />',
218 'discover' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg" class="stripe-discover-icon stripe-icon" alt="Discover" />',
219 'diners' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg" class="stripe-diners-icon stripe-icon" alt="Diners" />',
220 'jcb' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg" class="stripe-jcb-icon stripe-icon" alt="JCB" />',
221 'alipay' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
222 'wechat' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
223 'bancontact' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
224 'ideal' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDeal" />',
225 'p24' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
226 'giropay' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="Giropay" />',
227 'eps' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/eps.svg" class="stripe-eps-icon stripe-icon" alt="EPS" />',
228 'multibanco' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/multibanco.svg" class="stripe-multibanco-icon stripe-icon" alt="Multibanco" />',
229 'sofort' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sofort.svg" class="stripe-sofort-icon stripe-icon" alt="SOFORT" />',
230 'sepa' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sepa.svg" class="stripe-sepa-icon stripe-icon" alt="SEPA" />',
236 * Validates that the order meets the minimum order amount
241 * @param object $order
243 public function validate_minimum_order_amount( $order ) {
244 if ( $order->get_total() * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
245 /* translators: 1) dollar amount */
246 throw new WC_Stripe_Exception( 'Did not meet minimum amount', sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( WC_Stripe_Helper::get_minimum_amount() / 100 ) ) );
251 * Gets the transaction URL linked to Stripe dashboard.
256 public function get_transaction_url( $order ) {
257 if ( $this->testmode ) {
258 $this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
260 $this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
263 return parent::get_transaction_url( $order );
267 * Gets the saved customer id if exists.
272 public function get_stripe_customer_id( $order ) {
273 $customer = get_user_option( '_stripe_customer_id', $order->get_customer_id() );
275 if ( empty( $customer ) ) {
276 // Try to get it via the order.
277 return $order->get_meta( '_stripe_customer_id', true );
286 * Builds the return URL from redirects.
290 * @param object $order
291 * @param int $id Stripe session id.
293 public function get_stripe_return_url( $order = null, $id = null ) {
294 if ( is_object( $order ) ) {
295 if ( empty( $id ) ) {
299 $order_id = $order->get_id();
302 'utm_nooverride' => '1',
303 'order_id' => $order_id,
306 return wp_sanitize_redirect( esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) ) );
309 return wp_sanitize_redirect( esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) ) );
313 * Is $order_id a subscription?
314 * @param int $order_id
317 public function has_subscription( $order_id ) {
318 return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
322 * Generate the request for the payment.
326 * @param WC_Order $order
327 * @param object $prepared_source
330 public function generate_payment_request( $order, $prepared_source ) {
331 $settings = get_option( 'woocommerce_stripe_settings', array() );
332 $statement_descriptor = ! empty( $settings['statement_descriptor'] ) ? str_replace( "'", '', $settings['statement_descriptor'] ) : '';
333 $capture = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
334 $post_data = array();
335 $post_data['currency'] = strtolower( $order->get_currency() );
336 $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $post_data['currency'] );
337 /* translators: 1) blog name 2) order number */
338 $post_data['description'] = sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
339 $billing_email = $order->get_billing_email();
340 $billing_first_name = $order->get_billing_first_name();
341 $billing_last_name = $order->get_billing_last_name();
343 if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
344 $post_data['receipt_email'] = $billing_email;
347 switch ( $order->get_payment_method() ) {
349 if ( ! empty( $statement_descriptor ) ) {
350 $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
353 $post_data['capture'] = $capture ? 'true' : 'false';
356 if ( ! empty( $statement_descriptor ) ) {
357 $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
362 if ( method_exists( $order, 'get_shipping_postcode' ) && ! empty( $order->get_shipping_postcode() ) ) {
363 $post_data['shipping'] = array(
364 'name' => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ),
366 'line1' => $order->get_shipping_address_1(),
367 'line2' => $order->get_shipping_address_2(),
368 'city' => $order->get_shipping_city(),
369 'country' => $order->get_shipping_country(),
370 'postal_code' => $order->get_shipping_postcode(),
371 'state' => $order->get_shipping_state(),
376 $post_data['expand[]'] = 'balance_transaction';
379 __( 'customer_name', 'woocommerce-gateway-stripe' ) => sanitize_text_field( $billing_first_name ) . ' ' . sanitize_text_field( $billing_last_name ),
380 __( 'customer_email', 'woocommerce-gateway-stripe' ) => sanitize_email( $billing_email ),
381 'order_id' => $order->get_order_number(),
382 'site_url' => esc_url( get_site_url() ),
385 if ( $this->has_subscription( $order->get_id() ) ) {
387 'payment_type' => 'recurring',
391 $post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
393 if ( $prepared_source->customer ) {
394 $post_data['customer'] = $prepared_source->customer;
397 if ( $prepared_source->source ) {
398 $post_data['source'] = $prepared_source->source;
402 * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
405 * @param array $post_data
406 * @param WC_Order $order
407 * @param object $source
409 return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
413 * Store extra meta data for an order from a Stripe Response.
415 public function process_response( $response, $order ) {
416 WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
418 $order_id = $order->get_id();
419 $captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
421 // Store charge data.
422 $order->update_meta_data( '_stripe_charge_captured', $captured );
424 if ( isset( $response->balance_transaction ) ) {
425 $this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
428 if ( 'yes' === $captured ) {
430 * Charge can be captured but in a pending state. Payment methods
431 * that are asynchronous may take couple days to clear. Webhook will
432 * take care of the status changes.
434 if ( 'pending' === $response->status ) {
435 $order_stock_reduced = $order->get_meta( '_order_stock_reduced', true );
437 if ( ! $order_stock_reduced ) {
438 wc_reduce_stock_levels( $order_id );
441 $order->set_transaction_id( $response->id );
442 /* translators: transaction id */
443 $order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $response->id ) );
446 if ( 'succeeded' === $response->status ) {
447 $order->payment_complete( $response->id );
449 /* translators: transaction id */
450 $message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
451 $order->add_order_note( $message );
454 if ( 'failed' === $response->status ) {
455 $localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
456 $order->add_order_note( $localized_message );
457 throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
460 $order->set_transaction_id( $response->id );
462 if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
463 wc_reduce_stock_levels( $order_id );
466 /* translators: transaction id */
467 $order->update_status( 'on-hold', sprintf( __( 'Stripe charge authorized (Charge ID: %s). Process order to take payment, or cancel to remove the pre-authorization.', 'woocommerce-gateway-stripe' ), $response->id ) );
470 if ( is_callable( array( $order, 'save' ) ) ) {
474 do_action( 'wc_gateway_stripe_process_response', $response, $order );
480 * Sends the failed order email to admin.
484 * @param int $order_id
487 public function send_failed_order_email( $order_id ) {
488 $emails = WC()->mailer()->get_emails();
489 if ( ! empty( $emails ) && ! empty( $order_id ) ) {
490 $emails['WC_Email_Failed_Order']->trigger( $order_id );
499 * @param object $order
500 * @return object $details
502 public function get_owner_details( $order ) {
503 $billing_first_name = $order->get_billing_first_name();
504 $billing_last_name = $order->get_billing_last_name();
508 $name = $billing_first_name . ' ' . $billing_last_name;
509 $email = $order->get_billing_email();
510 $phone = $order->get_billing_phone();
512 if ( ! empty( $phone ) ) {
513 $details['phone'] = $phone;
516 if ( ! empty( $name ) ) {
517 $details['name'] = $name;
520 if ( ! empty( $email ) ) {
521 $details['email'] = $email;
524 $details['address']['line1'] = $order->get_billing_address_1();
525 $details['address']['line2'] = $order->get_billing_address_2();
526 $details['address']['state'] = $order->get_billing_state();
527 $details['address']['city'] = $order->get_billing_city();
528 $details['address']['postal_code'] = $order->get_billing_postcode();
529 $details['address']['country'] = $order->get_billing_country();
531 return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
535 * Get source object by source id.
538 * @param string $source_id The source ID to get source object for.
540 public function get_source_object( $source_id = '' ) {
541 if ( empty( $source_id ) ) {
545 $source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
547 if ( ! empty( $source_object->error ) ) {
548 throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
551 return $source_object;
555 * Checks if card is a prepaid card.
558 * @param object $source_object
561 public function is_prepaid_card( $source_object ) {
564 && ( 'token' === $source_object->object || 'source' === $source_object->object )
565 && 'prepaid' === $source_object->card->funding
570 * Checks if source is of legacy type card.
573 * @param string $source_id
576 public function is_type_legacy_card( $source_id ) {
577 return ( preg_match( '/^card_/', $source_id ) );
581 * Checks if payment is via saved payment source.
586 public function is_using_saved_payment_method() {
587 $payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
589 return ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
593 * Get payment source. This can be a new token/source or existing WC token.
594 * If user is logged in and/or has WC account, create an account on Stripe.
595 * This way we can attribute the payment to the user to better fight fraud.
599 * @param string $user_id
600 * @param bool $force_save_source Should we force save payment source.
602 * @throws Exception When card was not added or for and invalid card.
605 public function prepare_source( $user_id, $force_save_source = false, $existing_customer_id = null ) {
606 $customer = new WC_Stripe_Customer( $user_id );
607 if ( ! empty( $existing_customer_id ) ) {
608 $customer->set_id( $existing_customer_id );
611 $force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
614 $wc_token_id = false;
615 $payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
618 // New CC info was entered and we have a new source to process.
619 if ( ! empty( $_POST['stripe_source'] ) ) {
620 $source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
621 $source_id = $source_object->id;
623 // This checks to see if customer opted to save the payment method to file.
624 $maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
627 * This is true if the user wants to store the card to their account.
628 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
629 * actually reusable. Either that or force_save_source is true.
631 if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
632 $response = $customer->add_source( $source_object->id );
634 if ( ! empty( $response->error ) ) {
635 throw new WC_Stripe_Exception( print_r( $response, true ), $this->get_localized_error_message_from_response( $response ) );
638 } elseif ( $this->is_using_saved_payment_method() ) {
639 // Use an existing token, and then process the payment.
640 $wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
641 $wc_token = WC_Payment_Tokens::get( $wc_token_id );
643 if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
644 WC()->session->set( 'refresh_totals', true );
645 throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
648 $source_id = $wc_token->get_token();
650 if ( $this->is_type_legacy_card( $source_id ) ) {
653 } elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
654 $stripe_token = wc_clean( $_POST['stripe_token'] );
655 $maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
657 // This is true if the user wants to store the card to their account.
658 if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
659 $response = $customer->add_source( $stripe_token );
661 if ( ! empty( $response->error ) ) {
662 throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
664 $source_id = $response;
666 $source_id = $stripe_token;
671 $customer_id = $customer->get_id();
672 if ( ! $customer_id ) {
673 $customer->set_id( $customer->create_customer() );
674 $customer_id = $customer->get_id();
676 $customer_id = $customer->update_customer();
679 if ( empty( $source_object ) && ! $is_token ) {
680 $source_object = self::get_source_object( $source_id );
683 return (object) array(
684 'token_id' => $wc_token_id,
685 'customer' => $customer_id,
686 'source' => $source_id,
687 'source_object' => $source_object,
692 * Get payment source from an order. This could be used in the future for
693 * a subscription as an example, therefore using the current user ID would
694 * not work - the customer won't be logged in :)
696 * Not using 2.6 tokens for this part since we need a customer AND a card
697 * token, and not just one.
701 * @param object $order
704 public function prepare_order_source( $order = null ) {
705 $stripe_customer = new WC_Stripe_Customer();
706 $stripe_source = false;
708 $source_object = false;
711 $order_id = $order->get_id();
713 $stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
715 if ( $stripe_customer_id ) {
716 $stripe_customer->set_id( $stripe_customer_id );
719 $source_id = $order->get_meta( '_stripe_source_id', true );
721 // Since 4.0.0, we changed card to source so we need to account for that.
722 if ( empty( $source_id ) ) {
723 $source_id = $order->get_meta( '_stripe_card_id', true );
725 // Take this opportunity to update the key name.
726 $order->update_meta_data( '_stripe_source_id', $source_id );
728 if ( is_callable( array( $order, 'save' ) ) ) {
734 $stripe_source = $source_id;
735 $source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
736 } elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
738 * We can attempt to charge the customer's default source
739 * by sending empty source id.
745 return (object) array(
746 'token_id' => $token_id,
747 'customer' => $stripe_customer ? $stripe_customer->get_id() : false,
748 'source' => $stripe_source,
749 'source_object' => $source_object,
754 * Save source to order.
758 * @param WC_Order $order For to which the source applies.
759 * @param stdClass $source Source information.
761 public function save_source_to_order( $order, $source ) {
762 // Store source in the order.
763 if ( $source->customer ) {
764 $order->update_meta_data( '_stripe_customer_id', $source->customer );
767 if ( $source->source ) {
768 $order->update_meta_data( '_stripe_source_id', $source->source );
771 if ( is_callable( array( $order, 'save' ) ) ) {
777 * Updates Stripe fees/net.
778 * e.g usage would be after a refund.
782 * @param object $order The order object
783 * @param int $balance_transaction_id
785 public function update_fees( $order, $balance_transaction_id ) {
786 $balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
788 if ( empty( $balance_transaction->error ) ) {
789 if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
790 // Fees and Net needs to both come from Stripe to be accurate as the returned
791 // values are in the local currency of the Stripe account, not from WC.
792 $fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
793 $net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
795 // Current data fee & net.
796 $fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
797 $net_current = WC_Stripe_Helper::get_stripe_net( $order );
800 $fee = (float) $fee_current + (float) $fee_refund;
801 $net = (float) $net_current + (float) $net_refund;
803 WC_Stripe_Helper::update_stripe_fee( $order, $fee );
804 WC_Stripe_Helper::update_stripe_net( $order, $net );
806 $currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
807 WC_Stripe_Helper::update_stripe_currency( $order, $currency );
809 if ( is_callable( array( $order, 'save' ) ) ) {
814 WC_Stripe_Logger::log( 'Unable to update fees/net meta for order: ' . $order->get_id() );
823 * @param int $order_id
824 * @param float $amount
827 public function process_refund( $order_id, $amount = null, $reason = '' ) {
828 $order = wc_get_order( $order_id );
836 $order_currency = $order->get_currency();
837 $captured = $order->get_meta( '_stripe_charge_captured', true );
838 $charge_id = $order->get_transaction_id();
840 if ( ! $charge_id ) {
844 if ( ! is_null( $amount ) ) {
845 $request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
848 // If order is only authorized, don't pass amount.
849 if ( 'yes' !== $captured ) {
850 unset( $request['amount'] );
854 $request['metadata'] = array(
859 $request['charge'] = $charge_id;
860 WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
862 $request = apply_filters( 'wc_stripe_refund_request', $request, $order );
864 $intent = $this->get_intent_from_order( $order );
865 $intent_cancelled = false;
867 // If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
868 if ( ! empty( $intent->error ) ) {
870 $intent_cancelled = true;
871 } elseif ( 'requires_capture' === $intent->status ) {
872 $result = WC_Stripe_API::request(
874 'payment_intents/' . $intent->id . '/cancel'
876 $intent_cancelled = true;
878 if ( ! empty( $result->error ) ) {
881 $charge = end( $result->charges->data );
882 $response = end( $charge->refunds->data );
887 if ( ! $intent_cancelled ) {
888 $response = WC_Stripe_API::request( $request, 'refunds' );
891 if ( ! empty( $response->error ) ) {
892 WC_Stripe_Logger::log( 'Error: ' . $response->error->message );
896 } elseif ( ! empty( $response->id ) ) {
897 $order->update_meta_data( '_stripe_refund_id', $response->id );
899 $amount = wc_price( $response->amount / 100 );
901 if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
902 $amount = wc_price( $response->amount );
905 if ( isset( $response->balance_transaction ) ) {
906 $this->update_fees( $order, $response->balance_transaction );
909 /* translators: 1) dollar amount 2) transaction id 3) refund message */
910 $refund_message = ( isset( $captured ) && 'yes' === $captured ) ? sprintf( __( 'Refunded %1$s - Refund ID: %2$s - Reason: %3$s', 'woocommerce-gateway-stripe' ), $amount, $response->id, $reason ) : __( 'Pre-Authorization Released', 'woocommerce-gateway-stripe' );
912 $order->add_order_note( $refund_message );
913 WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
920 * Add payment method via account screen.
921 * We don't store the token locally, but to the Stripe API.
926 public function add_payment_method() {
928 $error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
931 if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
935 $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
937 $source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
939 $source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
941 if ( isset( $source_object ) ) {
942 if ( ! empty( $source_object->error ) ) {
946 $source_id = $source_object->id;
947 } elseif ( isset( $_POST['stripe_token'] ) ) {
948 $source_id = wc_clean( $_POST['stripe_token'] );
951 $response = $stripe_customer->add_source( $source_id );
953 if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
958 wc_add_notice( $error_msg, 'error' );
959 WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
963 do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
966 'result' => 'success',
967 'redirect' => wc_get_endpoint_url( 'payment-methods' ),
972 * Gets the locale with normalization that only Stripe accepts.
975 * @return string $locale
977 public function get_locale() {
978 $locale = get_locale();
981 * Stripe expects Norwegian to only be passed NO.
982 * But WP has different dialects.
984 if ( 'NO' === substr( $locale, 3, 2 ) ) {
987 $locale = substr( get_locale(), 0, 2 );
994 * Change the idempotency key so charge can
995 * process order as a different transaction.
998 * @param string $idempotency_key
999 * @param array $request
1001 public function change_idempotency_key( $idempotency_key, $request ) {
1002 $customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
1003 $source = ! empty( $request['source'] ) ? $request['source'] : $customer;
1004 $count = $this->retry_interval;
1006 return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
1010 * Checks if request is the original to prevent double processing
1011 * on WC side. The original-request header and request-id header
1012 * needs to be the same to mean its the original request.
1015 * @param array $headers
1017 public function is_original_request( $headers ) {
1018 if ( $headers['original-request'] === $headers['request-id'] ) {
1026 * Generates the request when creating a new payment intent.
1028 * @param WC_Order $order The order that is being paid for.
1029 * @param object $prepared_source The source that is used for the payment.
1030 * @return array The arguments for the request.
1032 public function generate_create_intent_request( $order, $prepared_source ) {
1033 // The request for a charge contains metadata for the intent.
1034 $full_request = $this->generate_payment_request( $order, $prepared_source );
1037 'source' => $prepared_source->source,
1038 'amount' => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1039 'currency' => strtolower( $order->get_currency() ),
1040 'description' => $full_request['description'],
1041 'metadata' => $full_request['metadata'],
1042 'capture_method' => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1043 'payment_method_types' => array(
1048 if ( $prepared_source->customer ) {
1049 $request['customer'] = $prepared_source->customer;
1052 if ( isset( $full_request['statement_descriptor'] ) ) {
1053 $request['statement_descriptor'] = $full_request['statement_descriptor'];
1056 if ( isset( $full_request['shipping'] ) ) {
1057 $request['shipping'] = $full_request['shipping'];
1061 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1064 * @param array $request
1065 * @param WC_Order $order
1066 * @param object $source
1068 return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1072 * Create the level 3 data array to send to Stripe when making a purchase.
1074 * @param WC_Order $order The order that is being paid for.
1075 * @return array The level 3 data to send to Stripe.
1077 public function get_level3_data_from_order( $order ) {
1078 // Get the order items. Don't need their keys, only their values.
1079 // Order item IDs are used as keys in the original order items array.
1080 $order_items = array_values( $order->get_items() );
1081 $currency = $order->get_currency();
1083 $stripe_line_items = array_map(function( $item ) use ( $currency ) {
1084 $product_id = $item->get_variation_id()
1085 ? $item->get_variation_id()
1086 : $item->get_product_id();
1087 $product_description = substr( $item->get_name(), 0, 26 );
1088 $quantity = $item->get_quantity();
1089 $unit_cost = WC_Stripe_Helper::get_stripe_amount( ( $item->get_subtotal() / $quantity ), $currency );
1090 $tax_amount = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
1091 $discount_amount = WC_Stripe_Helper::get_stripe_amount( $item->get_subtotal() - $item->get_total(), $currency );
1093 return (object) array(
1094 'product_code' => (string) $product_id, // Up to 12 characters that uniquely identify the product.
1095 'product_description' => $product_description, // Up to 26 characters long describing the product.
1096 'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
1097 'quantity' => $quantity, // The number of items of this type sold, as a non-negative integer.
1098 'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
1099 'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
1103 $level3_data = array(
1104 '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”.
1105 '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.
1106 'line_items' => $stripe_line_items,
1109 // The customer’s U.S. shipping ZIP code.
1110 $shipping_address_zip = $order->get_shipping_postcode();
1111 if ( $this->is_valid_us_zip_code( $shipping_address_zip ) ) {
1112 $level3_data['shipping_address_zip'] = $shipping_address_zip;
1115 // The merchant’s U.S. shipping ZIP code.
1116 $store_postcode = get_option( 'woocommerce_store_postcode' );
1117 if ( $this->is_valid_us_zip_code( $store_postcode ) ) {
1118 $level3_data['shipping_from_zip'] = $store_postcode;
1121 return $level3_data;
1125 * Create a new PaymentIntent.
1127 * @param WC_Order $order The order that is being paid for.
1128 * @param object $prepared_source The source that is used for the payment.
1129 * @return object An intent or an error.
1131 public function create_intent( $order, $prepared_source ) {
1132 $request = $this->generate_create_intent_request( $order, $prepared_source );
1134 // Create an intent that awaits an action.
1135 $intent = WC_Stripe_API::request( $request, 'payment_intents' );
1136 if ( ! empty( $intent->error ) ) {
1140 $order_id = $order->get_id();
1141 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1143 // Save the intent ID to the order.
1144 $this->save_intent_to_order( $order, $intent );
1150 * Updates an existing intent with updated amount, source, and customer.
1152 * @param object $intent The existing intent object.
1153 * @param WC_Order $order The order.
1154 * @param object $prepared_source Currently selected source.
1155 * @return object An updated intent.
1157 public function update_existing_intent( $intent, $order, $prepared_source ) {
1160 if ( $prepared_source->source !== $intent->source ) {
1161 $request['source'] = $prepared_source->source;
1164 $new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1165 if ( $intent->amount !== $new_amount ) {
1166 $request['amount'] = $new_amount;
1169 if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1170 $request['customer'] = $prepared_source->customer;
1173 if ( $this->has_subscription( $order ) ) {
1174 // If this is a failed subscription order payment, the intent should be
1175 // prepared for future usage.
1176 $request['setup_future_usage'] = 'off_session';
1179 if ( empty( $request ) ) {
1183 $level3_data = $this->get_level3_data_from_order( $order );
1184 return WC_Stripe_API::request_with_level3_data(
1186 "payment_intents/$intent->id",
1193 * Confirms an intent if it is the `requires_confirmation` state.
1196 * @param object $intent The intent to confirm.
1197 * @param WC_Order $order The order that the intent is associated with.
1198 * @param object $prepared_source The source that is being charged.
1199 * @return object Either an error or the updated intent.
1201 public function confirm_intent( $intent, $order, $prepared_source ) {
1202 if ( 'requires_confirmation' !== $intent->status ) {
1206 // Try to confirm the intent & capture the charge (if 3DS is not required).
1207 $confirm_request = array(
1208 'source' => $prepared_source->source,
1211 $level3_data = $this->get_level3_data_from_order( $order );
1212 $confirmed_intent = WC_Stripe_API::request_with_level3_data(
1214 "payment_intents/$intent->id/confirm",
1219 if ( ! empty( $confirmed_intent->error ) ) {
1220 return $confirmed_intent;
1223 // Save a note about the status of the intent.
1224 $order_id = $order->get_id();
1225 if ( 'succeeded' === $confirmed_intent->status ) {
1226 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1227 } elseif ( 'requires_action' === $confirmed_intent->status ) {
1228 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1231 return $confirmed_intent;
1235 * Saves intent to order.
1238 * @param WC_Order $order For to which the source applies.
1239 * @param stdClass $intent Payment intent information.
1241 public function save_intent_to_order( $order, $intent ) {
1242 $order->update_meta_data( '_stripe_intent_id', $intent->id );
1244 if ( is_callable( array( $order, 'save' ) ) ) {
1250 * Retrieves the payment intent, associated with an order.
1253 * @param WC_Order $order The order to retrieve an intent for.
1254 * @return obect|bool Either the intent object or `false`.
1256 public function get_intent_from_order( $order ) {
1257 $intent_id = $order->get_meta( '_stripe_intent_id' );
1260 return $this->get_intent( 'payment_intents', $intent_id );
1263 // The order doesn't have a payment intent, but it may have a setup intent.
1264 $intent_id = $order->get_meta( '_stripe_setup_intent' );
1267 return $this->get_intent( 'setup_intents', $intent_id );
1274 * Retrieves intent from Stripe API by intent id.
1276 * @param string $intent_type Either 'payment_intents' or 'setup_intents'.
1277 * @param string $intent_id Intent id.
1278 * @return object|bool Either the intent object or `false`.
1279 * @throws Exception Throws exception for unknown $intent_type.
1281 private function get_intent( $intent_type, $intent_id ) {
1282 if ( ! in_array( $intent_type, [ 'payment_intents', 'setup_intents' ] ) ) {
1283 throw new Exception( "Failed to get intent of type $intent_type. Type is not allowed" );
1286 $response = WC_Stripe_API::request( array(), "$intent_type/$intent_id", 'GET' );
1288 if ( $response && isset( $response->{ 'error' } ) ) {
1289 $error_response_message = print_r( $response, true );
1290 WC_Stripe_Logger::log("Failed to get Stripe intent $intent_type/$intent_id.");
1291 WC_Stripe_Logger::log("Response: $error_response_message");
1299 * Locks an order for payment intent processing for 5 minutes.
1302 * @param WC_Order $order The order that is being paid.
1303 * @param stdClass $intent The intent that is being processed.
1304 * @return bool A flag that indicates whether the order is already locked.
1306 public function lock_order_payment( $order, $intent = null ) {
1307 $order_id = $order->get_id();
1308 $transient_name = 'wc_stripe_processing_intent_' . $order_id;
1309 $processing = get_transient( $transient_name );
1311 // Block the process if the same intent is already being handled.
1312 if ( "-1" === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1316 // Save the new intent as a transient, eventually overwriting another one.
1317 set_transient( $transient_name, empty( $intent ) ? '-1' : $intent->id, 5 * MINUTE_IN_SECONDS );
1323 * Unlocks an order for processing by payment intents.
1326 * @param WC_Order $order The order that is being unlocked.
1328 public function unlock_order_payment( $order ) {
1329 $order_id = $order->get_id();
1330 delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1334 * Given a response from Stripe, check if it's a card error where authentication is required
1335 * to complete the payment.
1337 * @param object $response The response from Stripe.
1338 * @return boolean Whether or not it's a 'authentication_required' error
1340 public function is_authentication_required_for_payment( $response ) {
1341 return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1342 || ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1346 * Creates a SetupIntent for future payments, and saves it to the order.
1348 * @param WC_Order $order The ID of the (free/pre- order).
1349 * @param object $prepared_source The source, entered/chosen by the customer.
1350 * @return string The client secret of the intent, used for confirmation in JS.
1352 public function setup_intent( $order, $prepared_source ) {
1353 $order_id = $order->get_id();
1354 $setup_intent = WC_Stripe_API::request( array(
1355 'payment_method' => $prepared_source->source,
1356 'customer' => $prepared_source->customer,
1357 'confirm' => 'true',
1358 ), 'setup_intents' );
1360 if ( is_wp_error( $setup_intent ) ) {
1361 WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1362 } elseif ( 'requires_action' === $setup_intent->status ) {
1363 $order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1366 return $setup_intent->client_secret;
1371 * Create and confirm a new PaymentIntent.
1373 * @param WC_Order $order The order that is being paid for.
1374 * @param object $prepared_source The source that is used for the payment.
1375 * @param float $amount The amount to charge. If not specified, it will be read from the order.
1376 * @return object An intent or an error.
1378 public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1379 // The request for a charge contains metadata for the intent.
1380 $full_request = $this->generate_payment_request( $order, $prepared_source );
1383 'amount' => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1384 'currency' => $full_request['currency'],
1385 'description' => $full_request['description'],
1386 'metadata' => $full_request['metadata'],
1387 'payment_method_types' => array(
1390 'off_session' => 'true',
1391 'confirm' => 'true',
1392 'confirmation_method' => 'automatic',
1395 if ( isset( $full_request['statement_descriptor'] ) ) {
1396 $request['statement_descriptor'] = $full_request['statement_descriptor'];
1399 if ( isset( $full_request['customer'] ) ) {
1400 $request['customer'] = $full_request['customer'];
1403 if ( isset( $full_request['source'] ) ) {
1404 $is_source = 'src_' === substr( $full_request['source'], 0, 4 );
1405 $request[ $is_source ? 'source' : 'payment_method' ] = $full_request['source'];
1409 * Filter the value of the request.
1412 * @param array $request
1413 * @param WC_Order $order
1414 * @param object $source
1416 $request = apply_filters('wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1418 if ( isset( $full_request['shipping'] ) ) {
1419 $request['shipping'] = $full_request['shipping'];
1422 $level3_data = $this->get_level3_data_from_order( $order );
1423 $intent = WC_Stripe_API::request_with_level3_data(
1429 $is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1431 if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1435 $intent_id = ( ! empty( $intent->error )
1436 ? $intent->error->payment_intent->id
1439 $payment_intent = ( ! empty( $intent->error )
1440 ? $intent->error->payment_intent
1443 $order_id = $order->get_id();
1444 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1446 // Save the intent ID to the order.
1447 $this->save_intent_to_order( $order, $payment_intent );
1453 * Checks if subscription has a Stripe customer ID and adds it if doesn't.
1455 * Fix renewal for existing subscriptions affected by https://github.com/woocommerce/woocommerce-gateway-stripe/issues/1072.
1456 * @param int $order_id subscription renewal order id.
1458 public function ensure_subscription_has_customer_id( $order_id ) {
1459 $subscriptions_ids = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) );
1460 foreach( $subscriptions_ids as $subscription_id => $subscription ) {
1461 if ( ! metadata_exists( 'post', $subscription_id, '_stripe_customer_id' ) ) {
1462 $stripe_customer = new WC_Stripe_Customer( $subscription->get_user_id() );
1463 update_post_meta( $subscription_id, '_stripe_customer_id', $stripe_customer->get_id() );
1464 update_post_meta( $order_id, '_stripe_customer_id', $stripe_customer->get_id() );
1469 /** Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions.
1471 * @param string $zip The ZIP code to verify.
1474 public function is_valid_us_zip_code( $zip ) {
1475 return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip );