599 lines
23 KiB
PHP
599 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* Class WC_REST_Payments_Orders_Controller
|
|
*
|
|
* @package WooCommerce\Payments\Admin
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
use WCPay\Core\Server\Request\Create_Intention;
|
|
use WCPay\Core\Server\Request\Get_Intention;
|
|
use WCPay\Logger;
|
|
use WCPay\Constants\Order_Status;
|
|
use WCPay\Constants\Intent_Status;
|
|
use WCPay\Constants\Payment_Method;
|
|
|
|
/**
|
|
* REST controller for order processing.
|
|
*/
|
|
class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller {
|
|
|
|
/**
|
|
* Endpoint path.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $rest_base = 'payments/orders';
|
|
|
|
/**
|
|
* Instance of WC_Payment_Gateway_WCPay
|
|
*
|
|
* @var WC_Payment_Gateway_WCPay
|
|
*/
|
|
private $gateway;
|
|
|
|
/**
|
|
* WC_Payments_Customer_Service instance for working with customer information
|
|
*
|
|
* @var WC_Payments_Customer_Service
|
|
*/
|
|
private $customer_service;
|
|
|
|
/**
|
|
* WC_Payments_Order_Service instance for updating order statuses.
|
|
*
|
|
* @var WC_Payments_Order_Service
|
|
*/
|
|
private $order_service;
|
|
|
|
/**
|
|
* WC_Payments_REST_Controller constructor.
|
|
*
|
|
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
|
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
|
|
* @param WC_Payments_Customer_Service $customer_service Customer class instance.
|
|
* @param WC_Payments_Order_Service $order_service Order Service class instance.
|
|
*/
|
|
public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) {
|
|
parent::__construct( $api_client );
|
|
$this->gateway = $gateway;
|
|
$this->customer_service = $customer_service;
|
|
$this->order_service = $order_service;
|
|
}
|
|
|
|
/**
|
|
* Configure REST API routes.
|
|
*/
|
|
public function register_routes() {
|
|
register_rest_route(
|
|
$this->namespace,
|
|
$this->rest_base . '/(?P<order_id>\w+)/capture_terminal_payment',
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'capture_terminal_payment' ],
|
|
'permission_callback' => [ $this, 'check_permission' ],
|
|
'args' => [
|
|
'payment_intent_id' => [
|
|
'required' => true,
|
|
],
|
|
],
|
|
]
|
|
);
|
|
register_rest_route(
|
|
$this->namespace,
|
|
$this->rest_base . '/(?P<order_id>\w+)/capture_authorization',
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'capture_authorization' ],
|
|
'permission_callback' => [ $this, 'check_permission' ],
|
|
'args' => [
|
|
'payment_intent_id' => [
|
|
'required' => true,
|
|
],
|
|
],
|
|
]
|
|
);
|
|
register_rest_route(
|
|
$this->namespace,
|
|
$this->rest_base . '/(?P<order_id>\w+)/cancel_authorization',
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'cancel_authorization' ],
|
|
'permission_callback' => [ $this, 'check_permission' ],
|
|
'args' => [
|
|
'payment_intent_id' => [
|
|
'required' => true,
|
|
],
|
|
],
|
|
]
|
|
);
|
|
register_rest_route(
|
|
$this->namespace,
|
|
$this->rest_base . '/(?P<order_id>\w+)/create_terminal_intent',
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'create_terminal_intent' ],
|
|
'permission_callback' => [ $this, 'check_permission' ],
|
|
]
|
|
);
|
|
register_rest_route(
|
|
$this->namespace,
|
|
$this->rest_base . '/(?P<order_id>\d+)/create_customer',
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'create_customer' ],
|
|
'permission_callback' => [ $this, 'check_permission' ],
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Given an intent ID and an order ID, add the intent ID to the order and capture it.
|
|
* Use-cases: Mobile apps using it for `card_present` and `interac_present` payment types.
|
|
*
|
|
* @param WP_REST_Request $request Full data about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function capture_terminal_payment( WP_REST_Request $request ) {
|
|
try {
|
|
$intent_id = $request['payment_intent_id'];
|
|
$order_id = $request['order_id'];
|
|
|
|
// Do not process non-existing orders.
|
|
$order = wc_get_order( $order_id );
|
|
if ( false === $order ) {
|
|
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
|
}
|
|
|
|
// Do not process orders with refund(s).
|
|
if ( 0 < $order->get_total_refunded() ) {
|
|
return new WP_Error(
|
|
'wcpay_refunded_order_uncapturable',
|
|
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
// Do not process already processed orders to prevent double-charging.
|
|
$processed_order_intent_statuses = [
|
|
Intent_Status::SUCCEEDED,
|
|
Intent_Status::CANCELED,
|
|
Intent_Status::PROCESSING,
|
|
];
|
|
$stored_intent_id = $order->get_meta( WC_Payments_Order_Service::INTENT_ID_META_KEY );
|
|
$stored_intent_status = $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY );
|
|
if (
|
|
in_array( $stored_intent_status, $processed_order_intent_statuses, true ) ||
|
|
( $stored_intent_id && $stored_intent_id !== $intent_id )
|
|
) {
|
|
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured for completed or processed orders.', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
|
|
// Do not process intents that can't be captured.
|
|
$request = Get_Intention::create( $intent_id );
|
|
$request->set_hook_args( $order );
|
|
$intent = $request->send();
|
|
|
|
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
|
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
|
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
|
if ( $intent_meta_order_id !== $order->get_id() ) {
|
|
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
|
|
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
if ( ! $intent->is_authorized() ) {
|
|
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
|
|
// Update the order: set the payment method and attach intent attributes.
|
|
$order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
|
|
$order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) );
|
|
$this->order_service->attach_intent_info_to_order( $order, $intent );
|
|
|
|
$this->order_service->update_order_status_from_intent( $order, $intent );
|
|
|
|
// Certain payments (eg. Interac) are captured on the client-side (mobile app).
|
|
// The client may send us the captured intent to link it to its WC order.
|
|
// Doing so via this endpoint is more reliable than depending on the payment_intent.succeeded event.
|
|
$is_intent_captured = Intent_Status::SUCCEEDED === $intent->get_status();
|
|
$result_for_captured_intent = [
|
|
'status' => Intent_Status::SUCCEEDED,
|
|
'id' => $intent->get_id(),
|
|
];
|
|
|
|
$result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false, $intent_metadata );
|
|
|
|
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
|
|
$http_code = $result['http_code'] ?? 502;
|
|
$extra_details = $result['extra_details'] ?? [];
|
|
$error_type = $result['error_code'] ?? null;
|
|
$error_code = 'wcpay_capture_error';
|
|
|
|
$message = sprintf(
|
|
// translators: %s: the error message.
|
|
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
|
|
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
|
);
|
|
|
|
if ( 'amount_too_small' === $error_type && ! empty( $extra_details ) ) {
|
|
// Make it easier to parse the error metadata for the mobile apps.
|
|
$error_code = 'wcpay_capture_error_amount_too_small';
|
|
$message = esc_html( wp_json_encode( $extra_details ) );
|
|
}
|
|
|
|
return new WP_Error(
|
|
$error_code,
|
|
$message,
|
|
[ 'status' => $http_code ]
|
|
);
|
|
}
|
|
// Store receipt generation URL for mobile applications in order meta-data.
|
|
$order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) );
|
|
|
|
// Add payment method for future subscription payments.
|
|
$generated_card = $intent->get_charge()->get_payment_method_details()[ Payment_Method::CARD_PRESENT ]['generated_card'] ?? null;
|
|
// If we don't get a generated card, e.g. because a digital wallet was used, we can still return that the initial payment was successful.
|
|
// The subscription will not be activated and customers will need to provide a new payment method for renewals.
|
|
if ( $generated_card ) {
|
|
$has_subscriptions = function_exists( 'wcs_order_contains_subscription' ) &&
|
|
function_exists( 'wcs_get_subscriptions_for_order' ) &&
|
|
function_exists( 'wcs_is_manual_renewal_required' ) &&
|
|
wcs_order_contains_subscription( $order_id );
|
|
if ( $has_subscriptions ) {
|
|
$token = WC_Payments::get_token_service()->add_payment_method_to_user( $generated_card, $order->get_user() );
|
|
$this->gateway->add_token_to_order( $order, $token );
|
|
foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription ) {
|
|
$subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
|
|
// Where the setting doesn't force manual renewals, we should turn them off, because we have an auto-renewal token now.
|
|
if ( ! wcs_is_manual_renewal_required() ) {
|
|
$subscription->set_requires_manual_renewal( false );
|
|
}
|
|
$subscription->save();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actualize order status.
|
|
$this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] );
|
|
|
|
return rest_ensure_response(
|
|
[
|
|
'status' => $result['status'],
|
|
'id' => $result['id'],
|
|
]
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
Logger::error( 'Failed to capture a terminal payment via REST API: ' . $e );
|
|
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Captures an authorization.
|
|
* Use-cases: Merchants manually capturing a payment when they enable "capture later" option.
|
|
*
|
|
* @param WP_REST_Request $request Full data about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function capture_authorization( WP_REST_Request $request ) {
|
|
try {
|
|
$intent_id = $request['payment_intent_id'];
|
|
$order_id = $request['order_id'];
|
|
|
|
// Do not process non-existing orders.
|
|
$order = wc_get_order( $order_id );
|
|
if ( false === $order ) {
|
|
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
|
}
|
|
|
|
// Do not process orders with refund(s).
|
|
if ( 0 < $order->get_total_refunded() ) {
|
|
return new WP_Error(
|
|
'wcpay_refunded_order_uncapturable',
|
|
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
// Do not process intents that can't be captured.
|
|
$request = Get_Intention::create( $intent_id );
|
|
$request->set_hook_args( $order );
|
|
$intent = $request->send();
|
|
|
|
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
|
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
|
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
|
if ( $intent_meta_order_id !== $order->get_id() ) {
|
|
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
|
|
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
if ( ! $intent->is_authorized() ) {
|
|
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
|
|
$this->add_fraud_outcome_manual_entry( $order, 'approve' );
|
|
|
|
$result = $this->gateway->capture_charge( $order, true, $intent_metadata );
|
|
|
|
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
|
|
$error_code = $result['error_code'] ?? null;
|
|
$extra_details = $result['extra_details'] ?? [];
|
|
return new WP_Error(
|
|
'wcpay_capture_error',
|
|
sprintf(
|
|
// translators: %s: the error message.
|
|
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
|
|
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
|
),
|
|
[
|
|
'status' => $result['http_code'] ?? 502,
|
|
'extra_details' => $extra_details,
|
|
'error_type' => $error_code,
|
|
]
|
|
);
|
|
}
|
|
|
|
$order->save_meta_data();
|
|
|
|
return rest_ensure_response(
|
|
[
|
|
'status' => $result['status'],
|
|
'id' => $result['id'],
|
|
]
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
Logger::error( 'Failed to capture an authorization via REST API: ' . $e );
|
|
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns customer id from order. Create or update customer if needed.
|
|
* Use-cases:
|
|
* - It was used by older versions of our mobile apps to add the customer details to Payment Intents.
|
|
* - It is used by the apps to set customer details on Payment Intents for an order containing subscriptions. Required for capturing renewal payments off session.
|
|
*
|
|
* @param WP_REST_Request $request Full data about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function create_customer( $request ) {
|
|
try {
|
|
$order_id = $request['order_id'];
|
|
|
|
// Do not process non-existing orders.
|
|
$order = wc_get_order( $order_id );
|
|
if ( false === $order || ! ( $order instanceof WC_Order ) ) {
|
|
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
|
}
|
|
|
|
$disallowed_order_statuses = apply_filters(
|
|
'wcpay_create_customer_disallowed_order_statuses',
|
|
[
|
|
Order_Status::COMPLETED,
|
|
Order_Status::CANCELLED,
|
|
Order_Status::REFUNDED,
|
|
Order_Status::FAILED,
|
|
]
|
|
);
|
|
if ( $order->has_status( $disallowed_order_statuses ) ) {
|
|
return new WP_Error( 'wcpay_invalid_order_status', __( 'Invalid order status', 'woocommerce-payments' ), [ 'status' => 400 ] );
|
|
}
|
|
|
|
$order_user = $order->get_user();
|
|
$customer_id = $this->order_service->get_customer_id_for_order( $order );
|
|
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
|
|
$is_guest_customer = false === $order_user;
|
|
|
|
// If the order is created for a registered customer, try extracting it's Stripe customer ID.
|
|
if ( ! $customer_id && ! $is_guest_customer ) {
|
|
$customer_id = $this->customer_service->get_customer_id_by_user_id( $order_user->ID );
|
|
}
|
|
|
|
$order_user = $is_guest_customer ? new WP_User() : $order_user;
|
|
$customer_id = $customer_id
|
|
? $this->customer_service->update_customer_for_user( $customer_id, $order_user, $customer_data )
|
|
: $this->customer_service->create_customer_for_user( $order_user, $customer_data );
|
|
|
|
$this->order_service->set_customer_id_for_order( $order, $customer_id );
|
|
$order->save();
|
|
|
|
return rest_ensure_response(
|
|
[
|
|
'id' => $customer_id,
|
|
]
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
Logger::error( 'Failed to create / update customer from order via REST API: ' . $e );
|
|
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new in-person payment intent for the given order ID without confirming it.
|
|
* Use-cases: Mobile apps using it for `card_present` payment types. (`interac_present` is handled by the apps via Stripe SDK).
|
|
*
|
|
* @param WP_REST_Request $request Full data about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function create_terminal_intent( $request ) {
|
|
// Do not process non-existing orders.
|
|
$order = wc_get_order( $request['order_id'] );
|
|
if ( false === $order ) {
|
|
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
|
}
|
|
try {
|
|
$currency = strtolower( $order->get_currency() );
|
|
$customer_id = $request->get_param( 'customer_id' );
|
|
$metadata = $request->get_param( 'metadata' ) ?? [];
|
|
$metadata['order_number'] = $order->get_order_number();
|
|
|
|
$wcpay_server_request = Create_Intention::create();
|
|
$wcpay_server_request->set_currency_code( $currency );
|
|
$wcpay_server_request->set_amount( WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ) );
|
|
if ( $customer_id ) {
|
|
$wcpay_server_request->set_customer( $customer_id );
|
|
}
|
|
$wcpay_server_request->set_metadata( $metadata );
|
|
$wcpay_server_request->set_payment_method_types( $this->get_terminal_intent_payment_method( $request ) );
|
|
$wcpay_server_request->set_capture_method( 'manual' === $this->get_terminal_intent_capture_method( $request ) );
|
|
$wcpay_server_request->set_hook_args( $order );
|
|
$intent = $wcpay_server_request->send();
|
|
|
|
return rest_ensure_response(
|
|
[
|
|
'id' => ! empty( $intent ) ? $intent->get_id() : null,
|
|
]
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
Logger::error( 'Failed to create an intention via REST API: ' . $e );
|
|
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return terminal intent payment method array based on payment methods request.
|
|
*
|
|
* @param WP_REST_Request $request Request object.
|
|
* @param array $default_value - default value.
|
|
*
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ): array {
|
|
$payment_methods = $request->get_param( 'payment_methods' );
|
|
if ( null === $payment_methods ) {
|
|
return $default_value;
|
|
}
|
|
|
|
if ( ! is_array( $payment_methods ) ) {
|
|
throw new \Exception( 'Invalid param \'payment_methods\'!' );
|
|
}
|
|
|
|
foreach ( $payment_methods as $value ) {
|
|
if ( ! in_array( $value, Payment_Method::IPP_ALLOWED_PAYMENT_METHODS, true ) ) {
|
|
throw new \Exception( 'One or more payment methods are not supported!' );
|
|
}
|
|
}
|
|
|
|
return $payment_methods;
|
|
}
|
|
|
|
/**
|
|
* Return terminal intent capture method based on capture method request.
|
|
*
|
|
* @param WP_REST_Request $request Request object.
|
|
* @param string $default_value default value.
|
|
*
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ): string {
|
|
$capture_method = $request->get_param( 'capture_method' );
|
|
if ( null === $capture_method ) {
|
|
return $default_value;
|
|
}
|
|
|
|
if ( ! in_array( $capture_method, [ 'manual', 'automatic' ], true ) ) {
|
|
throw new \Exception( 'Invalid param \'capture_method\'!' );
|
|
}
|
|
|
|
return $capture_method;
|
|
}
|
|
|
|
/**
|
|
* Cancels an authorization.
|
|
* Use-cases: Merchants manually canceling when blocking an on hold review by Fraud & Risk tools.
|
|
*
|
|
* @param WP_REST_Request $request Full data about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function cancel_authorization( WP_REST_Request $request ) {
|
|
try {
|
|
$intent_id = $request['payment_intent_id'];
|
|
$order_id = $request['order_id'];
|
|
|
|
// Do not process non-existing orders.
|
|
$order = wc_get_order( $order_id );
|
|
if ( false === $order ) {
|
|
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
|
}
|
|
|
|
// Do not process orders with refund(s).
|
|
if ( 0 < $order->get_total_refunded() ) {
|
|
return new WP_Error(
|
|
'wcpay_refunded_order_uncapturable',
|
|
__( 'Payment cannot be canceled for partially or fully refunded orders.', 'woocommerce-payments' ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
// Do not process intents that can't be canceled.
|
|
$request = Get_Intention::create( $intent_id );
|
|
$request->set_hook_args( $order );
|
|
$intent = $request->send();
|
|
|
|
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
|
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
|
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
|
if ( $intent_meta_order_id !== $order->get_id() ) {
|
|
Logger::error( 'Payment cancellation rejected due to failed validation: order id on intent is incorrect or missing.' );
|
|
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
if ( ! in_array( $intent->get_status(), [ Intent_Status::REQUIRES_CAPTURE ], true ) ) {
|
|
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
|
}
|
|
|
|
$this->add_fraud_outcome_manual_entry( $order, 'block' );
|
|
|
|
$result = $this->gateway->cancel_authorization( $order );
|
|
|
|
if ( Intent_Status::CANCELED !== $result['status'] ) {
|
|
return new WP_Error(
|
|
'wcpay_cancel_error',
|
|
sprintf(
|
|
// translators: %s: the error message.
|
|
__( 'Payment cancel failed to complete with the following message: %s', 'woocommerce-payments' ),
|
|
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
|
),
|
|
[ 'status' => $result['http_code'] ?? 502 ]
|
|
);
|
|
}
|
|
|
|
$order->save_meta_data();
|
|
|
|
return rest_ensure_response(
|
|
[
|
|
'status' => $result['status'],
|
|
'id' => $result['id'],
|
|
]
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
Logger::error( 'Failed to cancel an authorization via REST API: ' . $e );
|
|
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the fraud_outcome_manual_entry meta to the order.
|
|
*
|
|
* @param \WC_Order $order Order object.
|
|
* @param string $action User action.
|
|
*/
|
|
private function add_fraud_outcome_manual_entry( $order, $action ) {
|
|
$current_user = wp_get_current_user();
|
|
$order->add_meta_data(
|
|
'_wcpay_fraud_outcome_manual_entry',
|
|
[
|
|
'type' => 'fraud_outcome_manual_' . $action,
|
|
'user' => [
|
|
'id' => $current_user->ID,
|
|
'username' => $current_user->user_login,
|
|
],
|
|
'action' => 'block' === $action ? 'blocked' : 'approved',
|
|
'datetime' => time(),
|
|
]
|
|
);
|
|
}
|
|
}
|