PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 5.4.3
WooCommerce Square v5.4.3
5.4.3 5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.2.0 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.7.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 4.0.0 4.1.0 4.2.0 4.2.1 4.2.2 4.2.3 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.9.0 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9 5.0.0 5.0.1 5.1.0 5.1.1 5.1.2 5.2.0 5.3.0 5.3.1 5.3.2 5.3.3
woocommerce-square / includes / Framework / PaymentGateway / Handlers / Capture.php
woocommerce-square / includes / Framework / PaymentGateway / Handlers Last commit date
Capture.php 11 months ago
Capture.php
453 lines
1 <?php
2 /**
3 * WooCommerce Payment Gateway Framework
4 *
5 * This source file is subject to the GNU General Public License v3.0
6 * that is bundled with this package in the file license.txt.
7 * It is also available through the world-wide-web at this URL:
8 * http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
9 * If you did not receive a copy of the license and are unable to
10 * obtain it through the world-wide-web, please send an email
11 * to license@skyverge.com so we can send you a copy immediately.
12 *
13 * @since 3.0.0
14 * @author WooCommerce / SkyVerge
15 * @copyright Copyright (c) 2013-2019, SkyVerge, Inc.
16 * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
17 *
18 * Modified by WooCommerce on 01 December 2021.
19 */
20
21 namespace WooCommerce\Square\Framework\PaymentGateway\Handlers;
22
23 use WooCommerce\Square\Framework\PaymentGateway\Payment_Gateway;
24 use WooCommerce\Square\Framework\Plugin_Compatibility;
25 use WooCommerce\Square\Framework\Square_Helper;
26 use WooCommerce\Square\Framework\PaymentGateway\Api\Payment_Gateway_API_Response;
27 use WooCommerce\Square\Framework\Compatibility\Order_Compatibility;
28 use WooCommerce\Square\Utilities\Money_Utility;
29
30 defined( 'ABSPATH' ) || exit;
31
32 /**
33 * The transaction capture handler.
34 *
35 * @since 3.0.0
36 */
37 class Capture {
38
39
40 /** @var Payment_Gateway payment gateway instance */
41 private $gateway;
42
43
44 /**
45 * Capture constructor.
46 *
47 * @since 3.0.0
48 *
49 * @param Payment_Gateway $gateway payment gateway instance
50 */
51 public function __construct( Payment_Gateway $gateway ) {
52
53 $this->gateway = $gateway;
54
55 // auto-capture on order status change if enabled
56 if ( $gateway->supports_capture() && $gateway->is_paid_capture_enabled() ) {
57 add_action( 'woocommerce_order_status_changed', array( $this, 'maybe_capture_paid_order' ), 10, 3 );
58 }
59 }
60
61
62 /**
63 * Captures an order on status change to a "paid" status.
64 *
65 * @internal
66 *
67 * @since 3.0.0
68 *
69 * @param int $order_id order ID
70 * @param string $old_status status being changed
71 * @param string $new_status new order status
72 */
73 public function maybe_capture_paid_order( $order_id, $old_status, $new_status ) {
74
75 $paid_statuses = wc_get_is_paid_statuses();
76
77 // bail if changing to a non-paid status or from a paid status
78 if ( ! in_array( $new_status, $paid_statuses, true ) || in_array( $old_status, $paid_statuses, true ) ) {
79 return;
80 }
81
82 $order = wc_get_order( $order_id );
83
84 if ( ! $order ) {
85 return;
86 }
87
88 $payment_method = Order_Compatibility::get_prop( $order, 'payment_method' );
89
90 if ( $payment_method !== $this->get_gateway()->get_id() ) {
91 return;
92 }
93
94 $this->maybe_perform_capture( $order );
95 }
96
97
98 /**
99 * Perform a capture on an order if it can be captured.
100 *
101 * This acts as a wrapper for when the process should just bail without logging any errors or order notes, like when
102 * performing capture via bulk action.
103 *
104 * @since 3.0.0
105 *
106 * @param \WC_Order $order order object
107 * @param float|null $amount amount to capture
108 * @return bool
109 */
110 public function maybe_perform_capture( \WC_Order $order, $amount = null ) {
111
112 // don't log any errors for for orders that can't be captured
113 if ( ! $this->order_can_be_captured( $order ) ) {
114 return false;
115 }
116
117 $result = $this->perform_capture( $order, $amount );
118
119 return ! empty( $result['success'] );
120 }
121
122
123 /**
124 * Performs a credit card capture for an order.
125 *
126 * @since 3.0.0
127 *
128 * @param \WC_Order $order WooCommerce order object
129 * @param float|null $amount amount to capture
130 * @return array {
131 * Capture transaction results
132 *
133 * @type bool $success whether the capture was successful
134 * @type int $code result code
135 * @type string $message result message
136 * }
137 */
138 public function perform_capture( \WC_Order $order, $amount = null ) {
139
140 $order = $this->get_gateway()->get_order_for_capture( $order, $amount );
141
142 try {
143
144 // notify if the gateway doesn't support captures when this is called directly
145 if ( ! $this->get_gateway()->supports_capture() ) {
146
147 $message = "{$this->get_gateway()->get_method_title()} does not support payment captures";
148
149 Plugin_Compatibility::wc_doing_it_wrong( __METHOD__, $message, '3.0.0' );
150
151 throw new \Exception( $message, 500 );
152 }
153
154 // don't try to capture failed/cancelled/fully refunded transactions
155 if ( ! $this->is_order_ready_for_capture( $order ) ) {
156 throw new \Exception( __( 'Order cannot be captured', 'woocommerce-square' ), 400 );
157 }
158
159 // don't re-capture fully captured orders
160 if ( $this->has_order_authorization_expired( $order ) ) {
161 throw new \Exception( __( 'Transaction authorization has expired', 'woocommerce-square' ), 400 );
162 }
163
164 // don't re-capture fully captured orders
165 if ( $this->is_order_fully_captured( $order ) ) {
166 throw new \Exception( __( 'Transaction has already been fully captured', 'woocommerce-square' ), 400 );
167 }
168
169 // generally unavailable
170 if ( ! $this->order_can_be_captured( $order ) ) {
171 throw new \Exception( __( 'Transaction cannot be captured', 'woocommerce-square' ), 400 );
172 }
173
174 // Handle capture for orders where the payment was split between Square Gift and Credit cards.
175 if ( $this->get_gateway()->get_order_meta( $order, 'charge_type' ) === $this->get_gateway()::CHARGE_TYPE_PARTIAL ) {
176 $transaction_id = $this->get_gateway()->get_order_meta( $order, 'trans_id' );
177 $gift_card_transaction_id = $this->get_gateway()->get_order_meta( $order, 'gift_card_trans_id' );
178 $square_order_id = $this->get_gateway()->get_order_meta( $order, 'square_order_id' );
179
180 $response = $this->get_gateway()->get_api()->pay_order(
181 array(
182 $transaction_id,
183 $gift_card_transaction_id,
184 ),
185 $square_order_id
186 );
187 } else {
188 // attempt the capture
189 $response = $this->get_gateway()->get_api()->capture_payment( $order );
190 }
191
192 $gift_card_purchase_type = \WooCommerce\Square\Handlers\Order::get_gift_card_purchase_type( $order );
193
194 if ( 'new' === $gift_card_purchase_type ) {
195 $this->get_gateway()->create_gift_card( $order );
196 } elseif ( 'load' === $gift_card_purchase_type ) {
197 $gan = \WooCommerce\Square\Handlers\Order::get_gift_card_gan( $order );
198 $this->get_gateway()->load_gift_card( $gan, $order );
199 }
200
201 // bail early if the capture wasn't approved
202 if ( $response->has_errors() ) {
203
204 $this->do_capture_failed( $order, $response );
205
206 throw new \Exception( $response->get_status_code() . ' - ' . $response->get_status_message() );
207 }
208
209 $message = sprintf(
210 /* translators: Placeholders: %1$s - payment gateway title (such as Authorize.net, Braintree, etc), %2$s - transaction amount. Definitions: Capture, as in capture funds from a credit card. */
211 esc_html__( '%1$s Capture total of %2$s Approved', 'woocommerce-square' ),
212 $this->get_gateway()->get_method_title(),
213 wc_price( $order->capture->amount, array( 'currency' => Order_Compatibility::get_prop( $order, 'currency', 'view' ) ) )
214 );
215
216 // adds the transaction id (if any) to the order note
217 if ( $response->get_data() instanceof \Square\Models\PayOrderResponse && ! empty( $response->get_transaction_ids() ) ) {
218 /** @var \Square\Models\Order $order_response */
219 $square_order = $response->get_order();
220 $message .= ' ' . $this->get_gateway()->build_split_payment_order_note( $order, $square_order );
221 /* translators: %s list of transaction IDs for split payments. */
222 $message .= ' ' . sprintf( esc_html__( '(Transaction IDs %s)', 'woocommerce-square' ), implode( ', ', $response->get_transaction_ids() ) );
223 } elseif ( $response->get_transaction_id() ) {
224 /* translators: %s transaction ID */
225 $message .= ' ' . sprintf( esc_html__( '(Transaction ID %s)', 'woocommerce-square' ), $response->get_transaction_id() );
226 }
227
228 $order->add_order_note( $message );
229
230 // add the standard capture data to the order
231 $this->do_capture_success( $order, $response );
232
233 // if the original auth amount has been captured, complete payment
234 if ( $this->get_gateway()->get_order_meta( $order, 'capture_total' ) >= $order->get_total() ) {
235
236 // prevent stock from being reduced when payment is completed as this is done when the charge was authorized
237 add_filter( 'woocommerce_payment_complete_reduce_order_stock', '__return_false', 100 );
238
239 // complete the order
240 $order->payment_complete();
241 }
242
243 return array(
244 'success' => true,
245 'code' => 200,
246 'message' => $message,
247 );
248
249 } catch ( \Exception $exception ) {
250
251 // add an order note if this isn't a general error
252 if ( 500 !== $exception->getCode() ) {
253
254 $note_message = sprintf(
255 /* translators: Placeholders: %1$s - payment gateway title (such as Authorize.net, Braintree, etc), %2$s - failure message. Definitions: "capture" as in capturing funds from a credit card. */
256 esc_html__( '%1$s Capture Failed: %2$s', 'woocommerce-square' ),
257 $this->get_gateway()->get_method_title(),
258 $exception->getMessage()
259 );
260
261 $order->add_order_note( $note_message );
262 }
263
264 return array(
265 'success' => false,
266 'code' => $exception->getCode(),
267 'message' => $exception->getMessage(),
268 );
269 }
270 }
271
272
273 /**
274 * Adds the standard capture data to an order.
275 *
276 * @since 3.0.0
277 *
278 * @param \WC_Order $order the order object
279 * @param Payment_Gateway_API_Response $response transaction response
280 */
281 public function do_capture_success( \WC_Order $order, Payment_Gateway_API_Response $response ) {
282
283 $total_captured = (float) $this->get_gateway()->get_order_meta( $order, 'capture_total' ) + (float) $order->capture->amount;
284
285 $this->get_gateway()->update_order_meta( $order, 'capture_total', Square_Helper::number_format( $total_captured ) );
286 $this->get_gateway()->update_order_meta( $order, 'charge_captured', $this->get_gateway()->supports_partial_capture() && $this->get_gateway()->is_partial_capture_enabled() && $total_captured < (float) $this->get_order_capture_maximum( $order ) ? 'partial' : 'yes' );
287
288 // add capture transaction ID
289 if ( $response && $response->get_transaction_id() ) {
290 $this->get_gateway()->update_order_meta( $order, 'capture_trans_id', $response->get_transaction_id() );
291 }
292 }
293
294
295 /**
296 * Lets gateways handle any specific capture failure results for the order.
297 *
298 * @since 3.0.0
299 *
300 * @param \WC_Order $order WooCommerce order object
301 * @param Payment_Gateway_API_Response $response API response object
302 */
303 public function do_capture_failed( \WC_Order $order, Payment_Gateway_API_Response $response ) { }
304
305
306 /** Conditional Methods *******************************************************************************************/
307
308
309 /**
310 * Determines if an order is eligible for capture.
311 *
312 * @since 3.0.0
313 *
314 * @param \WC_Order $order order object
315 * @return bool
316 */
317 public function order_can_be_captured( \WC_Order $order ) {
318
319 // check whether the charge has already been captured by this gateway
320 if ( ! $this->is_order_ready_for_capture( $order ) || $this->is_order_fully_captured( $order ) ) {
321 return false;
322 }
323
324 // if for any reason the authorization can not be captured
325 if ( 'no' === $this->get_gateway()->get_order_meta( $order, 'auth_can_be_captured' ) ) {
326 return false;
327 }
328
329 // authorization hasn't already been captured, but has it expired?
330 return ! $this->has_order_authorization_expired( $order );
331 }
332
333
334 /**
335 * Determines if an order is ready for capture.
336 *
337 * The base implementation of this method checks for a valid order status and that a transaction ID is set.
338 *
339 * @since 3.0.0
340 *
341 * @param \WC_Order $order order object
342 * @return bool
343 */
344 public function is_order_ready_for_capture( \WC_Order $order ) {
345
346 return ! in_array( $order->get_status(), array( 'cancelled', 'refunded', 'failed' ), true ) && $this->get_gateway()->get_order_meta( $order, 'trans_id' );
347 }
348
349
350 /**
351 * Determines if an order has been fully captured
352 *
353 * @since 3.0.0
354 *
355 * @param \WC_Order $order
356 * @return bool
357 */
358 public function is_order_fully_captured( \WC_Order $order ) {
359
360 $captured = 'yes' === $this->get_gateway()->get_order_meta( $order, 'charge_captured' );
361
362 if ( ! $captured && $this->get_gateway()->supports_partial_capture() && $this->get_gateway()->is_partial_capture_enabled() ) {
363 $captured = (float) $this->get_gateway()->get_order_meta( $order, 'capture_total' ) >= (float) $this->get_order_capture_maximum( $order );
364 }
365
366 return $captured;
367 }
368
369
370 /**
371 * Determines if an order's authorization has expired.
372 *
373 * @since 3.0.0
374 *
375 * @param \WC_Order $order
376 * @return bool
377 */
378 public function has_order_authorization_expired( \WC_Order $order ) {
379
380 $transaction_date = $this->get_gateway()->get_order_meta( Order_Compatibility::get_prop( $order, 'id' ), 'trans_date' );
381
382 $transaction_time = strtotime( $transaction_date );
383
384 return $transaction_date && floor( ( time() - $transaction_time ) / 3600 ) > $this->get_gateway()->get_authorization_time_window();
385 }
386
387
388 /**
389 * Determines if an order's authorization has been captured, even partially.
390 *
391 * @since 3.0.0
392 *
393 * @param \WC_Order $order order object
394 * @return bool
395 */
396 public function is_order_captured( \WC_Order $order ) {
397
398 return in_array( $this->get_gateway()->get_order_meta( $order, 'charge_captured' ), array( 'yes', 'partial' ), true );
399 }
400
401
402 /** Getter Methods ************************************************************************************************/
403
404
405 /**
406 * Gets the maximum amount that can be captured from an order.
407 *
408 * Gateways can override this for an value above or below the order total.
409 * For instance, some processors allow capturing an amount a certain
410 * percentage higher than the payment total.
411 *
412 * @since 3.0.0
413 *
414 * @param \WC_Order $order WooCommerce order object
415 * @return float
416 */
417 public function get_order_capture_maximum( \WC_Order $order ) {
418
419 return $this->get_order_authorization_amount( $order );
420 }
421
422
423 /**
424 * Gets the amount originally authorized for an order.
425 *
426 * @since 3.0.0
427 *
428 * @param \WC_Order $order order object
429 * @return float
430 */
431 public function get_order_authorization_amount( \WC_Order $order ) {
432
433 // if a specific auth amount was stored, use it
434 // otherwise, use the order total
435 $amount = $this->get_gateway()->get_order_meta( $order, 'authorization_amount' );
436 $amount = $amount ? $amount : $order->get_total();
437
438 return (float) $amount;
439 }
440
441
442 /**
443 * Gets the payment gateway instance.
444 *
445 * @since 3.0.0
446 *
447 * @return Payment_Gateway
448 */
449 protected function get_gateway() {
450 return $this->gateway;
451 }
452 }
453