<?php
/**
 * Swedbank Pay API class file.
 *
 * @package SwedbankPay\Checkout\WooCommerce
 */

namespace SwedbankPay\Checkout\WooCommerce;

defined( 'ABSPATH' ) || exit;

use WP_Error;
use WC_Log_Levels;
use WC_Order;
use WC_Payment_Gateway;
use Swedbank_Pay_Payment_Gateway_Checkout;
use Krokedil\Swedbank\Pay\Helpers\Order;
use Krokedil\Swedbank\Pay\Helpers\Cart;
use Krokedil\Swedbank\Pay\Utility\LogUtility;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Client\Exception as ClientException;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Data\ResponseInterface as ResponseServiceInterface;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\TransactionCapture;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\TransactionCancel;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\TransactionReversal;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\GetPaymentorder;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Resource\Response\PaymentorderResponse;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\Transaction\Resource\Request\TransactionObject;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\Purchase;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\Resource\PaymentorderObject;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Client\Client;
use KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Request\Verify;

/**
 * Swedbank_Pay_Api Class.
 *
 * @SuppressWarnings(PHPMD.CamelCaseClassName)
 * @SuppressWarnings(PHPMD.CamelCaseMethodName)
 * @SuppressWarnings(PHPMD.CamelCaseParameterName)
 * @SuppressWarnings(PHPMD.CamelCasePropertyName)
 * @SuppressWarnings(PHPMD.CamelCaseVariableName)
 * @SuppressWarnings(PHPMD.MissingImport)
 * @SuppressWarnings(PHPMD.StaticAccess)
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 * @SuppressWarnings(PHPMD.TooManyFields)
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class Swedbank_Pay_Api {
	public const MODE_TEST = 'test';
	public const MODE_LIVE = 'live';

	public const INTENT_AUTOCAPTURE   = 'AutoCapture';
	public const INTENT_AUTHORIZATION = 'Authorization';
	public const INTENT_SALE          = 'Sale';

	public const OPERATION_PURCHASE = 'Purchase';

	public const TYPE_VERIFICATION  = 'Verification';
	public const TYPE_AUTHORIZATION = 'Authorization';
	public const TYPE_CAPTURE       = 'Capture';
	public const TYPE_SALE          = 'Sale';
	public const TYPE_CANCELLATION  = 'Cancellation';
	public const TYPE_REVERSAL      = 'Reversal';

	/**
	 * The access token for authenticating with the Swedbank Pay API.
	 *
	 * @var string
	 */
	private $access_token;

	/**
	 * The payee ID for the Swedbank Pay API.
	 *
	 * @var string
	 */
	private $payee_id;

	/**
	 * The mode for the Swedbank Pay API (test or live).
	 *
	 * @var string
	 */
	private $mode;

	/**
	 * An array to store payment orders, indexed by order ID or other identifiers.
	 *
	 * @var array
	 */
	private $payment_orders = array();

	/**
	 * The payment gateway instance, which can be either Swedbank_Pay_Payment_Gateway_Checkout or WC_Payment_Gateway.
	 *
	 * @var Swedbank_Pay_Payment_Gateway_Checkout|WC_Payment_Gateway
	 */
	private $gateway;

	/**
	 * Constructor for the Swedbank_Pay_Api class.
	 *
	 * @param Swedbank_Pay_Payment_Gateway_Checkout|WC_Payment_Gateway $gateway The payment gateway instance to use for API interactions. This can be an instance of Swedbank_Pay_Payment_Gateway_Checkout or WC_Payment_Gateway, depending on the context in which the API class is being used.
	 */
	public function __construct( $gateway ) {
		$this->gateway = $gateway;
	}

	/**
	 * Prepare item for API.
	 *
	 * Sanitize and apply necessary limitations for compatibility with the API.
	 *
	 * @param array $items The Swedbank Pay order items.
	 * @return array The prepared items.
	 */
	public static function prepare_for_api( $items ) {
		if ( isset( $item[ Swedbank_Pay_Order_Item::FIELD_DESCRIPTION ] ) ) {
			// limited to 40 characters in the API.
			$item[ Swedbank_Pay_Order_Item::FIELD_DESCRIPTION ] = mb_substr( trim( $items[ Swedbank_Pay_Order_Item::FIELD_DESCRIPTION ] ), 0, 40 );
		}

		return $items;
	}

	/**
	 * Set the access token for the API client.
	 *
	 * @param string $access_token The access token to set.
	 * @return self Returns the instance of the class for method chaining.
	 */
	public function set_access_token( $access_token ) {
		$this->access_token = $access_token;

		return $this;
	}

	/**
	 * Get the access token for the API client.
	 *
	 * @return string The access token.
	 */
	public function get_access_token() {
		return $this->access_token;
	}

	/**
	 * Set the payee ID for the API client.
	 *
	 * @param string $payee_id The payee ID to set.
	 * @return self Returns the instance of the class for method chaining.
	 */
	public function set_payee_id( $payee_id ) {
		$this->payee_id = $payee_id;

		return $this;
	}

	/**
	 * Set the mode for the API client.
	 *
	 * @param string $mode The mode to set (test or live).
	 * @return self Returns the instance of the class for method chaining.
	 */
	public function set_mode( $mode ) {
		$this->mode = $mode;

		return $this;
	}

	/**
	 * Extracts host URLs from an array of URLs.
	 *
	 * This method filters out invalid URLs and returns a unique list of host URLs.
	 *
	 * @param array $urls An array of URLs to extract host URLs from.
	 * @return array An array of unique host URLs.
	 */
	public static function get_host_urls( $urls ) {
		$result = array();
		foreach ( $urls as $url ) {
			if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
				$parsed   = wp_parse_url( $url );
				$result[] = sprintf( '%s://%s', $parsed['scheme'], $parsed['host'] );
			}
		}

		return array_values( array_unique( $result ) );
	}

	/**
	 * Get the Swedbank Pay client.
	 *
	 * This method creates a new Client instance, sets the access token, payee ID, mode (test or production),
	 * and user agent. It also applies a filter to allow modification of the client.
	 *
	 * @hook swedbank_pay_client
	 * @return Client
	 */
	public static function get_client() {
		$client = new Client();

		$user_agent = "{$client->getUserAgent()} swedbank-pay-payment-menu/" . SWEDBANK_PAY_VERSION;
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$user_agent .= ' ' . sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
		}

		$settings = get_option( 'woocommerce_payex_checkout_settings' );
		$client->setAccessToken( $settings['access_token'] ?? '' )
				->setPayeeId( $settings['payee_id'] ?? '' )
				->setMode( wc_string_to_bool( $settings['testmode'] ?? 'no' ) ? Client::MODE_TEST : Client::MODE_PRODUCTION )
				->setUserAgent( $user_agent )
				->setApiVersion( '3.1' )
				->setHeaders();

		$base_url = $client->getBaseUrl();
		// Replace payex.com with swedbankpay.com. Can be disabled using the filter `swedbank_pay_replace_base_url` and returning false instead of true.
		if ( apply_filters( 'swedbank_pay_replace_base_url', true ) && strpos( $base_url, 'payex.com' ) !== false ) {
			$base_url = str_replace( 'payex.com', 'swedbankpay.com', $base_url );
			$client->setBaseUrl( $base_url );
		}

		return apply_filters( 'swedbank_pay_client', $client );
	}

	/**
	 * Create a Client for payment.
	 *
	 * @param WC_Order    $order WC Order.
	 * @param string|null $instrument The instrument to use for the payment, e.g. 'CreditCard'. This is optional and may not be needed for all flows or gateways.
	 *
	 * @return WP_Error|ResponseServiceInterface
	 */
	public function initiate_purchase( WC_Order $order, $instrument = null ) {
		$helper = new Order( $order );

		$payment_order = $helper->get_payment_order();

		// Set the instrument if provided.
		if ( ! empty( $instrument ) ) {
			$payment_order->setInstrument( $instrument );
		}

		$payment_order_object = new PaymentorderObject();
		$payment_order_object->setPaymentorder( $payment_order );

		$purchase_request = new Purchase( $payment_order_object );
		$purchase_request->setClient( self::get_client() );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service
			 */
			$response_service = $purchase_request->send();
			LogUtility::log_request( '[CHECKOUT]: Initiate purchase', $purchase_request, WC_Log_Levels::DEBUG );

			return $response_service;
		} catch ( ClientException $e ) {

			LogUtility::log_request(
				'[CHECKOUT]: Initiate purchase',
				$purchase_request,
				WC_Log_Levels::ERROR,
				array(
					'error' => sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() ),
				)
			);

			return Swedbank_Pay()->system_report()->request(
				new WP_Error(
					400,
					$this->format_error_message( $purchase_request->getClient()->getResponseBody(), $e->getMessage() )
				)
			);
		}
	}

	/**
	 * Create a Client for payment.
	 *
	 * @param string|null $instrument The instrument to use for the payment, e.g. 'CreditCard'. This is optional and may not be needed for all flows or gateways.
	 *
	 * @return WP_Error|ResponseServiceInterface
	 */
	public function initiate_embedded_purchase( $instrument = null ) {
		$helper = new Cart();

		// Required for zero amount orders. Only checks for subscriptions.
		$is_verify = Swedbank_Pay_Subscription::cart_has_zero_order();

		$payment_order = $helper->get_payment_order( $is_verify );

		// Set the instrument if provided.
		if ( ! empty( $instrument ) ) {
			$payment_order->setInstrument( $instrument );
		}

		$payment_order_object = new PaymentorderObject();
		$payment_order_object->setPaymentorder( $payment_order );

		if ( $is_verify ) {
			$purchase_request = new Verify( $payment_order_object );
			$purchase_request->setExpands( array( 'paid' ) );
		} else {
			$purchase_request = new Purchase( $payment_order_object );
		}

		$purchase_request->setClient( self::get_client() );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service
			 */
			$response_service = $purchase_request->send();
			LogUtility::log_request( '[CHECKOUT]: Initiate embedded purchase', $purchase_request );

			return $response_service;
		} catch ( ClientException $e ) {

			LogUtility::log_request(
				'[CHECKOUT]: Initiate embedded purchase',
				$purchase_request,
				WC_Log_Levels::ERROR,
				array(
					'error' => sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() ),
				)
			);

			return Swedbank_Pay()->system_report()->request(
				new WP_Error(
					400,
					$this->format_error_message( $purchase_request->getClient()->getResponseBody(), $e->getMessage() )
				)
			);
		}
	}

	/**
	 * Get a embedded payment.
	 *
	 * @return WP_Error|array
	 */
	public function get_embedded_purchase() {
		$payment_order_id  = explode( '/', WC()->session->get( 'swedbank_pay_paymentorder_id' ) );
		$payment_order_id  = array_pop( $payment_order_id );
		$context           = array(
			'payment_order_id' => $payment_order_id,
		);
		LogUtility::$title = "[CHECKOUT]: Get embedded checkout for payment order ID #{$context['payment_order_id']}";

		$view_session_url = WC()->session->get( 'swedbank_pay_view_session_url' );
		$result           = $this->request( 'GET', $view_session_url );
		if ( is_wp_error( Swedbank_Pay()->system_report()->request( $result ) ) ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $result->get_error_message() );
			Swedbank_Pay()->logger()->error(
				"[CHECKOUT]: Get embedded checkout for payment order ID #{$context['payment_order_id']}",
				$context
			);

			return $result;
		}

		return $result;
	}

	/**
	 * Update a embedded payment.
	 *
	 * @param string|null $instrument The instrument to use for the payment, e.g. 'CreditCard'. This is optional and may not be needed for all flows or gateways.
	 * @param WC_Order|null $order The order object to get the order number from if it exists.
	 *
	 * @return WP_Error|ResponseServiceInterface
	 */
	public function update_embedded_purchase( $instrument = null, $order = null ) {
		$update_payment_url = WC()->session->get( 'swedbank_pay_update_order_url' );
		$helper             = new Cart();

		$payment_order = $helper->get_update_payment_order( $order );

		// Set the instrument if provided.
		if ( ! empty( $instrument ) ) {
			$payment_order->setInstrument( $instrument );
		}

		$payment_order_object = new PaymentorderObject();
		$payment_order_object->setPaymentorder( $payment_order );

		$payment_order_id = explode( '/', WC()->session->get( 'swedbank_pay_paymentorder_id' ) );
		$payment_order_id = array_pop( $payment_order_id );

		$context = array(
			'payment_order_id' => $payment_order_id,
		);

		LogUtility::$title = "[CHECKOUT]: Update embedded checkout for payment order ID #{$context['payment_order_id']}";
		$result            = $this->request( 'PATCH', $update_payment_url, $payment_order_object );
		if ( is_wp_error( Swedbank_Pay()->system_report()->request( $result ) ) ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $result->get_error_message() );
			Swedbank_Pay()->logger()->error(
				"[CHECKOUT]: Update embedded purchase for payment order ID #{$context['payment_order_id']}",
				$context
			);

			return $result;
		}

		return $result;
	}

	/**
	 * Do API Request.
	 *
	 * @param string              $method HTTP method (GET, POST, etc.).
	 * @param string              $url The API endpoint URL to send the request to. This can be a full URL or just the endpoint path.
	 * @param array|string|object $params The parameters to include in the API request. This can be an associative array, a JSON string, or an object that can be converted to an array. The method will handle the conversion and sanitization of the parameters as needed.
	 *
	 * @return array|\WP_Error
	 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
	 */
	public function request( $method, $url, $params = array() ) {
		// Get rid of full url. There's should be an endpoint only.
		if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
			$parsed = wp_parse_url( $url );
			$url    = $parsed['path'];
			if ( ! empty( $parsed['query'] ) ) {
				$url .= '?' . $parsed['query'];
			}
		}

		if ( empty( $url ) ) {
			return new WP_Error( 'validation', 'Invalid url' );
		}

		// Process params.
		array_walk_recursive(
			$params,
			function ( &$input ) {
				if ( is_object( $input ) && method_exists( $input, 'toArray' ) ) {
					$input = $input->toArray();
				}
			}
		);

		$context = array(
			'method'           => $method,
			'url'              => $url,
			'params'           => wp_json_encode( $params, JSON_PRETTY_PRINT ),
			'action'           => $action ?? 'api_request',
			'payment_order_id' => isset( WC()->session ) ? WC()->session->get( 'swedbank_pay_paymentorder_id' ) : null,
		);

		$start = microtime( true );

		try {
			$client        = self::get_client()->request( $method, $url, $params );
			$response_body = $client->getResponseBody();
			$result        = json_decode( $response_body, true );
			$time          = microtime( true ) - $start;

			$context['time'] = $time;
			LogUtility::log_request( '', $client, WC_Log_Levels::DEBUG, $context );

			return $result;
		} catch ( \KrokedilSwedbankPayDeps\SwedbankPay\Api\Client\Exception $exception ) {
			$http_code = (int) self::get_client()->getResponseCode();
			$time      = microtime( true ) - $start;

			$context['error'] = sprintf(
				'[%.4F] Client Exception. Check debug info: %s',
				$time,
				self::get_client()->getDebugInfo()
			);
			$client           = empty( $client ) ? self::get_client() : $client;
			LogUtility::log_request( '', $client, WC_Log_Levels::ERROR, $context );

			// https://tools.ietf.org/html/rfc7807 .
			$response_body = self::get_client()->getResponseBody() ?? '{}';
			$data          = json_decode( $response_body, true );
			if ( json_last_error() === JSON_ERROR_NONE &&
				isset( $data['title'] ) &&
				isset( $data['detail'] )
			) {
				// Format error message.
				$message = sprintf( '%s. %s', $data['title'], $data['detail'] );

				// Get details.
				if ( isset( $data['problems'] ) ) {
					$detailed = '';
					$problems = $data['problems'];
					foreach ( $problems as $problem ) {
						$detailed .= sprintf( '%s: %s', $problem['name'], $problem['description'] ) . "\r\n";
					}

					if ( ! empty( $detailed ) ) {
						$message .= "\r\n" . $detailed;
					}
				}

				return new WP_Error( $http_code, $message );
			}

			return new WP_Error( 'api_generic', 'API Exception. Please check logs' );
		}
	}

	/**
	 * Check if payment is captured.
	 *
	 * @param string $payment_id_url The URL of the payment order to check, which should include the payment order ID.
	 *
	 * @return bool
	 */
	public function is_captured( $payment_id_url ) {
		$payment_order_id  = explode( '/', $payment_id_url );
		$payment_order_id  = array_pop( $payment_order_id );
		LogUtility::$title = "[ORDER MANAGEMENT]: Check if payment is captured for payment ID #{$payment_order_id}";

		// Fetch transactions list.
		$result            = $this->request( 'GET', "{$payment_id_url}/financialtransactions" );
		$transactions_list = $result['financialTransactions']['financialTransactionsList'];

		// Check if have captured transactions.
		foreach ( $transactions_list as $transaction ) {
			// TYPE_SALE is a so-called "1-phase" transaction, which means that the amount is captured immediately after the authorization.
			// Attempting to still capture these will result in an error from the API.
			if ( in_array( $transaction['type'], array( self::TYPE_CAPTURE, self::TYPE_SALE ) ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
				// @todo Calculate captured amount
				return true;
			}
		}

		return false;
	}

	/**
	 * Finalize Payment Order.
	 *
	 * @param WC_Order    $order WC order.
	 * @param string|null $transaction_number Swedbank transaction ID.
	 *
	 * @return true|WP_Error
	 */
	public function finalize_payment( $order, $transaction_number = null ) {
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );

		$context = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $payment_order_id,
		);

		Swedbank_Pay()->logger()->info( "[FINALIZE]: Finalize payment for order #{$order->get_order_number()}. Payment ID: {$payment_order_id}. Transaction number: {$transaction_number}", $context );
		if ( empty( $payment_order_id ) ) {
			Swedbank_Pay()->logger()->error( "[FINALIZE]: Payment Order ID is missing for order #{$order->get_order_number()}.", $context );
			return new WP_Error(
				'error',
				sprintf(
					'Payment Order ID is missing. Order ID: %s',
					$order->get_id()
				)
			);
		}

		LogUtility::$title = "[FINALIZE]: Retrieve paid transaction for order #{$order->get_order_number()}";
		$data              = $this->request( 'GET', "{$payment_order_id}/paid" );
		if ( is_wp_error( $data ) ) {
			$context['error'] = $data->get_error_message();
			Swedbank_Pay()->logger()->error( "[FINALIZE]: Failed to retrieve paid transaction for order #{$order->get_order_number()}.", $context );

			return $data;
		}

		if ( ! $transaction_number ) {
			$transaction_number            = $data['paid']['number'] ?? '';
			$context['transaction_number'] = $transaction_number;
		}

		LogUtility::$title = "[FINALIZE]: Retrieve transactions list for order #{$order->get_order_number()}";
		$result            = $this->request( 'GET', "{$payment_order_id}/financialtransactions" );
		$transactions_list = $result['financialTransactions']['financialTransactionsList'] ?? array();
		// @todo Sort by "created" field using array_multisort
		foreach ( $transactions_list as $transaction ) {
			if ( $transaction_number === $transaction['number'] ) {
				Swedbank_Pay()->logger()->debug( "[FINALIZE]: Handle transaction #{$transaction_number} for order #{$order->get_order_number()}.", $context );
				return $this->process_transaction( $order, $transaction );
			}
		}

		// Some Authorize, Sale transaction are not in the list
		// Financial transaction list is empty, initiate workaround / failback.
		if ( 0 === count( $transactions_list ) ) {
			Swedbank_Pay()->logger()->debug( "[FINALIZE]: Transaction list is empty for order #{$order->get_order_number()}. Run workaround/failback for transaction #{$transaction_number}", $context );

			$transaction = array(
				'id'             => "{$payment_order_id}/financialtransactions/" . uniqid( 'fake' ),
				'created'        => date( 'Y-m-d H:i:s' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
				'updated'        => date( 'Y-m-d H:i:s' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
				'type'           => $data['paid']['transactionType'] ?? '',
				'number'         => $transaction_number,
				'amount'         => $data['paid']['amount'] ?? 0,
				'vatAmount'      => 0,
				'description'    => $data['paid']['id'],
				'payeeReference' => $data['paid']['payeeReference'] ?? '',
			);

			return $this->process_transaction( $order, $transaction );
		}

		return new WP_Error(
			'not_found',
			sprintf(
				'Transaction #%s not found in the transactions list.',
				$transaction_number
			)
		);
	}

	/**
	 * Process Transaction.
	 *
	 * @param WC_Order $order The WooCommerce order object to process the transaction for.
	 * @param array    $transaction An associative array containing transaction details, such as 'number', 'type', 'amount', etc.
	 *
	 * @return true|WP_Error
	 */
	public function process_transaction( WC_Order $order, array $transaction ) {
		$transaction_id = $transaction['number'];
		$context        = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'transaction_id'   => $transaction_id,
			'transaction_type' => $transaction['type'],
		);

		// Reload order meta to ensure we have the latest changes and avoid conflicts from parallel scripts.
		$order->read_meta_data();

		// Don't update order status if transaction ID was applied before.
		$transactions = $order->get_meta( '_swedbank_pay_transactions' );
		$transactions = empty( $transactions ) ? array() : (array) $transactions;

		// For free trial subscriptions orders, the list of transactions will always be empty. To allow still processing the transaction, we need to allow an empty list to continue.
		if ( ! empty( $transactions ) && in_array( $transaction_id, $transactions, true ) ) {
			Swedbank_Pay()->logger()->debug(
				"[PROCESS TRANSACTION]: Skip transaction processing #{$transaction_id}. Order number: {$order->get_order_number()}",
				$context
			);

			return true;
		}

		Swedbank_Pay()->logger()->debug(
			sprintf( '[PROCESS TRANSACTION]: Process transaction for order #%s: %s', $order->get_order_number(), wp_json_encode( $transaction ) ),
			$context
		);

		// Fetch payment info.
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );
		if ( empty( $payment_order_id ) ) {
			Swedbank_Pay()->logger()->error( "[PROCESS TRANSACTION]: Payment Order ID is missing for order #{$order->get_order_number()}. Cannot retrieve payment order details.", $context );
			return new \WP_Error( 'missing_payment_id', 'Payment order ID is unknown.' );
		}

		LogUtility::$title = "[PROCESS TRANSACTION]: Retrieve payment order for order #{$order->get_order_number()}";
		$payment_order     = $this->get_payment_order( $payment_order_id );
		if ( is_wp_error( $payment_order ) ) {
			$context['error'] = $payment_order->get_error_message();
			Swedbank_Pay()->logger()->error( "[PROCESS TRANSACTION]: Failed to retrieve payment order for order #{$order->get_order_number()}.", $context );
			return $payment_order;
		}

		// Apply action.
		switch ( $transaction['type'] ) {
			case self::TYPE_VERIFICATION: // This is always the case for free trial subscription orders.
			case self::TYPE_AUTHORIZATION:
			case self::TYPE_SALE:
				if ( empty( $order->get_date_paid() ) ) {
					// List of different messages based on the transaction type.
					$messages = array(
						// translators: 1: transaction ID.
						self::TYPE_SALE          => __( 'Payment has been completed. Transaction: %s', 'swedbank-pay-payment-menu' ),
						// translators: 1: transaction ID.
						self::TYPE_AUTHORIZATION => __( 'Payment has been authorized. Transaction: %s', 'swedbank-pay-payment-menu' ),
						// translators: 1: transaction ID.
						self::TYPE_VERIFICATION  => __( 'Payment has been verified. Transaction: %s', 'swedbank-pay-payment-menu' ),
					);
					$message  = sprintf( $messages[ $transaction['type'] ], $transaction_id );
					$order->payment_complete( $transaction_id );
					$order->add_order_note( $message );
				}
				break;
			case self::TYPE_CAPTURE:
				$remaining_capture = $payment_order->getRemainingCaptureAmount();
				$is_full_capture   = null === $remaining_capture;

				if ( $is_full_capture ) {
					Swedbank_Pay()->logger()->debug(
						// translators: 1: payment order ID, 2: transaction ID, 3: transaction amount, 4: order amount.
						sprintf(
							'[PROCESS TRANSACTION]: Warning: Payment Order ID: %s. Transaction %s. Transaction amount: %s. Order amount: %s. Field remainingCaptureAmount is missing. Full action?',
							$payment_order_id,
							$transaction_id,
							$transaction['amount'] / 100,
							$order->get_total()
						),
						$context
					);
				}

				$captured_amount = wc_price( $transaction['amount'] / 100, array( 'currency' => $order->get_currency() ) );

				// Add the order note for the capture based on if it was full or partial.
				if ( $is_full_capture ) {
					$message = sprintf(
						// translators: 1: transaction ID, 2: transaction amount.
						__( 'Payment has been fully captured. Transaction: %1$s. Amount: %2$s', 'swedbank-pay-payment-menu' ),
						$transaction_id,
						$captured_amount
					);
					$order->add_order_note( $message );
				} else {
					$price   = wc_price( $remaining_capture / 100, array( 'currency' => $order->get_currency() ) );
					$message = sprintf(
						// translators: 1: transaction ID, 2: transaction amount, 3: remaining amount.
						__( 'Payment has been partially captured: Transaction: %1$s. Amount: %2$s. Remaining amount: %3$s', 'swedbank-pay-payment-menu' ),
						$transaction_id,
						$captured_amount,
						$price
					);
					$order->add_order_note( $message );
				}
				break;
			case self::TYPE_CANCELLATION:
				$this->update_order_status(
					$order,
					'cancelled',
					$transaction_id,
					// translators: 1: transaction ID.
					sprintf( __( 'Payment has been cancelled. Transaction: %s', 'swedbank-pay-payment-menu' ), $transaction_id )
				);

				break;
			case self::TYPE_REVERSAL:
				$remaining_reversal = $payment_order->getRemainingReversalAmount();
				$is_full_refund     = null === $remaining_reversal;

				if ( $is_full_refund ) {
					Swedbank_Pay()->logger()->debug(
						sprintf(
							'[PROCESS TRANSACTION]: Warning: Payment Order ID: %s. Transaction %s. Transaction amount: %s. Order amount: %s. Field remainingReversalAmount is missing. Full action?', //phpcs:ignore
							$payment_order_id,
							$transaction_id,
							$transaction['amount'] / 100,
							$order->get_total()
						),
						$context
					);
				}

				// Update order status.
				if ( $is_full_refund ) {
					remove_action(
						'woocommerce_order_status_changed',
						__CLASS__ . '::order_status_changed_transaction',
						0
					);

					// Prevent refund creation.
					remove_action(
						'woocommerce_order_status_refunded',
						'wc_order_fully_refunded'
					);

					$message = sprintf(
						'Payment has been refunded. Transaction: %s. Amount: %s',
						$transaction_id,
						$transaction['amount'] / 100
					);

					$order->update_status(
						'refunded',
						$message
					);
				} else {
					$remaining_amount = $remaining_reversal / 100;

					$message = sprintf(
						'Payment has been partially refunded: Transaction: %s. Amount: %s. Remaining amount: %s', //phpcs:ignore
						$transaction_id,
						$transaction['amount'] / 100,
						$remaining_amount
					);

					$order->add_order_note(
						$message
					);
				}

				// @todo Create Credit Memo.
				// @todo Prent duplicated Credit Memo creation (by backend, by admin, by transaction callback).
				break;
			default:
				Swedbank_Pay()->logger()->debug( sprintf( '[PROCESS TRANSACTION]: Unknown transaction type %s for order #%s', $transaction['type'], $order->get_order_number() ), $context );
				return new WP_Error( sprintf( 'Error: Unknown type %s', $transaction['type'] ) );
		}

		// Save transaction ID.
		$transactions[] = $transaction_id;
		$order->update_meta_data( '_swedbank_pay_transactions', $transactions );
		$order->save();

		Swedbank_Pay()->logger()->debug( "[PROCESS TRANSACTION]: Transaction #{$transaction['number']} for order #{$order->get_order_number()} has been processed.", $context );

		return true;
	}

	/**
	 * Update Order Status.
	 *
	 * @param WC_Order    $order The order to update.
	 * @param string      $status The new status to set for the order. This should be a valid WooCommerce order status slug, e.g. 'pending', 'processing', 'completed', etc.
	 * @param string|null $transaction_id The transaction ID associated with the status update, if applicable. This is optional and can be null if not needed.
	 * @param string|null $message An optional message to add as an order note when updating the status. This can provide additional context for the status change.
	 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
	 * @SuppressWarnings(PHPMD.ElseExpression)
	 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
	 */
	public function update_order_status( WC_Order $order, $status, $transaction_id = null, $message = null ) {
		remove_action(
			'woocommerce_order_status_changed',
			Swedbank_Pay_Admin::class . '::order_status_changed_transaction',
			0
		);

		$order_id = $order->get_id();

		$context = array(
			'action'           => 'update_order_status',
			'order_id'         => $order_id,
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $order->get_meta( '_payex_paymentorder_id' ),
			'transaction_id'   => $transaction_id,
			'status'           => $status,
		);

		Swedbank_Pay()->logger()->info( "[UPDATE STATUS]: Update order #{$context['order_number']} status to '{$context['status']}' with transaction ID: {$context['transaction_id']}'", $context );

		switch ( $status ) {
			case 'checkout-draft':
			case 'pending':
				// Set pending.
				if ( ! $order->has_status( 'pending' ) ) {
					$order->update_status( 'pending', $message );
				} elseif ( $message ) {
					$order->add_order_note( $message );
				}

				break;
			case 'on-hold':
				// Set on-hold.
				if ( ! $order->has_status( 'on-hold' ) ) {
					$order->update_status( 'on-hold', $message );
				} elseif ( $message ) {
					$order->add_order_note( $message );
				}

				$order->set_transaction_id( $transaction_id );

				break;
			case 'processing':
			case 'completed':
				if ( empty( $order->get_date_paid() ) ) {
					$order->payment_complete( $transaction_id );
					if ( $message ) {
						$order->add_order_note( $message );
					}
				} else {
					$order->update_status(
						apply_filters(
							'woocommerce_payment_complete_order_status', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
							$status,
							$order->get_id(),
							$order
						),
						$message
					);

					$order->set_transaction_id( $transaction_id );
				}

				break;
			case 'cancelled':
				// Set cancelled.
				if ( ! $order->has_status( 'cancelled' ) ) {
					$order->update_status( 'cancelled', $message );
				} elseif ( $message ) {
					$order->add_order_note( $message );
				}

				$order->set_transaction_id( $transaction_id );

				break;
			case 'refunded':
				$order->update_status( 'refunded', $message );
				$order->set_transaction_id( $transaction_id );

				break;
			case 'failed':
				if ( ! $order->is_paid() ) {
					$order->update_status( 'failed', $message );
				} elseif ( $message ) {
					$order->add_order_note( $message );
				}

				$order->set_transaction_id( $transaction_id );

				break;
			default:
				$order->update_status( $status, $message );
		}

		$order->save();
	}

	/**
	 * Fetch the typed v3.1 paymentOrder resource for the given ID, with per-request caching.
	 *
	 * @param string $payment_order_id Payment order resource path, e.g. "/psp/paymentorders/...".
	 * @return \KrokedilSwedbankPayDeps\SwedbankPay\Api\Service\Paymentorder\V3\Resource\Data\PaymentOrderInterface|\WP_Error
	 */
	private function get_payment_order( $payment_order_id ) {
		if ( empty( $payment_order_id ) ) {
			return new WP_Error( 'missing_payment_id', 'Payment order ID is unknown.' );
		}

		if ( isset( $this->payment_orders[ $payment_order_id ] ) ) {
			return $this->payment_orders[ $payment_order_id ];
		}

		$request = ( new GetPaymentorder() )
			->setClient( self::get_client() )
			->setRequestEndpoint( $payment_order_id );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response
			 */
			$response = $request->send();
		} catch ( ClientException $e ) {
			return new WP_Error(
				'get_paymentorder',
				$this->format_error_message( $request->getClient()->getResponseBody(), $e->getMessage() )
			);
		}

		/**
		 * ResponseServiceInterface is the expected type from the send() method.
		 *
		 * @var PaymentorderResponse $resource
		 */
		$resource      = $response->getResponseResource();
		$payment_order = $resource ? $resource->getPaymentOrder() : null;
		if ( ! $payment_order ) {
			return new WP_Error( 'get_paymentorder', 'Payment order resource missing in response.' );
		}

		$this->payment_orders[ $payment_order_id ] = $payment_order;

		return $payment_order;
	}

	/**
	 * Can Capture.
	 *
	 * @param WC_Order $order The order to check.
	 *
	 * @return bool
	 */
	public function can_capture( WC_Order $order ) {
		LogUtility::$title = "[ORDER MANAGEMENT]: Retrieve capture status for order #{$order->get_order_number()}";
		$payment_order     = $this->get_payment_order( $order->get_meta( '_payex_paymentorder_id' ) );
		if ( is_wp_error( $payment_order ) ) {
			return false;
		}

		$remaining = $payment_order->getRemainingCaptureAmount();
		return null !== $remaining && (float) $remaining > 0.1;
	}

	/**
	 * Can Cancel.
	 *
	 * @param WC_Order $order The order to check.
	 *
	 * @return bool
	 */
	public function can_cancel( WC_Order $order ) {
		LogUtility::$title = "[ORDER MANAGEMENT]: Retrieve cancellation status for order #{$order->get_order_number()}";
		$payment_order     = $this->get_payment_order( $order->get_meta( '_payex_paymentorder_id' ) );
		if ( is_wp_error( $payment_order ) ) {
			return false;
		}

		$remaining = $payment_order->getRemainingCancellationAmount();
		return null !== $remaining && (float) $remaining > 0.1;
	}

	/**
	 * Can Refund.
	 *
	 * @param WC_Order $order The order to check refund capability for.
	 *
	 * @return bool
	 */
	public function can_refund( WC_Order $order ) {
		LogUtility::$title = "[ORDER MANAGEMENT]: Retrieve refund status for order #{$order->get_order_number()}";
		$payment_order     = $this->get_payment_order( $order->get_meta( '_payex_paymentorder_id' ) );
		if ( is_wp_error( $payment_order ) ) {
			return false;
		}

		$remaining = $payment_order->getRemainingReversalAmount();
		return null !== $remaining && (float) $remaining > 0.1;
	}

	/**
	 * Capture Checkout.
	 *
	 * @param WC_Order $order The order to capture.
	 * @param array    $items Optional list of items to capture, used for partial captures. If empty, a full capture will be attempted. Each item should be an array with keys 'reference', 'name', 'quantity', 'unit', 'unitPrice', and 'totalAmount'.
	 *
	 * @return \WP_Error|array
	 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
	 * @SuppressWarnings(PHPMD.NPathComplexity)
	 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
	 * @SuppressWarnings(PHPMD.MissingImport)
	 * @SuppressWarnings(PHPMD.ElseExpression)
	 */
	public function capture_checkout( WC_Order $order, array $items = array() ) {
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );

		if ( empty( $payment_order_id ) ) {
			return new \WP_Error( 'missing_payment_id', 'Unable to get the payment order ID' );
		}

		$context = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $payment_order_id,
			'action'           => 'capture_checkout',
		);

		$helper           = new Order( $order, $items );
		$transaction_data = $helper->get_transaction_data()->setDescription( sprintf( 'Capture for Order #%s', $order->get_order_number() ) );

		$transaction = new TransactionObject();
		$transaction->setTransaction( $transaction_data );

		$request_service = ( new TransactionCapture( $transaction ) )
			->setClient( self::get_client() )
			->setPaymentOrderId( $payment_order_id )
			->setExpands( array( 'financialtransactions', 'paid' ) );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service */
			$response_service = $request_service->send();

			LogUtility::log_request(
				'[ORDER MANAGEMENT]: capture checkout',
				$request_service->getClient(),
				WC_Log_Levels::DEBUG,
				$context
			);

			$transaction = $this->financial_transaction_to_array(
				$response_service->getResponseResource()->getLatestFinancialTransaction()
			);

			if ( is_wp_error( $transaction ) ) {
				$context['error'] = $transaction->get_error_message();
				LogUtility::log_request(
					'[ORDER MANAGEMENT]: capture checkout',
					$request_service->getClient(),
					WC_Log_Levels::ERROR,
					$context
				);

				return $transaction;
			}

			$this->process_transaction( $order, $transaction );

			return $transaction;
		} catch ( ClientException $e ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() );
			LogUtility::log_request(
				'[ORDER MANAGEMENT]: capture checkout',
				$request_service->getClient(),
				WC_Log_Levels::ERROR,
				$context
			);

			return new WP_Error(
				'capture',
				$this->format_error_message( $request_service->getClient()->getResponseBody(), $e->getMessage() )
			);
		}
	}

	/**
	 * Cancel the Swedbank payment.
	 *
	 * @param \WC_Order $order The order object.
	 * @return array|WP_Error
	 */
	public function cancel_checkout( WC_Order $order ) {
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );
		if ( empty( $payment_order_id ) ) {
			return new \WP_Error( 'missing_payment_id', 'Unable to get the payment order ID' );
		}

		$context = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $payment_order_id,
			'action'           => 'cancel_checkout',
		);

		$helper = new Order( $order );

		$transaction_data =
		( $helper->get_transaction_data() )
			->setDescription( sprintf( 'Cancel Order #%s', $order->get_order_number() ) )
			->setPayeeReference(
				apply_filters(
					'swedbank_pay_payee_reference',
					swedbank_pay_generate_payee_reference( $order->get_id() )
				)
			);

		$transaction = new TransactionObject();
		$transaction->setTransaction( $transaction_data );

		$request_service = ( new TransactionCancel( $transaction ) )
			->setClient( self::get_client() )
			->setPaymentOrderId( $payment_order_id )
			->setExpands( array( 'financialtransactions', 'paid' ) );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service
			 */
			$response_service = $request_service->send();

			LogUtility::log_request(
				'[ORDER MANAGEMENT]: cancel checkout',
				$request_service->getClient(),
				WC_Log_Levels::DEBUG,
				$context
			);

			$financial_transaction = $response_service->getResponseResource()->getLatestFinancialTransaction();

			// A cancellation only applies to an authorized (uncaptured) amount and its financial transaction is optional, so the financial transactions list may be empty.
			if ( ! empty( $financial_transaction ) ) {
				$transaction = $this->financial_transaction_to_array( $financial_transaction );

				$this->process_transaction( $order, $transaction );

				return $transaction;
			}

			// No financial transaction was returned, so confirm the cancellation through the payment order status instead.
			$payment_order = $response_service->getResponseResource()->getPaymentOrder();
			$status        = $payment_order ? $payment_order->getStatus() : null;

			if ( 'Cancelled' !== $status ) {
				$context['error'] = sprintf( 'Cancellation not confirmed. Payment order status: %s.', $status );
				LogUtility::log_request(
					'[ORDER MANAGEMENT]: cancel checkout',
					$request_service->getClient(),
					WC_Log_Levels::ERROR,
					$context
				);

				return new WP_Error(
					'cancel',
					__( 'The payment could not be cancelled. Please verify the payment status in Swedbank Pay.', 'swedbank-pay-payment-menu' )
				);
			}

			$transaction_id = $payment_order->getNumber();

			$this->update_order_status(
				$order,
				'cancelled',
				$transaction_id,
				// translators: 1: transaction ID.
				sprintf( __( 'Payment has been cancelled. Transaction: %s', 'swedbank-pay-payment-menu' ), $transaction_id )
			);

			return array(
				'number' => $transaction_id,
				'type'   => self::TYPE_CANCELLATION,
			);
		} catch ( ClientException $e ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() );
			LogUtility::log_request(
				'[ORDER MANAGEMENT]: cancel checkout',
				$request_service->getClient(),
				WC_Log_Levels::ERROR,
				$context
			);

			return new WP_Error(
				'cancel',
				$this->format_error_message( $request_service->getClient()->getResponseBody(), $e->getMessage() )
			);
		}
	}

	/**
	 * Refund amount for an order.
	 *
	 * @param WC_Order $order The order object.
	 * @param float    $amount The amount to refund.
	 *
	 * @return array|\WP_Error Returns the refunded transaction data or WP_Error on failure.
	 * @throws ClientException Throws ClientException on API errors.
	 */
	public function refund_amount( WC_Order $order, $amount ) {
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );
		if ( empty( $payment_order_id ) ) {
			return new WP_Error( 0, 'Unable to get the payment order ID' );
		}

		$context = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $payment_order_id,
			'action'           => 'refund_amount',
			'amount'           => $amount,
		);

		$helper           = new Order( $order );
		$transaction_data = $helper->get_transaction_data()
			->setAmount( round( $amount * 100 ) )
			->setVatAmount( 0 )
			->setDescription( sprintf( 'Refund Order #%s.', $order->get_order_number() ) );

		$transaction = new TransactionObject();
		$transaction->setTransaction( $transaction_data );

		$request_service = ( new TransactionReversal( $transaction ) )
			->setClient( self::get_client() )
			->setPaymentOrderId( $payment_order_id )
			->setExpands( array( 'financialtransactions', 'paid' ) );

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service
			 */
			$response_service = $request_service->send();

			LogUtility::log_request(
				'[ORDER MANAGEMENT]: refund amount',
				$request_service->getClient(),
				WC_Log_Levels::DEBUG,
				$context
			);

			$transaction = $this->financial_transaction_to_array(
				$response_service->getResponseResource()->getLatestFinancialTransaction()
			);

			if ( is_wp_error( $transaction ) ) {
				$context['error'] = $transaction->get_error_message();
				LogUtility::log_request(
					'[ORDER MANAGEMENT]: refund amount',
					$request_service->getClient(),
					WC_Log_Levels::ERROR,
					$context
				);

				return $transaction;
			}

			$this->process_transaction( $order, $transaction );

			return $transaction;
		} catch ( ClientException $e ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() );
			LogUtility::log_request(
				'[ORDER MANAGEMENT]: refund amount',
				$request_service->getClient(),
				WC_Log_Levels::ERROR,
				$context
			);

			return new WP_Error(
				'refund',
				$this->format_error_message( $request_service->getClient()->getResponseBody(), $e->getMessage() )
			);
		}
	}

	/**
	 * Refund Checkout.
	 *
	 * @param \WC_Order_Refund $refund_order The refund order object.
	 *
	 * @return \WP_Error|array
	 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
	 * @SuppressWarnings(PHPMD.NPathComplexity)
	 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
	 * @SuppressWarnings(PHPMD.MissingImport)
	 * @SuppressWarnings(PHPMD.ElseExpression)
	 */
	public function refund_checkout( \WC_Order_Refund $refund_order ) {
		$order            = wc_get_order( $refund_order->get_parent_id() );
		$payment_order_id = $order->get_meta( '_payex_paymentorder_id' );
		if ( empty( $payment_order_id ) ) {
			return new WP_Error( 0, 'Unable to get the payment order ID' );
		}
		$helper           = new Order( $refund_order );
		$transaction_data = $helper->get_transaction_data();
		$amount           = $transaction_data->getAmount();
		$transaction_data = $transaction_data
			->setDescription( sprintf( 'Refund Order #%s', $order->get_order_number() ) );

		$transaction = new TransactionObject();
		$transaction->setTransaction( $transaction_data );

		$request_service = ( new TransactionReversal( $transaction ) )
			->setClient( self::get_client() )
			->setPaymentOrderId( $payment_order_id )
			->setExpands( array( 'financialtransactions', 'paid' ) );

		$context = array(
			'order_id'         => $order->get_id(),
			'order_number'     => $order->get_order_number(),
			'payment_order_id' => $payment_order_id,
			'action'           => 'refund_payment',
			'amount'           => $amount,
		);

		try {
			/**
			 * ResponseServiceInterface is the expected type from the send() method.
			 *
			 * @var ResponseServiceInterface $response_service */
			$response_service = $request_service->send();

			LogUtility::log_request(
				'[ORDER MANAGEMENT]: refund amount',
				$request_service->getClient(),
				WC_Log_Levels::DEBUG,
				$context
			);

			$transaction = $this->financial_transaction_to_array(
				$response_service->getResponseResource()->getLatestFinancialTransaction()
			);

			if ( is_wp_error( $transaction ) ) {
				$context['error'] = $transaction->get_error_message();
				LogUtility::log_request(
					'[ORDER MANAGEMENT]: refund amount',
					$request_service->getClient(),
					WC_Log_Levels::ERROR,
					$context
				);

				return $transaction;
			}

			$this->process_transaction( $order, $transaction );

			return $transaction;
		} catch ( ClientException $e ) {
			$context['error'] = sprintf( '%s: API Exception: %s', __METHOD__, $e->getMessage() );
			LogUtility::log_request(
				'[ORDER MANAGEMENT]: refund amount',
				$request_service->getClient(),
				WC_Log_Levels::ERROR,
				$context
			);

			return new WP_Error(
				'refund',
				$this->format_error_message( $request_service->getClient()->getResponseBody(), $e->getMessage() )
			);
		}
	}

	/**
	 * Bridge a typed v3.1 FinancialTransaction to the array shape that
	 * {@see process_transaction()} expects.
	 *
	 * @param mixed $ft FinancialTransaction|null.
	 * @return array|WP_Error WP_Error when no transaction is available, otherwise the array shape.
	 */
	private function financial_transaction_to_array( $ft ) {
		if ( empty( $ft ) ) {
			return new WP_Error(
				'missing_financial_transaction',
				'No financial transaction was returned by the API.'
			);
		}

		return array(
			'number'         => $ft->getNumber(),
			'type'           => $ft->getType(),
			'amount'         => $ft->getAmount(),
			'created'        => $ft->getCreated(),
			'updated'        => $ft->getUpdated(),
			'description'    => $ft->getDescription(),
			'payeeReference' => $ft->getPayeeReference(),
		);
	}

	/**
	 * Parse and format error response
	 *
	 * @param string $response_body The raw response body from the API, expected to be a JSON string containing error details. The method will attempt to decode this and extract meaningful error messages to present to the user.
	 * @param string $err_msg The original error message from the exception, used as fallback if the response body doesn't contain more details about the error.
	 *
	 * @return string
	 */
	private function format_error_message( $response_body, $err_msg = '' ) {
		$response_body = json_decode( $response_body, true );
		if ( json_last_error() !== JSON_ERROR_NONE ) {
			return $response_body;
		}

		$message = isset( $response_body['detail'] ) ? $response_body['detail'] : '';
		if ( isset( $response_body['problems'] ) && count( $response_body['problems'] ) > 0 ) {
			foreach ( $response_body['problems'] as $problem ) {
				// Specify error message for invalid phone numbers. It's such fields like:
				// Payment.Cardholder.Msisdn
				// Payment.Cardholder.HomePhoneNumber
				// Payment.Cardholder.WorkPhoneNumber
				// Payment.Cardholder.BillingAddress.Msisdn
				// Payment.Cardholder.ShippingAddress.Msisdn.
				if ( ( strpos( $problem['name'], 'Msisdn' ) !== false ) ||
					strpos( $problem['name'], 'HomePhoneNumber' ) !== false ||
					strpos( $problem['name'], 'WorkPhoneNumber' ) !== false
				) {
					$message = 'Your phone number format is wrong. Please input with country code, for example like this +46707777777'; //phpcs:ignore

					break;
				}

				if ( strpos( $problem['name'], 'StreetAddress' ) !== false ) {
					$message = 'Street address can have a max length of 40 and only contain normal characters';

					break;
				}

				$message .= "\n" . sprintf( '%s: %s', $problem['name'], $problem['description'] );
			}
		}

		if ( empty( $message ) ) {
			$message = ! empty( $err_msg ) ? $err_msg : __( 'Error', 'woocommerce' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
		}

		return $message;
	}
}
