<?php
/**
 * –bstract Receipt Sync Module
 *
 * @package northmill-online-shared
 */

declare(strict_types=1);

namespace Northmill_Online_Shared\Modules\Sync;

use Exception;
use Northmill_Online_Shared\Abstracts\Hub_API\Factories\Receipt_Factory;
use Northmill_Online_Shared\Abstracts\Hub_API\Factories\Receipt_Order_Validator;
use Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\Hub_Account_Setup;
use Northmill_Online_Shared\Abstracts\Hub_API\Models\Hub_API_Properties;
use Northmill_Online_Shared\Data\Payment_Methods;
use Northmill_Online_Shared\Helpers\General_Helpers;
use Northmill_Online_Shared\Modules\Admin\Settings\Settings_Fields_Module;
use Northmill_Online_Shared\Modules\Sync\Orders\Models\Order_Base_Model;
use Northmill_Online_Shared\Modules\Sync\Receipts\Receipt_Bookkeeping_Module;
use Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\WP_Repository;
use Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Model;
use Northmill_Online_Shared\Helpers\Hook_Helpers;
use Northmill_Online_Shared\Modules\Abstract_Module;
use Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use Northmill_Online_Shared\Modules\Sync\Receipts\Receipt_Status_Enum;
use Northmill_Online_Shared\Modules\Sync\Receipts\Receipt_Status_Module;
use Northmill_Online_Shared\Abstract_Shared_Base;
use WC_Abstract_Order;
use WC_Order;
use WC_Order_Refund;
use WP_Error;

/**
 * Class Abstract_Receipt_Sync_Module
 */
class Receipt_Sync_Module extends Abstract_Module {

	public const RECEIPT_RETRY_EVENT_HOOK      = 'mf_retry_post_receipt';
	public const ORDER_META_RECEIPT_RESPONSE   = 'mf_receipt_response';
	public const ORDER_META_RECEIPT_ID         = 'mf_receipt_id';
	public const ORDER_META_RECEIPT_VOUCHER_ID = 'mf_receipt_voucher_id';
	public const ORDER_META_RECEIPT_REATTEMPTS = '_mf_receipt_reattempts';


	/**
	 * Receipt status module.
	 *
	 * @var Receipt_Status_Module
	 */
	public Receipt_Status_Module $status;

	/**
	 * Receipt bookkeeping module.
	 *
	 * @var Receipt_Bookkeeping_Module
	 */
	public Receipt_Bookkeeping_Module $bookkeeping;

	/**
	 * Class constructor.
	 *
	 * @param Abstract_Shared_Base $shared Shared instance.
	 */
	public function __construct( Abstract_Shared_Base $shared ) {
		parent::__construct( $shared );

		$this->status      = new Receipt_Status_Module( $shared );
		$this->bookkeeping = new Receipt_Bookkeeping_Module( $shared );
	}

	/**
	 * Set up the module.
	 *
	 * @return void
	 */
	public function setup(): void {
		if ( ! Hub_Account_Setup::is_setting_up_hub_accounts() ) {
			return;
		}
		add_action( 'woocommerce_after_checkout_validation', array( $this, 'after_checkout_validation' ), 9, 2 );
		add_action( 'woocommerce_new_order', array( $this, 'maybe_update_order_receipt_status' ), 5 );
		add_action( 'woocommerce_checkout_order_processed', array( $this, 'validate_order_for_receipt' ), 10, 3 );

		add_action( self::RECEIPT_RETRY_EVENT_HOOK, array( $this, 'retry_post_receipt' ) );

		add_filter( 'northmill_online_shared_before_order_sync_' . $this->shared->package_info->get_service_name_underscored(), array( $this, 'before_order_sync' ), 10, 2 );
		add_filter( 'northmill_online_shared_before_order_refund_sync_' . $this->shared->package_info->get_service_name_underscored(), array( $this, 'before_order_refund_sync' ), 10, 3 );

		add_filter( 'woocommerce_order_actions', array( $this, 'add_custom_order_receipt_actions' ), 10, 2 );
		add_action( 'woocommerce_order_action_northmill_retry_receipt', array( $this, 'order_action_retry_receipt' ) );

		do_action(
			'northmill_online_shared_sync_receipt_setup_' . $this->shared->package_info->get_service_name_underscored(),
			$this,
			$this->shared
		);
	}

	/**
	 * Check if the receipt function is enabled.
	 *
	 * @return bool
	 */
	public function is_enabled(): bool {
		return $this->shared->admin->settings->is_setting_enabled( Settings_Fields_Module::CREATE_RECEIPTS );
	}

	/**
	 * Check if the receipt function is set up.
	 *
	 * @return bool
	 */
	public function is_setup(): bool {
		if ( ! self::is_enabled() ) {
			return false;
		}

		$bo_account = WP_Repository::get_current_main_account();

		if ( ! $bo_account ) {
			return false;
		}

		if ( empty( $bo_account->org_name ) || empty( $bo_account->org_num ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get the relevant instance of the receipt bookkeeping setup class.
	 *
	 * @return Receipt_Bookkeeping_Module
	 */
	public function get_bookkeeping_setup_instance(): Receipt_Bookkeeping_Module {
		return $this->bookkeeping;
	}

	/**
	 * Validate checkout for receipt.
	 *
	 * @param array    $checkout_data  Posted data.
	 * @param WP_Error $errors         Error object.
	 */
	public function after_checkout_validation( array $checkout_data, WP_Error $errors ): void {
		if ( ! $this->is_enabled() ) {
			return;
		}

		$bookkeeping_setup = $this->get_bookkeeping_setup_instance();

		if ( ! $this->is_wc_decimals_correct() ) {
			$this->add_checkout_error(
				$errors,
				'Price decimals setup incorrectly when checking out with receipt creation.',
				array(
					'wc_price_decimals' => $this->get_wc_price_decimals(),
					'required_value'    => Receipt_Factory::RECEIPT_ROUNDING_PRECISION,
				)
			);
		}

		if ( 'yes' !== get_option( 'woocommerce_calc_taxes' ) ) {
			$this->add_checkout_error(
				$errors,
				'Taxes aren\'t enabled when checking out with receipt creation.',
				array(
					'payment_method' => $checkout_data['payment_method'],
				)
			);
		}

		// TODO: Make this only check for Hub Account of order (if multiple accounts).
		if (
			! $bookkeeping_setup->is_required_income_account_codes_set(
				$checkout_data['payment_method']
			)
		) {
			$this->add_checkout_error( $errors );
		}

		// TODO: Make this only check for Hub Account of order (if multiple accounts).
		if ( ! $bookkeeping_setup->is_required_vat_account_codes_set() ) {
			$this->add_checkout_error( $errors );
		}
	}

	/**
	 * Add checkout error.
	 *
	 * @param WP_Error    $errors      Error object.
	 * @param string|null $log_message Log message.
	 * @param array       $log_context Log context.
	 *
	 * @return void
	 */
	private function add_checkout_error(
		WP_Error $errors,
		string $log_message = null,
		array $log_context = array()
	): void {
		$errors->add(
			'checkout',
			__( 'Store setup incorrectly, please contact us.', 'northmill-online-shared' )
		);
		if ( ! empty( $log_message ) ) {
			$this->shared->logger->log(
				'checkout',
				$log_message,
				$log_context,
				Log_Level_Enum::ERROR
			);
		}
	}

	/**
	 * Maybe update order receipt status.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return void
	 */
	public function maybe_update_order_receipt_status( int $order_id ): void {
		if ( ! $this->is_enabled() ) {
			$this->shared->logger->log(
				'order-sync',
				'Receipts are not enabled',
				array(
					'order_id' => $order_id,
				),
				Log_Level_Enum::DEBUG
			);

			return;
		}

		// If this order lacks receipt, set status.
		if ( ! $this->status->get_order_receipt_status( $order_id ) ) {
			$this->status->update_order_receipt_status( $order_id, Receipt_Status_Enum::NEEDED );
		} else {
			$this->shared->logger->log(
				'order-sync',
				'Order already has receipt status',
				array(
					'order_id' => $order_id,
				),
				Log_Level_Enum::DEBUG
			);
		}
	}

	/**
	 * Validate order for receipt creation.
	 *
	 * @param int      $order_id The ID of the order.
	 * @param array    $posted_data The posted data.
	 * @param WC_Order $order The WooCommerce order.
	 *
	 * @throws Exception If validation fails.
	 * @noinspection PhpUnusedParameterInspection Parameter from WP Hook.
	 */
	public function validate_order_for_receipt( int $order_id, array $posted_data, WC_Order $order ): void {
		if (
			! $this->is_enabled() ||
			! $this->can_payment_method_create_receipt( $order->get_payment_method() )
		) {
			return;
		}

		$receipt_factory = new Receipt_Factory( $order );

		$receipt_validator = new Receipt_Order_Validator( $receipt_factory );
		$validated         = $receipt_validator->validate_placed_order_for_receipt();

		if ( is_wp_error( $validated ) ) {
			$this->shared->logger->log(
				'checkout',
				'Order validation failed',
				array(
					'errors' => $validated->get_error_messages(),
					'order'  => $order->get_id(),
				),
				Log_Level_Enum::ERROR
			);

			$order->set_status( 'failed' );
			$order->save();

			throw new Exception(
				esc_html__(
					'Unable to create order, receipt validation failed. Please contact us if this error persists.',
					'northmill-online-shared'
				)
			);
		}
	}

	/**
	 * Get whether stock management should be done on receipt creation.
	 *
	 * @return bool
	 */
	public function do_stock_management_on_receipt_creation(): bool {
		return Hook_Helpers::apply_scoped_filters(
			'northmill_online_shared_do_stock_management_on_receipt_creation',
			true
		);
	}


	/**
	 * Retrieves a receipt based on a given order, including optional extra data.
	 *
	 * @param WC_Order $order      The order for which the receipt should be generated.
	 *
	 * @return Receipt_Model|WP_Error Returns a Receipt_Model on success, WP_Error on failure.
	 */
	public function get_receipt_from_order( WC_Order $order ): Receipt_Model|WP_Error {
		try {
			$receipt_factory = new Receipt_Factory( $order );
		} catch ( Exception $e ) {
			return new WP_Error(
				'failed_receipt_factory',
				$e->getMessage()
			);
		}

		$stock_adjustment = $this->do_stock_management_on_receipt_creation();
		return $receipt_factory->create_receipt( $stock_adjustment );
	}

	/**
	 * Generates a receipt from a given refund order.
	 *
	 * @param WC_Order_Refund $order The refund order from which to generate a receipt.
	 * @param bool            $restock_items Whether to restock items.
	 *
	 * @return Receipt_Model|WP_Error Returns a Receipt_Model on success, WP_Error on failure.
	 */
	public function get_receipt_from_refund_order( WC_Order_Refund $order, bool $restock_items ): Receipt_Model|WP_Error {
		$do_stock_management = $this->do_stock_management_on_receipt_creation();

		if ( ! $do_stock_management ) {
			// Force restocking to false if receipts manage stock is disabled.
			$restock_items = false;
		} else {
			// Check if there are any quantities, else set restock to false.
			$order_items = $order->get_items();
			if ( empty( $order_items ) ) {
				$restock_items = false;
			} else {
				$quantity_order_items = array_filter(
					$order_items,
					function ( $order_item ) {
						return $order_item->get_quantity() !== 0;
					}
				);
				if ( count( $quantity_order_items ) < 1 ) {
					$restock_items = false;
				}
			}
		}

		try {
			$receipt_factory = new Receipt_Factory( $order );
		} catch ( Exception $e ) {
			return new WP_Error(
				'failed_receipt_factory',
				$e->getMessage()
			);
		}

		return $receipt_factory->create_receipt(
			$restock_items
		);
	}

	/**
	 * Posts a receipt to Hub.
	 *
	 * @param Receipt_Model            $receipt_model The model containing the receipt data to be posted.
	 * @param WC_Order|WC_Order_Refund $order         The order or refund order for which the receipt should be posted.
	 *
	 * @return false|string False if the operation fails, or a string containing the response from Hub if successful.
	 */
	protected function post_receipt_to_hub( Receipt_Model $receipt_model, WC_Order|WC_Order_Refund $order ): false|string {
		if ( is_a( $order, WC_Order_Refund::class ) ) {
			$order = wc_get_order( $order->get_parent_id() );
		}

		$order_instance = new Order_Base_Model( $order );

		return $order_instance->get_backoffice_account()->api->post_receipt( $receipt_model );
	}

	/**
	 * Maybe post the receipt to Hub API.
	 *
	 * @param WC_Order|WC_Order_Refund $order WC order.
	 * @param bool                     $restock_items Whether to restock items. Only used for refunds.
	 * @param bool                     $schedule_retry_on_failure Whether to schedule a retry on failure.
	 *
	 * @return void
	 */
	public function maybe_post_receipt_to_hub(
		WC_Order|WC_Order_Refund $order,
		bool $restock_items = false,
		bool $schedule_retry_on_failure = true
	): void {

		if ( ! $this->is_enabled() ) {
			$this->shared->logger->log(
				'order-sync',
				'Receipts are not enabled. Receipt not created.',
				array(
					'order_id' => $order->get_id(),
				),
				Log_Level_Enum::DEBUG
			);

			return;
		}

		$order_id     = $order->get_id();
		$is_refund    = is_a( $order, WC_Order_Refund::class );
		$parent_order = $is_refund ? wc_get_order( $order->get_parent_id() ) : null;

		$needs_receipt = $is_refund ?
			$this->order_refund_needs_receipt( $order ) :
			$this->order_needs_receipt( $order );

		$this->shared->logger->log(
			'order-sync',
			'Preparing order receipt',
			array(
				'order_id'        => $order_id,
				'is_refund'       => $is_refund,
				'parent_order_id' => $is_refund ? $order->get_parent_id() : null,
				'needs_receipt'   => $needs_receipt,
			),
			Log_Level_Enum::DEBUG
		);

		if ( ! $needs_receipt ) {
			return;
		}

		$receipt_model = $is_refund ?
			$this->get_receipt_from_refund_order( $order, $restock_items ) :
			$this->get_receipt_from_order( $order );

		$note_order = $is_refund ? $parent_order : $order;

		if ( is_wp_error( $receipt_model ) ) { // Preparing the receipt failed.
			$this->status->update_order_receipt_status( $order_id, Receipt_Status_Enum::FAILED );

			$this->shared->logger->log(
				'order-sync',
				'Preparing order receipt failed',
				array(
					'order_id'     => $order_id,
					'error'        => $receipt_model->get_error_message(),
					'is_refund'    => $is_refund,
					'parent_order' => $is_refund ? $order->get_parent_id() : null,
				),
				Log_Level_Enum::ERROR
			);
			$note_order->add_order_note(
				__(
					'Preparing receipt for Hub failed. See log for error message.',
					'northmill-online-shared'
				)
			);

		} else { // We can safely send the receipt.

			$receipt_response = $this->post_receipt_to_hub( $receipt_model, $order );
			update_post_meta( $order_id, self::ORDER_META_RECEIPT_RESPONSE, $receipt_response );

			if ( ! $receipt_response ) {
				// Failure.
				$this->handle_post_receipt_failure( $order, $schedule_retry_on_failure );
			} else {
				$this->status->update_order_receipt_status( $order_id, Receipt_Status_Enum::SENT );

				$this->shared->logger->log(
					'order-sync',
					'Order receipt sent successfully.',
					array(
						'order_id'     => $order_id,
						'is_refund'    => $is_refund,
						'parent_order' => $is_refund ? $order->get_parent_id() : null,
					),
					Log_Level_Enum::DEBUG
				);

				$receipt_response_obj = json_decode( $receipt_response );

				// Success.
				$receipt_id = $receipt_response_obj->Receipt->Id;
				$voucher_id = $receipt_response_obj->Receipt->Voucher->Id;
				update_post_meta( $order_id, self::ORDER_META_RECEIPT_ID, $receipt_id );
				update_post_meta( $order_id, self::ORDER_META_RECEIPT_VOUCHER_ID, $voucher_id );

				if ( $is_refund ) {
					// translators: %1$s: Receipt ID, %2$s: Voucher ID.
					$text = esc_html__(
						'Order refund receipt sent successfully. (Receipt: %1$s; Voucher: %2$s)',
						'northmill-online-shared'
					);
				} else {
					// translators: %1$s: Receipt ID, %2$s: Voucher ID.
					$text = esc_html__(
						'Order receipt sent successfully. (Receipt: %1$s; Voucher: %2$s)',
						'northmill-online-shared'
					);
				}

				$note_order->add_order_note(
					sprintf(
						$text,
						$receipt_id,
						$voucher_id
					)
				);
			}
		}
	}

	/**
	 * Get payment methods that when used for an order should never create receipts.
	 *
	 * @return array
	 */
	public function get_payment_methods_not_creating_receipts(): array {
		return Hook_Helpers::apply_scoped_filters(
			'northmill_online_shared_payment_methods_not_needing_receipt',
			array(
				'PayedFromCashier', // This is a custom payment method set in Sync when orders are synced as paid from Hub.
				Payment_Methods::WC_COD,
				Payment_Methods::NMC_MF_INVOICE,
			)
		);
	}

	/**
	 * Check if a payment method should create a receipt.
	 *
	 * @param string $payment_method_id The payment method ID.
	 *
	 * @return bool
	 */
	public function can_payment_method_create_receipt( string $payment_method_id ): bool {
		return ! in_array( $payment_method_id, $this->get_payment_methods_not_creating_receipts(), true );
	}

	/**
	 * Determines if a given order needs a receipt.
	 *
	 * @param WC_Order $order The order to be checked.
	 *
	 * @return bool True if a receipt should be generated for the order, otherwise false.
	 */
	protected function order_needs_receipt( WC_Order $order ): bool {
		if ( ! $this->is_setup() ) {
			return false;
		}

		// Only create receipts for orders that need receipts AND are paid.
		if (
			! $this->can_payment_method_create_receipt( $order->get_payment_method() ) ||
			! $this->status->is_order_receipt_status( $order->get_id(), Receipt_Status_Enum::NEEDED ) ||
			is_null( $order->get_date_paid() )
		) {
			$this->shared->logger->log(
				'order-sync',
				'Order receipt not needed.',
				array(
					'order_id'                   => $order->get_id(),
					'status'                     => $this->status->get_order_receipt_status( $order->get_id() ),
					'paid'                       => $order->get_date_paid(),
					'payment_method'             => $order->get_payment_method(),
					'disallowed_payment_methods' => $this->get_payment_methods_not_creating_receipts(),
				),
				Log_Level_Enum::DEBUG
			);

			return false;
		}

		return true;
	}

	/**
	 * Determines if a given REFUND order needs a receipt.
	 *
	 * @param WC_Order_Refund $order The order to be checked.
	 *
	 * @return bool True if a receipt should be generated for the order, otherwise false.
	 */
	protected function order_refund_needs_receipt( WC_Order_Refund $order ): bool {
		if ( ! $this->is_setup() ) {
			return false;
		}

		// Only create if parent order have sent receipt.
		$parent_order = wc_get_order( $order->get_parent_id() );
		if ( ! $this->status->is_order_receipt_status( $parent_order->get_id(), Receipt_Status_Enum::SENT ) ) {
			$this->shared->logger->log(
				'order-sync',
				'Receipt not created for parent order. Skipping refund receipt.',
				array(
					'order_id' => $order->get_id(),
				),
				Log_Level_Enum::DEBUG
			);

			return false;
		}

		return true;
	}

	/**
	 * Handle failure when posting receipt.
	 *
	 * @param WC_Order|WC_Order_Refund $order WC order.
	 * @param bool                     $schedule_retry_on_failure Whether to schedule a retry on failure.
	 */
	private function handle_post_receipt_failure( WC_Order|WC_Order_Refund $order, bool $schedule_retry_on_failure = true ): void {
		$is_refund  = is_a( $order, WC_Order_Refund::class );
		$note_order = $is_refund ? wc_get_order( $order->get_parent_id() ) : $order;

		$reattempt_count = get_post_meta( $order->get_id(), self::ORDER_META_RECEIPT_REATTEMPTS, true );
		if ( ! $reattempt_count ) {
			$reattempt_count = 0;
		}

		if ( ! $schedule_retry_on_failure || $reattempt_count >= 3 ) {
			// Failure.
			$this->status->update_order_receipt_status( $order->get_id(), Receipt_Status_Enum::FAILED );

			$this->shared->logger->log(
				'order-sync',
				'Final receipt sync attempt failed',
				array(
					'order_id'     => $order->get_id(),
					'is_refund'    => $is_refund,
					'parent_order' => $is_refund ? $order->get_parent_id() : null,
				),
				Log_Level_Enum::CRITICAL
			);

			if ( $is_refund ) {
				$note_order->add_order_note( __( 'Final refund receipt sync attempt failed.', 'northmill-online-shared' ) );
			} else {
				$note_order->add_order_note( __( 'Final receipt sync attempt failed.', 'northmill-online-shared' ) );
			}
			return;
		}

		++$reattempt_count;
		update_post_meta( $order->get_id(), self::ORDER_META_RECEIPT_REATTEMPTS, $reattempt_count );

		$reattempt_delay_in_minutes = 5 * $reattempt_count;

		$this->shared->logger->log(
			'order-sync',
			'Failed to send order receipt to Hub',
			array(
				'order_id'     => $order->get_id(),
				'attempt'      => $reattempt_count,
				'is_refund'    => $is_refund,
				'parent_order' => $is_refund ? $order->get_parent_id() : null,
			),
			Log_Level_Enum::ERROR
		);

		// translators: %d: Delay in minutes.
		$retry_text = _n(
			'Retrying in %d minute.',
			'Retrying in %d minutes.',
			$reattempt_delay_in_minutes,
			'northmill-online-shared'
		);

		if ( $is_refund ) {
			$text = __(
				'Failed to send refund order receipt to Hub.',
				'northmill-online-shared'
			);
		} else {
			$text = __(
				'Failed to send order receipt to Hub.',
				'northmill-online-shared'
			);
		}
		$text .= ' ' . $retry_text;

		$note_order->add_order_note(
			sprintf(
				$text,
				$reattempt_delay_in_minutes,
			)
		);
		wp_schedule_single_event(
			time() + ( MINUTE_IN_SECONDS * $reattempt_delay_in_minutes ),
			self::RECEIPT_RETRY_EVENT_HOOK,
			array( $order->get_id() )
		);
	}

	/**
	 * Retry posting a receipt to Hub for a given order.
	 *
	 * @param int $order_id The WooCommerce order ID.
	 */
	public function retry_post_receipt( int $order_id ): void {
		$order      = wc_get_order( $order_id );
		$is_refund  = is_a( $order, WC_Order_Refund::class );
		$note_order = $is_refund ? wc_get_order( $order->get_parent_id() ) : $order;

		if ( $order ) {
			if ( $is_refund ) {
				$text = __( 'Retrying refund receipt sync.', 'northmill-online-shared' );
			} else {
				$text = __( 'Retrying receipt sync.', 'northmill-online-shared' );
			}
			$note_order->add_order_note( $text );
			$this->maybe_post_receipt_to_hub( $order );
		}
	}


	/**
	 * Filters payment status based on receipt status.
	 * - Orders creating receipts, that still hasn't sent their receipt, should sync as not paid.
	 *
	 * @param array|object      $mf_order Array to send to Apps.
	 * @param WC_Abstract_Order $order    WooCommerce order or refund order.
	 *
	 * @return array|object Type will be same as passed in for $mf_order.
	 */
	public function before_order_sync( array|object $mf_order, WC_Abstract_Order $order ): array|object {
		if ( ! $this->is_enabled() ) {
			return $mf_order;
		}

		$was_array = false;

		if ( is_array( $mf_order ) ) {
			$mf_order  = (object) $mf_order;
			$was_array = true;
		}

		if (
			$this->status->is_order_receipt_status( $order->get_id(), Receipt_Status_Enum::NEEDED ) &&
			'FullyPaid' === $mf_order->{ Hub_API_Properties::ORDER_PAYMENT_STATUS }
		) {
			$mf_order->{ Hub_API_Properties::ORDER_PAYMENT_STATUS } = 'NotPaid';
		}

		return $was_array ? (array) $mf_order : $mf_order;
	}

	/**
	 * Filters payment status based on receipt status.
	 *  - Has not sent receipt AND FullyPaid => Set payment status to NotPaid
	 *
	 * @param array           $mf_order     Array to send to Apps.
	 * @param WC_Order_Refund $order WooCommerce order. Not WC_Order_Refund class, but WC_Order.
	 *                                  See refund_order function in Orders.php.
	 * @param WC_Order        $parent_order Parent order for refund.
	 *
	 * @return array
	 */
	public function before_order_refund_sync( array $mf_order, WC_Order_Refund $order, WC_Order $parent_order ): array {
		// Is receipts enabled?
		if ( ! $this->is_enabled() ) {
			return $mf_order;
		}

		// Make sure the parent order has sent receipt.
		if ( ! $this->status->is_order_receipt_status( $parent_order->get_id(), Receipt_Status_Enum::SENT ) ) {
			return $mf_order;
		}

		// Has not sent receipt AND FullyPaid => Set payment status to NotPaid.
		if (
			! $this->status->is_order_receipt_status( $order->get_id(), Receipt_Status_Enum::SENT ) &&
			'FullyPaid' === $mf_order[ Hub_API_Properties::ORDER_PAYMENT_STATUS ]
		) {
			$mf_order[ Hub_API_Properties::ORDER_PAYMENT_STATUS ] = 'NotPaid';
		}

		return $mf_order;
	}

	/**
	 * Add custom actions in the order page.
	 *
	 * @param array         $actions The existing order actions.
	 * @param WC_Order|null $order   The current order object.
	 *
	 * @return array The modified order actions.
	 */
	public function add_custom_order_receipt_actions( array $actions, WC_Order|null $order ): array {
		if ( ! $order || ! $this->is_enabled() ) {
			return $actions;
		}

		if ( ! $this->status->is_order_receipt_status( $order->get_id(), Receipt_Status_Enum::SENT ) ) {
			$actions['northmill_retry_receipt'] = __( 'Retry sending receipt', 'northmill-online-shared' );
		}

		return $actions;
	}

	/**
	 * Retry sending a receipt for a given order.
	 *
	 * @param WC_Order $wc_order The WooCommerce order.
	 */
	public function order_action_retry_receipt( WC_Order $wc_order ): void {
		$this->shared->logger->log(
			'order-sync',
			'Attempting to retry sending receipt',
			array(
				'order'          => $wc_order->get_id(),
				'receipt_status' => $this->status->get_order_receipt_status( $wc_order->get_id() ),
			),
			Log_Level_Enum::DEBUG
		);

		if ( $this->status->is_order_receipt_status( $wc_order->get_id(), Receipt_Status_Enum::SENT ) ) {
			$this->shared->logger->log(
				'order-sync',
				'Attempted to retry sending already sent receipt',
				array(
					'order' => $wc_order->get_id(),
				),
				Log_Level_Enum::WARNING
			);
			return;
		}

		$is_status_needed = $this->status->update_order_receipt_status( $wc_order->get_id(), Receipt_Status_Enum::NEEDED );

		if ( ! $is_status_needed ) {
			$this->shared->logger->log(
				'order-sync',
				'Failed to set order receipt status to needed',
				array(
					'order'  => $wc_order->get_id(),
					'status' => $this->status->get_order_receipt_status( $wc_order->get_id() ),
				),
				Log_Level_Enum::ERROR
			);
			return;
		}

		// Remove all scheduled retries.
		wp_clear_scheduled_hook( self::RECEIPT_RETRY_EVENT_HOOK, array( $wc_order->get_id() ) );

		$this->maybe_post_receipt_to_hub( $wc_order );
	}

	/**
	 * Print disabled status for receipt select input.
	 *
	 * @return bool Whether the field is disabled
	 */
	public function get_receipt_select_input_disabled_status(): bool {
		$return = false;
		if (
			! self::is_wc_decimals_correct() ) {
			$return = true;
		}

		$receipts_helpers = $this->shared->sync->receipts->bookkeeping;

		if ( ! $receipts_helpers->is_required_income_account_codes_set( null, false ) ) {
			$return = true;
		}

		if ( ! $receipts_helpers->is_required_vat_account_codes_set( false ) ) {
			$return = true;
		}

		// You should always be able to disable receipt creation if it is already enabled.
		return $this->shared->sync->receipts->is_enabled() ? false : $return;
	}

	/**
	 * Get extra HTML for receipts enabled field.
	 *
	 * @return string
	 */
	public function get_receipts_enabled_field_extra_html(): string {
		ob_start();
		if ( ! self::is_wc_decimals_correct() ) {
			echo '<p class="description" style="color: red;">';
			echo wp_kses_post(
				sprintf(
				/* translators: %1$s: Start of WooCommerce settings page a tag. %2$s: end of a tag. */
					__( 'Number of decimals must be set to 2 in WooCommerce, set it %1$shere%2$s.', 'northmill-online-shared' ),
					'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=general' ) ) . '" target="_blank">',
					'</a>'
				)
			);
			echo '</p>';
		}

		$receipts_setup              = $this->shared->sync->receipts->bookkeeping;
		$invalid_gateway_collections = $receipts_setup->get_invalid_payment_gateway_collections();

		if ( ! empty( $invalid_gateway_collections ) ) {
			echo '<p class="description" style="color: red;">';
			esc_html_e( 'The following payment methods are missing income account codes:', 'northmill-online-shared' );
			echo '</p>';
			echo '<ul class="description" style="color: red;">';

			foreach ( $invalid_gateway_collections as $invalid_gateway_collection ) {
				$payment_gateway    = $invalid_gateway_collection->payment_gateway;
				$backoffice_account = $invalid_gateway_collection->backoffice_account;
				$iac_pay_type       = $invalid_gateway_collection->iac_pay_type;

				echo wp_kses_post(
					'<li>' .
					'<i>' .
					$payment_gateway->method_title . ' - ' . General_Helpers::truncate_string( $payment_gateway->description ?? '' ) .
					' (' . $iac_pay_type . ') ' .
					'</i>' .
					__( 'in Hub account', 'northmill-online-shared' ) . ' ' .
					'<i>' .
					$backoffice_account->get_display_name() .
					'</i>' .
					'</li>'
				);
			}
			echo '</ul>';
		}

		$invalid_vat_bo_accounts = $receipts_setup->get_invalid_vat_hub_accounts();
		if ( ! empty( $invalid_vat_bo_accounts ) ) {
			echo '<p class="description" style="color: red;">';
			esc_html_e( 'The following Hub accounts are missing are missing VAT account codes. Account codes are required for 0%, 6%, 12% and 25%.', 'northmill-online-shared' );
			echo '</p>';
			echo '<ul class="description" style="color: red;">';
			foreach ( $invalid_vat_bo_accounts as $bo_accounts ) {
				echo wp_kses_post(
					'<li>' . $bo_accounts->get_display_name() . '</li>'
				);
			}
			echo '</ul>';
		}

		return ob_get_clean();
	}

	/**
	 * Check if WooCommerce decimals are set up correct.
	 *
	 * @return bool
	 */
	public function is_wc_decimals_correct(): bool {
		return Receipt_Factory::RECEIPT_ROUNDING_PRECISION === $this->get_wc_price_decimals();
	}

	/**
	 * Get price decimals from WC settings.
	 *
	 * @return int Price decimals, or -1 if wc_get_price_decimals doesn't exist.
	 */
	public function get_wc_price_decimals(): int {
		return function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : -1;
	}
}
