<?php

namespace routes;

use classes\authentication;
use classes\economic;
use classes\economic_transfer_queue;
use classes\economic_transfer_queue_details_summary;
use classes\economic_v2_compare_engine;
use classes\economic_v2_line_normalizer;
use classes\economic_v2_revenue_statistics_service;
use classes\invoice_store;
use classes\invoice_collection_bulk_action_service;
use classes\invoicing_period_utils;
use classes\response;
use classes\router;
use Exception;
use objects\collected_order_invoices_o;
use objects\customer_fixed_pricing_o;
use objects\logs_o;
use objects\orders_o;
use objects\users_o;
use traits\route_t;

class orderInvoicesRoute
{
    use route_t;

    public function run(): void
    {
        global /** @var response $response */
        /** @var router $router */
        $router, $response;

        /** Collected order invoices > GET */
        $this->get('/collected-invoices', function () {
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES', 'User accessed the list of collected order invoices');
                $collected_order_invoices = new collected_order_invoices_o();
                // Check if the request contains an ID
                if (self::isParametersSet(['id'])) {
                    self::requireType((int)self::getParameter('id'), self::type_int());
                    $collected_order_invoices->select((int)self::getParameter('id'));
                    $collected_order_invoices->requireSelected();
                    $response->success($collected_order_invoices->asArray());
                }
                // Define the users
                $users = new users_o();
                // Define the collected order invoices
                $tmp_collected_order_invoices = new collected_order_invoices_o();
                // Return the list of collected order invoices
                $startTime = microtime(true);
                // Build a short-lived cache key to coalesce concurrent identical requests
                $cacheTtl = 15; // seconds
                $pageParam = (string)($response->getRequestParameter('page') ?? '1');
                $limitParam = (string)($response->getRequestParameter('limit') ?? '1000');
                $searchParam = (string)($response->getRequestParameter('search') ?? '');
                $orderParam = (string)($response->getRequestParameter('order') ?? 'id:ASC');
                $filtersParam = (string)($response->getRequestParameter('filters') ?? '');
                $cacheKey = 'collected_invoices:list:' . md5(json_encode([
                        'p' => $pageParam,
                        'l' => $limitParam,
                        's' => $searchParam,
                        'o' => $orderParam,
                        'f' => $filtersParam,
                    ], JSON_UNESCAPED_UNICODE));

                $redis = new \classes\redis();
                $cachedPayload = $redis->get($cacheKey);
                if ($cachedPayload) {
                    // Cached payload contains both meta and data
                    $payload = json_decode($cachedPayload, true);
                    if (isset($payload['meta']) && is_array($payload['meta'])) {
                        foreach ($payload['meta'] as $k => $v) {
                            $response->add_meta($k, $v);
                        }
                    }
                    $durationMs = (int)round((microtime(true) - $startTime) * 1000);
                    (new logs_o())->add(
                        'orderInvoices',
                        'global',
                        1,
                        $user->id,
                        'LIST_COLLECTED_INVOICES_TIMING_CACHE_HIT',
                        'Duration(ms): ' . $durationMs . ', page=' . ((int)$response->getRequestParameter('page')) . ', limit=' . ((int)$response->getRequestParameter('limit')) . ', search=' . (string)($response->getRequestParameter('search') ?? '') . ', order=' . (string)($response->getRequestParameter('order') ?? '')
                    );
                    $response->success($payload['data'] ?? []);
                }

                // Cache miss: compute and cache
                $result = $collected_order_invoices->listObjectsWithPaginationIfSet(
                    function ($collected_order_invoice) use ($tmp_collected_order_invoices, $users) {
                        // Select the orders for each collected order invoice
                        $tmp_collected_order_invoices->select((int)$collected_order_invoice['id']);
                        return $this->getOrderInvoiceDetails($collected_order_invoice, $users, $tmp_collected_order_invoices);
                    },
                    null,
                    [],
                    // Set the where, to where a non-deleted order is connected to the collected order invoice
                    (new pagination_helper())->where->addCondition(pagination_condition_where::CUSTOM('EXISTS (SELECT 1 FROM orders o WHERE o.invoice_collection_id = collected_order_invoices.id AND o.deleted_at IS NULL)'))
                );
                // Capture current pagination meta for caching
                $meta = [
                    'pagination' => [
                        'page' => (int)$pageParam,
                        'per_page' => (int)$limitParam,
                        // We don't have the total directly here; the response object already has it,
                        // but we cache the meta as provided by the client for consistency.
                    ]
                ];
                // Store combined payload
                $redis->setEx($cacheKey, json_encode([
                    'data' => $result,
                    'meta' => $meta,
                ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), $cacheTtl);
                $durationMs = (int)round((microtime(true) - $startTime) * 1000);
                // Add timing log for performance monitoring
                (new logs_o())->add(
                    'orderInvoices',
                    'global',
                    1,
                    $user->id,
                    'LIST_COLLECTED_INVOICES_TIMING',
                    'Duration(ms): ' . $durationMs . ', page=' . ((int)$response->getRequestParameter('page')) . ', limit=' . ((int)$response->getRequestParameter('limit')) . ', search=' . (string)($response->getRequestParameter('search') ?? '') . ', order=' . (string)($response->getRequestParameter('order') ?? '')
                );
                $response->success($result);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES', 'User tried to access the list of collected order invoices without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices' => 'List ALL collected order invoices. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Compare with E-conomic > GET */
        $this->get('/collected-invoices/economic/compare', function () {
            global $response;
            self::requirePermission('compare_collected_invoice_economic');
            //$user = (new authentication())->get_user();
            self::requireParameters(['collected_invoice_id']);
            self::requireType((int)self::getParameter('collected_invoice_id'), self::type_int());
            $collected_invoice_id = (int)self::getParameter('collected_invoice_id');
            self::requireMinValue($collected_invoice_id, 1);
            self::requireMaxValue($collected_invoice_id, 999999999);
            // Define variables
            $draft_id = null; // E-Conomic draft ID
            $booked_id = null; // E-Conomic booked invoice ID
            $warnings = [];
            // Select the collected order invoice
            /** @var collected_order_invoices_o $invoice */
            $invoice = (new collected_order_invoices_o())->select($collected_invoice_id);
            $invoice->requireSelected(); // Require that the collected order invoice exists
            // Get the E-Conomic draft ID and booked invoice ID
            try {
                $draft_id = $invoice->getInvoiceDraftId();

            } catch (Exception $e) {
                $warnings[] = 'Error fetching draft ID: ' . $e->getMessage();
            }
            try {
                $booked_id = $invoice->getInvoiceBookedId();

            } catch (Exception $e) {
                $warnings[] = 'Error fetching booked invoice ID: ' . $e->getMessage();
            }
            // Define the variables for comparison
            $draft_total = null;
            $booked_total = null;
            // Get the total price of the draft (if it exists)
            if ($draft_id !== null) {
                try {
                    $economic = new economic();
                    $draft_invoice = $economic->getInvoiceDraft((int)$draft_id);
                    $draft_total = $draft_invoice['total_amount'] ?? null;
                    $internal_total = $invoice->getTotalAmount();
                    if ($draft_total === null) {
                        $warnings[] = 'Could not fetch total amount for draft invoice ID ' . $draft_id;
                    } elseif (abs((float)$draft_total - (float)$internal_total) > 0.01) {
                        $warnings[] = 'Total amount mismatch for draft invoice ID ' . $draft_id . ': E-Conomic total is ' . $draft_total . ', internal total is ' . $internal_total;
                    }
                } catch (Exception $e) {
                    $warnings[] = 'Error comparing draft total: ' . $e->getMessage();
                }
            }
            // Get the total price of the booked invoice (if it exists)
            if ($booked_id !== null) {
                try {
                    $economic = new economic();
                    $booked_invoice = $economic->getInvoiceBookedFromExternalId((string)$invoice->external_id->value());
                    /**
                     * stdClass Object ( [bookedInvoiceNumber] => 28368 [orderNumber] => 30342 [date] => 2025-12-31 [currency] => DKK [exchangeRate] => 100 [netAmount] => 694 [netAmountInBaseCurrency] => 694 [grossAmount] => 867.5 [grossAmountInBaseCurrency] => 867.5 [vatAmount] => 173.5 [roundingAmount] => 0 [remainder] => 0 [remainderInBaseCurrency] => 0 [dueDate] => 2026-01-08 [paymentTerms] => stdClass Object ( [paymentTermsNumber] => 1 [daysOfCredit] => 8 [name] => Netto 8 dage [paymentTermsType] => net [self] => https://restapi.e-conomic.com/payment-terms/1 ) [customer] => stdClass Object ( [customerNumber] => 42493959 [self] => https://restapi.e-conomic.com/customers/42493959 ) [recipient] => stdClass Object ( [name] => Dejen Transport ApS [address] => Æblehaven 144, st [zip] => 4000 [city] => Roskilde [vatZone] => stdClass Object ( [name] => Domestic [vatZoneNumber] => 1 [enabledForCustomer] => 1 [enabledForSupplier] => 1 [self] => https://restapi.e-conomic.com/vat-zones/1 ) ) [references] => stdClass Object ( [other] => 2ced7a3f-fab2-edbd-075f-ce981867c340 ) [layout] => stdClass Object ( [layoutNumber] => 12 [self] => https://restapi.e-conomic.com/layouts/12 ) [pdf] => stdClass Object ( [download] => https://restapi.e-conomic.com/invoices/booked/28368/pdf ) [lines] => Array ( [0] => stdClass Object ( [lineNumber] => 1 [sortKey] => 1 [description] => [ 01/12/2025 00:00 PLENO #38679 ] [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [1] => stdClass Object ( [lineNumber] => 2 [sortKey] => 2 [description] => Reference: [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [2] => stdClass Object ( [lineNumber] => 3 [sortKey] => 3 [description] => # Vaskeabonnementer [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [3] => stdClass Object ( [lineNumber] => 4 [sortKey] => 4 [description] => Trækker [quantity] => 2 [unitNetPrice] => 579 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 25 [vatAmount] => 289.5 [totalNetAmount] => 1158 [product] => stdClass Object ( [productNumber] => 1 [self] => https://restapi.e-conomic.com/products/1 ) [departmentalDistribution] => stdClass Object ( [departmentalDistributionNumber] => 75 [name] => Vaskeaftaler og fastprisaftaler [barred] => [distributionType] => department [distributions] => Array ( [0] => stdClass Object ( [percentage] => 100 [department] => stdClass Object ( [departmentNumber] => 75 [self] => https://restapi.e-conomic.com/departments/75 ) ) ) [self] => https://restapi.e-conomic.com/departmental-distributions/departments/75 ) ) [4] => stdClass Object ( [lineNumber] => 5 [sortKey] => 5 [description] => Reference: [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [5] => stdClass Object ( [lineNumber] => 6 [sortKey] => 6 [description] => # EH89254 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [6] => stdClass Object ( [lineNumber] => 7 [sortKey] => 7 [description] => Spot Free- Lastbil [quantity] => 2 [unitNetPrice] => 39 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 25 [vatAmount] => 19.5 [totalNetAmount] => 78 [product] => stdClass Object ( [productNumber] => 33 [self] => https://restapi.e-conomic.com/products/33 ) [departmentalDistribution] => stdClass Object ( [departmentalDistributionNumber] => 75 [name] => Vaskeaftaler og fastprisaftaler [barred] => [distributionType] => department [distributions] => Array ( [0] => stdClass Object ( [percentage] => 100 [department] => stdClass Object ( [departmentNumber] => 75 [self] => https://restapi.e-conomic.com/departments/75 ) ) ) [self] => https://restapi.e-conomic.com/departmental-distributions/departments/75 ) ) [7] => stdClass Object ( [lineNumber] => 8 [sortKey] => 8 [description] => Reference: [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [8] => stdClass Object ( [lineNumber] => 9 [sortKey] => 9 [description] => # EH89254 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) [9] => stdClass Object ( [lineNumber] => 10 [sortKey] => 10 [description] => Rabat [quantity] => 1 [unitNetPrice] => -542 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 25 [vatAmount] => -135.5 [totalNetAmount] => -542 [product] => stdClass Object ( [productNumber] => TotDiscount [self] => https://restapi.e-conomic.com/products/TotDiscount ) [departmentalDistribution] => stdClass Object ( [departmentalDistributionNumber] => 75 [name] => Vaskeaftaler og fastprisaftaler [barred] => [distributionType] => department [distributions] => Array ( [0] => stdClass Object ( [percentage] => 100 [department] => stdClass Object ( [departmentNumber] => 75 [self] => https://restapi.e-conomic.com/departments/75 ) ) ) [self] => https://restapi.e-conomic.com/departmental-distributions/departments/75 ) ) [10] => stdClass Object ( [lineNumber] => 11 [sortKey] => 11 [discountPercentage] => 0 [unitCostPrice] => 0 [vatRate] => 0 ) ) [sent] => https://restapi.e-conomic.com/invoices/booked/28368/sent [self] => https://restapi.e-conomic.com/invoices/booked/28368 )
                     */
                    $booked_total = $booked_invoice->total_amount ?? null;
                    // Remove tax from booked total if prices are stored as tax inclusive
                    $booked_total = $booked_total - ($booked_invoice->vat_amount ?? 0);
                    $internal_total = $invoice->getTotalAmount();
                    if ($booked_total === null) {
                        $warnings[] = 'Could not fetch total amount for booked invoice ID ' . $booked_id;
                    } elseif (abs((float)$booked_total - (float)$internal_total) > 0.01) {
                        $warnings[] = 'Total amount mismatch for booked invoice ID ' . $booked_id . ': E-Conomic total is ' . $booked_total . ', internal total is ' . $internal_total;
                    }
                } catch (Exception $e) {
                    $warnings[] = 'Error comparing booked total: ' . $e->getMessage();
                }
            }
            // Determine HTTP status
            // 404 = Not found (in either draft or booked)
            // 200 = OK (Found one, and matches internal total)
            // 409 = Conflict (Both found but do not match)
            $httpStatus = match (true) {
                $draft_id === null && $booked_id === null => 404,
                ($draft_total !== null && $internal_total === (float)$draft_total) || ($booked_total !== null && $internal_total === (float)$booked_total) => 200,
                default => 409,
            };
            // Return the comparison result
            $response->success([
                'collected_invoice_id' => $collected_invoice_id,
                'draft_id' => $draft_id,
                'booked_id' => $booked_id,
                'warnings' => $warnings,
                'draft_total' => $draft_total,
                'booked_total' => $booked_total,
                'difference' => ((float)($draft_total ?? $booked_total) - (float)$invoice->getTotalAmount()),
                'internal_total' => $internal_total,
                //'order_ids' => $invoice->getOrderIds(),
                ], $httpStatus);

        },
            [
                'compare_collected_invoice_economic' => 'Compare collected order invoices with E-Conomic. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > E-conomic V2 details > GET */
        $this->get('/collected-invoices/economic/v2/details', function () {
            global $response;
            self::requirePermission('view_collected_invoice_economic_v2_details');
            $collected_invoice_id = $this->requireCollectedInvoiceId();

            $payload = $this->buildEconomicV2DetailsPayload($collected_invoice_id);
            $response->success($payload);
        },
            [
                'view_collected_invoice_economic_v2_details' => 'View normalized internal/draft/booked e-conomic invoice details (V2).',
            ]
        );

        /** Collected order invoices > E-conomic PDF > GET */
        $this->get('/collected-invoices/economic/pdf', function () {
            global $response;
            self::requirePermission('download_collected_invoice_economic_pdf');
            $collected_invoice_id = $this->requireCollectedInvoiceId();
            self::requireParameters(['type']);

            $type = strtolower((string)self::getParameter('type'));
            if (!in_array($type, ['draft', 'booked'], true)) {
                $response->error('type must be either draft or booked', 400);
            }

            $payload = $this->downloadCollectedEconomicInvoicePdf($collected_invoice_id, $type);
            $response->success($payload);
        },
            [
                'download_collected_invoice_economic_pdf' => 'Download draft/booked e-conomic PDF for a collected order invoice.',
            ]
        );

        /** Collected order invoices > E-conomic V2 compare > GET */
        $this->get('/collected-invoices/economic/v2/compare', function () {
            global $response;
            self::requirePermission('compare_collected_invoice_economic_v2');
            $collected_invoice_id = $this->requireCollectedInvoiceId();

            $details = $this->buildEconomicV2DetailsPayload($collected_invoice_id);
            $comparison = economic_v2_compare_engine::compare(
                $details['internal']['normalized'],
                $details['draft']['exists'] ? $details['draft']['normalized'] : null,
                $details['booked']['exists'] ? $details['booked']['normalized'] : null
            );

            $response->success([
                'collected_invoice_id' => $collected_invoice_id,
                'details' => $details,
                'comparison' => $comparison,
                'warnings' => array_values(array_unique(array_merge(
                    (array)($details['warnings'] ?? []),
                    (array)($comparison['warnings'] ?? [])
                ))),
            ]);
        },
            [
                'compare_collected_invoice_economic_v2' => 'Compare normalized internal invoice with draft/booked e-conomic targets (V2).',
            ]
        );

        /** Collected order invoices > E-conomic V2 compare bulk > POST */
        $this->post('/collected-invoices/economic/v2/compare/bulk', function () {
            global $response;
            self::requirePermission('compare_collected_invoice_economic_v2_bulk');
            self::requireParameters(['collected_invoice_ids']);

            $collected_invoice_ids = self::getParameter('collected_invoice_ids');
            if (!is_array($collected_invoice_ids)) {
                $response->error('collected_invoice_ids must be an array', 400);
            }

            $normalized_ids = array_values(array_unique(array_filter(array_map(static function ($id) {
                return (int)$id;
            }, $collected_invoice_ids), static function ($id) {
                return $id > 0;
            })));

            if (empty($normalized_ids)) {
                $response->error('collected_invoice_ids must contain at least one positive integer', 400);
            }

            if (count($normalized_ids) > 200) {
                $response->error('Maximum 200 collected_invoice_ids per bulk compare request', 400);
            }

            $results = [];
            $errors = [];

            foreach ($normalized_ids as $collected_invoice_id) {
                try {
                    $details = $this->buildEconomicV2DetailsPayload((int)$collected_invoice_id);
                    $comparison = economic_v2_compare_engine::compare(
                        $details['internal']['normalized'],
                        $details['draft']['exists'] ? $details['draft']['normalized'] : null,
                        $details['booked']['exists'] ? $details['booked']['normalized'] : null
                    );

                    $results[] = [
                        'collected_invoice_id' => (int)$collected_invoice_id,
                        'details' => $details,
                        'comparison' => $comparison,
                        'warnings' => array_values(array_unique(array_merge(
                            (array)($details['warnings'] ?? []),
                            (array)($comparison['warnings'] ?? [])
                        ))),
                    ];
                } catch (Exception $e) {
                    $errors[] = [
                        'collected_invoice_id' => (int)$collected_invoice_id,
                        'error' => $e->getMessage(),
                    ];
                }
            }

            $response->success([
                'requested' => count($normalized_ids),
                'compared' => count($results),
                'failed' => count($errors),
                'results' => $results,
                'errors' => $errors,
            ]);
        },
            [
                'compare_collected_invoice_economic_v2_bulk' => 'Compare multiple collected invoices against draft/booked e-conomic targets (V2).',
            ]
        );

        /** Collected order invoices > E-conomic V2 revenue statistics > GET */
        $this->get('/collected-invoices/economic/v2/revenue-statistics', function () {
            global $response;
            self::requirePermission('view_collected_invoice_economic_v2_revenue_statistics');

            $dateFrom = (string)(self::fromRequest('dateFrom') ?? date('Y-m-01'));
            $dateTo = (string)(self::fromRequest('dateTo') ?? date('Y-m-d'));
            self::requireDateFormat($dateFrom, self::FORMAT_DATE());
            self::requireDateFormat($dateTo, self::FORMAT_DATE());
            if (strtotime($dateFrom) > strtotime($dateTo)) {
                $response->error('dateFrom must be before or equal to dateTo', 400);
            }

            $barred = strtolower(trim((string)(self::fromRequest('barred') ?? 'all')));
            self::requireInArray($barred, ['all', 'barred', 'active']);

            $currency = self::fromRequest('currency');
            $currency = ($currency !== null && trim($currency) !== '') ? strtoupper(trim($currency)) : null;
            if ($currency !== null && !preg_match('/^[A-Z]{3}$/', $currency)) {
                $response->error('currency must be a 3-letter ISO code (e.g. DKK)', 400);
            }

            $max_pages = (int)(self::fromRequest('max_pages') ?? 10);
            self::requireMinValue($max_pages, 1);
            self::requireMaxValue($max_pages, 200);

            $payload = (new economic_v2_revenue_statistics_service())->getBookedRevenueStatistics([
                'dateFrom' => $dateFrom,
                'dateTo' => $dateTo,
                'customer_numbers' => $this->parseIntegerListParameter('customer_numbers'),
                'department_numbers' => $this->parseIntegerListParameter('department_numbers'),
                'currency' => $currency,
                'barred' => $barred,
                'max_pages' => $max_pages,
            ]);

            $response->success($payload);
        },
            [
                'view_collected_invoice_economic_v2_revenue_statistics' => 'View aggregated booked revenue statistics from e-conomic (V2), including barred-customer filtering.',
            ]
        );

        /** Collected order invoices > Ready to invoice > GET */
        $this->get('/collected-invoices/ready-to-invoice', function () {
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_READY_TO_INVOICE', 'User accessed the list of collected order invoices ready to invoice');
                $collected_order_invoices = new collected_order_invoices_o();
                // Define the users
                $users = new users_o();
                // Define the collected order invoices
                $tmp_collected_order_invoices = new collected_order_invoices_o();
                // Return the list of collected order invoices
                $response->success($collected_order_invoices->listObjectsWithPaginationIfSet(
                    function ($collected_order_invoice) use ($tmp_collected_order_invoices, $users) {
                        // Select the orders for each collected order invoice
                        $tmp_collected_order_invoices->select((int)$collected_order_invoice['id']);
                        return $this->getOrderInvoiceDetails($collected_order_invoice, $users, $tmp_collected_order_invoices);
                    },
                    $collected_order_invoices->forceRestrictFilters(
                        [
                            // This makes sure that the user can only see orders from the departments they explicitly have access to
                            'customer_number' => $collected_order_invoices->listCustomersWithIndividualOrderInvoicing(),
                        ]
                    )
                ));
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_READY_TO_INVOICE', 'User tried to access the list of collected order invoices ready to invoice without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices' => 'List ALL collected order invoices. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > POST */
        $this->post('/collected-invoices', function () {
            global $response;
            self::requirePermission('add_collected_invoice');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE', 'User added a collected order invoice');
                // Require the customer number, and validate its type and length
                self::requireParameters(['customer_number']);
                self::requireType((int)self::getParameter('customer_number'), self::type_int());
                self::requireMinLength('customer_number', 1);
                self::requireMaxLength('customer_number', 10);
                // Require the customer number to be above 0
                self::requireMinValue((int)self::getParameter('customer_number'), 1);
                // Validate the customer number against the database
                $customer = (new users_o())->select((int)self::getParameter('customer_number'));
                $customer->requireSelected();
                // Define the variables
                $name = null;
                $notes = null;
                $processor = null;
                $closed_at = null;
                // Check if the name is set
                if (self::isParametersSet(['name'])) {
                    self::requireType((string)self::getParameter('name'), self::type_string());
                    self::requireMinLength('name', 1);
                    self::requireMaxLength('name', 255);
                    $name = (string)self::getParameter('name');
                }
                // Check if the notes are set
                if (self::isParametersSet(['notes'])) {
                    self::requireType((string)self::getParameter('notes'), self::type_string());
                    $notes = (string)self::getParameter('notes');
                }
                // Check if the processor is set
                if (self::isParametersSet(['processor'])) {
                    self::requireType((int)self::getParameter('processor'), self::type_int());
                    $processor = (int)self::getParameter('processor');
                }
                // Check if the closed_at is set
                if (self::isParametersSet(['closed_at'])) {
                    // Check if the closed date is set, and validate its type
                    if (!!self::getParameter('closed_at')) {
                        self::requireType((string)self::getParameter('closed_at'), self::type_string());
                        $closed_at = (string)self::getParameter('closed_at');
                        // Check if the closed date is valid
                        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $closed_at)) {
                            $response->error('Invalid closed date format', 400);
                        }
                        // Set the time to 00:00:01z
                        $closed_at = date('Y-m-d H:i:s', strtotime($closed_at . ' 00:00:01'));
                    }
                }

                // Add the collected order invoice
                $collected_order_invoices = new collected_order_invoices_o();
                $collected_order_invoices->add(
                    $customer->id,
                    $name,
                    $notes,
                    $processor,
                    $closed_at ?: null,
                );
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE', 'User tried to add a collected order invoice without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice' => 'Add a collected order invoice. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Move to customer > POST */
        $this->post('/collected-invoices/move-to-customer', function () {
            global $response;
            self::requirePermission('move_collected_invoice_customer');
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'MOVE_COLLECTED_INVOICE_CUSTOMER', 'User tried to move a collected order invoice without a valid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters(['id', 'customer_number']);
            self::requireType((int)self::getParameter('id'), self::type_int());
            self::requireType((int)self::getParameter('customer_number'), self::type_int());
            self::requireMinValue((int)self::getParameter('id'), 1);
            self::requireMinValue((int)self::getParameter('customer_number'), 1);
            self::requireMaxValue((int)self::getParameter('customer_number'), 999999999);

            $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
            $collected_order_invoices->requireSelected();

            try {
                $move_result = $collected_order_invoices->moveToCustomer((int)self::getParameter('customer_number'));
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 400);
            }

            (new logs_o())->add(
                'orderInvoices',
                'global',
                1,
                $user->id,
                'MOVE_COLLECTED_INVOICE_CUSTOMER',
                'User moved collected order invoice #' . (int)$collected_order_invoices->id . ' to customer #' . (int)self::getParameter('customer_number')
            );

            $response->add_meta('move', $move_result);
            $response->success($collected_order_invoices->asArray());
        },
            [
                'move_collected_invoice_customer' => 'Move a collected order invoice and its orders to another customer. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Split > POST */
        $this->post('/collected-invoices/split', function () {
            global $response;
            self::requirePermission('split_collected_invoice');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'SPLIT_COLLECTED_INVOICE', 'User split a collected order invoice');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Split the collected order invoice
                $collected_order_invoices->split();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'SPLIT_COLLECTED_INVOICE', 'User tried to split a collected order invoice without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'split_collected_invoice' => 'Split a collected order invoice. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Split by month > POST */
        $this->post('/collected-invoices/split-by-month', function () {
            global $response, $db;
            self::requirePermission('split_collected_invoice');
            $user = (new authentication())->get_user();
            if ($user) {
                self::requireParameters(['dateFrom', 'dateTo']);

                try {
                    $date_range = invoicing_period_utils::normalizeDateRange(
                        (string)self::getParameter('dateFrom'),
                        (string)self::getParameter('dateTo')
                    );
                } catch (\InvalidArgumentException $e) {
                    $response->error($e->getMessage(), 400);
                }

                $preview = false;
                if (self::isParametersSet(['preview'])) {
                    $preview_raw = self::getParameter('preview');
                    if (is_bool($preview_raw)) {
                        $preview = $preview_raw;
                    } elseif (is_numeric($preview_raw)) {
                        $preview = ((int)$preview_raw) === 1;
                    } elseif (is_string($preview_raw)) {
                        $normalized_preview = strtolower(trim($preview_raw));
                        if (!in_array($normalized_preview, ['true', 'false', '1', '0'], true)) {
                            $response->error('preview must be a boolean', 400);
                        }
                        $preview = in_array($normalized_preview, ['true', '1'], true);
                    } else {
                        $response->error('preview must be a boolean', 400);
                    }
                }
                (new logs_o())->add(
                    'orderInvoices',
                    'global',
                    1,
                    $user->id,
                    $preview ? 'PREVIEW_SPLIT_COLLECTED_INVOICE_BY_MONTH' : 'SPLIT_COLLECTED_INVOICE_BY_MONTH',
                    $preview ? 'User previewed splitting collected order invoices by month' : 'User split collected order invoices by order month'
                );

                $invoice_collection_ids = [];
                if (self::isParametersSet(['invoice_collection_ids'])) {
                    $invoice_collection_ids_raw = self::getParameter('invoice_collection_ids');
                    if (!is_array($invoice_collection_ids_raw)) {
                        $response->error('invoice_collection_ids must be an array', 400);
                    }

                    foreach ($invoice_collection_ids_raw as $invoice_collection_id_raw) {
                        if (is_array($invoice_collection_id_raw) || is_object($invoice_collection_id_raw) || !is_numeric($invoice_collection_id_raw)) {
                            $response->error('invoice_collection_ids must contain only positive integer ids', 400);
                        }

                        $invoice_collection_id = (int)$invoice_collection_id_raw;
                        if ($invoice_collection_id < 1 || $invoice_collection_id > 999999999) {
                            $response->error('invoice_collection_ids must contain only positive integer ids', 400);
                        }

                        $invoice_collection_ids[] = $invoice_collection_id;
                    }

                    $invoice_collection_ids = array_values(array_unique($invoice_collection_ids));
                    if (empty($invoice_collection_ids)) {
                        $response->error('invoice_collection_ids must contain at least one id', 400);
                    }
                } else {
                    $date_from = $db->escape_string($date_range['dateFrom']);
                    $date_to = $db->escape_string($date_range['dateTo']);
                    $sql = "SELECT DISTINCT invoice_collection_id
                            FROM orders
                            WHERE created_at BETWEEN '$date_from' AND '$date_to'
                            AND invoice_collection_id IS NOT NULL
                            AND invoice_collection_id > 0
                            AND deleted_at IS NULL";
                    $query_result = $db->query($sql);
                    while ($row = $query_result->fetch_assoc()) {
                        $invoice_collection_id = (int)($row['invoice_collection_id'] ?? 0);
                        if ($invoice_collection_id > 0) {
                            $invoice_collection_ids[] = $invoice_collection_id;
                        }
                    }
                }

                $items = [];
                $changed = [];
                $skipped = [];
                foreach ( array_values(array_unique($invoice_collection_ids)) as $invoice_collection_id ) {
                    try {
                        $collected_order_invoices = (new collected_order_invoices_o())->select($invoice_collection_id);
                        $collected_order_invoices->requireSelected();
                        $split_result = $preview
                            ? $collected_order_invoices->previewSplitByOrderMonth()
                            : $collected_order_invoices->splitByOrderMonth();
                        $item = [
                            'invoice_collection_id' => $invoice_collection_id,
                            ...$split_result,
                        ];
                        if (($split_result['status'] ?? '') === 'changed') {
                            $changed[] = $item;
                        } else {
                            $skipped[] = $item;
                        }
                        $items[] = $item;
                    } catch (\Throwable $e) {
                        $item = [
                            'status' => 'skipped',
                            'invoice_collection_id' => $invoice_collection_id,
                            'preview' => $preview,
                            'reason' => 'not_splittable',
                            'message' => $e->getMessage(),
                        ];
                        $skipped[] = $item;
                        $items[] = $item;
                    }
                }

                $response->success([
                    'message' => $preview
                        ? 'Collected invoice monthly split preview completed'
                        : 'Collected invoice monthly split completed',
                    'preview' => $preview,
                    'dateFrom' => $date_range['dateFrom'],
                    'dateTo' => $date_range['dateTo'],
                    'processed_count' => count($items),
                    'changed_count' => count($changed),
                    'skipped_count' => count($skipped),
                    'changed' => $changed,
                    'skipped' => $skipped,
                    'items' => $items,
                ]);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'SPLIT_COLLECTED_INVOICE_BY_MONTH', 'User tried to split collected order invoices by month without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'split_collected_invoice' => 'Split collected order invoices by order month. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Bulk action preview > POST */
        $this->post('/collected-invoices/bulk-actions/preview', function () {
            global $response;
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'PREVIEW_COLLECTED_INVOICE_BULK_ACTION', 'User tried to preview a collected invoice bulk action without a valid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters(['action', 'invoice_collection_ids']);
            $action = (string)self::getParameter('action');
            $this->requireCollectedInvoiceBulkActionPermission($action);

            $invoice_collection_ids = self::getParameter('invoice_collection_ids');
            if (!is_array($invoice_collection_ids)) {
                $response->error('invoice_collection_ids must be an array', 400);
            }
            $options = self::isParametersSet(['options']) ? self::getParameter('options') : [];
            if (!is_array($options)) {
                $response->error('options must be an object', 400);
            }
            $locale = self::isParametersSet(['locale']) ? (string)self::getParameter('locale') : 'da';

            try {
                $preview = (new invoice_collection_bulk_action_service())->preview(
                    $action,
                    $invoice_collection_ids,
                    $options,
                    $locale
                );
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 400);
            }

            (new logs_o())->add(
                'orderInvoices',
                'global',
                1,
                $user->id,
                'PREVIEW_COLLECTED_INVOICE_BULK_ACTION',
                'User previewed collected invoice bulk action ' . $action . ' for ' . count($invoice_collection_ids) . ' invoice collections'
            );
            $response->success($preview);
        },
            [
                'reset_collected_invoice_economic' => 'Preview collected invoice bulk cleanup and price reset actions. This is a superuser-only route.',
                'move_collected_invoice' => 'Preview merging selected collected invoices. This is a superuser-only route.',
                'split_collected_invoice' => 'Preview splitting selected collected invoices by order month. This is a superuser-only route.',
                'add_collected_invoice_economic' => 'Preview queueing selected collected invoices for E-Conomic. This is a superuser-only route.',
            ]
        );

        /** Collected order invoices > Bulk action apply > POST */
        $this->post('/collected-invoices/bulk-actions/apply', function () {
            global $response;
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'APPLY_COLLECTED_INVOICE_BULK_ACTION', 'User tried to apply a collected invoice bulk action without a valid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters(['preview_id', 'action', 'invoice_collection_ids', 'confirmation_text']);
            $action = (string)self::getParameter('action');
            $this->requireCollectedInvoiceBulkActionPermission($action);

            $invoice_collection_ids = self::getParameter('invoice_collection_ids');
            if (!is_array($invoice_collection_ids)) {
                $response->error('invoice_collection_ids must be an array', 400);
            }
            $options = self::isParametersSet(['options']) ? self::getParameter('options') : [];
            if (!is_array($options)) {
                $response->error('options must be an object', 400);
            }
            $locale = self::isParametersSet(['locale']) ? (string)self::getParameter('locale') : 'da';

            try {
                $result = (new invoice_collection_bulk_action_service())->apply(
                    (string)self::getParameter('preview_id'),
                    $action,
                    $invoice_collection_ids,
                    $options,
                    (string)self::getParameter('confirmation_text'),
                    (int)$user->id,
                    $locale
                );
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 400);
            }

            $response->success($result);
        },
            [
                'reset_collected_invoice_economic' => 'Apply collected invoice bulk cleanup and price reset actions after confirmation. This is a superuser-only route.',
                'move_collected_invoice' => 'Apply merging selected collected invoices after confirmation. This is a superuser-only route.',
                'split_collected_invoice' => 'Apply splitting selected collected invoices by order month after confirmation. This is a superuser-only route.',
                'add_collected_invoice_economic' => 'Apply queueing selected collected invoices for E-Conomic after confirmation. This is a superuser-only route.',
            ]
        );

        /** Collected order invoices > E-Conomic > POST (queued) */
        $this->post('/collected-invoices/economic', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if ($user) {
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                self::requireMinValue((int)self::getParameter('id'), 1);

                $send_as_is = false;
                if (self::isParametersSet(['send_as_is'])) {
                    $send_as_is_raw = self::getParameter('send_as_is');
                    if (is_bool($send_as_is_raw)) {
                        $send_as_is = $send_as_is_raw;
                    } elseif (is_numeric($send_as_is_raw)) {
                        $send_as_is = ((int)$send_as_is_raw) === 1;
                    } elseif (is_string($send_as_is_raw)) {
                        $normalized_send_as_is = strtolower(trim($send_as_is_raw));
                        if (!in_array($normalized_send_as_is, ['true', 'false', '1', '0'], true)) {
                            $response->error('send_as_is must be a boolean', 400);
                        }
                        $send_as_is = in_array($normalized_send_as_is, ['true', '1'], true);
                    } else {
                        $response->error('send_as_is must be a boolean', 400);
                    }
                }

                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                try {
                    $this->assertCollectedInvoiceCanBeExportedToEconomic($collected_order_invoices);
                } catch (\Throwable $e) {
                    $response->error($e->getMessage(), 400);
                }

                if ($collected_order_invoices->booked_invoice_id->value() !== null) {
                    $response->error('Invoice has already been booked', 400);
                }

                if (!$this->isEconomicTransferQueueAvailable()) {
                    try {
                        $result = $this->exportCollectedInvoiceSynchronously($collected_order_invoices, $send_as_is);
                    } catch (\Throwable $e) {
                        $response->error($e->getMessage(), 400);
                    }

                    (new logs_o())->add(
                        'orderInvoices',
                        'global',
                        1,
                        (int)$user->id,
                        'ADD_COLLECTED_INVOICE_ECONOMIC_FALLBACK',
                        'Processed collected invoice transfer synchronously because queue dependencies are unavailable'
                    );
                    $response->success([
                        'message' => 'Collected invoice export processed synchronously',
                        'mode' => 'synchronous_fallback',
                        'result' => $result,
                    ]);
                }

                $queue = new economic_transfer_queue();
                $job = $queue->enqueue(
                    economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                    [
                        'collected_invoice_id' => (int)self::getParameter('id'),
                        'send_as_is' => $send_as_is,
                        'requested_by' => (int)$user->id,
                    ],
                    (int)$user->id
                );
                $job_id = (int)($job['id'] ?? 0);
                if ($job_id < 1) {
                    $response->error('Failed to enqueue collected invoice export job: missing queue job id in enqueue response', 500);
                }

                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_ECONOMIC_QUEUED', 'Queued collected invoice transfer to E-Conomic');
                $response->success([
                    'message' => 'Collected invoice export queued',
                    'job_id' => $job_id,
                    'job' => $job,
                ], 202);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_ECONOMIC', 'User tried to add a collected order invoice to E-Conomic without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice_economic' => 'Add a collected order invoice to E-Conomic. This is a superuser-only route.'
            ]
        );

        $this->get('/collected-invoices/economic/queue', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $statuses = $this->parseCollectedInvoiceQueueStatuses();
            ['limit' => $limit, 'offset' => $offset] = $this->parseCollectedInvoiceQueuePagination();

            $queue = new economic_transfer_queue();
            $jobs = $queue->listJobsForCreatedBy(
                $statuses,
                $limit,
                $offset,
                economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                (int)$user->id
            );
            $total_jobs = $this->countCollectedInvoiceQueueJobs($queue, $statuses, (int)$user->id);
            $has_more = ($offset + count($jobs)) < $total_jobs;

            $response->success([
                'items' => $this->withCollectedInvoiceQueueDetailsSummaryList($jobs),
                'count' => count($jobs),
                'total' => $total_jobs,
                'limit' => $limit,
                'offset' => $offset,
                'has_more' => $has_more,
            ]);
        },
            [
                'add_collected_invoice_economic' => 'List queued collected invoice transfer jobs.'
            ]
        );

        $this->get('/collected-invoices/economic/queue/status', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $job_id = $this->requireCollectedInvoiceQueueJobId();
            $job = $this->requireCollectedInvoiceQueueJobById($job_id, (int)$user->id);

            $response->success($this->withCollectedInvoiceQueueDetailsSummary($job));
        },
            [
                'add_collected_invoice_economic' => 'Get queued collected invoice transfer job status.'
            ]
        );

        $this->get('/collected-invoices/economic/queue/monitor', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $limit = $this->parseCollectedInvoiceQueueMonitorLimit();
            $queue = new economic_transfer_queue();

            $response->success($this->buildCollectedInvoiceQueueMonitorPayload(
                $queue,
                (int)$user->id,
                $limit
            ));
        },
            [
                'add_collected_invoice_economic' => 'Monitor visible collected invoice transfer queue jobs.'
            ]
        );

        $this->post('/collected-invoices/economic/queue/retry', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $job_id = $this->requireCollectedInvoiceQueueJobId();
            $job = $this->requireCollectedInvoiceQueueJobById($job_id, (int)$user->id, true);
            if ((int)($job['attempts'] ?? 0) >= (int)($job['max_attempts'] ?? 1)) {
                $response->error('Collected invoice queue job reached max retry attempts', 409);
            }

            try {
                $queue = new economic_transfer_queue();
                $retried = $queue->retryJobForUser($job_id, (int)$user->id);
            } catch (\Throwable $e) {
                $message = trim((string)$e->getMessage());
                $status_code = $this->resolveCollectedInvoiceQueueRetryErrorStatus($message);
                $response->error('Failed to retry collected invoice queue job: ' . $message, $status_code);
            }

            $response->success([
                'message' => 'Collected invoice queue job retried',
                'job' => $this->withCollectedInvoiceQueueDetailsSummary($retried),
            ]);
        },
            [
                'add_collected_invoice_economic' => 'Retry failed queued collected invoice transfer job.'
            ]
        );

        $this->post('/collected-invoices/economic/queue/dismiss', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $job_id = $this->requireCollectedInvoiceQueueJobId();
            $job = $this->requireCollectedInvoiceQueueJobById($job_id, (int)$user->id);
            $status = strtoupper((string)($job['status'] ?? ''));
            if (!in_array($status, [
                economic_transfer_queue::STATUS_COMPLETED,
                economic_transfer_queue::STATUS_FAILED,
            ], true)) {
                $response->error('Only completed or failed collected invoice queue jobs can be cleared', 409);
            }

            try {
                $queue = new economic_transfer_queue();
                $dismissed = $queue->dismissTerminalJobForUser($job_id, (int)$user->id);
            } catch (\Throwable $e) {
                $response->error('Failed to clear collected invoice queue job: ' . $e->getMessage(), 400);
            }

            $response->success([
                'message' => 'Collected invoice queue job cleared',
                'job' => $this->withCollectedInvoiceQueueDetailsSummary($dismissed),
            ]);
        },
            [
                'add_collected_invoice_economic' => 'Clear one completed or failed queued collected invoice transfer job for the current user.'
            ]
        );

        $this->post('/collected-invoices/economic/queue/dismiss-terminal', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $queue = new economic_transfer_queue();
            $dismissed_count = $queue->dismissTerminalJobsForUser(
                (int)$user->id,
                economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT
            );

            $response->success([
                'message' => 'Completed and failed collected invoice queue jobs cleared',
                'dismissed_count' => $dismissed_count,
            ]);
        },
            [
                'add_collected_invoice_economic' => 'Clear all visible completed or failed queued collected invoice transfer jobs for the current user.'
            ]
        );

        $this->post('/collected-invoices/economic/queue/run', function () {
            global $response;
            self::requirePermission('add_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            $this->ensureEconomicTransferQueueIsAvailable();

            $limit = 10;
            if (self::isParametersSet(['limit'])) {
                $limit_raw = self::getParameter('limit');
                if (!is_numeric($limit_raw)) {
                    $response->error('limit must be a positive integer', 400);
                }
                $limit = (int)$limit_raw;
            }
            $limit = max(1, min(10, $limit));

            $queue = new economic_transfer_queue();
            $batch = $this->runCollectedInvoiceQueueBatch($queue, $limit);
            $result = (array)($batch['result'] ?? []);

            $response->success([
                'message' => (bool)($batch['fallback'] ?? false)
                    ? 'Collected invoice queue batch processed using compatibility fallback'
                    : 'Collected invoice queue batch processed',
                'processed' => (int)($result['processed'] ?? 0),
                'completed' => (int)($result['completed'] ?? 0),
                'failed' => (int)($result['failed'] ?? 0),
                'jobs' => array_values(array_map('intval', (array)($result['jobs'] ?? []))),
                'limit' => $limit,
                'transfer_type' => economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                'fallback' => (bool)($batch['fallback'] ?? false),
            ]);
        },
            [
                'add_collected_invoice_economic' => 'Run one queued collected invoice transfer batch immediately.'
            ]
        );

        /** Collected order invoices > Move multiple > Registration numbers > POST */
        $this->post('/collected-invoices/move-multiple/registration-numbers', function () {
            global $response;
            self::requirePermission('move_collected_invoice');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            self::requireParameters(['registration_numbers', 'target_customer_number', 'from_date', 'to_date', 'closed_at']);
            $registration_numbers = self::getParameter('registration_numbers');
            if (!is_array($registration_numbers) || empty($registration_numbers)) {
                $response->error('registration_numbers must be a non-empty array', 400);
            }
            $target_customer_number = self::getParameter('target_customer_number');
            self::requireType((int)$target_customer_number, self::type_int());
            self::requireMinValue((int)$target_customer_number, 1);
            $closed_at = self::getParameter('closed_at');
            if (!!$closed_at) {
                self::requireDateFormat($closed_at, self::FORMAT_DATE());
                $closed_at = date('Y-m-d H:i:s', strtotime($closed_at . ' 23:59:59'));
            } else {
                $closed_at = null;
            }
            $from_date = self::getParameter('from_date');
            self::requireDateFormat($from_date, self::FORMAT_DATE());
            $to_date = self::getParameter('to_date');
            self::requireDateFormat($to_date, self::FORMAT_DATE());
            $from_date = date('Y-m-d 00:00:00', strtotime($from_date));
            $to_date = date('Y-m-d 23:59:59', strtotime($to_date));
            if (strtotime($from_date) > strtotime($to_date)) {
                $response->error('from_date must be before or equal to to_date', 400);
            }
            $customer = (new users_o())->getUserByCustomerNumber((int)$target_customer_number);
            $customer->requireSelected();

            $collected_order_invoices = new collected_order_invoices_o();
            $moved_invoices = [];
            // Create a new collected order invoice for the target customer
            $new_invoice_collection = $collected_order_invoices->add(
                $customer->customer_number->value(),
                null,
                null,
                null,
                $closed_at
            );
            if (!$new_invoice_collection->exists()) {
                $response->error('Failed to create new invoice collection');
            }

            foreach ($registration_numbers as $registration_number) {
                if (!is_string($registration_number) || trim($registration_number) === '') {
                    $response->error('Each registration number must be a non-empty string', 400);
                }
                $orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange($registration_number, $from_date, $to_date);
                if (empty($orders)) {
                    continue; // Skip if no orders found for this registration number in the date range
                }
                foreach ($orders as $order) {
                    $order->customer_id->set((int)$new_invoice_collection->customer_number->value());
                    $order->assignToInvoiceCollection((int)$new_invoice_collection->id);
                    $order->objectChanged();
                    $moved_invoices[] = $order->id;
                }
            }
            $response->success([
                'message' => 'Orders moved to new invoice collection',
                'moved_invoices' => $moved_invoices,
                'from_date' => $from_date,
                'to_date' => $to_date,
                'target_customer_number' => $target_customer_number,
            ]);
        });
        /** Collected order invoices > Move multiple > POST */
        $this->post('/collected-invoices/move-multiple', function () {
            // Used to move multiple orders, to a new collected order invoice, in one request, instead of having to move each order one by one
            global $response;
            self::requirePermission('move_collected_invoice');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }
            self::requireParameters(['order_ids', 'target_customer_number', 'closed_at']);
            $order_ids = self::getParameter('order_ids');
            if (!is_array($order_ids) || empty($order_ids)) {
                $response->error('order_ids must be a non-empty array', 400);
            }
            $target_customer_number = self::getParameter('target_customer_number');
            self::requireType((int)$target_customer_number, self::type_int());
            self::requireMinValue((int)$target_customer_number, 1);
            $closed_at = self::getParameter('closed_at');
            if (!!$closed_at) {
                self::requireDateFormat($closed_at, self::FORMAT_DATE());
                $closed_at = date('Y-m-d H:i:s', strtotime($closed_at . ' 23:59:59'));
            } else {
                $closed_at = null;
            }
            $customer = (new users_o())->select((int)$target_customer_number);
            $customer->requireSelected();

            $collected_order_invoices = new collected_order_invoices_o();
            $moved_invoices = [];
            // Create a new collected order invoice for the target customer
            $new_invoice_collection = $collected_order_invoices->add(
                $customer->id,
                null,
                null,
                null,
                $closed_at
            );
            if (!$new_invoice_collection->exists()) {
                $response->error('Failed to create new invoice collection');
            }
            foreach ($order_ids as $order_id) {
                self::requireType((int)$order_id, self::type_int());
                self::requireMinValue((int)$order_id, 1);
                $order = (new orders_o())->select((int)$order_id);
                $order->requireSelected();
                $order->customer_id->set((int)$new_invoice_collection->customer_number->value());
                $order->objectChanged();
                $order->assignToInvoiceCollection((int)$new_invoice_collection->id);
                $moved_invoices[] = $order->id;
            }
            $new_invoice_collection->clearCachedData();
            $response->success([
                'message' => 'Orders moved to new invoice collection',
                'moved_invoices' => $moved_invoices,
            ]);
        },
            [
                'move_collected_invoice' => 'Move multiple orders to a new collected order invoice. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > E-Conomic > Unlink and clear cached data > POST */
        $this->post('/collected-invoices/economic/unlink', function () {
            global $response;
            self::requirePermission('unlink_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'UNLINK_COLLECTED_INVOICE_ECONOMIC', 'User unlinked a collected order invoice from E-Conomic');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Unlink the collected order invoice from E-Conomic
                $collected_order_invoices->unlinkFromEconomic();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'UNLINK_COLLECTED_INVOICE_ECONOMIC', 'User tried to unlink a collected order invoice from E-Conomic without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'unlink_collected_invoice_economic' => 'Unlink a collected order invoice from E-Conomic. This is a superuser-only route. This will NOT delete the invoice in E-Conomic, but will clear the cached data in the system.'
            ]
        );

        /** Collected order invoices > E-Conomic > Remove special arrangements, and set all items to be included in the invoice > POST */
        $this->post('/collected-invoices/remove-special-arrangements', function () {
            global $response;
            self::requirePermission('reset_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'RESET_COLLECTED_INVOICE_ECONOMIC', 'User reset a collected order invoice in E-Conomic');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Remove special arrangements, and set all items to be included in the invoice
                $collected_order_invoices->removeSpecialArrangements(); // Remove any left-over special arrangement transactions (subscriptions / fixed price)
                //$collected_order_invoices->resetPricesOfItemsNotIncludedInInvoice(); // Reset the price of all items set not to be included in the invoice
                $collected_order_invoices->setAllItemsToBeIncludedInInvoice(); // Set all items to be included in the invoice
                $collected_order_invoices->objectChanged();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RESET_COLLECTED_INVOICE_ECONOMIC', 'User tried to reset a collected order invoice in E-Conomic without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'reset_collected_invoice_economic' => 'Reset a collected order invoice in E-Conomic. This is a superuser-only route. This will NOT delete the invoice in E-Conomic, but will remove any special arrangement transactions (subscriptions / fixed price), and set all items to be included in the invoice.'
            ]
        );
        /** Collected order invoices > E-Conomic > Reset prices of items not included in the invoice > POST */
        $this->post('/collected-invoices/reset-prices-of-items-not-included-in-invoice', function () {
            global $response;
            self::requirePermission('reset_collected_invoice_economic');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'RESET_COLLECTED_INVOICE_ECONOMIC', 'User reset a collected order invoice in E-Conomic');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Reset prices of items not included in the invoice
                $collected_order_invoices->resetPricesOfItemsNotIncludedInInvoice(); // Reset the price of all items set not to be included in the invoice
                $collected_order_invoices->objectChanged();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RESET_COLLECTED_INVOICE_ECONOMIC', 'User tried to reset a collected order invoice in E-Conomic without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'reset_collected_invoice_economic' => 'Reset a collected order invoice in E-Conomic. This is a superuser-only route. This will NOT delete the invoice in E-Conomic, but will reset the prices of all items set not to be included in the invoice.'
            ]
        );

        /** Collected order invoices > Stripe > BOOK > POST */
        $this->post('/collected-invoices/stripe/book', function () {
            global $response;
            self::requirePermission('add_collected_invoice_stripe');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_STRIPE', 'User added a collected order invoice to Stripe');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                try {
                    $this->assertCollectedInvoiceCanBeExportedToEconomic($collected_order_invoices);
                } catch (\Throwable $e) {
                    $response->error($e->getMessage(), 400);
                }
                // Require the external ID to be set
                if ($collected_order_invoices->external_id->value() === null) {
                    $response->error('Transaction has not been created in Stripe', 400);
                }
                // Require the booked invoice ID to be not set
                if ($collected_order_invoices->booked_invoice_id->value() !== null) {
                    $response->error('Invoice has already been booked', 400);
                }
                if (!$this->isEconomicTransferQueueAvailable()) {
                    try {
                        $result = $this->exportCollectedInvoiceSynchronously($collected_order_invoices, false);
                    } catch (\Throwable $e) {
                        $response->error($e->getMessage(), 400);
                    }

                    (new logs_o())->add(
                        'orderInvoices',
                        'global',
                        1,
                        (int)$user->id,
                        'ADD_COLLECTED_INVOICE_STRIPE_FALLBACK',
                        'Processed Stripe collected invoice export synchronously because queue dependencies are unavailable'
                    );
                    $response->success([
                        'message' => 'Stripe collected invoice export processed synchronously',
                        'mode' => 'synchronous_fallback',
                        'result' => $result,
                    ]);
                }

                $queue = new economic_transfer_queue();
                $job = $queue->enqueue(
                    economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                    [
                        'collected_invoice_id' => (int)self::getParameter('id'),
                        'requested_by' => (int)$user->id,
                    ],
                    (int)$user->id
                );
                $job_id = (int)($job['id'] ?? 0);
                if ($job_id < 1) {
                    $response->error('Failed to enqueue stripe collected invoice export job: missing queue job id in enqueue response', 500);
                }

                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_STRIPE_QUEUED', 'Queued Stripe collected invoice export to E-Conomic');
                $response->success([
                    'message' => 'Stripe collected invoice export queued',
                    'job_id' => $job_id,
                    'job' => $job,
                ], 202);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_STRIPE', 'User tried to add a collected order invoice to Stripe without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice_stripe' => 'Add a collected order invoice to Stripe. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Vehicle subscriptions > POST */
        $this->post('/collected-invoices/vehicle-subscriptions', function () {
            global $response;
            self::requirePermission('add_collected_invoice_vehicle_subscriptions');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_VEHICLE_SUBSCRIPTIONS', 'User added a collected order invoice for vehicle subscriptions');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Add the collected order invoice to E-Conomic
                $collected_order_invoices->addVehicleSubscriptionsTransaction();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_VEHICLE_SUBSCRIPTIONS', 'User tried to add a collected order invoice for vehicle subscriptions without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice_vehicle_subscriptions' => 'Add a collected order invoice for vehicle subscriptions. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Vehicle subscriptions > POST */
        $this->post('/collected-invoices/fixed-price', function () {
            global $response;
            self::requirePermission('add_collected_invoice_fixed_price');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_FIXED_PRICE', 'User added a collected order invoice fixed price modifications');
                // Require the ID, and validate its type and length
                self::requireParameters(['id']);
                self::requireType((int)self::getParameter('id'), self::type_int());
                self::requireMinLength('id', 1);
                self::requireMaxLength('id', 10);
                // Require the ID to be above 0
                self::requireMinValue((int)self::getParameter('id'), 1);
                // Validate the ID against the database
                $collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
                $collected_order_invoices->requireSelected();
                // Check if the customer has a fixed price
                $customer = new users_o();
                $customer->getUserByCustomerNumber((int)$collected_order_invoices->customer_number->value());
                $customer_fixed_pricing_o = new customer_fixed_pricing_o();
                if (!$customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$customer->customer_number->value())) {
                    $response->error('Customer does not have fixed pricing', 400);
                }
                $customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$customer->customer_number->value());
                $customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
                // Add the collected order invoice to E-Conomic
                $collected_order_invoices->overridePricesFixed((int)$customer_fixed_pricing_price);
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_FIXED_PRICE', 'User tried to add a collected order invoice fixed price modifications without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice_fixed_price' => 'Add a collected order invoice fixed price modifications. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Vehicle subscriptions > POST */
        $this->post('/collected-invoices/vehicle-subscriptions/custom', function () {
            global $response;
            self::requirePermission('add_collected_invoice_vehicle_subscriptions');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_VEHICLE_SUBSCRIPTIONS', 'User added a collected order invoice for vehicle subscriptions');
                // Require the ID, and validate its type and length
                self::requireParameters(['customer_number', 'month', 'year']);
                self::requireType((int)self::getParameter('customer_number'), self::type_int());
                self::requireMinLength('customer_number', 1);
                self::requireMaxLength('customer_number', 10);
                self::requireMinValue((int)self::getParameter('customer_number'), 1);
                $customer = (new users_o())->getUserByCustomerNumber((int)self::getParameter('customer_number'));
                $customer->requireSelected();
                // Require the month to be between 1 and 12
                self::requireType((int)self::getParameter('month'), self::type_int());
                self::requireMinLength('month', 1);
                self::requireMaxLength('month', 2);
                self::requireMinValue((int)self::getParameter('month'), 1);
                self::requireMaxValue((int)self::getParameter('month'), 12);
                // Require the year to be in the past 2 years
                self::requireType((int)self::getParameter('year'), self::type_int());
                self::requireMinLength('year', 1);
                self::requireMaxLength('year', 4);
                self::requireMinValue((int)self::getParameter('year'), date('Y') - 2);
                self::requireMaxValue((int)self::getParameter('year'), date('Y'));
                // Create the collected order invoice
                $collected_order_invoices = new collected_order_invoices_o();
                $collected_order_invoices->add(
                    (int)self::getParameter('customer_number'),
                );
                // Require the collected order invoice to be selected
                $collected_order_invoices->requireSelected();
                // Get the month and year from the request
                $month = (int)self::getParameter('month');
                $year = (int)self::getParameter('year');
                // Add a leading zero to the month if it's less than 10
                $month_with_prefix_if_applicable = str_pad($month, 2, '0', STR_PAD_LEFT);
                // Add a leading zero to the year if it's less than 4 digits
                $year_with_prefix_if_applicable = str_pad($year, 4, '0', STR_PAD_LEFT);
                // Generate the timestamp for the selected month and year, with the first day, and first second
                $timestamp_selected = mktime(0, 0, 1, $month_with_prefix_if_applicable, 1, $year_with_prefix_if_applicable);
                // MySQL requires the date to be in the format YYYY-MM-DD HH:MM:SS
                $timestamp_selected = date('Y-m-d H:i:s', $timestamp_selected);
                // Set the date to the first second of the date specified
                $collected_order_invoices->created_at->set($timestamp_selected);
                // Add the collected order invoice to E-Conomic
                $collected_order_invoices->addVehicleSubscriptionsTransaction();
                // This
                // Close the collected order invoice
                $collected_order_invoices->closed_at->set($timestamp_selected);
                // Update the collected order invoice
                $collected_order_invoices->objectChanged();
                // Return the collected order invoice
                $response->success($collected_order_invoices->asArray());
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_VEHICLE_SUBSCRIPTIONS', 'User tried to add a collected order invoice for vehicle subscriptions without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_collected_invoice_vehicle_subscriptions' => 'Add a collected order invoice for vehicle subscriptions. This is a superuser-only route.'
            ]
        );

        /** Collected order invoices > Open > GET Customers */
        $this->get('/collected-invoices/customers', function () {
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_CUSTOMERS', 'User accessed the list of collected order invoices customers');
                // Define the users
                $users_o = new users_o();
                $tmp_customer_numbers = $users_o->getCustomerNumbersWithAttributes([
                    'invoiceAllOrdersIndividually'
                ]);
                // Filter out the customers that do not have any open invoices
                $customer_numbers = [];
                //print_r($tmp_customer_numbers);
                foreach ( $tmp_customer_numbers as $customer_number ) {

                    // This is the customers open invoices, this might contain invoices that's empty, or only contains deleted orders.
                    $tmp_customer_open_invoice_ids = [];
                    // This is the net amount of the total transactions for the customer
                    $tmp_total_net_amount = 0;
                    // This is the number of invoices, that's not empty.
                    $tmp_customer_active_invoice_ids = [];
                    $collected_order_invoices = new collected_order_invoices_o();
                    $customer_open_invoices = $collected_order_invoices->listObjectsWithPaginationIfSet(
                        function ($collected_order_invoice) use ($tmp_total_net_amount, $users_o, $collected_order_invoices) {
                            // Select the orders for each collected order invoice
                            $collected_order_invoices->select((int)$collected_order_invoice['id']);
                            // Return the details
                            return $this->getOrderInvoiceDetails($collected_order_invoice, $users_o, $collected_order_invoices);
                        },
                        $collected_order_invoices->forceRestrictFilters([
                            'customer_number' => $customer_number,
                            'closed_at' => 'null',
                        ])
                    );
                    //print_r($customer_open_invoices);
                    // Check if the customer has any open invoices
                    if (count($customer_open_invoices) > 0) {
                        $tmp_customer_orders = 0;
                        // Get the total net amount of the open invoices
                        foreach ( $customer_open_invoices as $customer_open_invoice ) {
                            // Check if the invoice is empty
                            if (count($customer_open_invoice['orders']) === 0) {
                                continue;
                            }
                            // Add the invoice collected order invoice ID to the list (if it's not already in the list)
                            if (!in_array($customer_open_invoice['id'], $tmp_customer_open_invoice_ids)) {
                                $tmp_customer_active_invoice_ids[] = $customer_open_invoice['id'];
                            }
                            // Update the total net amount, and the number of orders
                            $tmp_customer_open_invoice_ids[] = $customer_open_invoice['id'];
                            $tmp_total_net_amount += $customer_open_invoice['total_net_amount'];
                            $tmp_customer_orders += count($customer_open_invoice['orders']);
                        }
                        // If the open invoices are not empty, add the customer to the list
                        if (empty($tmp_customer_orders)) {
                            continue;
                        }
                        $tmp_user = (new users_o())->getUserByCustomerNumber($customer_number);
                        $customer_numbers[] = array(
                            'customer_number' => $customer_number,
                            'customer_name' => $users_o->getCustomerName($customer_number),
                            'user_id' => $tmp_user->id,
                            'open_invoices' => $customer_open_invoices,
                            'active_invoices' => $tmp_customer_active_invoice_ids,
                            'orders' => $tmp_customer_orders,
                            'total_net_amount' => $tmp_total_net_amount,
                        );
                    }
                }
                // Return the list of customers
                $response->success($customer_numbers);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_CUSTOMERS', 'User tried to access the list of collected order invoices customers without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices_customers' => 'List ALL collected order invoices customers. This is a superuser-only route.'
            ]
        );

        $this->get('/collected-invoices/customers/invoicePerOrder', function () {
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_PER_ORDER', 'User accessed the list of collected order invoices customers with invoice per order');
                // Define the users
                $users_o = new users_o();
                $users_o->setView('customers_invoice_count_per_order');
                $result = $users_o
                    ->setSearchableFields([
                        'customer_number',
                        'customer_name',
                        'active_invoices',
                        'closed_invoices',
                    ])
                    ->listObjectsWithPaginationIfSet(
                        function ($collected_order_invoice) use ($users_o) {
                            return [
                                'customer_number' => (int)$collected_order_invoice['customer_number'],
                                'display_name' => (string)$users_o->getCustomerName((int)$collected_order_invoice['customer_number']),
                                'active_invoices' => (int)$collected_order_invoice['active_invoices'],
                                'closed_invoices' => (int)$collected_order_invoice['closed_invoices'],
                            ];
                        },
                        $users_o->forceRestrictFilters(
                            [
                                //'active_invoices' => 'NOT ZERO',
                            ]
                        )
                    );
                // Return the list of customers
                $response->success($result);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_PER_ORDER', 'User tried to access the list of collected order invoices customers with invoice per order without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices_customers_invoice_per_order' => 'List ALL collected order invoices customers with invoice per order. This is a superuser-only route.'
            ]
        );

        $this->get('/collected-invoices/customers/invoicePerMonth', function () {
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_PER_MONTH', 'User accessed the list of collected order invoices customers with invoice per month');
                // Define the users
                $users_o = new users_o();
                $users_o->setView('customers_invoice_count_per_month');
                $result = $users_o
                    ->setSearchableFields([
                        'customer_number',
                        'customer_name',
                        'active_invoices',
                        'closed_invoices',
                    ])
                    ->listObjectsWithPaginationIfSet(
                        function ($collected_order_invoice) use ($users_o) {
                            return [
                                'customer_number' => (int)$collected_order_invoice['customer_number'],
                                'display_name' => (string)$users_o->getCustomerName((int)$collected_order_invoice['customer_number']),
                                'active_invoices' => (int)$collected_order_invoice['active_invoices'],
                                'closed_invoices' => (int)$collected_order_invoice['closed_invoices'],
                            ];
                        },
                        $users_o->forceRestrictFilters(
                            [
                                //'active_invoices' => 'NOT ZERO',
                            ]
                        )
                    );
                // Return the list of customers
                $response->success($result);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_PER_MONTH', 'User tried to access the list of collected order invoices customers with invoice per month without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices_customers_invoice_per_month' => 'List ALL collected order invoices customers with invoice per month. This is a superuser-only route.'
            ]
        );


        $this->post('/collected-invoices/customers/invoiceTotals', function () {
            // This is a superuser-only route
            global $response;
            self::requirePermission('list_collected_invoices');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_TOTALS', 'User accessed the list of collected order invoices customers with invoice totals');
                // Require the customer_numbers parameter
                self::requireParameters(['customer_numbers']);
                // Print the customer_numbers parameter
                $customer_numbers = self::getParameter('customer_numbers');
                // validate the customer_numbers parameter
                if (!is_array($customer_numbers)) {
                    $response->error('customer_numbers must be an array', 400);
                }
                // Validate the customer_numbers parameter
                foreach ( $customer_numbers as $customer_number ) {
                    self::requireType((int)$customer_number, self::type_int());
                    if (!is_numeric($customer_number)) {
                        $response->error('customer_numbers must be an array of integers', 400);
                    }
                    if (!$customer_number || $customer_number < 1) {
                        $response->error('customer_numbers must be an array of integers greater than 0', 400);
                    }
                }
                // Now we can safely use the customer_numbers parameter
                // Define the result array
                $result = [];
                // Loop through the customer_numbers array, and get the invoice totals for each customer
                foreach ( $customer_numbers as $customer_number ) {
                    // Define the invoices array
                    $tmp_invoices = [
                        'closed_invoices' => [],
                        'open_invoices' => [],
                    ];
                    // Define the total net amount
                    $tmp_total_net_amount = [
                        'closed_invoices' => 0,
                        'open_invoices' => 0,
                    ];
                    // Define the collected order invoices
                    $collected_order_invoices = new collected_order_invoices_o();
                    // Get the collected order invoices for the customer
                    $collected_order_invoices->setView('invoices_with_completed_orders');
                    $tmp_invoice_collections = $collected_order_invoices->listObjectsWithPaginationIfSet(
                        function ($collected_order_invoice) use ($tmp_invoices, $customer_number, $collected_order_invoices) {
                            // Select the orders for each collected order invoice
                            $collected_order_invoices->select((int)$collected_order_invoice['id']);
                            // Get the details for the collected order invoice
                            return $this->getOrderInvoiceDetails($collected_order_invoice, new users_o(), $collected_order_invoices);
                        },
                        $collected_order_invoices->forceRestrictFilters([
                            'customer_number' => $customer_number,
                        ])
                    );
                    // Loop through the collected order invoices, and get the total net amount
                    foreach ( $tmp_invoice_collections as $tmp_invoice_collection ) {
                        // Update the total net amount
                        if ($tmp_invoice_collection['closed_at'] !== null) {
                            $tmp_total_net_amount['closed_invoices'] += $tmp_invoice_collection['total_net_amount'];
                            // Add the closed invoice to the closed invoices array
                            $tmp_invoices['closed_invoices'][] = $tmp_invoice_collection;
                        } else {
                            $tmp_total_net_amount['open_invoices'] += $tmp_invoice_collection['total_net_amount'];
                            // Add the open invoice to the open invoices array
                            $tmp_invoices['open_invoices'][] = $tmp_invoice_collection;
                        }
                    }
                    $tmp_user = (new users_o())->getUserByCustomerNumber($customer_number);
                    // Add the customer to the result array
                    $result[$customer_number] = [
                        'customer_number' => (int)$customer_number,
                        'customer_name' => (string)$tmp_user->display_name->value(),
                        'user_id' => (int)$tmp_user->id,
                        'invoices' => $tmp_invoices,
                        'total_net_amount' => $tmp_total_net_amount,
                    ];
                }

                // Return the list of customers
                $response->success($result);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_CUSTOMERS_INVOICE_TOTALS', 'User tried to access the list of collected order invoices customers with invoice totals without a valid session');
                $response->error('Invalid session', 400);
            }
        });

        $this->get('/collected-invoices/economic/overview', function () {
            global $response;
            self::requirePermission('list_collected_invoices_economic_overview');
            $user = (new authentication())->get_user();
            if ($user) {
                // Get the economic module
                $economic = new economic();
                // Get the collected order invoices
                $collected_order_invoices = new collected_order_invoices_o();
                /**
                 * Get the total invoices using E-conomic as the processor, with the given restrictions
                 * @param $collected_order_invoices collected_order_invoices_o
                 * @param $restrictions array
                 * @param $view string The MySQL view to use for the query
                 * @param $page int The page number to return
                 * @param $limit int The number of results to return per page
                 * @return int
                 */
                function getTotalInvoices(collected_order_invoices_o $collected_order_invoices, array $restrictions = [], string $view = 'invoices_with_completed_orders', int $page = 1, int $limit = 100000): int
                {
                    // Add the economic processor to the restrictions
                    $restrictions['processor'] = 1; // E-conomic
                    return count(
                        $collected_order_invoices->getTotalInvoices(
                            $collected_order_invoices,
                            $restrictions,
                            $view,
                            $page,
                            $limit,
                            1
                        )
                    );
                }

                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_ECONOMIC_OVERVIEW', 'User accessed the list of collected order invoices economic overview');
                // Define the result array
                $result = [
                    'total' => getTotalInvoices($collected_order_invoices),
                    'paid' => 0,
                    'unpaid' => 0,
                    'overdue' => 0,
                    'booked' => getTotalInvoices(
                        $collected_order_invoices,
                        [
                            'booked_invoice_id' => 'NOT NULL',
                            'external_id' => 'NOT NULL',
                        ],
                        'invoices_with_completed_orders'
                    ),
                    'draft' => getTotalInvoices(
                        $collected_order_invoices,
                        [
                            'booked_invoice_id' => null,
                            'error_message' => null,
                            'external_id' => 'NOT NULL',
                        ],
                        'invoices_with_completed_orders'
                    ),
                    'sent' => 0,
                    'error' => getTotalInvoices(
                        $collected_order_invoices,
                        [
                            'booked_invoice_id' => null,
                            'external_id' => 'NOT NULL',
                            'error_message' => 'NOT NULL',
                        ],
                        'invoices_with_completed_orders'
                    ),
                ];
                // Return the list of customers
                $response->success($result);
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_ECONOMIC_OVERVIEW', 'User tried to access the list of collected order invoices economic overview without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_collected_invoices_economic_overview' => 'List ALL collected order invoices overview. This is a superuser-only route.'
            ]
        );

        $this->post('/collected-invoices/economic/run/check-drafts', function () {
            global $response;
            self::requirePermission('module_economic_run_check_drafts');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'RUN_CHECK_DRAFTS', 'User ran the check drafts');
                // Get the economic module
                $economic = new economic();
                try {
                    // Run the check drafts
                    $economic->getTasks()->runCheckDrafts();
                } catch (Exception $e) {
                    // If the task fails, log the error and return an error response
                    (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RUN_CHECK_DRAFTS', 'User tried to run the check drafts, but it failed: ' . $e->getMessage());
                    $response->error('Failed to run the check drafts: ' . $e->getMessage(), 500);
                }
                // Return the result
                $response->success('Check drafts completed successfully');
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RUN_CHECK_DRAFTS', 'User tried to run the check drafts without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'module_economic_run_check_drafts' => 'Run the check drafts. This is a superuser-only route.'
            ]
        );

        $this->post('/collected-invoices/economic/run/check-errors', function () {
            global $response;
            self::requirePermission('module_economic_run_check_errors');
            $user = (new authentication())->get_user();
            if ($user) {
                (new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'RUN_CHECK_ERRORS', 'User ran the check errors');
                // Get the economic module
                $economic = new economic();
                try {
                    // Run the check errors
                    $economic->getTasks()->runCheckErrors();
                } catch (Exception $e) {
                    // If the task fails, log the error and return an error response
                    (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RUN_CHECK_ERRORS', 'User tried to run the check errors, but it failed: ' . $e->getMessage());
                    $response->error('Failed to run the check errors: ' . $e->getMessage(), 500);
                }
                // Return the result
                $response->success('Check errors completed successfully');
            } else {
                (new logs_o())->add('orderInvoices', 'global', 0, 0, 'RUN_CHECK_ERRORS', 'User tried to run the check errors without a valid session');
                $response->error('Invalid session', 400);
            }
        },
            [
                'module_economic_run_check_errors' => 'Run the check errors. This is a superuser-only route.'
            ]
        );
    }

    private function requireCollectedInvoiceId(): int
    {
        self::requireParameters(['collected_invoice_id']);
        self::requireType((int)self::getParameter('collected_invoice_id'), self::type_int());
        $collected_invoice_id = (int)self::getParameter('collected_invoice_id');
        self::requireMinValue($collected_invoice_id, 1);
        self::requireMaxValue($collected_invoice_id, 999999999);
        return $collected_invoice_id;
    }

    /**
     * Build normalized V2 details for a collected invoice and available e-conomic targets.
     * @throws Exception
     */
    private function buildEconomicV2DetailsPayload(int $collected_invoice_id): array
    {
        $warnings = [];
        $invoice = (new collected_order_invoices_o())->select($collected_invoice_id);
        $invoice->requireSelected();
        $this->requireCollectedInvoiceContextAccess($invoice);

        $draft_id = null;
        $booked_id = null;
        $draft_raw = null;
        $booked_raw = null;
        $customer = [
            'internal_customer_number' => (int)$invoice->customer_number->value() > 0 ? (int)$invoice->customer_number->value() : null,
            'draft_customer_number' => null,
            'booked_customer_number' => null,
            'exists' => false,
            'name' => null,
            'barred' => null,
        ];

        $economic = new economic();

        $persisted_booked_id = (int)$invoice->booked_invoice_id->value();
        if ($persisted_booked_id > 0) {
            // A booked invoice replaces its draft in e-conomic. Avoid reporting that expected
            // transition as a missing-draft warning.
            $booked_id = $persisted_booked_id;
        } else {
            $draft_error = null;
            try {
                $draft_id = $invoice->getInvoiceDraftId();
            } catch (Exception $e) {
                $draft_error = $e;
            }

            // Older rows may have an external ID but no persisted booked ID. Only probe the
            // booked endpoint when the draft is gone, then persist through getInvoiceBookedId().
            if ($draft_id === null) {
                try {
                    $booked_id = $invoice->getInvoiceBookedId();
                } catch (Exception $e) {
                    if ($draft_error !== null) {
                        $warnings[] = 'Draft id unavailable: ' . $draft_error->getMessage();
                    }
                    $warnings[] = 'Booked id unavailable: ' . $e->getMessage();
                }
            }
        }

        if ($draft_id !== null) {
            try {
                $draft_raw = $economic->invoices->draft->get((int)$draft_id);
            } catch (Exception $e) {
                $warnings[] = 'Unable to fetch draft invoice ' . (int)$draft_id . ': ' . $e->getMessage();
            }
        }

        if ($booked_id !== null) {
            try {
                $booked_raw = $economic->invoices->booked->getFromId((int)$booked_id);
            } catch (Exception $e) {
                $warnings[] = 'Unable to fetch booked invoice ' . (int)$booked_id . ': ' . $e->getMessage();
            }
        }

        $customer['draft_customer_number'] = $this->extractEconomicCustomerNumber($draft_raw);
        $customer['booked_customer_number'] = $this->extractEconomicCustomerNumber($booked_raw);
        if (
            $customer['internal_customer_number'] !== null &&
            $customer['draft_customer_number'] !== null &&
            (int)$customer['internal_customer_number'] !== (int)$customer['draft_customer_number']
        ) {
            $warnings[] = 'Draft invoice customer number mismatch: internal=' . (int)$customer['internal_customer_number'] . ', draft=' . (int)$customer['draft_customer_number'];
        }
        if (
            $customer['internal_customer_number'] !== null &&
            $customer['booked_customer_number'] !== null &&
            (int)$customer['internal_customer_number'] !== (int)$customer['booked_customer_number']
        ) {
            $warnings[] = 'Booked invoice customer number mismatch: internal=' . (int)$customer['internal_customer_number'] . ', booked=' . (int)$customer['booked_customer_number'];
        }
        if ($customer['internal_customer_number'] !== null) {
            try {
                $economic_customer_raw = $economic->customers->customers->get((int)$customer['internal_customer_number']);
                if (isset($economic_customer_raw->customerNumber)) {
                    $customer['exists'] = true;
                    $customer['name'] = isset($economic_customer_raw->name) ? (string)$economic_customer_raw->name : null;
                    $customer['barred'] = isset($economic_customer_raw->barred) ? (bool)$economic_customer_raw->barred : null;
                    if ($customer['barred'] === true) {
                        $warnings[] = 'The e-conomic customer is barred.';
                    }
                } else {
                    $warnings[] = 'Unable to resolve e-conomic customer ' . (int)$customer['internal_customer_number'] . '.';
                }
            } catch (\Throwable $e) {
                $warnings[] = 'Failed to fetch e-conomic customer ' . (int)$customer['internal_customer_number'] . ': ' . $e->getMessage();
            }
        }

        $internal_normalized = economic_v2_line_normalizer::normalizeInternalCollectedInvoice($invoice);
        $draft_normalized = $draft_raw !== null
            ? economic_v2_line_normalizer::normalizeDraftInvoice($draft_raw)
            : null;
        $booked_normalized = $booked_raw !== null
            ? economic_v2_line_normalizer::normalizeBookedInvoice($booked_raw)
            : null;

        $economic_state = $booked_id !== null
            ? 'booked'
            : ($draft_id !== null ? 'draft' : 'none');
        $available_pdf_type = $economic_state === 'none' ? null : $economic_state;

        return [
            'collected_invoice_id' => $collected_invoice_id,
            'external_id' => (string)$invoice->external_id->value(),
            'order_ids' => array_values(array_map(static function ($row) {
                return (int)($row['id'] ?? 0);
            }, $invoice->getOrderIds())),
            'economic' => [
                'draft_id' => $draft_id !== null ? (int)$draft_id : null,
                'booked_id' => $booked_id !== null ? (int)$booked_id : null,
                'state' => $economic_state,
                'available_pdf_type' => $available_pdf_type,
            ],
            'customer' => $customer,
            'internal' => [
                'normalized' => $internal_normalized,
            ],
            'draft' => [
                'exists' => $draft_raw !== null,
                'raw' => $this->toPlainArray($draft_raw),
                'normalized' => $draft_normalized,
            ],
            'booked' => [
                'exists' => $booked_raw !== null,
                'raw' => $this->toPlainArray($booked_raw),
                'normalized' => $booked_normalized,
            ],
            'warnings' => array_values(array_unique(array_merge(
                $warnings,
                (array)($internal_normalized['warnings'] ?? []),
                (array)($draft_normalized['warnings'] ?? []),
                (array)($booked_normalized['warnings'] ?? [])
            ))),
        ];
    }

    private function downloadCollectedEconomicInvoicePdf(int $collected_invoice_id, string $type): array
    {
        global $response;

        $invoice = (new collected_order_invoices_o())->select($collected_invoice_id);
        $invoice->requireSelected();
        $this->requireCollectedInvoiceContextAccess($invoice);

        $resolved_type = $type;
        $economic_invoice_id = null;
        $persisted_booked_id = (int)$invoice->booked_invoice_id->value();

        if ($persisted_booked_id > 0) {
            $resolved_type = 'booked';
            $economic_invoice_id = $persisted_booked_id;
        } else {
            try {
                $economic_invoice_id = $type === 'draft'
                    ? $invoice->getInvoiceDraftId()
                    : $invoice->getInvoiceBookedId();
            } catch (Exception $e) {
                // A legacy booked row can have only the former draft external ID persisted.
                // Fall back to the booked target when a requested draft no longer exists.
                if ($type === 'draft') {
                    try {
                        $economic_invoice_id = $invoice->getInvoiceBookedId();
                        $resolved_type = 'booked';
                    } catch (Exception $bookedException) {
                        $response->error('No e-conomic invoice found for collected invoice ' . $collected_invoice_id, 404);
                    }
                } else {
                    $response->error('No booked e-conomic invoice found for collected invoice ' . $collected_invoice_id, 404);
                }
            }
        }

        $economic_invoice_id = (int)$economic_invoice_id;
        if ($economic_invoice_id <= 0) {
            $response->error('No ' . $resolved_type . ' e-conomic invoice found for collected invoice ' . $collected_invoice_id, 404);
        }

        $economic = new economic();
        $invoice_path_file = $resolved_type === 'draft'
            ? $economic->invoices->pdf->getDraft($economic_invoice_id)
            : $economic->invoices->pdf->getBooked($economic_invoice_id);

        $store_key_id = 'economic_' . $resolved_type . '_' . $economic_invoice_id;
        $invoice_store = new invoice_store();
        try {
            $invoice_store->uploadFile('invoice_' . $store_key_id . '.pdf', $invoice_path_file);
        } finally {
            if (is_string($invoice_path_file) && file_exists($invoice_path_file)) {
                unlink($invoice_path_file);
            }
        }

        $user = (new authentication())->get_user();
        (new logs_o())->add(
            'orderInvoices',
            'global',
            1,
            $user ? (int)$user->id : 0,
            'DOWNLOAD_COLLECTED_ECONOMIC_INVOICE_PDF',
            'Downloaded ' . $type . ' e-conomic invoice PDF for collected invoice ' . $collected_invoice_id
        );

        return [
            'collected_invoice_id' => $collected_invoice_id,
            'type' => $resolved_type,
            'economic_invoice_id' => $economic_invoice_id,
            'url' => $invoice_store->getInvoiceDownloadUrl($store_key_id),
        ];
    }

    private function requireCollectedInvoiceContextAccess(collected_order_invoices_o $invoice): void
    {
        global $response;

        if ($this->hasPermission('superuser')) {
            return;
        }

        $invoice_customer_number = (int)$invoice->customer_number->value();
        if ($invoice_customer_number > 0 && $this->isOwnCustomerContext($invoice_customer_number)) {
            return;
        }

        if ($this->hasAccessToAllCollectedInvoiceDepartments((int)$invoice->id)) {
            return;
        }

        $response->error('Permission denied for requested collected invoice.', 403);
    }

    private function hasAccessToAllCollectedInvoiceDepartments(int $collected_invoice_id): bool
    {
        $orders = new orders_o();
        $order_departments = $orders->getFieldsWhere(
            [
                'invoice_collection_id' => $collected_invoice_id,
                'deleted_at' => null,
            ],
            ['department_id']
        );

        $department_ids = array_values(array_unique(array_filter(array_map(static function (array $order): int {
            return (int)($order['department_id'] ?? 0);
        }, $order_departments), static function (int $department_id): bool {
            return $department_id > 0;
        })));

        if (empty($department_ids)) {
            return false;
        }

        foreach ($department_ids as $department_id) {
            if (!$this->hasDepartmentAccess((string)$department_id)) {
                return false;
            }
        }

        return true;
    }

    private function extractEconomicCustomerNumber(mixed $invoice_raw): ?int
    {
        if ($invoice_raw === null) {
            return null;
        }
        $data = is_array($invoice_raw) ? $invoice_raw : $this->toPlainArray($invoice_raw);
        $value = $data['customer']['customerNumber']
            ?? $data['customer']['customer_number']
            ?? $data['customerNumber']
            ?? $data['customer_number']
            ?? null;
        if ($value === null) {
            return null;
        }
        $customer_number = (int)$value;
        return $customer_number > 0 ? $customer_number : null;
    }

    private function parseIntegerListParameter(string $parameter): array
    {
        if (!self::isParametersSet([$parameter])) {
            return [];
        }

        $raw = self::getParameter($parameter);
        $values = [];
        if (is_array($raw)) {
            $values = $raw;
        } elseif (is_string($raw)) {
            $values = explode(',', $raw);
        } elseif (is_numeric($raw)) {
            $values = [$raw];
        }

        $normalized = [];
        foreach ($values as $value) {
            $int_value = (int)$value;
            if ($int_value > 0) {
                $normalized[$int_value] = true;
            }
        }

        return array_values(array_map('intval', array_keys($normalized)));
    }

    private function toPlainArray(mixed $value): mixed
    {
        if ($value === null || is_scalar($value)) {
            return $value;
        }
        return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
    }

    /**
     * Synchronous fallback when queue components are unavailable in this deployment.
     * @throws Exception
     */
    private function exportCollectedInvoiceSynchronously(collected_order_invoices_o $collected_order_invoices, bool $send_as_is): array
    {
        $this->assertCollectedInvoiceCanBeExportedToEconomic($collected_order_invoices);

        if ($collected_order_invoices->external_id->value() === null) {
            if (!$send_as_is) {
                $customer_fixed_pricing_o = new customer_fixed_pricing_o();
                if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
                    $customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
                    $customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
                    $collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
                } else {
                    $collected_order_invoices->addVehicleSubscriptionsTransaction();
                }
            } else {
                $collected_order_invoices->removeSpecialArrangements();
                $collected_order_invoices->setAllItemsToBeIncludedInInvoice();
            }

            $collected_order_invoices->addToEconomic();
        } else {
            if ($collected_order_invoices->booked_invoice_id->value() !== null) {
                throw new Exception('Invoice has already been booked');
            }
            if ($collected_order_invoices->isDraftExisting()) {
                throw new Exception('Invoice draft already exists in E-Conomic');
            }

            $customer_fixed_pricing_o = new customer_fixed_pricing_o();
            if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
                $customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
                $customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
                $collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
            } else {
                $collected_order_invoices->addVehicleSubscriptionsTransaction();
            }

            $collected_order_invoices->addToEconomic(true);
        }

        return $collected_order_invoices->asArray();
    }

    private function isEconomicTransferQueueAvailable(): bool
    {
        return class_exists('\\classes\\economic_transfer_executor')
            && class_exists('\\classes\\economic_transfer_queue_schema_bootstrap')
            && class_exists(economic_transfer_queue::class);
    }

    private function ensureEconomicTransferQueueIsAvailable(): void
    {
        global $response;

        if (!$this->isEconomicTransferQueueAvailable()) {
            $response->error('Economic transfer queue is unavailable in this deployment', 503);
        }
    }

    private function parseCollectedInvoiceQueueStatuses(): array
    {
        global $response;

        if (!self::isParametersSet(['status'])) {
            return [];
        }

        $status_raw = (string)self::getParameter('status');
        $statuses = array_values(array_filter(array_map('trim', explode(',', $status_raw))));
        if ($statuses === []) {
            return [];
        }

        $allowed = [
            economic_transfer_queue::STATUS_QUEUED,
            economic_transfer_queue::STATUS_PROCESSING,
            economic_transfer_queue::STATUS_COMPLETED,
            economic_transfer_queue::STATUS_FAILED,
        ];

        $normalized = [];
        foreach ($statuses as $status) {
            $status = strtoupper($status);
            if (!in_array($status, $allowed, true)) {
                $response->error('status must contain only: ' . implode(', ', $allowed), 400);
            }
            $normalized[] = $status;
        }

        return array_values(array_unique($normalized));
    }

    private function parseCollectedInvoiceQueuePagination(): array
    {
        global $response;

        $limit = 50;
        if (self::isParametersSet(['limit'])) {
            $limit_raw = self::getParameter('limit');
            if (!is_numeric($limit_raw)) {
                $response->error('limit must be between 1 and 500', 400);
            }
            $limit = (int)$limit_raw;
            if ($limit < 1 || $limit > 500) {
                $response->error('limit must be between 1 and 500', 400);
            }
        }

        $offset = 0;
        if (self::isParametersSet(['offset'])) {
            $offset_raw = self::getParameter('offset');
            if (!is_numeric($offset_raw)) {
                $response->error('offset must be at least 0', 400);
            }
            $offset = (int)$offset_raw;
            if ($offset < 0) {
                $response->error('offset must be at least 0', 400);
            }
        }

        return [
            'limit' => $limit,
            'offset' => $offset,
        ];
    }

    private function parseCollectedInvoiceQueueMonitorLimit(): int
    {
        global $response;

        $limit = 50;
        if (self::isParametersSet(['limit'])) {
            $limit_raw = self::getParameter('limit');
            if (!is_numeric($limit_raw)) {
                $response->error('limit must be between 1 and 100', 400);
            }
            $limit = (int)$limit_raw;
            if ($limit < 1 || $limit > 100) {
                $response->error('limit must be between 1 and 100', 400);
            }
        }

        return $limit;
    }

    private function requireCollectedInvoiceQueueJobId(): int
    {
        global $response;

        if (!self::isParametersSet(['job_id'])) {
            $response->error('job_id is required', 400);
        }

        $job_id = self::getParameter('job_id');
        if (!is_numeric($job_id) || (int)$job_id < 1) {
            $response->error('job_id must be a positive integer', 400);
        }

        return (int)$job_id;
    }

    private function requireCollectedInvoiceQueueJobById(int $job_id, int $created_by, bool $mustBeFailed = false): array
    {
        global $response;

        $queue = new economic_transfer_queue();
        $job = $queue->getJobByIdForUser($job_id, $created_by);
        if ($job === null || ($job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT) {
            $response->error('Collected invoice queue job not found', 404);
        }

        if ($mustBeFailed && (string)($job['status'] ?? '') !== economic_transfer_queue::STATUS_FAILED) {
            $response->error('Collected invoice queue job can only be retried when status is FAILED', 409);
        }

        return $job;
    }

    private function resolveCollectedInvoiceQueueRetryErrorStatus(string $message): int
    {
        $normalized = strtolower(trim($message));
        if ($normalized === '') {
            return 400;
        }

        if (str_contains($normalized, 'not found')) {
            return 404;
        }

        $is_conflict = str_contains($normalized, 'only failed jobs can be retried')
            || str_contains($normalized, 'can only be retried when status is failed')
            || str_contains($normalized, 'max retry attempts')
            || str_contains($normalized, 'failed to retry queue job');

        return $is_conflict ? 409 : 400;
    }

    private function runCollectedInvoiceQueueBatch(economic_transfer_queue $queue, int $limit): array
    {
        if (method_exists($queue, 'processPendingByTransferType')) {
            return [
                'result' => $queue->processPendingByTransferType(
                    economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                    $limit
                ),
                'fallback' => false,
            ];
        }

        return [
            'result' => $queue->processPending($limit),
            'fallback' => true,
        ];
    }

    private function buildCollectedInvoiceQueueMonitorPayload(economic_transfer_queue $queue, int $user_id, int $limit): array
    {
        $jobs = $queue->listMonitorJobsForUser(
            $user_id,
            $limit,
            economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT
        );
        $jobs = $this->withCollectedInvoiceQueueDetailsSummaryList($jobs);

        $counts = [
            'queued' => 0,
            'in_progress' => 0,
            'failed' => 0,
            'completed' => 0,
            'total' => count($jobs),
        ];
        $progress_sum = 0;

        foreach ($jobs as $job) {
            $status = strtoupper((string)($job['status'] ?? ''));
            $job_progress = max(0, min(100, (int)($job['progress_percent'] ?? 0)));

            if ($status === economic_transfer_queue::STATUS_QUEUED) {
                $counts['queued']++;
                $progress_sum += 0;
                continue;
            }

            if ($status === economic_transfer_queue::STATUS_PROCESSING) {
                $counts['in_progress']++;
                $progress_sum += $job_progress;
                continue;
            }

            if ($status === economic_transfer_queue::STATUS_FAILED) {
                $counts['failed']++;
                $progress_sum += 100;
                continue;
            }

            if ($status === economic_transfer_queue::STATUS_COMPLETED) {
                $counts['completed']++;
                $progress_sum += 100;
            }
        }

        return [
            'jobs' => $jobs,
            'counts' => $counts,
            'progress_percent' => $counts['total'] > 0
                ? (int)round($progress_sum / $counts['total'])
                : 0,
            'limit' => $limit,
        ];
    }

    private function countCollectedInvoiceQueueJobs(economic_transfer_queue $queue, array $statuses, int $created_by): int
    {
        global $db;

        $created_by = max(0, $created_by);
        if ($created_by < 1) {
            return 0;
        }

        if (method_exists($queue, 'countJobsForCreatedBy')) {
            return max(0, (int)$queue->countJobsForCreatedBy(
                $statuses,
                economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
                $created_by
            ));
        }

        $conditions = [
            "transfer_type = '" . $db->escape_string(economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT) . "'",
            'created_by = ' . $created_by,
        ];

        if ($statuses !== []) {
            $escaped_statuses = array_map(static function (string $status) use ($db): string {
                return "'" . $db->escape_string(strtoupper(trim($status))) . "'";
            }, $statuses);
            $conditions[] = 'status IN (' . implode(',', $escaped_statuses) . ')';
        }

        $sql = 'SELECT COUNT(*) AS total FROM economic_transfer_queue_jobs WHERE ' . implode(' AND ', $conditions);
        $result = $db->query($sql);
        if (!$result instanceof \mysqli_result) {
            return 0;
        }

        $row = $result->fetch_assoc();
        if (!is_array($row) || !isset($row['total'])) {
            return 0;
        }

        return max(0, (int)$row['total']);
    }

    private function withCollectedInvoiceQueueDetailsSummary(array $job): array
    {
        $job['details_summary'] = economic_transfer_queue_details_summary::buildCollectedInvoiceSummary($job);
        $collected_invoice_id = (int)($job['details_summary']['target']['collected_invoice_id'] ?? 0);
        if ($collected_invoice_id > 0) {
            $job['details_summary']['customer'] = $this->resolveCollectedInvoiceQueueCustomerSummary(
                $job['details_summary']['customer'] ?? [],
                $collected_invoice_id
            );
        }
        return $job;
    }

    private function withCollectedInvoiceQueueDetailsSummaryList(array $jobs): array
    {
        return array_values(array_map(function (array $job): array {
            return $this->withCollectedInvoiceQueueDetailsSummary($job);
        }, $jobs));
    }

    private function resolveCollectedInvoiceQueueCustomerSummary(array $customer, int $collected_invoice_id): array
    {
        global $db;

        $customer_number = isset($customer['customer_number']) && is_numeric($customer['customer_number'])
            ? (int)$customer['customer_number']
            : null;
        $customer_name = is_string($customer['name'] ?? null) && trim((string)$customer['name']) !== ''
            ? trim((string)$customer['name'])
            : null;

        if ($customer_number !== null && $customer_name !== null) {
            return [
                'customer_number' => $customer_number,
                'name' => $customer_name,
            ];
        }

        $collected_invoice_id = max(0, $collected_invoice_id);
        if ($collected_invoice_id < 1) {
            return [
                'customer_number' => $customer_number,
                'name' => $customer_name,
            ];
        }

        $sql = "SELECT coi.customer_number, coi.name AS invoice_name, u.display_name
                FROM collected_order_invoices coi
                LEFT JOIN users u ON u.customer_number = coi.customer_number
                WHERE coi.id = $collected_invoice_id
                LIMIT 1";
        $result = $db->query($sql);
        if (!$result instanceof \mysqli_result) {
            return [
                'customer_number' => $customer_number,
                'name' => $customer_name,
            ];
        }

        $row = $result->fetch_assoc();
        if (!is_array($row)) {
            return [
                'customer_number' => $customer_number,
                'name' => $customer_name,
            ];
        }

        $resolved_customer_number = isset($row['customer_number']) && is_numeric($row['customer_number'])
            ? (int)$row['customer_number']
            : $customer_number;
        $display_name = trim((string)($row['display_name'] ?? ''));
        $invoice_name = trim((string)($row['invoice_name'] ?? ''));
        $resolved_name = $customer_name;
        if ($resolved_name === null && $display_name !== '' && strtolower($display_name) !== 'unnamed') {
            $resolved_name = $display_name;
        }
        if ($resolved_name === null && $invoice_name !== '') {
            $resolved_name = $invoice_name;
        }

        return [
            'customer_number' => $resolved_customer_number,
            'name' => $resolved_name,
        ];
    }

    /**
     * @throws Exception
     */
    private function assertCollectedInvoiceCanBeExportedToEconomic(collected_order_invoices_o $collected_order_invoices): void
    {
        $collected_order_invoices->requireSelected();
        (new economic())->assertCustomerNumberIsNotDraft((int)$collected_order_invoices->customer_number->value());
    }

    private function requireCollectedInvoiceBulkActionPermission(string $action): void
    {
        $permission = match ($action) {
            invoice_collection_bulk_action_service::ACTION_CLEAN_CUSTOMER_RULES,
            invoice_collection_bulk_action_service::ACTION_RESET_HIDDEN_PRICES => 'reset_collected_invoice_economic',
            invoice_collection_bulk_action_service::ACTION_MERGE => 'move_collected_invoice',
            invoice_collection_bulk_action_service::ACTION_SPLIT_BY_MONTH => 'split_collected_invoice',
            invoice_collection_bulk_action_service::ACTION_QUEUE_ECONOMIC => 'add_collected_invoice_economic',
            default => throw new Exception('Invalid invoice collection bulk action.'),
        };

        self::requirePermission($permission);
    }

    /**
     * @param $collected_order_invoice
     * @param users_o $users
     * @param collected_order_invoices_o $tmp_collected_order_invoices
     * @return array
     * @throws Exception
     */
    function getOrderInvoiceDetails($collected_order_invoice, users_o $users, collected_order_invoices_o $tmp_collected_order_invoices): array
    {
        return [
            'id' => (int)$collected_order_invoice['id'],
            'name' => (string)$collected_order_invoice['name'],
            'notes' => (string)$collected_order_invoice['notes'],
            'customer_number' => (int)$collected_order_invoice['customer_number'],
            'customer_name' => (string)$users->getCustomerName((int)$collected_order_invoice['customer_number']),
            'processor' => $collected_order_invoice['processor'] ? (int)$collected_order_invoice['processor'] : null,
            'external_id' => (string)$collected_order_invoice['external_id'],
            'closed_at' => $collected_order_invoice['closed_at'] ? (string)$collected_order_invoice['closed_at'] : null,
            'updated_at' => (string)$collected_order_invoice['updated_at'],
            'created_at' => (string)$collected_order_invoice['created_at'],
            'orders' => $tmp_collected_order_invoices->getOrders(),
            'total_net_amount' => (float)$tmp_collected_order_invoices->getTotalAmount(),
        ];
    }
}
