blob: a118a85a0908a2de0b07ecc34972689fa1844cc3 [file] [log] [blame]
swissChilif0cbdc32023-01-05 17:21:38 -05001<?php
2if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4}
5
6/**
7 * Handles and process orders from asyncronous flows.
8 *
9 * @since 4.0.0
10 */
11class WC_Stripe_Order_Handler extends WC_Stripe_Payment_Gateway {
12 private static $_this;
13
14 /**
15 * Constructor.
16 *
17 * @since 4.0.0
18 * @version 4.0.0
19 */
20 public function __construct() {
21 self::$_this = $this;
22
23 add_action( 'wp', [ $this, 'maybe_process_redirect_order' ] );
24 add_action( 'woocommerce_order_status_processing', [ $this, 'capture_payment' ] );
25 add_action( 'woocommerce_order_status_completed', [ $this, 'capture_payment' ] );
26 add_action( 'woocommerce_order_status_cancelled', [ $this, 'cancel_payment' ] );
27 add_action( 'woocommerce_order_status_refunded', [ $this, 'cancel_payment' ] );
28 add_filter( 'woocommerce_tracks_event_properties', [ $this, 'woocommerce_tracks_event_properties' ], 10, 2 );
29 }
30
31 /**
32 * Public access to instance object.
33 *
34 * @since 4.0.0
35 * @version 4.0.0
36 */
37 public static function get_instance() {
38 return self::$_this;
39 }
40
41 /**
42 * Processes payments.
43 * Note at this time the original source has already been
44 * saved to a customer card (if applicable) from process_payment.
45 *
46 * @since 4.0.0
47 * @since 4.1.8 Add $previous_error parameter.
48 * @param int $order_id
49 * @param bool $retry
50 * @param mix $previous_error Any error message from previous request.
51 */
52 public function process_redirect_payment( $order_id, $retry = true, $previous_error = false ) {
53 try {
54 $source = isset( $_GET['source'] ) ? wc_clean( wp_unslash( $_GET['source'] ) ) : '';
55
56 if ( empty( $source ) ) {
57 return;
58 }
59
60 if ( empty( $order_id ) ) {
61 return;
62 }
63
64 $order = wc_get_order( $order_id );
65
66 if ( ! is_object( $order ) ) {
67 return;
68 }
69
70 if ( $order->has_status( [ 'processing', 'completed', 'on-hold' ] ) ) {
71 return;
72 }
73
74 // Result from Stripe API request.
75 $response = null;
76
77 // This will throw exception if not valid.
78 $this->validate_minimum_order_amount( $order );
79
80 WC_Stripe_Logger::log( "Info: (Redirect) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
81
82 /**
83 * First check if the source is chargeable at this time. If not,
84 * webhook will take care of it later.
85 */
86 $source_info = WC_Stripe_API::retrieve( 'sources/' . $source );
87
88 if ( ! empty( $source_info->error ) ) {
89 throw new WC_Stripe_Exception( print_r( $source_info, true ), $source_info->error->message );
90 }
91
92 if ( 'failed' === $source_info->status || 'canceled' === $source_info->status ) {
93 throw new WC_Stripe_Exception( print_r( $source_info, true ), __( 'Unable to process this payment, please try again or use alternative method.', 'woocommerce-gateway-stripe' ) );
94 }
95
96 // If already consumed, then ignore request.
97 if ( 'consumed' === $source_info->status ) {
98 return;
99 }
100
101 // If not chargeable, then ignore request.
102 if ( 'chargeable' !== $source_info->status ) {
103 return;
104 }
105
106 // Prep source object.
107 $prepared_source = $this->prepare_order_source( $order );
108 $prepared_source->status = 'chargeable';
109
110 /*
111 * If we're doing a retry and source is chargeable, we need to pass
112 * a different idempotency key and retry for success.
113 */
114 if ( $this->need_update_idempotency_key( $prepared_source, $previous_error ) ) {
115 add_filter( 'wc_stripe_idempotency_key', [ $this, 'change_idempotency_key' ], 10, 2 );
116 }
117
118 // Make the request.
119 $response = WC_Stripe_API::request( $this->generate_payment_request( $order, $prepared_source ), 'charges', 'POST', true );
120 $headers = $response['headers'];
121 $response = $response['body'];
122
123 if ( ! empty( $response->error ) ) {
124 // Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
125 if ( $this->is_no_such_customer_error( $response->error ) ) {
126 delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
127 $order->delete_meta_data( '_stripe_customer_id' );
128 $order->save();
129 }
130
131 if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) {
132 // Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
133 $wc_token = WC_Payment_Tokens::get( $prepared_source->token_id );
134 $wc_token->delete();
135 $localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
136 $order->add_order_note( $localized_message );
137 throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
138 }
139
140 // We want to retry.
141 if ( $this->is_retryable_error( $response->error ) ) {
142 if ( $retry ) {
143 // Don't do anymore retries after this.
144 if ( 5 <= $this->retry_interval ) {
145 return $this->process_redirect_payment( $order_id, false, $response->error );
146 }
147
148 sleep( $this->retry_interval );
149
150 $this->retry_interval++;
151 return $this->process_redirect_payment( $order_id, true, $response->error );
152 } else {
153 $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
154 $order->add_order_note( $localized_message );
155 throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
156 }
157 }
158
159 $localized_messages = WC_Stripe_Helper::get_localized_messages();
160
161 if ( 'card_error' === $response->error->type ) {
162 $message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
163 } else {
164 $message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
165 }
166
167 throw new WC_Stripe_Exception( print_r( $response, true ), $message );
168 }
169
170 // To prevent double processing the order on WC side.
171 if ( ! $this->is_original_request( $headers ) ) {
172 return;
173 }
174
175 do_action( 'wc_gateway_stripe_process_redirect_payment', $response, $order );
176
177 $this->process_response( $response, $order );
178
179 } catch ( WC_Stripe_Exception $e ) {
180 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
181
182 do_action( 'wc_gateway_stripe_process_redirect_payment_error', $e, $order );
183
184 /* translators: error message */
185 $order->update_status( 'failed', sprintf( __( 'Stripe payment failed: %s', 'woocommerce-gateway-stripe' ), $e->getLocalizedMessage() ) );
186
187 wc_add_notice( $e->getLocalizedMessage(), 'error' );
188 wp_safe_redirect( wc_get_checkout_url() );
189 exit;
190 }
191 }
192
193 /**
194 * Processes the orders that are redirected.
195 *
196 * @since 4.0.0
197 * @version 4.0.0
198 */
199 public function maybe_process_redirect_order() {
200 if ( ! is_order_received_page() || empty( $_GET['client_secret'] ) || empty( $_GET['source'] ) ) {
201 return;
202 }
203
204 $order_id = isset( $_GET['order_id'] ) ? wc_clean( wp_unslash( $_GET['order_id'] ) ) : '';
205
206 $this->process_redirect_payment( $order_id );
207 }
208
209 /**
210 * Capture payment when the order is changed from on-hold to complete or processing.
211 *
212 * @since 3.1.0
213 * @version 4.0.0
214 * @param int $order_id
215 * @return stdClass|void Result of payment capture.
216 */
217 public function capture_payment( $order_id ) {
218 $order = wc_get_order( $order_id );
219
220 if ( 'stripe' === $order->get_payment_method() ) {
221 $charge = $order->get_transaction_id();
222 $captured = $order->get_meta( '_stripe_charge_captured', true );
223 $is_stripe_captured = false;
224
225 if ( $charge && 'no' === $captured ) {
226 $order_total = $order->get_total();
227
228 if ( 0 < $order->get_total_refunded() ) {
229 $order_total = $order_total - $order->get_total_refunded();
230 }
231
232 $intent = $this->get_intent_from_order( $order );
233 if ( $intent ) {
234 // If the order has a Payment Intent, then the Intent itself must be captured, not the Charge
235 if ( ! empty( $intent->error ) ) {
236 /* translators: error message */
237 $order->add_order_note( sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $intent->error->message ) );
238 } elseif ( 'requires_capture' === $intent->status ) {
239 $level3_data = $this->get_level3_data_from_order( $order );
240 $result = WC_Stripe_API::request_with_level3_data(
241 [
242 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ),
243 'expand[]' => 'charges.data.balance_transaction',
244 ],
245 'payment_intents/' . $intent->id . '/capture',
246 $level3_data,
247 $order
248 );
249
250 if ( ! empty( $result->error ) ) {
251 /* translators: error message */
252 $order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
253 } else {
254 $is_stripe_captured = true;
255 $result = end( $result->charges->data );
256 }
257 } elseif ( 'succeeded' === $intent->status ) {
258 $is_stripe_captured = true;
259 }
260 } else {
261 // The order doesn't have a Payment Intent, fall back to capturing the Charge directly
262
263 // First retrieve charge to see if it has been captured.
264 $result = WC_Stripe_API::retrieve( 'charges/' . $charge );
265
266 if ( ! empty( $result->error ) ) {
267 /* translators: error message */
268 $order->add_order_note( sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
269 } elseif ( false === $result->captured ) {
270 $level3_data = $this->get_level3_data_from_order( $order );
271 $result = WC_Stripe_API::request_with_level3_data(
272 [
273 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ),
274 'expand[]' => 'balance_transaction',
275 ],
276 'charges/' . $charge . '/capture',
277 $level3_data,
278 $order
279 );
280
281 if ( ! empty( $result->error ) ) {
282 /* translators: error message */
283 $order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
284 } else {
285 $is_stripe_captured = true;
286 }
287 } elseif ( true === $result->captured ) {
288 $is_stripe_captured = true;
289 }
290 }
291
292 if ( $is_stripe_captured ) {
293 /* translators: transaction id */
294 $order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $result->id ) );
295 $order->update_meta_data( '_stripe_charge_captured', 'yes' );
296
297 // Store other data such as fees
298 $order->set_transaction_id( $result->id );
299
300 if ( is_callable( [ $order, 'save' ] ) ) {
301 $order->save();
302 }
303
304 $this->update_fees( $order, $result->balance_transaction->id );
305 }
306
307 // This hook fires when admin manually changes order status to processing or completed.
308 do_action( 'woocommerce_stripe_process_manual_capture', $order, $result );
309 return $result;
310 }
311 }
312 }
313
314 /**
315 * Cancel pre-auth on refund/cancellation.
316 *
317 * @since 3.1.0
318 * @version 4.2.2
319 * @param int $order_id
320 */
321 public function cancel_payment( $order_id ) {
322 $order = wc_get_order( $order_id );
323
324 if ( 'stripe' === $order->get_payment_method() ) {
325 $captured = $order->get_meta( '_stripe_charge_captured', true );
326 if ( 'no' === $captured ) {
327 $this->process_refund( $order_id );
328 }
329
330 // This hook fires when admin manually changes order status to cancel.
331 do_action( 'woocommerce_stripe_process_manual_cancel', $order );
332 }
333 }
334
335 /**
336 * Filter. Adds additional meta data to Tracks events.
337 * Note that this filter is only called if WC_Site_Tracking::is_tracking_enabled.
338 *
339 * @since 4.5.1
340 * @param array Properties to be appended to.
341 * @param string Event name, e.g. orders_edit_status_change.
342 */
343 public function woocommerce_tracks_event_properties( $properties, $prefixed_event_name ) {
344 // Not the desired event? Bail.
345 if ( 'wcadmin_orders_edit_status_change' != $prefixed_event_name ) {
346 return $properties;
347 }
348
349 // Properties not an array? Bail.
350 if ( ! is_array( $properties ) ) {
351 return $properties;
352 }
353
354 // No payment_method in properties? Bail.
355 if ( ! array_key_exists( 'payment_method', $properties ) ) {
356 return $properties;
357 }
358
359 // Not stripe? Bail.
360 if ( 'stripe' != $properties['payment_method'] ) {
361 return $properties;
362 }
363
364 // Due diligence done. Collect the metadata.
365 $is_live = true;
366 $stripe_settings = get_option( 'woocommerce_stripe_settings', [] );
367 if ( array_key_exists( 'testmode', $stripe_settings ) ) {
368 $is_live = 'no' === $stripe_settings['testmode'];
369 }
370
371 $properties['admin_email'] = get_option( 'admin_email' );
372 $properties['is_live'] = $is_live;
373 $properties['woocommerce_gateway_stripe_version'] = WC_STRIPE_VERSION;
374 $properties['woocommerce_default_country'] = get_option( 'woocommerce_default_country' );
375
376 return $properties;
377 }
378}
379
380new WC_Stripe_Order_Handler();