<?php

namespace routes;

use classes\authentication;
use classes\slack;
use Exception;
use objects\customer_vehicles_o;
use objects\logs_o;
use objects\order_items_o;
use objects\orders_o;
use objects\products_o;
use objects\users_o;
use traits\route_t;

class InvoicingPeriodRoute
{
    use route_t;


    /**
     * @param array $collective_results
     * @return array
     * @throws Exception
     */
    private static function parseTheDepartmentIdsToDepartmentNames(array $collective_results): array
    {
        foreach ( $collective_results['total_department_totals'] as $department_id => $amount ) {
            $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value();
            if (empty($department_name)) {
                $department_name = 'Unknown Department (' . $department_id . ')';
            }
            $collective_results['total_department_totals_parsed'][$department_name] = $amount;
        }
        foreach ( $collective_results['total_department_totals_relative'] as $department_id => $amount ) {
            $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value();
            if (empty($department_name)) {
                $department_name = 'Unknown Department (' . $department_id . ')';
            }
            $collective_results['total_department_totals_relative_parsed'][$department_name] = $amount;
        }
        return $collective_results;
    }

    public function run(): void
    {
        $this->get('/superuser/invoicing/period', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                self::requireParameters([
                    'dateFrom',
                    'dateTo',
                ]);
                $dateFrom = $this->getParameter('dateFrom');
                $dateTo = $this->getParameter('dateTo');
                // Require the dateFrom and dateTo parameters to be valid dates
                self::requireDateFormat($dateFrom, 'Y-m-d');
                self::requireDateFormat($dateTo, 'Y-m-d');
                // Add a day to the dateTo parameter to include the end date in the range
                $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
                // Get the invoicing period for the user
                $response->success([...self::getInvoicingPeriod($dateFrom, $dateTo)]);
            } else {
                // Log the incident
                (new logs_o())->add('vehicles', 'global', 1, 0, 'LIST_OWN_VEHICLES', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () {
            // Require the user to be logged in
            global $response;
           $this->requirePermission('superuser_invoicing_period');
            self::requireParameters([
                'dateFrom',
                'dateTo',
            ]);
            $dateFrom = $this->getParameter('dateFrom');
            $dateTo = $this->getParameter('dateTo');
            // Require the dateFrom and dateTo parameters to be valid dates
            self::requireDateFormat($dateFrom, 'Y-m-d');
            self::requireDateFormat($dateTo, 'Y-m-d');
            // Add a day to the dateTo parameter to include the end date in the range
            $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
            $response->success([...self::getOriginalPrice(self::getFixedPricing($dateFrom, $dateTo))]);
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            self::requireParameters([
                'dateFrom',
                'dateTo',
            ]);
            $dateFrom = $this->getParameter('dateFrom');
            $dateTo = $this->getParameter('dateTo');
            // Require the dateFrom and dateTo parameters to be valid dates
            self::requireDateFormat($dateFrom, 'Y-m-d');
            self::requireDateFormat($dateTo, 'Y-m-d');
            // Add a day to the dateTo parameter to include the end date in the range
            $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
            $response->success([...self::getTransactionsWithItemsNotIncludedInInvoices(self::getVehicleSubscriptions($dateFrom, $dateTo))]);
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/wash-subscriptions/historical', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            self::requireParameters([
                'dateFrom',
                'dateTo',
            ]);
            $dateFrom = $this->getParameter('dateFrom');
            $dateTo = $this->getParameter('dateTo');
            // Require the dateFrom and dateTo parameters to be valid dates
            $this->requireDateFormat($dateFrom, 'Y-m-d');
            $this->requireDateFormat($dateTo, 'Y-m-d');
            $dateFrom = date('Y-m-d 00:00:00', strtotime($dateFrom));
            // Add a day to the dateTo parameter to include the end date in the range
            $dateTo = date('Y-m-d 23:59:59', strtotime($dateTo));
            // Get all orders in the date range that have:
            // - Department ID: 10
            // - Reference: Vaskeabonnementer
            $orders_o = new orders_o();
            $orders = $orders_o->getFieldsWhere([
                'department_id' => 10,
                'reference' => 'Vaskeabonnementer',
            ], ['id', 'created_at']);
            // Filter out the orders to only include those in the date range
            $orders = array_filter($orders, function ($order) use ($dateFrom, $dateTo) {
                $order_date = strtotime($order['created_at']);
                return $order_date >= strtotime($dateFrom) && $order_date <= strtotime($dateTo);
            });
            // Construct the order objects
            $orders = array_map(function ($order) {
                return (new orders_o())->select((int)$order['id']);
            }, $orders);
            // Filter out deleted orders
            $orders = array_filter($orders, function ($order) {
                return $order->deleted_at->value() === null;
            });
            // Define the warnings variable
            $warnings = [];
            // Define logs variable
            $logs = [];
            // Define the customer variable
            $unique_customers = [];
            foreach ( $orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                if (!in_array($customer_id, $unique_customers)) {
                    $unique_customers[] = $customer_id;
                } else {
                    $warnings[] = "Duplicate order for customer ID " . $customer_id . " in order ID " . $order->id;
                }
            }
            // Define the total subscription amount
            $total_subscription_amount = 0;
            // Define the collection of order items for all subscriptions
            $all_subscription_order_items = [];
            // Add up the total subscription amount and collect the order items
            /** @var orders_o $order */
            foreach ( $orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                $logs[] = "[{$order->id}] Processing order items for customer ID {$customer_id}";
                // Get the order items for the order
                $tmp_items = $order->getOrderItemObjects();
                /** @var order_items_o $item */
                foreach ( $tmp_items as $item ) {
                    $price = (int)$item->price->value();
                    $amount = (int)$item->quantity->value();
                    $total_subscription_amount += $price * $amount;
                    $all_subscription_order_items[] = $item;
                    $logs[] = "[{$order->created_at->value()}] Processing order item ID {$item->id}, price: {$price}, quantity: {$amount}, subtotal: +" . ($price * $amount) . " (total: {$total_subscription_amount}, items: " . count($all_subscription_order_items) . ")";
                }
            }
            unset($tmp_items, $item, $order, $customer_id, $price, $amount);
            // Define the total fixed pricing amount
            $total_fixed_pricing_amount = 0;
            $all_fixed_pricing_orders = [];
            $all_fixed_pricing_order_items = [];
            // Get all fixed pricing orders in the date range
            $fixed_pricing_orders = $orders_o->getFieldsWhere([
                'department_id' => 10,
                'reference' => 'Fast pris aftale',
            ], ['id', 'created_at']);
            // Filter out the orders to only include those in the date range
            $fixed_pricing_orders = array_filter($fixed_pricing_orders, function ($order) use ($dateFrom, $dateTo) {
                $order_date = strtotime($order['created_at']);
                $dateFrom = date('Y-m-d 00:00:00', strtotime($dateFrom));
                $dateToMonthEnd = date('Y-m-d 23:59:59', strtotime($dateTo));
                return $order_date >= strtotime($dateFrom) && $order_date <= strtotime($dateToMonthEnd);
            });
            // Construct the order objects
            $fixed_pricing_orders = array_map(function ($order) {
                return (new orders_o())->select((int)$order['id']);
            }, $fixed_pricing_orders);
            // Filter out deleted orders
            $fixed_pricing_orders = array_filter($fixed_pricing_orders, function ($order) {
                /** @var orders_o $order */
                return !$order->deleted_at->value();
            });
            $unique_customers_fixed_pricing = [];
            // Add up the total fixed pricing amount and collect the order items
            foreach ( $fixed_pricing_orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                $logs[] = "[{$order->id}] Processing fixed pricing order items for customer ID {$customer_id}";
                if (!in_array($customer_id, $unique_customers_fixed_pricing)) {
                    $unique_customers_fixed_pricing[] = $customer_id;
                } else {
                    $warnings[] = "Duplicate fixed pricing order for customer ID " . $customer_id . " in order ID " . $order->id;
                }
                $all_fixed_pricing_orders[] = $order;
                // Get the order items for the order
                $tmp_items = $order->getOrderItemObjects();
                /** @var order_items_o $item */
                foreach ( $tmp_items as $item ) {
                    $price = (int)$item->price->value();
                    $amount = (int)$item->quantity->value();
                    $total_fixed_pricing_amount += $price * $amount;
                    $all_fixed_pricing_order_items[] = $item;
                    $logs[] = "[{$order->created_at->value()}] Processing fixed pricing order item ID {$item->id}, price: {$price}, quantity: {$amount}, subtotal: +" . ($price * $amount) . " (total fixed pricing: {$total_fixed_pricing_amount}, items: " . count($all_fixed_pricing_order_items) . ")";
                }
            }

            // Get the total combined amount
            $total_combined_amount = $total_subscription_amount + $total_fixed_pricing_amount;

            // Return the orders
            $response->success([
                "hello" => "world",
                "orders" => count($orders),
                "unique_customers" => count($unique_customers),
                "warnings" => $warnings,
                "logs" => $logs,
                "total_subscription_amount" => $total_subscription_amount,
                "all_subscription_order_items" => count($all_subscription_order_items),
                "total_fixed_pricing_amount" => $total_fixed_pricing_amount,
                "all_fixed_pricing_order_items" => count($all_fixed_pricing_order_items),
                "total_combined_amount" => $total_combined_amount,
                "unique_customers_fixed_pricing" => count($unique_customers_fixed_pricing),
                "fixed_pricing_orders" => count($fixed_pricing_orders),
            ]);
        });
    }

    /**
     * Get the transactions with items, that are not included in invoices. (include_in_invoice = 0 - order_items)
     * Requirements:
     * - The transaction must not be deleted. (deleted_at is null - orders))
     * - The transaction contains at least one item that is not included in invoices. (include_in_invoice = 0 - order_items)
     * - The transaction must be within the specified date range. (created_at between dateFrom and dateTo - orders)
     * - The transaction item must not be deleted. (deleted_at is null - order_items)
     * Returns an array of customers with their transactions and items.
     * @param array $customersWithSubscriptions The customers with subscriptions.
     * @param array $dateRange The date range to filter the transactions. (dateFrom, dateTo)
     * @return array The customers with their transactions and items.
     * @throws Exception
     * @example
     * [
     *   [
     *     'customer_number' => 12345678,
     *     'customer_name' => 'Customer Name',
     *     'transactions' => [
     *       [
     *         'id' => 1,
     *         'date' => '2023-01-01',
     *         'amount' => 100.00,
     *         'booked' => true,
     *       ],
     *       [
     *         'id' => 2,
     *         'date' => '2023-01-02',
     *         'amount' => 200.00,
     *         'booked' => false,
     *       ],
     *     ],
     *     'requires_action' => false,
     *     'meta' => [
     *       'subscription' => [
     *         'id' => 1,
     *         'name' => 'Subscription Name',
     *         'price' => 100.00,
     *         'description' => 'Subscription Description',
     *       ],
     *     ],
     *   ],
     *   [
     *     'customer_number' => 87654321,
     *     'customer_name' => 'Another Customer',
     *     'transactions' => [
     *       [
     *         'id' => 3,
     *         'date' => '2023-01-03',
     *         'amount' => 300.00,
     *         'booked' => true,
     *       ],
     *     ],
     *     'requires_action' => true,
     *     'meta' => [
     *       'subscription' => [
     *         'id' => 2,
     *         'name' => 'Another Subscription',
     *         'price' => 200.00,
     *         'description' => 'Another Description',
     *       ],
     *     ],
     *   ],
     * ]
     * @see orders_o::getTransactionsWithItemsNotIncludedInInvoices
     * @see order_items_o::include_in_invoice
     * @see orders_o::deleted_at
     * @see order_items_o::deleted_at
     * @see orders_o::created_at
     * @see orders_o::customer_id
     * @see self::getVehicleSubscriptions()
     */
    private static function getTransactionsWithItemsNotIncludedInInvoices(array $customersWithSubscriptions): array
    {
        global $response;
        // Loop through each customer and get their transactions with items not included in invoices
        foreach ( $customersWithSubscriptions as &$customer ) {
            // Initialize the meta['subscription'] array if it doesn't exist
            if (!isset($customer['meta']['subscription'])) {
                $customer['meta']['subscription'] = [
                    'vehicles' => [], // Array of vehicle registration numbers
                    'subscription_total' => 0, // Total price of the subscriptions for the customer
                    'subscriptions' => [], // Array of subscriptions for the customer (['registration' => ['type' => (int), 'price' => (int), 'distributions' => <(int: departmentId)>[]])
                    'subscription_price_department_distribution' => [
                        // departmentId => price / (number of unique distributions)
                        // This is calculated after the subscriptions have been added
                        // This is used to distribute the subscription price across the transactions
                    ]
                ];
            }
            // Get the vehicles for the customer
            $vehicles_o = new customer_vehicles_o();
            // Get the vehicles for the customer (as an array of customer_vehicles_o objects)
            $customer_vehicles = array_map(function ($vehicle) {
                return (new customer_vehicles_o())->select((int)$vehicle['id']);
            }, $vehicles_o->getFieldsWhere([
                'customer_id' => (int)$customer['customer_number'],
                'wash_subscription' => 1, // Only get vehicles with a wash subscription
            ], ['id']));
            // Add the vehicle registration numbers to the meta['subscription']['vehicles'] array
            foreach ( $customer_vehicles as $vehicle ) {
                if ($vehicle instanceof customer_vehicles_o) {
                    $customer['meta']['subscription']['vehicles'][] = $vehicle->reg->value();
                } else {
                    $customer['meta']['subscription']['vehicles'][] = 'Unknown Vehicle';
                }
            }
            // Calculate the total price of the subscriptions for the customer
            foreach ( $customer_vehicles as $vehicle ) {
                if ($vehicle instanceof customer_vehicles_o) {
                    // Get the transaction ids covered by the subscription for the vehicle
                    // This is done to be able to distribute the subscription price across the transactions
                    // We only want to get the transactions that are in the customer's transactions array, (this is to avoid getting transactions that are outside the date range)
                    // We also want to make sure that the transactions are for the correct vehicle (reg_1 = vehicle reg), and that the transactions are not deleted (deleted_at is null)
                    $transaction_ids_covered_by_subscription = array_map(function ($transaction) {
                        return (int)$transaction;
                    }, $vehicle->getSubscriptionAppliedTransactionsFromList(array_map(
                        function ($transaction) {
                            return (int)$transaction['id'];
                        }, (new orders_o())->getFieldsWhereIn([
                        'id' => array_map(function ($transaction) {
                            return (int)$transaction['id'];
                        }, $customer['transactions']),
                        'reg_1' => $vehicle->reg->value(),
                    ], ['id'])
                    )));
                    // Get the subscription details for the vehicle
                    $subscription = [
                        'type' => (int)$vehicle->type->value(),
                        'price' => (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice(),
                        // Get the unique department ids for the transactions covered by the subscription
                        'distributions' => array_unique(array_map(function ($transaction_id) {
                            // Get the department id for the transaction(s) covered by the subscription
                            $transaction = (new orders_o())->select((int)$transaction_id);
                            return (int)$transaction->department_id->value();
                        }, $transaction_ids_covered_by_subscription)),
                    ];
                    $customer['meta']['subscription']['subscriptions'][$vehicle->reg->value()] = $subscription;
                    $customer['meta']['subscription']['subscription_total'] += $subscription['price'];
                    // Add the subscription price per transaction to the meta['subscription']['subscription_price_department_distribution'] array
                    foreach ( $subscription['distributions'] as $department_id ) {
                        if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                            $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                        }
                        // Divide the subscription price by the number of unique distributions
                        $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription['price'] / count($subscription['distributions']);
                    }
                    // If the vehicle has no transactions covered by the subscription, we need to run through the list of fallback options
                    if (count($transaction_ids_covered_by_subscription) === 0) {
                        // Run fallback options
                        self::attemptSubscriptionFallbacks($vehicle, $customer);
                    }
                }
            }
        }
        // Get the department distribution for all customers (sum of all customers)
        $collective_results = [
            'total_subscription_price' => 0,
            'subscription_price_department_distribution' => [
                // departmentId => price
            ],
        ];
        unset($customer); // Unset the reference to avoid issues
        foreach ( $customersWithSubscriptions as $customer ) {
            if (isset($customer['meta']['subscription'])) {
                $collective_results['total_subscription_price'] += $customer['meta']['subscription']['subscription_total'];
                foreach ( $customer['meta']['subscription']['subscription_price_department_distribution'] as $department_id => $price ) {
                    if (!isset($collective_results['subscription_price_department_distribution'][$department_id])) {
                        $collective_results['subscription_price_department_distribution'][$department_id] = 0;
                    }
                    $collective_results['subscription_price_department_distribution'][$department_id] += $price;
                }
                // Verify that the sum of the department distribution equals the total subscription price for the customer
                $sum_of_distribution = array_sum($customer['meta']['subscription']['subscription_price_department_distribution']);
                $difference = $customer['meta']['subscription']['subscription_total'] - $sum_of_distribution;
                if (abs($difference) > 0.01) {
                    // Debug info
                    $debug_info = [
                        'customer_number' => $customer['customer_number'],
                        'subscription_total' => $customer['meta']['subscription']['subscription_total'],
                        'sum_of_distribution' => $sum_of_distribution,
                        'difference' => $difference,
                    ];
                    // Send slack alert
                    $message = "Subscription price distribution mismatch for customer " . $customer['customer_number'] . " (" . (new users_o())->getCustomerName((int)$customer['customer_number']) . ")\n";
                    $message .= "Subscription total: " . $customer['meta']['subscription']['subscription_total'] . "\n";
                    $message .= "Distribution total: " . $sum_of_distribution . "\n";
                    $message .= "Difference: " . $difference . "\n";
                    $message .= "Debug info: " . print_r($debug_info, true);
                    // Describe what to check
                    (new slack())->send_message($message, 'Subscription Price Distribution Mismatch');
                    // Throw an error
                    throw new Exception('Subscription price distribution does not equal total subscription price for customer ' . $customer['customer_number'] . '. Difference: ' . $difference);
                }
            }
        }
        // Parse the department ids to department names
        $parsed_distribution = [];
        foreach ( $collective_results['subscription_price_department_distribution'] as $department_id => $price ) {
            $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value();
            if (empty($department_name)) {
                $department_name = 'Unknown Department (' . $department_id . ')';
            }
            $parsed_distribution[$department_name] = $price;
        }
        $collective_results['subscription_price_department_distribution_parsed'] = $parsed_distribution;
        // Include the collective results in the response
        $response->add_include('collective_subscription_results', $collective_results);
        // Send summary to slack
        $slack_message = "Subscription Price Distribution Summary:\n";
        $slack_message .= "Total Subscription Price: " . $collective_results['total_subscription_price'] . "\n";
        $slack_message .= "Department Distribution:\n";
        $tmp_total = 0;
        foreach ( $collective_results['subscription_price_department_distribution_parsed'] as $department_name => $price ) {
            $slack_message .= "- " . $department_name . ": " . $price . "\n";
            $tmp_total += $price;
        }
        $slack_message .= "Total Distribution: " . $tmp_total . "\n";
        (new slack())->send_message($slack_message, 'Subscription Price Distribution Summary');
        return $customersWithSubscriptions;
    }

    /**
     * Attempt fallback options to find transactions covered by the subscription.
     * This is used when a vehicle has a subscription, but no transactions in the given date range.
     * This function processes the following fallback options:
     * 1. Attempt to divide the subscription price across the departments the customer has subscription transactions in.
     * 2. Attempt to get the last transaction the vehicle was washed in, and use that department.
     * 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer.
     * 4. If no departments are found, assign the subscription to a default department (e.g., department ID 1).
     * @param customer_vehicles_o $vehicle The vehicle object.
     * @param array $customer The customer object (by reference).
     * @retuns bool True if a fallback was applied, false otherwise.
     * @throws Exception If an error occurs while processing the fallbacks.
     */
    private static function attemptSubscriptionFallbacks(customer_vehicles_o $vehicle, array &$customer): bool
    {
        if (self::debug) echo "Attempting fallbacks for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] .  " - " . (new users_o())->getCustomerName((int)$customer['customer_number']) . "\n";
        // Run the fallback options in order
        $subscription_price = (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice();
        if (self::divideSubscriptionAcrossCustomerDepartments($customer, $subscription_price)) {
            return true;
        }
        if (self::useLastTransactionDepartment($customer, $vehicle, $subscription_price)) {
            return true;
        }
        if (self::useRecentCustomerTransactions($customer, $subscription_price)) {
            return true;
        }
        if (self::useDefaultDepartment($customer, $subscription_price)) {
            return true;
        }
        // If no fallback was applied, return false
        if (self::debug) echo "All fallbacks failed for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] . "\n";
        return false; // No fallback applied
    }
    const debug = false;
    // 1. Attempt to divide the subscription price across the departments the customer has transactions in.
    private static function divideSubscriptionAcrossCustomerDepartments(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 1: Dividing subscription price across customer departments\n";
        // Get the unique department ids for the customer's transactions
        $department_ids = array_unique(array_map(/**
         * @throws Exception
         */ function ($transaction) {
            $transaction_obj = (new orders_o())->select((int)$transaction['id']);
            return (int)$transaction_obj->department_id->value();
        }, $customer['transactions']));
        // Remove department id 10 (automatic) from the list
        $department_ids = array_filter($department_ids, function ($department_id) {
            return $department_id !== 10;
        });
        if (count($department_ids) > 0) {
            foreach ( $department_ids as $department_id ) {
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Divide the subscription price by the number of unique departments
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription_price / count($department_ids);
            }
            // Debug:
            if (self::debug) echo "Fallback 1 applied: Divided subscription price across customer departments. Departments: " . implode(', ', $department_ids) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 1 not applied: No departments found in customer's transactions\n";
        return false; // No departments found
    }
    // 2. Attempt to get the last transaction the vehicle was washed in, and use that department.

    /**
     * @throws Exception
     */
    private static function useLastTransactionDepartment(array &$customer, customer_vehicles_o $vehicle, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 2: Using last transactions departments\n";
        // Get the last transaction the vehicle was washed in
        $last_transaction_ids = $vehicle->getLastTransactions(10); // Get the last 10 transactions
        $distribution_department_ids = []; // Array to hold the department ids from the last transactions, together with their counts (1 = 10%, 2 = 20%, etc.)
        // Calculate the department distribution from the last transactions
        foreach ( $last_transaction_ids as $transaction_id ) {
            $transaction = (new orders_o())->select((int)$transaction_id);
            if ($transaction instanceof orders_o) {
                $department_id = (int)$transaction->department_id->value();
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($distribution_department_ids[$department_id])) {
                    $distribution_department_ids[$department_id] = 0;
                }
                $distribution_department_ids[$department_id]++;
            }
        }
        // If we have department ids from the last transactions, use them to distribute the subscription price
        if (count($distribution_department_ids) > 0) {
            $total_counts = array_sum($distribution_department_ids);
            foreach ( $distribution_department_ids as $department_id => $count ) {
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Distribute the subscription price based on the count of the department in the last transactions
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price;
            }
            // Debug:
            if (self::debug) echo "Fallback 2 applied: Used last transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 2 not applied: No last transaction found for vehicle\n";
        return false; // No last transaction found
    }
    // 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer.
    private static function useRecentCustomerTransactions(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 3: Using recent customer transactions\n";
        // Get the last 10 transactions of the customer
        $recent_transaction_ids = array_slice(array_map(function ($transaction) {
            return (int)$transaction['id'];
        }, (new orders_o())->getFieldsWhere([
            'customer_id' => (int)$customer['customer_number'],
            'deleted_at' => null,
        ], ['id', 'created_at'])), 0, 10);
        $distribution_department_ids = []; // Array to hold the department ids from the recent transactions, together with their counts (1 = 10%, 2 = 20%, etc.)
        // Calculate the department distribution from the recent transactions
        foreach ( $recent_transaction_ids as $transaction_id ) {
            $transaction = (new orders_o())->select((int)$transaction_id);
            if ($transaction instanceof orders_o) {
                $department_id = (int)$transaction->department_id->value();
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($distribution_department_ids[$department_id])) {
                    $distribution_department_ids[$department_id] = 0;
                }
                $distribution_department_ids[$department_id]++;
            }
        }
        // If we have department ids from the recent transactions, use them to distribute the subscription price
        if (count($distribution_department_ids) > 0) {
            $total_counts = array_sum($distribution_department_ids);
            foreach ( $distribution_department_ids as $department_id => $count ) {
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Distribute the subscription price based on the count of the department in the recent transactions
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price;
            }
            // Debug:
            if (self::debug) echo "Fallback 3 applied: Used recent customer transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 3 not applied: No recent transactions found for customer\n";
        return false; // No recent transactions found
    }
    // 4. If no departments are found, assign the subscription to the customers default department (e.g., department ID 1).
    private static function useDefaultDepartment(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 4: Using default department\n";
        $default_department_id = (new users_o)->getUserByCustomerNumber((int)$customer['customer_number'])->getDefaultDepartment();
        if (!empty($default_department_id)) {
            if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id])) {
                $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] = 0;
            }
            $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] += $subscription_price;
            // Debug:
            if (self::debug) echo "Fallback 4 applied: Used default department ID " . $default_department_id . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 4 not applied: No default department found for customer\n";
        return false; // No default department found
    }

    /**
     * Get the original price for each customer, and transaction in the fixed pricing array.
     *
     * @param array $fixed_pricing The fixed pricing array.
     * @return array The fixed pricing array with the original price added.
     * @throws Exception
     */
    private static function getOriginalPrice(array $fixed_pricing): array
    {
        global $response;
        foreach ( $fixed_pricing as &$customer ) {
            if (isset($customer['meta']['fixed_pricing'])) {
                $original_price = 0;
                $department_totals = []; // Array to hold totals per department
                foreach ( $customer['transactions'] as $transaction ) {
                    $transaction_obj = (new orders_o())->select((int)$transaction['id']);
                    // Check if the transaction is included in invoicing
                    if (!$transaction_obj->isIncludedInInvoicing()) {
                        continue; // Skip transactions not included in invoicing
                    }
                    // Get the transaction department id
                    $department_id = (int)$transaction_obj->department_id->value();
                    // If the department id is 10 (automatic), skip it
                    if ($department_id === 10) {
                        continue;
                    }
                    $transaction['original_price'] = $transaction_obj->getNetAmountForOrderItemsOriginal();
                    $original_price += $transaction['original_price'];
                    // Initialize the department total if it doesn't exist
                    if (!isset($department_totals[$department_id])) {
                        $department_totals[$department_id] = 0;
                    }
                    // Add the transaction amount to the department total
                    $department_totals[$department_id] += $transaction['original_price'];
                }
                $customer['meta']['fixed_pricing']['original_price'] = $original_price;
                $customer['meta']['fixed_pricing']['department_totals'] = $department_totals;
                // Take the relative price of the department totals in relation to the fixed price
                // This is done to see how much each department contributes to the fixed price
                $total = array_sum($department_totals);
                foreach ( $department_totals as $department_id => $amount ) {
                    if ($total > 0) {
                        $department_totals[$department_id] = ($amount / $total) * $customer['meta']['fixed_pricing']['price'];
                    } else {
                        $department_totals[$department_id] = 0;
                    }
                }
                $customer['meta']['fixed_pricing']['department_totals_relative'] = $department_totals;
            }
        }
        // Calculate the total original price for all customers
        $collective_results = [
            'total_fixed_price' => 0,
            'total_original_price' => 0,
            'total_department_totals' => [],
            'total_department_totals_relative' => [],
        ];
        unset($customer); // Unset the reference to avoid issues
        foreach ( $fixed_pricing as $customer ) {
            if (isset($customer['meta']['fixed_pricing'])) {
                $collective_results['total_fixed_price'] += $customer['meta']['fixed_pricing']['price'];
                $collective_results['total_original_price'] += $customer['meta']['fixed_pricing']['original_price'];
                // Sum the department totals
                foreach ( $customer['meta']['fixed_pricing']['department_totals'] as $department_id => $amount ) {
                    if (!isset($collective_results['total_department_totals'][$department_id])) {
                        $collective_results['total_department_totals'][$department_id] = 0;
                    }
                    $collective_results['total_department_totals'][$department_id] += $amount;
                }
                // Sum the relative department totals
                foreach ( $customer['meta']['fixed_pricing']['department_totals_relative'] as $department_id => $amount ) {
                    if (!isset($collective_results['total_department_totals_relative'][$department_id])) {
                        $collective_results['total_department_totals_relative'][$department_id] = 0;
                    }
                    $collective_results['total_department_totals_relative'][$department_id] += $amount;
                }
            }
        }
        // Parse the department ids to department names
        $collective_results = self::parseTheDepartmentIdsToDepartmentNames($collective_results);
        // Include the collective results in the response
        $response->add_include('collective_fixed_pricing_results', $collective_results);
        // Send slack message with the collective results
        $slack_message = "Fixed Pricing Invoicing Period Summary:\n";
        $slack_message .= "Total Fixed Price: " . number_format($collective_results['total_fixed_price'], 2) . " DKK\n";
        $slack_message .= "Total Original Price: " . number_format($collective_results['total_original_price'], 2) . " DKK\n";
        $slack_message .= "Department Totals:\n";
        $tmp_sum = 0;
        foreach ( $collective_results['total_department_totals_parsed'] as $department_name => $amount ) {
            $slack_message .= "- " . $department_name . ": " . number_format($amount, 2) . " DKK\n";
            $tmp_sum += $amount;
        }
        $slack_message .= "Total Department Totals: " . number_format($tmp_sum, 2) . " DKK\n";
        $slack_message .= "Relative Department Totals:\n";
        $tmp_sum = 0;
        foreach ( $collective_results['total_department_totals_relative_parsed'] as $department_name => $amount ) {
            $slack_message .= "- " . $department_name . ": " . number_format($amount, 2) . " DKK\n";
            $tmp_sum += $amount;
        }
        $slack_message .= "Total Relative Department Totals: " . number_format($tmp_sum, 2) . " DKK\n";
        (new slack())->send_message($slack_message, 'Fixed Pricing Invoicing Period Summary');
        return $fixed_pricing;
    }

    /**
     * @throws Exception
     */
    private static function getInvoicingPeriod(string $dateFrom, string $dateTo): array
    {
        //$customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo)

        $customersWithTransactions = self::debugGetTime(function () use ($dateFrom, $dateTo) {
            return self::getCustomersWithTransactions($dateFrom, $dateTo);
        }, 'customers_with_transactions');
        $types = [];
        // Add the customers with transactions to the types array
        $types['all'] = $customersWithTransactions;
        $types['vehicle_subscriptions'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getVehicleSubscriptions($dateFrom, $dateTo, $customersWithTransactions);
        }, 'vehicle_subscriptions');
        $types['fixed_pricing'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getFixedPricing($dateFrom, $dateTo, $customersWithTransactions);
        }, 'fixed_pricing');
        $types['tank_cleaning'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getTankCleaning($dateFrom, $dateTo, $customersWithTransactions);
        }, 'tank_cleaning');
        $types['special_arrangements'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getSpecialArrangements($dateFrom, $dateTo, $customersWithTransactions);
        }, 'special_arrangements');
        $types['invoice_per_order'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getInvoicingPerOrder($dateFrom, $dateTo, $customersWithTransactions);
        }, 'invoice_per_order');
        $types['possible_duplicates'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) {
            return self::getPossibleDuplicates($dateFrom, $dateTo, $customersWithTransactions);
        }, 'possible_duplicates');
        return [
            'dateFrom' => $dateFrom,
            'dateTo' => $dateTo,
            'types' => $types,
        ];
    }

    /**
     * Debugging function to measure the time taken by a function.
     *
     * @param callable $function The function to debug.
     * @return mixed The result of the function.
     */
    private static function debugGetTime(callable $function, string $debug_label = ''): mixed
    {
        global $response;
        $start_time = microtime(true) * 1000; // Start time in milliseconds
        $result = $function();
        $end_time = microtime(true) * 1000; // End time in milliseconds
        $execution_time = $end_time - $start_time;
        $response->add_include(
            'debug_invoicing_period_' . $debug_label,
            [
                'execution_time' => $execution_time,
            ]
        );
        return $result;
    }

    /**
     * @throws Exception
     */
    private static function getCustomersWithTransactions(string $dateFrom, string $dateTo): array
    {
        // Define the customers with orders in the specified date range
        $customers = self::debugGetTime(function () use ($dateFrom, $dateTo) {
            return (new orders_o())->getCustomersWithOrdersInDateRange($dateFrom, $dateTo);
        }, 'customers_with_orders_in_date_range');
        //$customers = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo);

        /**
         * // user_id => customer_number,
         * @example
         * [
         *   '123' => '12345678',
         *    '456' => '87654321'
         *  ]
         */
        $customer_numbers = [];
        $tmp = [];
        self::debugGetTime(function () use ($customers, &$customer_numbers) {
            // Process the customer numbers to ensure they are unique
            foreach ( $customers as $customer ) {
                $customer_number = (int)$customer->customer_number->value();
                if (empty($customer_number)) {
                    // Skip if the customer number is empty
                    continue;
                }
                // Check if the customer number is already in the array
                // This ensures that we only process each customer number once
                // We use (int)$customer_number to ensure that the customer number is an integer
                if (isset($customer_numbers[$customer_number])) {
                    continue;
                }
                // Add the customer number to the array
                $customer_numbers[$customer_number] = $customer->id;
            }
        }, 'process_customer_numbers');
        // Get the transactions for the customers in the specified date range
        /**
         * @example
         * [
         *    '12345678' => [
         *       orders_o,
         *       orders_o,
         *      ]
         * ]
         * @var $customer_number_transactions
         */
        self::debugGetTime(function () use ($customer_numbers, $dateFrom, $dateTo, &$customer_number_transactions) {
            $customer_number_transactions = (new orders_o())->getTransactionsForCustomersInDateRange(array_keys($customer_numbers), $dateFrom, $dateTo);
        }, 'get_transactions_for_customers_in_date_range');
        // Calculate the total amount for each transaction, to minimize the number of queries
        self::debugGetTime(function () use ($customer_number_transactions) {
            // Get all transaction ids
            $transaction_ids = [];
            foreach ( $customer_number_transactions as $customer_number => $transactions ) {
                foreach ( $transactions as $transaction ) {
                    if ($transaction instanceof orders_o) {
                        $transaction_ids[] = $transaction->id;
                    }
                }
            }
            // Get the total amount for each transaction
            $transaction_totals = (new orders_o())->getNetAmountForOrders($transaction_ids);
            // Add the total amount to each transaction
            foreach ( $customer_number_transactions as $customer_number => $transactions ) {
                foreach ( $transactions as $transaction ) {
                    if ($transaction instanceof orders_o) {
                        // Set the total amount for the transaction
                        $transaction->setTemporaryNetAmount($transaction_totals[$transaction->id] ?? 0);
                    }
                }
            }
        }, 'calculate_transaction_totals');
        // Get the customer names from the cache
        $customer_names = (new \objects\users_o())->getCustomerNames(array_keys($customer_numbers));
        // Process the customer numbers to ensure they are unique
        self::debugGetTime(function () use ($customer_numbers, $customer_number_transactions, &$tmp, $customer_names) {
            foreach ( $customer_numbers as $customer_number => $user_id ) {
                if (empty($customer_number)) {
                    // Skip if the customer number is empty
                    continue;
                }
                // Check if the customer number is already in the array
                if (isset($tmp[$customer_number])) {
                    // If the customer number is already in the array, skip it
                    continue;
                }
                // If the customer number is not in the array, add it
                // Construct the customer object with transactions
                $tmp[] = self::constructCustomerObject(
                    (int)$customer_number,
                    $customer_names[(int)$customer_number] ?? 'Unknown Customer',
                    //(new \objects\users_o())->getCustomerName((int)$customer_number),
                    $customer_number_transactions[(int)$customer_number] ?? [],
                    false,
                    (int)$user_id,
                );
            }
        }, 'construct_customer_objects');
        return $tmp;
    }

    /**
     * @param int $customer_number
     * @param string $customer_name
     * @param orders_o[] $transactions
     * @param bool $requires_action
     * @return array
     * @throws Exception
     */
    protected static function constructCustomerObject(
        int    $customer_number,
        string $customer_name,
        array  $transactions = [],
        bool   $requires_action = false,
        ?int   $user_id = null,
        ?array $meta = null
    ): array
    {
        return [
            'id' => $user_id ?? (new users_o())->getUserByCustomerNumber((int)$customer_number)->id,
            'customer_number' => $customer_number,
            'customer_name' => $customer_name,
            'transactions' => $parsed_transactions = array_map(function ($transaction) {
                return self::constructTransactionObject($transaction);
            }, $transactions),
            'requires_action' => self::checkRequiresAction($parsed_transactions, $requires_action),
            'meta' => $meta ?? [],
        ];
    }

    /**
     * @throws Exception
     */
    private static function constructTransactionObject(orders_o $transaction): array
    {
        return [
            'id' => $transaction->id,
            'date' => $transaction->created_at->value(),
            'amount' => $transaction->temporary_net_amount, // Use the temporary net amount set earlier
            'booked' => $transaction->isBooked(true),
            'excluded' => !$transaction->isIncludedInInvoicing()
        ];
    }

    private static function checkRequiresAction(array $parsed_transactions, bool $requires_action): bool
    {
        // If requires_action is already set to true, return true
        if ($requires_action) {
            return true;
        }
        // Check if any transaction is not booked
        foreach ( $parsed_transactions as $transaction ) {
            if (!$transaction['booked'] && !$transaction['excluded']) {
                return true;
            }
        }
        // If all transactions are booked, return false
        return false;
    }

    /**
     * @throws Exception
     */
    private static function getVehicleSubscriptions(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        // Since these are monthly subscriptions, we don't need to filter by transactions
        $customer_numbers = (new \objects\users_o())->getCustomersWithVehicleSubscriptions();
        // Get all customers with vehicle subscriptions
        $subscriptions = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $subscriptions[] = (self::getCustomerFromList((int)$customer_number, $customersWithTransactions)) ?? self::constructCustomerObject(
                (int)$customer_number,
                (new \objects\users_o())->getCustomerName((int)$customer_number) ?? 'Unknown Customer',
                [],
                true
            );
        }
        return $subscriptions;
    }

    /**
     * Get the customer from the list of customers with transactions.
     *
     * @param int $customer_number The customer number to search for.
     * @param array $customersWithTransactions The list of customers with transactions.
     * @return array|null The customer object if found, null otherwise.
     */
    private static function getCustomerFromList(int $customer_number, array $customersWithTransactions): ?array
    {
        // Search for the customer in the list of customers with transactions
        foreach ( $customersWithTransactions as $customer ) {
            if ($customer['customer_number'] === $customer_number) {
                return $customer;
            }
        }
        // If the customer is not found, return null
        return null;
    }

    /**
     * @throws Exception
     */
    private static function getFixedPricing(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        // Get all customers with fixed pricing

        $customer_numbers = (new \objects\users_o())->getCustomersWithFixedPricing();
        // Get the customers fixed pricing
        $tmp_fixed_pricing = array_map(function ($arr) {
            // Return the customer number and price
            return [
                'customer_number' => (int)$arr['customer_number'],
                'price' => (float)$arr['price'],
                'description' => (string)($arr['description'] ?? ''),
            ];
        }, (new \objects\customer_fixed_pricing_o())->getFieldsWhere(['customer_number' => $customer_numbers], ['customer_number', 'price', 'description']));

        // Get all customers with fixed pricing
        $fixed_pricing = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $fixed_pricing[] = self::getCustomerFromList(
                (int)$customer_number,
                $customersWithTransactions
            );
            // Check if the last entry is null, if so, create a new customer object
            if (end($fixed_pricing) === null) {
                $fixed_pricing[count($fixed_pricing) - 1] = self::constructCustomerObject(
                    (int)$customer_number,
                    (new \objects\users_o())->getCustomerName((int)$customer_number) ?? (new \objects\users_o())->select((int)$customer_number)->display_name->value(),
                    [],
                    true,
                );
            }
            // Add the fixed pricing to the customer object
            $fixed_pricing[count($fixed_pricing) - 1]['meta']['fixed_pricing'] = self::getObjectFromArray(
                $tmp_fixed_pricing,
                function ($item) use ($customer_number) {
                    return $item['customer_number'] === $customer_number;
                }
            );
        }
        return $fixed_pricing;
    }

    /**
     * Get an object from an array based on a callback function.
     *
     * @param array $array The array to search in.
     * @param callable $callback The callback function to use for searching.
     * @return mixed|null The found object or null if not found.
     */
    private static function getObjectFromArray(array $array, callable $callback): mixed
    {
        // Iterate through the array and apply the callback function to each element
        foreach ( $array as $item ) {
            // If the callback returns true, return the item
            if ($callback($item)) {
                return $item;
            }
        }
        // If no item matches the callback, return null
        return null;
    }

    /**
     * @throws Exception
     */
    private static function getTankCleaning(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        // Filter out customers that do not have any transactions in the specified date range
        $customer_numbers = (new \objects\users_o())->getCustomersWithTankCleaning();
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);
        // Get all customers with tank cleaning
        $tank_cleaning = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $tank_cleaning[] = self::getCustomerFromList(
                (int)$customer_number,
                $customersWithTransactions
            );
            // Add the tank cleaning to the list if it has transactions
        }
        return $tank_cleaning;
    }

    /**
     * Filters the customer numbers based on whether they have transactions in the specified date range.
     *
     * @param array $customer_numbers The customer numbers to filter.
     * @param array $customersWithTransactions The customers with transactions in the specified date range.
     */
    private static function filterCustomersWithTransactions(array &$customer_numbers, array $customersWithTransactions): void
    {
        // Filter out customers that do not have any transactions in the specified date range
        $customer_numbers = array_filter($customer_numbers, function ($customer_number) use ($customersWithTransactions) {
            // Check if the customer has any transactions in the specified date range
            foreach ( $customersWithTransactions as $customer ) {
                if ($customer['customer_number'] === $customer_number) {
                    return true;
                }
            }
            return false;
        });
    }

    /**
     * @throws Exception
     */
    private static function getSpecialArrangements(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        // Get all customers with tank cleaning
        $customer_numbers = (new \objects\users_o())->getCustomersWithSpecialArrangements();
        // Filter out customers that do not have any transactions in the specified date range
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);
        $special_arrangements = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $special_arrangements[] = self::getCustomerFromList(
                (int)$customer_number,
                $customersWithTransactions
            );
        }
        return $special_arrangements;
    }

    /**
     * @throws Exception
     */
    private static function getInvoicingPerOrder(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // Get all customers with the invoicing per order attribute
        $customer_numbers = (new users_o())->getCustomerNumbersWithAttributes(['invoiceAllOrdersIndividually']);
        // Filter out customers that do not have any transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);

        $invoicing_per_order = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            // Add the invoicing per order to the list
            $invoicing_per_order[] = self::getCustomerFromList((int)$customer_number, $customersWithTransactions);
        }
        return $invoicing_per_order;
    }

    /**
     * @throws Exception
     */
    private static function getPossibleDuplicates(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
        }
        // Get orders with the same reg_1, that has been created within 24 hours of each other
        $orders = (new orders_o())->getOrdersWithPossibleDuplicates($dateFrom, $dateTo);
        // Get the customer numbers from the orders
        $tmp_customer_arr = [];
        // Remove duplicates from the customer numbers
        $possible_duplicates = [];
        /** @var int $customer_number */
        foreach ( $orders as $order ) {
            // Get the customer number from the order
            $customer_number = (int)$order[0]['object']->customer_id->value();
            // Check if the customer number is already in the array
            if (isset($tmp_customer_arr[$customer_number])) {
                continue;
            }
            // Add the customer number to the array
            $tmp_customer_arr[$customer_number] = true;
            // Get the customer from the list of customers with transactions
            $customer = self::getCustomerFromList($customer_number, $customersWithTransactions);
            // Add the customer to the possible duplicates array
            $possible_duplicates[] = self::constructCustomerObject(
                $customer_number,
                (new \objects\users_o())->getCustomerName($customer_number) ?? 'Unknown Customer',
                array_map(function ($transaction) {
                    // Construct the transaction object from the order
                    return $transaction['object'];
                }, $order),
                false, // Requires action because there are possible duplicates
                $customer['id'] ?? null // Use the id from the customer object if available
            );
        }
        return $possible_duplicates;
    }
}