<?php

/**
 * Creates receipts.
 *
 * @package northmill-online-shared
 */
declare (strict_types=1);
namespace Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Factories;

use Automattic\WooCommerce\Utilities\NumberUtil;
use Exception;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstract_Shared_Base;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Article_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Customer_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Discount_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Merchant_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Order_Reference_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Payment_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Vat_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Voucher_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Models\Receipts\Receipt_Voucher_Row_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Data\Payment_Methods;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Helpers\Hook_Helpers;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Sync\Bookkeeping\Article_Account_Codes\Article_Account_Codes;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Sync\Bookkeeping\Vat_Account_Codes\Vat_Accounts;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Sync\Customers\Customer_Sync;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Sync\Orders\Models\Order_Base_Model;
use stdClass;
use WC_DateTime;
use WC_Order;
use WC_Order_Item;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WC_Order_Item_Tax;
use WC_Order_Refund;
use WC_Product;
use WC_Tax;
use WP_Error;
defined('ABSPATH') || exit;
/**
 * Class Abstract_Receipt_Factory
 */
class Receipt_Factory
{
    /**
     * The precision to which numbers should be rounded.
     */
    public const RECEIPT_ROUNDING_PRECISION = 2;
    /**
     * If this receipt is for a refund.
     *
     * @var bool $is_for_refund
     */
    public bool $is_for_refund = \false;
    /**
     * WooCommerce order or refund order.
     *
     * @var WC_Order|WC_Order_Refund $wc_order
     */
    public WC_Order|WC_Order_Refund $wc_order;
    /**
     * Order base model.
     *
     * @var Order_Base_Model $order_model
     */
    protected Order_Base_Model $order_model;
    /**
     * Parent order if this is a refund order.
     *
     * @var WC_Order|null $parent_order
     */
    public ?WC_Order $parent_order = null;
    /**
     * Shipping article SKU.
     *
     * @var string $shipping_article_sku
     */
    public string $shipping_article_sku;
    /**
     * Shipping article account code.
     *
     * @var int|null $shipping_account_code
     */
    public ?int $shipping_account_code;
    /**
     * Hub order number.
     *
     * @var string|null $hub_order_number
     */
    public ?string $hub_order_number;
    /**
     * Order items.
     *
     * @var WC_Order_Item[]
     */
    public array $order_items;
    /**
     * Receipt date.
     *
     * @var WC_DateTime|null $receipt_date
     */
    public ?WC_DateTime $receipt_date;
    /**
     * Payment method bookkeeping account.
     *
     * @var int $payment_method_account
     */
    public int $payment_method_account;
    /**
     * Order payment method.
     *
     * @var string $order_payment_method
     */
    public string $order_payment_method;
    /**
     * Order reference.
     *
     * @var Receipt_Order_Reference_Model|false $order_reference
     */
    public Receipt_Order_Reference_Model|false $order_reference;
    /**
     * Order reference index.
     *
     * @var int|null $order_reference_index
     */
    public ?int $order_reference_index = null;
    /**
     * Receipt articles.
     *
     * @var Receipt_Article_Model[]|null[]
     */
    public array $articles;
    /**
     * Receipt VAT.
     *
     * @var Receipt_Vat_Model[]|null[]
     */
    public array $vat = array();
    /**
     * Receipt voucher.
     *
     * @var Receipt_Voucher_Model|null $voucher
     */
    public Receipt_Voucher_Model|null $voucher;
    /**
     * Constructor.
     *
     * @param WC_Order|WC_Order_Refund $order       WooCommerce order or refund order.
     * @param Order_Base_Model|null    $order_model The order base model.
     *
     * @throws Exception If an invalid order model class is provided by filter.
     */
    public function __construct(WC_Order|WC_Order_Refund $order, ?Order_Base_Model $order_model = null)
    {
        $this->wc_order = $order;
        if ('shop_order_refund' === $order->get_type()) {
            $this->is_for_refund = \true;
            $this->parent_order = wc_get_order($order->get_parent_id());
        }
        $class_name = $this->is_for_refund ? $this->get_order_refund_model_class() : $this->get_order_model_class();
        $order_for_order_model = $this->is_for_refund ? $this->parent_order : $order;
        $this->order_model = $order_model ?? new $class_name($order_for_order_model);
        $this->shipping_article_sku = $this->get_shipping_article_sku();
        $this->shipping_account_code = $this->get_shipping_account_code();
        if (!$this->is_for_refund) {
            // Normal order.
            $this->hub_order_number = $this->order_model->get_hub_order_number();
        } else {
            // Refund order.
            $order_model_class_name = $this->get_order_model_class();
            $order_model = new $order_model_class_name($this->parent_order);
            $this->hub_order_number = $order_model->get_hub_order_number();
        }
    }
    /**
     * Get the order model class.
     *
     * @throws Exception If the class does not extend Order_Base_Model.
     */
    private function get_order_model_class(): string
    {
        $class_name = Hook_Helpers::apply_scoped_filters('northmill_online_shared_receipt_order_model_class', Order_Base_Model::class, $this->wc_order->get_id());
        return $this->validate_order_class($class_name);
    }
    /**
     * Get the order refund model class.
     *
     * @throws Exception If the class does not extend Order_Base_Model.
     */
    private function get_order_refund_model_class(): string
    {
        $class_name = Hook_Helpers::apply_scoped_filters('northmill_online_shared_receipt_order_refund_model_class', Order_Base_Model::class, $this->wc_order->get_id());
        return $this->validate_order_class($class_name);
    }
    /**
     * Ensure that the class name is valid.
     *
     * @param string $class_name Class name to validate.
     *
     * @return string
     * @throws Exception If the class does not extend Order_Base_Model.
     */
    private function validate_order_class(string $class_name): string
    {
        // Class must extend Order_Base_Model.
        if (Order_Base_Model::class !== $class_name && !is_subclass_of($class_name, Order_Base_Model::class)) {
            throw new Exception(esc_html('Class ' . $class_name . ' must extend ' . Order_Base_Model::class));
        }
        return $class_name;
    }
    /**
     * Create a receipt from WooCommerce order or refund order.
     *
     * @param bool $do_stock_management If the receipt should be in charge of stock.
     *
     * @return Receipt_Model|WP_Error Receipt on success, else WP_Error
     *
     * @noinspection PhpUnused To be used in package loading this composer package.
     */
    public function create_receipt(bool $do_stock_management = \false): WP_Error|Receipt_Model
    {
        /**
         * First, some validation that is only relevant when the order has actually been paid.
         */
        $receipt_date = !$this->is_for_refund ? $this->wc_order->get_date_paid() : $this->wc_order->get_date_created();
        if (is_null($receipt_date)) {
            return new WP_Error('order_not_paid', __('Order has not been paid', 'northmill-online-shared'));
        }
        $this->receipt_date = $receipt_date;
        /**
         * Make sure we can get Hub order id.
         */
        if (empty($this->hub_order_number)) {
            if (!$this->is_for_refund) {
                // Normal order.
                $message = 'Order has not correctly been synced to Hub: ' . $this->wc_order->get_id();
            } else {
                $message = 'Order for refund has not correctly been synced to Hub: ' . $this->parent_order->get_id();
            }
            return new WP_Error('missing_order_hub_id', $message);
        }
        $organization_number = $this->order_model->get_backoffice_account()->org_num;
        $company_name = $this->order_model->get_backoffice_account()->org_name;
        /**
         * Many of the class properties are set in the validate_order_for_receipt method.
         */
        $receipt_validator = new Receipt_Order_Validator($this);
        $validation_result = $receipt_validator->validate_placed_order_for_receipt();
        if (is_wp_error($validation_result)) {
            return $validation_result;
        }
        $internal_note = null;
        if ($this->is_for_refund) {
            $internal_note = $this->wc_order->get_reason();
        }
        $merchant = $this->create_merchant($organization_number, $company_name, WC()->countries->get_base_address(), WC()->countries->get_base_address_2(), WC()->countries->get_base_postcode(), WC()->countries->get_base_city(), WC()->countries->get_base_country());
        $customer_full_name = !$this->is_for_refund ? $this->wc_order->get_formatted_billing_full_name() : $this->parent_order->get_formatted_billing_full_name();
        $customer_company = !$this->is_for_refund ? $this->wc_order->get_billing_company() : $this->parent_order->get_billing_company();
        $customers = array($this->create_customer($this->get_customer_number(), $customer_full_name, $customer_company));
        $text_rows = array();
        $payments = array($this->create_payment($this->wc_order->get_total(), $this->order_payment_method, $text_rows));
        $order_references = $this->order_reference ? array($this->order_reference) : array();
        return new Receipt_Model(receipt_number: $this->wc_order->get_id(), internal_note: $internal_note, transaction_date: $this->receipt_date->format('Y-m-d\TH:i:s\.\0\0\0\Z'), total_price_inc_vat: $this->wc_order->get_total(), total_price_ex_vat: $this->wc_order->get_total() - $this->wc_order->get_total_tax(), total_vat: $this->wc_order->get_total_tax(), do_stock_management: $do_stock_management, merchant: $merchant, customers: $customers, articles: $this->articles, payments: $payments, orders: $order_references, vat: $this->vat, voucher: $this->voucher, text_rows: $text_rows, receipt_type: apply_filters('northmill_online_shared_create_receipt_receipt_type', 'Online', $this->wc_order->get_id()), origin: 'WooCommerce - ' . wp_parse_url(get_site_url(), \PHP_URL_HOST), id: null, cashier_name: null, rounding: null, control_unit: null, discounts: array(), recipients: array(), client_details: null);
    }
    /**
     * Generate order reference.
     *
     * @return void
     */
    public function generate_order_reference(): void
    {
        if (!empty($this->hub_order_number)) {
            $origin = Hook_Helpers::apply_scoped_filters('northmill_online_shared_receipt_origin', 'AppsOrder', $this->wc_order->get_id(), $this->wc_order);
            $this->order_reference_index = 0;
            $this->order_reference = self::create_order_reference(
                $this->order_reference_index,
                $this->hub_order_number,
                is_a($this->wc_order, 'WC_Order') ? 'Paid' : 'Cancelled',
                // WC_Order_Refund.
                $origin
            );
        } else {
            $this->order_reference = \false;
        }
    }
    /**
     * Generate articles for receipt.
     *
     * @return void
     */
    public function generate_articles(): void
    {
        if (!isset($this->order_reference)) {
            $this->generate_order_reference();
        }
        foreach ($this->order_items as $order_item) {
            $this->articles[] = $this->maybe_create_article($order_item, $this->order_reference_index, __('Discount', 'northmill-online-shared'));
        }
        $shipping = $this->maybe_create_shipping($this->shipping_article_sku, $this->shipping_account_code);
        if (!is_null($shipping)) {
            $this->articles[] = $shipping;
        }
    }
    /**
     * Generate VAT for receipt.
     *
     * @return void
     */
    public function generate_vat(): void
    {
        $tax_items = $this->wc_order->get_taxes();
        if (!empty($tax_items)) {
            foreach ($tax_items as $tax_item) {
                if (!$tax_item->get_tax_total() && !$tax_item->get_shipping_tax_total()) {
                    // Ignore empty tax totals.
                    continue;
                }
                $vat_row = $this->maybe_create_vat($tax_item, $this->articles, $this->get_vat_account_codes());
                $this->vat[] = $vat_row;
            }
        }
    }
    /**
     * Generate voucher for receipt.
     *
     * @return void
     */
    public function generate_voucher(): void
    {
        $this->voucher = $this->create_voucher($this->articles, $this->vat, $this->payment_method_account);
    }
    /**
     * Get article account code for shipping.
     *
     * @return int|null
     */
    protected function get_shipping_account_code(): ?int
    {
        return $this->order_model->get_backoffice_account()->get_shipping_settings()->get_article_account_code();
    }
    /**
     * Get shipping article SKU.
     *
     * @return string
     */
    protected function get_shipping_article_sku(): string
    {
        return $this->order_model->get_backoffice_account()->get_shipping_settings()->get_article_sku();
    }
    /**
     * Get VAT account codes.
     *
     * @return array
     */
    protected function get_vat_account_codes(): array
    {
        return Vat_Accounts::get_default_account_codes($this->order_model->get_backoffice_account());
    }
    /**
     * Get bookkeeping income account code from the payment method.
     *
     * @param string $payment_method Payment method.
     *
     * @return int|null
     */
    public function get_income_account_code_from_payment_method(string $payment_method): ?int
    {
        $receipt_bookkeeping_module = Abstract_Shared_Base::base_instance()->sync->receipts->bookkeeping;
        $account_code = $receipt_bookkeeping_module->get_income_account_code_from_hub_account($payment_method, $this->order_model->get_backoffice_account());
        return is_int($account_code) ? $account_code : null;
    }
    /**
     * Get bookkeeping article account code by product.
     *
     * @param WC_Product $product Product.
     *
     * @return int|null
     */
    protected function get_article_account_code_by_product(WC_Product $product): null|int
    {
        $aac = Article_Account_Codes::get_article_account_code_by_product($product, $this->order_model->get_backoffice_account());
        return is_int($aac) ? $aac : null;
    }
    /**
     * Get Hub customer number.
     *
     * @return string
     */
    protected function get_customer_number(): string
    {
        $customer = $this->order_model->get_customer();
        if (!empty($customer)) {
            if (empty($customer->CustomerNumber)) {
                $customer->CustomerNumber = Customer_Sync::get_hub_customer_number($customer, $this->order_model->get_backoffice_account());
                if (!$customer->CustomerNumber) {
                    $customer->CustomerNumber = '';
                }
            }
        }
        return $customer->CustomerNumber;
    }
    /**
     * Create an article from order item product.
     *
     * @param WC_Order_Item_Product|WC_Order_Item $order_item            Order item.
     * @param int|null                            $order_reference_index Reference to order.
     * @param string                              $coupon_name           The coupon name is only used if the article is discounted.
     * @param bool                                $include_bookkeeping   Whether to generate bookkeeping, which includes API request.
     *
     * @return Receipt_Article_Model|null Article on success, else null.
     */
    public function maybe_create_article(WC_Order_Item_Product|WC_Order_Item $order_item, ?int $order_reference_index, string $coupon_name, bool $include_bookkeeping = \true): ?Receipt_Article_Model
    {
        $product = $order_item->get_product();
        if (!is_a($product, 'WC_Product')) {
            return null;
            // Product can't be found, abort.
        }
        $vat = !$this->is_for_refund ? $this->get_order_row_tax_rate($order_item, $this->wc_order) : $this->get_order_row_tax_rate($order_item, $this->parent_order);
        $price = (float) $product->get_regular_price();
        if ('no' === get_option('woocommerce_prices_include_tax')) {
            $tax_amount = $price * ($vat / 100);
            $price = $price + $tax_amount;
        }
        $sales_price = $this->wc_order->get_item_total($order_item, \true);
        $discount_amount = 0;
        if ($sales_price < $price) {
            $discount_amount = abs($price - $this->wc_order->get_item_total($order_item, \true, \false));
        }
        $attributes = array();
        $serial_number = $this->get_serial_number($product);
        if ($serial_number) {
            /**
             * Misspelling of "Attribute" is from API.
             *
             * @noinspection SpellCheckingInspection
             */
            $attributes[] = array('AttrbuteType' => 'SerialNumber', 'Value' => $serial_number);
        }
        $quantity = $order_item->get_quantity();
        if ($quantity < 0) {
            // Refund rows should contain positive quantities and negative unit prices.
            $quantity *= -1;
            $price *= -1;
        }
        $discounts = array();
        if ($discount_amount) {
            $discounts[] = new Receipt_Discount_Model('amount', $discount_amount * $quantity, $coupon_name, null);
        }
        $article_name = $order_item->get_name();
        if (is_a($product, 'WC_Product_Variation')) {
            // WooCommerce sometimes doesn't add all the proper attributes to the name of the variation. This will force add them to the title.
            $possible_attributes = array_keys($product->get_attributes());
            $title_base = get_post_field('post_title', $product->get_parent_id());
            $meta_data = $order_item->get_formatted_meta_data();
            $meta_data = array_filter($meta_data, function ($v) use ($possible_attributes) {
                return in_array($v->key, $possible_attributes, \true);
            });
            if (!empty($meta_data)) {
                $meta_data = array_map(function ($v) {
                    return $v->value;
                }, $meta_data);
                $title_separator = ' - ';
                $attribute_separator = ', ';
                $title_suffix = trim(wp_strip_all_tags(implode($attribute_separator, $meta_data)));
                if ($title_suffix && $title_base) {
                    if ($title_base === $article_name) {
                        // Attributes are missing from the article name entirely.
                        $article_name = $article_name . $title_separator . $title_suffix;
                    } else {
                        // Some attributes are in the title.
                        $article_name = $article_name . $attribute_separator . $title_suffix;
                    }
                }
            }
        }
        if ($include_bookkeeping) {
            $bookkeeping_account = $this->get_article_account_code_by_product($product);
        } else {
            $bookkeeping_account = null;
        }
        return new Receipt_Article_Model($order_reference_index, $product->get_sku(), $article_name, $quantity, $this->wc_order->get_item_total($order_item, \true), $price, $vat, $this->wc_order->get_line_total($order_item, \true), $this->wc_order->get_line_total($order_item), $order_item->get_total_tax(), $bookkeeping_account, 'product', $order_item->get_taxes(), $discounts, $attributes);
    }
    /**
     * Create a VAT model.
     *
     * @param WC_Order_Item_Tax       $tax_item     Tax item to create VAT for.
     * @param Receipt_Article_Model[] $articles     Articles to create VAT for.
     * @param array                   $vat_accounts Account codes for VAT rates.
     *
     * @return Receipt_Vat_Model|null
     */
    private static function maybe_create_vat(WC_Order_Item_Tax $tax_item, array $articles, array $vat_accounts): ?Receipt_Vat_Model
    {
        $total_amount = 0.0;
        $total_vat = 0.0;
        foreach ($articles as $article) {
            $article_tax_total = $article->get_tax_total($tax_item->get_rate_id());
            if (is_null($article_tax_total)) {
                continue;
            }
            $total_amount += floatval($article->get_total_price_inc_vat());
            $total_vat = $total_vat + NumberUtil::round($article_tax_total, self::RECEIPT_ROUNDING_PRECISION);
        }
        if (!$total_amount) {
            return null;
        }
        $vat_rate = $tax_item->get_rate_percent('edit');
        $vat_account = null;
        if (0 === intval($vat_rate)) {
            $vat_account = intval($vat_accounts[0]);
        } elseif (6 === intval($vat_rate)) {
            $vat_account = intval($vat_accounts[6]);
        } elseif (12 === intval($vat_rate)) {
            $vat_account = intval($vat_accounts[12]);
        } elseif (25 === intval($vat_rate)) {
            $vat_account = intval($vat_accounts[25]);
        }
        if (empty($vat_account)) {
            return null;
        }
        return new Receipt_Vat_Model($vat_rate, $total_vat, $total_amount, $vat_account);
    }
    /**
     * Get the shipping article.
     *
     * @param string   $shipping_sku     Shipping article SKU.
     * @param int|null $shipping_account Shipping article account code.
     *
     * @return Receipt_Article_Model|null Article on success, null if no shipping.
     */
    private function maybe_create_shipping(string $shipping_sku, ?int $shipping_account): ?Receipt_Article_Model
    {
        $shipping_tax_rate = 0.0;
        $shipping_tax = floatval($this->wc_order->get_shipping_tax());
        $shipping_total = floatval($this->wc_order->get_shipping_total()) + $shipping_tax;
        $taxes_totals = null;
        if ($shipping_tax) {
            /**
             * Tax line items.
             *
             * @var WC_Order_Item_Shipping $tax_line
             */
            $tax_lines = $this->wc_order->get_items('shipping');
            if (!empty($tax_lines)) {
                foreach ($tax_lines as $tax_line) {
                    $taxes_totals = $tax_line->get_taxes();
                    $shipping_tax_total = array_reduce($tax_line->get_taxes()['total'], function ($carry, $item) {
                        return $carry + floatval($item);
                    });
                    $shipping_tax_rate = ceil(100 * ((floatval($this->wc_order->get_shipping_total()) + $shipping_tax_total) / floatval($this->wc_order->get_shipping_total()) - 1));
                }
            }
        }
        if (!$shipping_total > 0) {
            return null;
        }
        if (is_null($taxes_totals)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Receipt created with shipping VAT as 0 for order #' . $this->wc_order->get_id(), array(), Log_Level_Enum::WARNING);
            $taxes_totals = array();
        }
        return new Receipt_Article_Model(null, $shipping_sku, __('Shipping', 'northmill-online-shared'), 1, $shipping_total, '', $shipping_tax_rate, $shipping_total, $this->wc_order->get_shipping_total(), $shipping_tax, $shipping_account, 'shipping', $taxes_totals);
    }
    /**
     * Create a voucher for order.
     *
     * @param Receipt_Article_Model[] $articles               Articles to create voucher for.
     * @param Receipt_Vat_Model[]     $vats                   Vats to create voucher for.
     * @param ?int                    $payment_method_account Payment method account code.
     *
     * @return Receipt_Voucher_Model|false
     */
    private function create_voucher(array $articles, array $vats, ?int $payment_method_account): Receipt_Voucher_Model|null
    {
        if (!$payment_method_account) {
            return null;
        }
        $rows = array(array('AccountNumber' => $payment_method_account, 'DebitAmount' => $this->wc_order->get_total(), 'CreditAmount' => 0));
        foreach ($articles as $article) {
            if (floatval($article->get_total_price_ex_vat()) !== 0.0 && ('product' === $article->get_type() || 'shipping' === $article->get_type())) {
                $account_number = $article->get_bookkeeping_account();
                $account_number = is_numeric($account_number) ? (int) $account_number : 0;
                $rows[] = array('AccountNumber' => $account_number, 'DebitAmount' => 0, 'CreditAmount' => $article->get_total_price_ex_vat());
            }
        }
        if (!empty($vats)) {
            foreach ($vats as $vat) {
                if (floatval($vat->get_total_vat()) !== 0.0) {
                    $account_number = $vat->get_bookkeeping_account();
                    $account_number = is_numeric($account_number) ? (int) $account_number : 0;
                    $rows[] = array('AccountNumber' => $account_number, 'DebitAmount' => 0, 'CreditAmount' => $vat->get_total_vat());
                }
            }
        }
        // Adjust Debit and Credit amounts for negative values.
        $rows = array_map(function ($row) {
            $debit_amount = floatval($row['DebitAmount']);
            $credit_amount = floatval($row['CreditAmount']);
            if ($debit_amount < 0) {
                $row['CreditAmount'] = (string) ($credit_amount + abs($debit_amount));
                $row['DebitAmount'] = '0';
            }
            if ($credit_amount < 0) {
                $row['DebitAmount'] = (string) ($debit_amount + abs($credit_amount));
                $row['CreditAmount'] = '0';
            }
            return $row;
        }, $rows);
        // Transform into voucher rows.
        $rows = array_map(function ($row) {
            return new Receipt_Voucher_Row_Model($row['AccountNumber'], $row['DebitAmount'], $row['CreditAmount']);
        }, $rows);
        $voucher_date = !$this->is_for_refund ? $this->wc_order->get_date_paid() : $this->wc_order->get_date_created();
        if (!$this->is_for_refund) {
            $voucher_text = sprintf(
                // translators: %s is the order number.
                __('WooCommerce Order #%s', 'northmill-online-shared'),
                $this->hub_order_number
            );
        } else {
            $voucher_text = sprintf(
                // translators: %1$d is the refund id, %2$s is the order number.
                __('WooCommerce refund (%1$d) of Order #%2$s', 'northmill-online-shared'),
                $this->wc_order->get_id(),
                $this->hub_order_number
            );
        }
        return new Receipt_Voucher_Model(!$this->is_for_refund ? $this->wc_order->get_order_number() : $this->parent_order->get_order_number() . '-refund-' . $this->wc_order->get_id(), $rows, is_null($voucher_date) ? null : $voucher_date->format('Y-m-d\TH:i:s'), $voucher_text);
    }
    /**
     * Create a merchant.
     *
     * @param string $organization_number Organization number.
     * @param string $name          Name of merchant.
     * @param string $address_line1 Address line 1.
     * @param string $address_line2 Address line 2.
     * @param string $zip_code      Zip code.
     * @param string $city          City.
     * @param string $country       Country.
     *
     * @return Receipt_Merchant_Model
     */
    private function create_merchant(string $organization_number, string $name, string $address_line1, string $address_line2, string $zip_code, string $city, string $country): Receipt_Merchant_Model
    {
        return new Receipt_Merchant_Model($organization_number, $name, $address_line1, $address_line2, $zip_code, $city, $country);
    }
    /**
     * Create customer.
     *
     * @param string      $customer_number Customer number.
     * @param string      $full_name       Full name of customer.
     * @param string|null $company_name    Company name.
     *
     * @return Receipt_Customer_Model
     */
    private function create_customer(string $customer_number, string $full_name, ?string $company_name): Receipt_Customer_Model
    {
        return new Receipt_Customer_Model($customer_number, $full_name, 'Customer', $company_name);
    }
    /**
     * Create the receipt payment model.
     *
     * @param float|string $amount               Payment amount.
     * @param string       $order_payment_method Payment method.
     * @param array        $text_rows            Text rows to add to the receipt model. Pass by reference.
     *
     * @return Receipt_Payment_Model
     */
    private function create_payment(float|string $amount, string $order_payment_method, array &$text_rows): Receipt_Payment_Model
    {
        $transaction_id = !$this->is_for_refund ? $this->wc_order->get_transaction_id() : $this->parent_order->get_transaction_id();
        switch (strtolower($order_payment_method)) {
            case Payment_Methods::NMC_BNPL:
                $receipt_payment_type = 'NorthmillBNPL';
                $receipt_payment_name = __('Pay later', 'northmill-online-shared');
                $row1 = new stdClass();
                $row1->RowNum = 0;
                $row1->Text = __('Pay later transaction ID', 'northmill-online-shared');
                $text_rows[] = $row1;
                $row2 = new stdClass();
                $row2->RowNum = 1;
                $row2->Text = $transaction_id;
                $text_rows[] = $row2;
                break;
            case Payment_Methods::NMC_SWISH:
                $receipt_payment_type = 'Swish';
                $receipt_payment_name = __('Swish', 'northmill-online-shared');
                break;
            case Payment_Methods::NMC_NMI:
                $receipt_payment_type = 'CardPayment';
                $receipt_payment_name = __('Card Payment', 'northmill-online-shared');
                break;
            default:
                $receipt_payment_type = 'NamedPayment';
                $receipt_payment_name = __('Online Payment', 'northmill-online-shared');
                break;
        }
        return new Receipt_Payment_Model($receipt_payment_name, $amount, $receipt_payment_type, $transaction_id, $order_payment_method);
    }
    /**
     * Create a receipt order reference model.
     *
     * @param int         $index              Index of order reference.
     * @param string      $order_number       Order number.
     * @param string|null $order_change_state Order change state.
     * @param string      $origin             Order origin.
     *
     * @return Receipt_Order_Reference_Model
     */
    private static function create_order_reference(int $index, string $order_number, ?string $order_change_state, string $origin): Receipt_Order_Reference_Model
    {
        return new Receipt_Order_Reference_Model($index, $order_number, $order_change_state, $origin);
    }
    /**
     * Get tax percentage for order item of order.
     *
     * @param WC_Order_Item_Product $order_item Order item to get tax percentage for.
     * @param WC_Order              $order      Order that the item refers to.
     *
     * @return float
     */
    protected function get_order_row_tax_rate(WC_Order_Item_Product $order_item, WC_Order $order): float
    {
        $tax_percentage = 0;
        $tax_class = $order_item->get_tax_class();
        $args = array('country' => $order->get_billing_country(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'city' => $order->get_billing_city(), 'tax_class' => $tax_class);
        if (empty($args['country'])) {
            // Default to base.
            $args['country'] = WC()->countries->get_base_country();
            $args['state'] = WC()->countries->get_base_state();
            $args['postcode'] = WC()->countries->get_base_postcode();
            $args['city'] = WC()->countries->get_base_city();
        }
        $tax_data = WC_Tax::find_rates($args);
        if (!empty($tax_data)) {
            foreach ($tax_data as $tax_rate) {
                $tax_percentage = $tax_rate['rate'];
            }
        }
        return (float) $tax_percentage;
    }
    /**
     * Gets the serial number of a product.
     *
     * @param WC_Product $product Product to get serial number for.
     *
     * @return string Serial number, empty string on not found.
     */
    protected function get_serial_number(WC_Product $product): string
    {
        return is_a($product, 'WC_Product_Variation') ? get_post_meta($product->get_parent_id(), '_custom_product_barcodes_field', \true) : get_post_meta($product->get_id(), '_custom_product_barcodes_field', \true);
    }
}
