]> _ Git - physioassist-wordpress.git/blob
fca4ac0b413c91d848ab9bc6ec5e049093e46e82
[physioassist-wordpress.git] /
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3         exit;
4 }
5
6 // phpcs:disable WordPress.Files.FileName
7
8 /**
9  * Abstract class that will be inherited by all payment methods.
10  *
11  * @extends WC_Payment_Gateway_CC
12  *
13  * @since 4.0.0
14  */
15 abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
16         /**
17          * Displays the admin settings webhook description.
18          *
19          * @since 4.1.0
20          * @return mixed
21          */
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;">&nbsp;%s&nbsp;</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() );
25         }
26
27         /**
28          * Displays the save to account checkbox.
29          *
30          * @since 4.1.0
31          */
32         public function save_payment_method_checkbox() {
33                 printf(
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>
37                         </p>',
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' ) ) )
40                 );
41         }
42
43         /**
44          * Checks to see if request is invalid and that
45          * they are worth retrying.
46          *
47          * @since 4.0.5
48          * @param array $error
49          */
50         public function is_retryable_error( $error ) {
51                 return (
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
57                 );
58         }
59
60         /**
61          * Checks to see if error is of same idempotency key
62          * error due to retries with different parameters.
63          *
64          * @since 4.1.0
65          * @param array $error
66          */
67         public function is_same_idempotency_error( $error ) {
68                 return (
69                         $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 )
72                 );
73         }
74
75         /**
76          * Checks to see if error is of invalid request
77          * error and it is no such customer.
78          *
79          * @since 4.1.0
80          * @param array $error
81          */
82         public function is_no_such_customer_error( $error ) {
83                 return (
84                         $error &&
85                         'invalid_request_error' === $error->type &&
86                         preg_match( '/No such customer/i', $error->message )
87                 );
88         }
89
90         /**
91          * Checks to see if error is of invalid request
92          * error and it is no such token.
93          *
94          * @since 4.1.0
95          * @param array $error
96          */
97         public function is_no_such_token_error( $error ) {
98                 return (
99                         $error &&
100                         'invalid_request_error' === $error->type &&
101                         preg_match( '/No such token/i', $error->message )
102                 );
103         }
104
105         /**
106          * Checks to see if error is of invalid request
107          * error and it is no such source.
108          *
109          * @since 4.1.0
110          * @param array $error
111          */
112         public function is_no_such_source_error( $error ) {
113                 return (
114                         $error &&
115                         'invalid_request_error' === $error->type &&
116                         preg_match( '/No such source/i', $error->message )
117                 );
118         }
119
120         /**
121          * Checks to see if error is of invalid request
122          * error and it is no such source linked to customer.
123          *
124          * @since 4.1.0
125          * @param array $error
126          */
127         public function is_no_linked_source_error( $error ) {
128                 return (
129                         $error &&
130                         'invalid_request_error' === $error->type &&
131                         preg_match( '/does not have a linked source with ID/i', $error->message )
132                 );
133         }
134
135         /**
136          * Check to see if we need to update the idempotency
137          * key to be different from previous charge request.
138          *
139          * @since 4.1.0
140          * @param object $source_object
141          * @param object $error
142          * @return bool
143          */
144         public function need_update_idempotency_key( $source_object, $error ) {
145                 return (
146                         $error &&
147                         1 < $this->retry_interval &&
148                         ! empty( $source_object ) &&
149                         'chargeable' === $source_object->status &&
150                         self::is_same_idempotency_error( $error )
151                 );
152         }
153
154         /**
155          * Checks if keys are set and valid.
156          *
157          * @since 4.0.6
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).
159          */
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
163
164                 if ( $this->testmode ) {
165                         return preg_match( '/^pk_test_/', $this->publishable_key )
166                                 && preg_match( '/^[rs]k_test_/', $this->secret_key );
167                 } else {
168                         return preg_match( '/^pk_live_/', $this->publishable_key )
169                             && preg_match( '/^[rs]k_live_/', $this->secret_key );
170                 }
171         }
172
173         /**
174          * Check if we need to make gateways available.
175          *
176          * @since 4.1.3
177          */
178         public function is_available() {
179                 if ( 'yes' === $this->enabled ) {
180                         return $this->are_keys_set();
181                 }
182
183                 return parent::is_available();
184         }
185
186         /**
187          * Checks if we need to process pre orders when
188          * pre orders is in the cart.
189          *
190          * @since 4.1.0
191          * @param int $order_id
192          * @return bool
193          */
194         public function maybe_process_pre_orders( $order_id ) {
195                 return (
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' )
200                 );
201         }
202
203         /**
204          * All payment icons that work with Stripe. Some icons references
205          * WC core icons.
206          *
207          * @since 4.0.0
208          * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
209          * @return array
210          */
211         public function payment_icons() {
212                 return apply_filters(
213                         'wc_stripe_payment_icons',
214                         array(
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" />',
231                         )
232                 );
233         }
234
235         /**
236          * Validates that the order meets the minimum order amount
237          * set by Stripe.
238          *
239          * @since 4.0.0
240          * @version 4.0.0
241          * @param object $order
242          */
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 ) ) );
247                 }
248         }
249
250         /**
251          * Gets the transaction URL linked to Stripe dashboard.
252          *
253          * @since 4.0.0
254          * @version 4.0.0
255          */
256         public function get_transaction_url( $order ) {
257                 if ( $this->testmode ) {
258                         $this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
259                 } else {
260                         $this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
261                 }
262
263                 return parent::get_transaction_url( $order );
264         }
265
266         /**
267          * Gets the saved customer id if exists.
268          *
269          * @since 4.0.0
270          * @version 4.0.0
271          */
272         public function get_stripe_customer_id( $order ) {
273                 $customer = get_user_option( '_stripe_customer_id', $order->get_customer_id() );
274
275                 if ( empty( $customer ) ) {
276                         // Try to get it via the order.
277                         return $order->get_meta( '_stripe_customer_id', true );
278                 } else {
279                         return $customer;
280                 }
281
282                 return false;
283         }
284
285         /**
286          * Builds the return URL from redirects.
287          *
288          * @since 4.0.0
289          * @version 4.0.0
290          * @param object $order
291          * @param int $id Stripe session id.
292          */
293         public function get_stripe_return_url( $order = null, $id = null ) {
294                 if ( is_object( $order ) ) {
295                         if ( empty( $id ) ) {
296                                 $id = uniqid();
297                         }
298
299                         $order_id = $order->get_id();
300
301                         $args = array(
302                                 'utm_nooverride' => '1',
303                                 'order_id'       => $order_id,
304                         );
305
306                         return wp_sanitize_redirect( esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) ) );
307                 }
308
309                 return wp_sanitize_redirect( esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) ) );
310         }
311
312         /**
313          * Is $order_id a subscription?
314          * @param  int  $order_id
315          * @return boolean
316          */
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 ) ) );
319         }
320
321         /**
322          * Generate the request for the payment.
323          *
324          * @since 3.1.0
325          * @version 4.5.4
326          * @param  WC_Order $order
327          * @param  object $prepared_source
328          * @return array()
329          */
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();
342
343                 if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
344                         $post_data['receipt_email'] = $billing_email;
345                 }
346
347                 switch ( $order->get_payment_method() ) {
348                         case 'stripe':
349                                 if ( ! empty( $statement_descriptor ) ) {
350                                         $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
351                                 }
352
353                                 $post_data['capture'] = $capture ? 'true' : 'false';
354                                 break;
355                         case 'stripe_sepa':
356                                 if ( ! empty( $statement_descriptor ) ) {
357                                         $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
358                                 }
359                                 break;
360                 }
361
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() ),
365                                 'address' => array(
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(),
372                                 )
373                         );
374                 }
375
376                 $post_data['expand[]'] = 'balance_transaction';
377
378                 $metadata = array(
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() ),
383                 );
384
385                 if ( $this->has_subscription( $order->get_id() ) ) {
386                         $metadata += array(
387                                 'payment_type' => 'recurring',
388                         );
389                 }
390
391                 $post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
392
393                 if ( $prepared_source->customer ) {
394                         $post_data['customer'] = $prepared_source->customer;
395                 }
396
397                 if ( $prepared_source->source ) {
398                         $post_data['source'] = $prepared_source->source;
399                 }
400
401                 /**
402                  * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
403                  *
404                  * @since 3.1.0
405                  * @param array $post_data
406                  * @param WC_Order $order
407                  * @param object $source
408                  */
409                 return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
410         }
411
412         /**
413          * Store extra meta data for an order from a Stripe Response.
414          */
415         public function process_response( $response, $order ) {
416                 WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
417
418                 $order_id = $order->get_id();
419                 $captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
420
421                 // Store charge data.
422                 $order->update_meta_data( '_stripe_charge_captured', $captured );
423
424                 if ( isset( $response->balance_transaction ) ) {
425                         $this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
426                 }
427
428                 if ( 'yes' === $captured ) {
429                         /**
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.
433                          */
434                         if ( 'pending' === $response->status ) {
435                                 $order_stock_reduced = $order->get_meta( '_order_stock_reduced', true );
436
437                                 if ( ! $order_stock_reduced ) {
438                                         wc_reduce_stock_levels( $order_id );
439                                 }
440
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 ) );
444                         }
445
446                         if ( 'succeeded' === $response->status ) {
447                                 $order->payment_complete( $response->id );
448
449                                 /* translators: transaction id */
450                                 $message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
451                                 $order->add_order_note( $message );
452                         }
453
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 );
458                         }
459                 } else {
460                         $order->set_transaction_id( $response->id );
461
462                         if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
463                                 wc_reduce_stock_levels( $order_id );
464                         }
465
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 ) );
468                 }
469
470                 if ( is_callable( array( $order, 'save' ) ) ) {
471                         $order->save();
472                 }
473
474                 do_action( 'wc_gateway_stripe_process_response', $response, $order );
475
476                 return $response;
477         }
478
479         /**
480          * Sends the failed order email to admin.
481          *
482          * @since 3.1.0
483          * @version 4.0.0
484          * @param int $order_id
485          * @return null
486          */
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 );
491                 }
492         }
493
494         /**
495          * Get owner details.
496          *
497          * @since 4.0.0
498          * @version 4.0.0
499          * @param object $order
500          * @return object $details
501          */
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();
505
506                 $details = array();
507
508                 $name  = $billing_first_name . ' ' . $billing_last_name;
509                 $email = $order->get_billing_email();
510                 $phone = $order->get_billing_phone();
511
512                 if ( ! empty( $phone ) ) {
513                         $details['phone'] = $phone;
514                 }
515
516                 if ( ! empty( $name ) ) {
517                         $details['name'] = $name;
518                 }
519
520                 if ( ! empty( $email ) ) {
521                         $details['email'] = $email;
522                 }
523
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();
530
531                 return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
532         }
533
534         /**
535          * Get source object by source id.
536          *
537          * @since 4.0.3
538          * @param string $source_id The source ID to get source object for.
539          */
540         public function get_source_object( $source_id = '' ) {
541                 if ( empty( $source_id ) ) {
542                         return '';
543                 }
544
545                 $source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
546
547                 if ( ! empty( $source_object->error ) ) {
548                         throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
549                 }
550
551                 return $source_object;
552         }
553
554         /**
555          * Checks if card is a prepaid card.
556          *
557          * @since 4.0.6
558          * @param object $source_object
559          * @return bool
560          */
561         public function is_prepaid_card( $source_object ) {
562                 return (
563                         $source_object
564                         && ( 'token' === $source_object->object || 'source' === $source_object->object )
565                         && 'prepaid' === $source_object->card->funding
566                 );
567         }
568
569         /**
570          * Checks if source is of legacy type card.
571          *
572          * @since 4.0.8
573          * @param string $source_id
574          * @return bool
575          */
576         public function is_type_legacy_card( $source_id ) {
577                 return ( preg_match( '/^card_/', $source_id ) );
578         }
579
580         /**
581          * Checks if payment is via saved payment source.
582          *
583          * @since 4.1.0
584          * @return bool
585          */
586         public function is_using_saved_payment_method() {
587                 $payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
588
589                 return ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
590         }
591
592         /**
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.
596          *
597          * @since 3.1.0
598          * @version 4.0.0
599          * @param string $user_id
600          * @param bool $force_save_source Should we force save payment source.
601          *
602          * @throws Exception When card was not added or for and invalid card.
603          * @return object
604          */
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 );
609                 }
610
611                 $force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
612                 $source_object     = '';
613                 $source_id         = '';
614                 $wc_token_id       = false;
615                 $payment_method    = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
616                 $is_token          = false;
617
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;
622
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' ] );
625
626                         /**
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.
630                          */
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 );
633
634                                 if ( ! empty( $response->error ) ) {
635                                         throw new WC_Stripe_Exception( print_r( $response, true ), $this->get_localized_error_message_from_response( $response ) );
636                                 }
637                         }
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 );
642
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' ) );
646                         }
647
648                         $source_id = $wc_token->get_token();
649
650                         if ( $this->is_type_legacy_card( $source_id ) ) {
651                                 $is_token = true;
652                         }
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' ] );
656
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 );
660
661                                 if ( ! empty( $response->error ) ) {
662                                         throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
663                                 }
664                                 $source_id    = $response;
665                         } else {
666                                 $source_id    = $stripe_token;
667                                 $is_token     = true;
668                         }
669                 }
670
671                 $customer_id = $customer->get_id();
672                 if ( ! $customer_id ) {
673                         $customer->set_id( $customer->create_customer() );
674                         $customer_id = $customer->get_id();
675                 } else {
676                         $customer_id = $customer->update_customer();
677                 }
678
679                 if ( empty( $source_object ) && ! $is_token ) {
680                         $source_object = self::get_source_object( $source_id );
681                 }
682
683                 return (object) array(
684                         'token_id'      => $wc_token_id,
685                         'customer'      => $customer_id,
686                         'source'        => $source_id,
687                         'source_object' => $source_object,
688                 );
689         }
690
691         /**
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 :)
695          *
696          * Not using 2.6 tokens for this part since we need a customer AND a card
697          * token, and not just one.
698          *
699          * @since 3.1.0
700          * @version 4.0.0
701          * @param object $order
702          * @return object
703          */
704         public function prepare_order_source( $order = null ) {
705                 $stripe_customer = new WC_Stripe_Customer();
706                 $stripe_source   = false;
707                 $token_id        = false;
708                 $source_object   = false;
709
710                 if ( $order ) {
711                         $order_id = $order->get_id();
712
713                         $stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
714
715                         if ( $stripe_customer_id ) {
716                                 $stripe_customer->set_id( $stripe_customer_id );
717                         }
718
719                         $source_id = $order->get_meta( '_stripe_source_id', true );
720
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 );
724
725                                 // Take this opportunity to update the key name.
726                                 $order->update_meta_data( '_stripe_source_id', $source_id );
727
728                                 if ( is_callable( array( $order, 'save' ) ) ) {
729                                         $order->save();
730                                 }
731                         }
732
733                         if ( $source_id ) {
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 ) ) {
737                                 /*
738                                  * We can attempt to charge the customer's default source
739                                  * by sending empty source id.
740                                  */
741                                 $stripe_source = '';
742                         }
743                 }
744
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,
750                 );
751         }
752
753         /**
754          * Save source to order.
755          *
756          * @since 3.1.0
757          * @version 4.0.0
758          * @param WC_Order $order For to which the source applies.
759          * @param stdClass $source Source information.
760          */
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 );
765                 }
766
767                 if ( $source->source ) {
768                         $order->update_meta_data( '_stripe_source_id', $source->source );
769                 }
770
771                 if ( is_callable( array( $order, 'save' ) ) ) {
772                         $order->save();
773                 }
774         }
775
776         /**
777          * Updates Stripe fees/net.
778          * e.g usage would be after a refund.
779          *
780          * @since 4.0.0
781          * @version 4.0.6
782          * @param object $order The order object
783          * @param int $balance_transaction_id
784          */
785         public function update_fees( $order, $balance_transaction_id ) {
786                 $balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
787
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;
794
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 );
798
799                                 // Calculation.
800                                 $fee = (float) $fee_current + (float) $fee_refund;
801                                 $net = (float) $net_current + (float) $net_refund;
802
803                                 WC_Stripe_Helper::update_stripe_fee( $order, $fee );
804                                 WC_Stripe_Helper::update_stripe_net( $order, $net );
805
806                                 $currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
807                                 WC_Stripe_Helper::update_stripe_currency( $order, $currency );
808
809                                 if ( is_callable( array( $order, 'save' ) ) ) {
810                                         $order->save();
811                                 }
812                         }
813                 } else {
814                         WC_Stripe_Logger::log( 'Unable to update fees/net meta for order: ' . $order->get_id() );
815                 }
816         }
817
818         /**
819          * Refund a charge.
820          *
821          * @since 3.1.0
822          * @version 4.0.0
823          * @param  int $order_id
824          * @param  float $amount
825          * @return bool
826          */
827         public function process_refund( $order_id, $amount = null, $reason = '' ) {
828                 $order = wc_get_order( $order_id );
829
830                 if ( ! $order ) {
831                         return false;
832                 }
833
834                 $request = array();
835
836                 $order_currency = $order->get_currency();
837                 $captured       = $order->get_meta( '_stripe_charge_captured', true );
838                 $charge_id      = $order->get_transaction_id();
839
840                 if ( ! $charge_id ) {
841                         return false;
842                 }
843
844                 if ( ! is_null( $amount ) ) {
845                         $request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
846                 }
847
848                 // If order is only authorized, don't pass amount.
849                 if ( 'yes' !== $captured ) {
850                         unset( $request['amount'] );
851                 }
852
853                 if ( $reason ) {
854                         $request['metadata'] = array(
855                                 'reason' => $reason,
856                         );
857                 }
858
859                 $request['charge'] = $charge_id;
860                 WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
861
862                 $request = apply_filters( 'wc_stripe_refund_request', $request, $order );
863
864                 $intent = $this->get_intent_from_order( $order );
865                 $intent_cancelled = false;
866                 if ( $intent ) {
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 ) ) {
869                                 $response = $intent;
870                                 $intent_cancelled = true;
871                         } elseif ( 'requires_capture' === $intent->status ) {
872                                 $result = WC_Stripe_API::request(
873                                         array(),
874                                         'payment_intents/' . $intent->id . '/cancel'
875                                 );
876                                 $intent_cancelled = true;
877
878                                 if ( ! empty( $result->error ) ) {
879                                         $response = $result;
880                                 } else {
881                                         $charge = end( $result->charges->data );
882                                         $response = end( $charge->refunds->data );
883                                 }
884                         }
885                 }
886
887                 if ( ! $intent_cancelled ) {
888                         $response = WC_Stripe_API::request( $request, 'refunds' );
889                 }
890
891                 if ( ! empty( $response->error ) ) {
892                         WC_Stripe_Logger::log( 'Error: ' . $response->error->message );
893
894                         return $response;
895
896                 } elseif ( ! empty( $response->id ) ) {
897                         $order->update_meta_data( '_stripe_refund_id', $response->id );
898
899                         $amount = wc_price( $response->amount / 100 );
900
901                         if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
902                                 $amount = wc_price( $response->amount );
903                         }
904
905                         if ( isset( $response->balance_transaction ) ) {
906                                 $this->update_fees( $order, $response->balance_transaction );
907                         }
908
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' );
911
912                         $order->add_order_note( $refund_message );
913                         WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
914
915                         return true;
916                 }
917         }
918
919         /**
920          * Add payment method via account screen.
921          * We don't store the token locally, but to the Stripe API.
922          *
923          * @since 3.0.0
924          * @version 4.0.0
925          */
926         public function add_payment_method() {
927                 $error     = false;
928                 $error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
929                 $source_id = '';
930
931                 if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
932                         $error = true;
933                 }
934
935                 $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
936
937                 $source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
938
939                 $source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
940
941                 if ( isset( $source_object ) ) {
942                         if ( ! empty( $source_object->error ) ) {
943                                 $error = true;
944                         }
945
946                         $source_id = $source_object->id;
947                 } elseif ( isset( $_POST['stripe_token'] ) ) {
948                         $source_id = wc_clean( $_POST['stripe_token'] );
949                 }
950
951                 $response = $stripe_customer->add_source( $source_id );
952
953                 if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
954                         $error = true;
955                 }
956
957                 if ( $error ) {
958                         wc_add_notice( $error_msg, 'error' );
959                         WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
960                         return;
961                 }
962
963                 do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
964
965                 return array(
966                         'result'   => 'success',
967                         'redirect' => wc_get_endpoint_url( 'payment-methods' ),
968                 );
969         }
970
971         /**
972          * Gets the locale with normalization that only Stripe accepts.
973          *
974          * @since 4.0.6
975          * @return string $locale
976          */
977         public function get_locale() {
978                 $locale = get_locale();
979
980                 /*
981                  * Stripe expects Norwegian to only be passed NO.
982                  * But WP has different dialects.
983                  */
984                 if ( 'NO' === substr( $locale, 3, 2 ) ) {
985                         $locale = 'no';
986                 } else {
987                         $locale = substr( get_locale(), 0, 2 );
988                 }
989
990                 return $locale;
991         }
992
993         /**
994          * Change the idempotency key so charge can
995          * process order as a different transaction.
996          *
997          * @since 4.0.6
998          * @param string $idempotency_key
999          * @param array $request
1000          */
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;
1005
1006                 return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
1007         }
1008
1009         /**
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.
1013          *
1014          * @since 4.0.6
1015          * @param array $headers
1016          */
1017         public function is_original_request( $headers ) {
1018                 if ( $headers['original-request'] === $headers['request-id'] ) {
1019                         return true;
1020                 }
1021
1022                 return false;
1023         }
1024
1025         /**
1026          * Generates the request when creating a new payment intent.
1027          *
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.
1031          */
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 );
1035
1036                 $request = array(
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(
1044                                 'card',
1045                         ),
1046                 );
1047
1048                 if ( $prepared_source->customer ) {
1049                         $request['customer'] = $prepared_source->customer;
1050                 }
1051
1052                 if ( isset( $full_request['statement_descriptor'] ) ) {
1053                         $request['statement_descriptor'] = $full_request['statement_descriptor'];
1054                 }
1055
1056                 if ( isset( $full_request['shipping'] ) ) {
1057                         $request['shipping'] = $full_request['shipping'];
1058                 }
1059
1060                 /**
1061                  * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1062                  *
1063                  * @since 3.1.0
1064                  * @param array $request
1065                  * @param WC_Order $order
1066                  * @param object $source
1067                  */
1068                 return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1069         }
1070
1071         /**
1072          * Create the level 3 data array to send to Stripe when making a purchase.
1073          *
1074          * @param WC_Order $order The order that is being paid for.
1075          * @return array          The level 3 data to send to Stripe.
1076          */
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();
1082
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 );
1092
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.
1100                         );
1101                 }, $order_items);
1102
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,
1107                 );
1108
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;
1113                 }
1114
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;
1119                 }
1120
1121                 return $level3_data;
1122         }
1123
1124         /**
1125          * Create a new PaymentIntent.
1126          *
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.
1130          */
1131         public function create_intent( $order, $prepared_source ) {
1132                 $request = $this->generate_create_intent_request( $order, $prepared_source );
1133
1134                 // Create an intent that awaits an action.
1135                 $intent = WC_Stripe_API::request( $request, 'payment_intents' );
1136                 if ( ! empty( $intent->error ) ) {
1137                         return $intent;
1138                 }
1139
1140                 $order_id = $order->get_id();
1141                 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1142
1143                 // Save the intent ID to the order.
1144                 $this->save_intent_to_order( $order, $intent );
1145
1146                 return $intent;
1147         }
1148
1149         /**
1150          * Updates an existing intent with updated amount, source, and customer.
1151          *
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.
1156          */
1157         public function update_existing_intent( $intent, $order, $prepared_source ) {
1158                 $request = array();
1159
1160                 if ( $prepared_source->source !== $intent->source ) {
1161                         $request['source'] = $prepared_source->source;
1162                 }
1163
1164                 $new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1165                 if ( $intent->amount !== $new_amount ) {
1166                         $request['amount'] = $new_amount;
1167                 }
1168
1169                 if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1170                         $request['customer'] = $prepared_source->customer;
1171                 }
1172
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';
1177                 }
1178
1179                 if ( empty( $request ) ) {
1180                         return $intent;
1181                 }
1182
1183                 $level3_data = $this->get_level3_data_from_order( $order );
1184                 return WC_Stripe_API::request_with_level3_data(
1185                         $request,
1186                         "payment_intents/$intent->id",
1187                         $level3_data,
1188                         $order
1189                 );
1190         }
1191
1192         /**
1193          * Confirms an intent if it is the `requires_confirmation` state.
1194          *
1195          * @since 4.2.1
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.
1200          */
1201         public function confirm_intent( $intent, $order, $prepared_source ) {
1202                 if ( 'requires_confirmation' !== $intent->status ) {
1203                         return $intent;
1204                 }
1205
1206                 // Try to confirm the intent & capture the charge (if 3DS is not required).
1207                 $confirm_request = array(
1208                         'source' => $prepared_source->source,
1209                 );
1210
1211                 $level3_data = $this->get_level3_data_from_order( $order );
1212                 $confirmed_intent = WC_Stripe_API::request_with_level3_data(
1213                         $confirm_request,
1214                         "payment_intents/$intent->id/confirm",
1215                         $level3_data,
1216                         $order
1217                 );
1218
1219                 if ( ! empty( $confirmed_intent->error ) ) {
1220                         return $confirmed_intent;
1221                 }
1222
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" );
1229                 }
1230
1231                 return $confirmed_intent;
1232         }
1233
1234         /**
1235          * Saves intent to order.
1236          *
1237          * @since 3.2.0
1238          * @param WC_Order $order For to which the source applies.
1239          * @param stdClass $intent Payment intent information.
1240          */
1241         public function save_intent_to_order( $order, $intent ) {
1242                 $order->update_meta_data( '_stripe_intent_id', $intent->id );
1243
1244                 if ( is_callable( array( $order, 'save' ) ) ) {
1245                         $order->save();
1246                 }
1247         }
1248
1249         /**
1250          * Retrieves the payment intent, associated with an order.
1251          *
1252          * @since 4.2
1253          * @param WC_Order $order The order to retrieve an intent for.
1254          * @return obect|bool     Either the intent object or `false`.
1255          */
1256         public function get_intent_from_order( $order ) {
1257                 $intent_id = $order->get_meta( '_stripe_intent_id' );
1258
1259                 if ( $intent_id ) {
1260                         return $this->get_intent( 'payment_intents', $intent_id );
1261                 }
1262
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' );
1265
1266                 if ( $intent_id ) {
1267                         return $this->get_intent( 'setup_intents', $intent_id );
1268                 }
1269
1270                 return false;
1271         }
1272
1273         /**
1274          * Retrieves intent from Stripe API by intent id.
1275          *
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.
1280          */
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" );
1284                 }
1285
1286                 $response = WC_Stripe_API::request( array(), "$intent_type/$intent_id", 'GET' );
1287
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");
1292                         return false;
1293                 }
1294
1295                 return $response;
1296         }
1297
1298         /**
1299          * Locks an order for payment intent processing for 5 minutes.
1300          *
1301          * @since 4.2
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.
1305          */
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 );
1310
1311                 // Block the process if the same intent is already being handled.
1312                 if ( "-1" === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1313                         return true;
1314                 }
1315
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 );
1318
1319                 return false;
1320         }
1321
1322         /**
1323          * Unlocks an order for processing by payment intents.
1324          *
1325          * @since 4.2
1326          * @param WC_Order $order The order that is being unlocked.
1327          */
1328         public function unlock_order_payment( $order ) {
1329                 $order_id = $order->get_id();
1330                 delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1331         }
1332
1333         /**
1334          * Given a response from Stripe, check if it's a card error where authentication is required
1335          * to complete the payment.
1336          *
1337          * @param object $response The response from Stripe.
1338          * @return boolean Whether or not it's a 'authentication_required' error
1339          */
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 );
1343         }
1344
1345         /**
1346          * Creates a SetupIntent for future payments, and saves it to the order.
1347          *
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.
1351          */
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' );
1359
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 );
1364                         $order->save();
1365
1366                         return $setup_intent->client_secret;
1367                 }
1368         }
1369
1370         /**
1371          * Create and confirm a new PaymentIntent.
1372          *
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.
1377          */
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 );
1381
1382                 $request = array(
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(
1388                                 'card',
1389                         ),
1390                         'off_session'          => 'true',
1391                         'confirm'              => 'true',
1392                         'confirmation_method'  => 'automatic',
1393                 );
1394
1395                 if ( isset( $full_request['statement_descriptor'] ) ) {
1396                         $request['statement_descriptor'] = $full_request['statement_descriptor'];
1397                 }
1398
1399                 if ( isset( $full_request['customer'] ) ) {
1400                         $request['customer'] = $full_request['customer'];
1401                 }
1402
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'];
1406                 }
1407
1408                 /**
1409                  * Filter the value of the request.
1410                  *
1411                  * @since 4.5.0
1412                  * @param array $request
1413                  * @param WC_Order $order
1414                  * @param object $source
1415                  */
1416                 $request = apply_filters('wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1417
1418                 if ( isset( $full_request['shipping'] ) ) {
1419                         $request['shipping'] = $full_request['shipping'];
1420                 }
1421
1422                 $level3_data = $this->get_level3_data_from_order( $order );
1423                 $intent = WC_Stripe_API::request_with_level3_data(
1424                         $request,
1425                         'payment_intents',
1426                         $level3_data,
1427                         $order
1428                 );
1429                 $is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1430
1431                 if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1432                         return $intent;
1433                 }
1434
1435                 $intent_id      = ( ! empty( $intent->error )
1436                         ? $intent->error->payment_intent->id
1437                         : $intent->id
1438                 );
1439                 $payment_intent = ( ! empty( $intent->error )
1440                         ? $intent->error->payment_intent
1441                         : $intent
1442                 );
1443                 $order_id       = $order->get_id();
1444                 WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1445
1446                 // Save the intent ID to the order.
1447                 $this->save_intent_to_order( $order, $payment_intent );
1448
1449                 return $intent;
1450         }
1451
1452         /**
1453          * Checks if subscription has a Stripe customer ID and adds it if doesn't.
1454          *
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.
1457          */
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() );
1465                         }
1466                 }
1467         }
1468
1469         /** Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions.
1470          *
1471          * @param string $zip The ZIP code to verify.
1472          * @return boolean
1473          */
1474         public function is_valid_us_zip_code( $zip ) {
1475                 return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip );
1476         }
1477 }