$amount ) { $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value(); if (empty($department_name)) { $department_name = 'Unknown Department (' . $department_id . ')'; } $collective_results['total_department_totals_parsed'][$department_name] = $amount; } foreach ( $collective_results['total_department_totals_relative'] as $department_id => $amount ) { $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value(); if (empty($department_name)) { $department_name = 'Unknown Department (' . $department_id . ')'; } $collective_results['total_department_totals_relative_parsed'][$department_name] = $amount; } return $collective_results; } public function run(): void { $this->get('/superuser/invoicing/period', function () { // Require the user to be logged in global $response; $this->requirePermission('superuser_invoicing_period'); // Get the user object $user = (new authentication())->get_user(); // Check if the request was successful if ($user) { self::requireParameters([ 'dateFrom', 'dateTo', ]); $dateFrom = $this->getParameter('dateFrom'); $dateTo = $this->getParameter('dateTo'); // Require the dateFrom and dateTo parameters to be valid dates self::requireDateFormat($dateFrom, 'Y-m-d'); self::requireDateFormat($dateTo, 'Y-m-d'); // Add a day to the dateTo parameter to include the end date in the range $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day')); // Get the invoicing period for the user $response->success([...self::getInvoicingPeriod($dateFrom, $dateTo)]); } else { // Log the incident (new logs_o())->add('vehicles', 'global', 1, 0, 'LIST_OWN_VEHICLES', 'No user found, or invalid session'); // Return an error $response->error('Invalid session', 400); } }, [ 'superuser_invoicing_period' => 'Get the invoicing period for superusers', ] ); $this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () { // Require the user to be logged in global $response; //$this->requirePermission('superuser_invoicing_period'); self::requireParameters([ 'dateFrom', 'dateTo', ]); $dateFrom = $this->getParameter('dateFrom'); $dateTo = $this->getParameter('dateTo'); // Require the dateFrom and dateTo parameters to be valid dates self::requireDateFormat($dateFrom, 'Y-m-d'); self::requireDateFormat($dateTo, 'Y-m-d'); // Add a day to the dateTo parameter to include the end date in the range $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day')); $response->success([...self::getOriginalPrice(self::getFixedPricing($dateFrom, $dateTo))]); }, [ 'superuser_invoicing_period' => 'Get the invoicing period for superusers', ] ); $this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () { // Require the user to be logged in global $response; //$this->requirePermission('superuser_invoicing_period'); self::requireParameters([ 'dateFrom', 'dateTo', ]); $dateFrom = $this->getParameter('dateFrom'); $dateTo = $this->getParameter('dateTo'); // Require the dateFrom and dateTo parameters to be valid dates self::requireDateFormat($dateFrom, 'Y-m-d'); self::requireDateFormat($dateTo, 'Y-m-d'); // Add a day to the dateTo parameter to include the end date in the range $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day')); $response->success([...self::getTransactionsWithItemsNotIncludedInInvoices(self::getVehicleSubscriptions($dateFrom, $dateTo))]); }, [ 'superuser_invoicing_period' => 'Get the invoicing period for superusers', ] ); } /** * Get the transactions with items, that are not included in invoices. (include_in_invoice = 0 - order_items) * Requirements: * - The transaction must not be deleted. (deleted_at is null - orders)) * - The transaction contains at least one item that is not included in invoices. (include_in_invoice = 0 - order_items) * - The transaction must be within the specified date range. (created_at between dateFrom and dateTo - orders) * - The transaction item must not be deleted. (deleted_at is null - order_items) * Returns an array of customers with their transactions and items. * @param array $customersWithSubscriptions The customers with subscriptions. * @param array $dateRange The date range to filter the transactions. (dateFrom, dateTo) * @return array The customers with their transactions and items. * @throws Exception * @example * [ * [ * 'customer_number' => 12345678, * 'customer_name' => 'Customer Name', * 'transactions' => [ * [ * 'id' => 1, * 'date' => '2023-01-01', * 'amount' => 100.00, * 'booked' => true, * ], * [ * 'id' => 2, * 'date' => '2023-01-02', * 'amount' => 200.00, * 'booked' => false, * ], * ], * 'requires_action' => false, * 'meta' => [ * 'subscription' => [ * 'id' => 1, * 'name' => 'Subscription Name', * 'price' => 100.00, * 'description' => 'Subscription Description', * ], * ], * ], * [ * 'customer_number' => 87654321, * 'customer_name' => 'Another Customer', * 'transactions' => [ * [ * 'id' => 3, * 'date' => '2023-01-03', * 'amount' => 300.00, * 'booked' => true, * ], * ], * 'requires_action' => true, * 'meta' => [ * 'subscription' => [ * 'id' => 2, * 'name' => 'Another Subscription', * 'price' => 200.00, * 'description' => 'Another Description', * ], * ], * ], * ] * @see orders_o::getTransactionsWithItemsNotIncludedInInvoices * @see order_items_o::include_in_invoice * @see orders_o::deleted_at * @see order_items_o::deleted_at * @see orders_o::created_at * @see orders_o::customer_id * @see self::getVehicleSubscriptions() */ private static function getTransactionsWithItemsNotIncludedInInvoices(array $customersWithSubscriptions): array { global $response; // Loop through each customer and get their transactions with items not included in invoices foreach ( $customersWithSubscriptions as &$customer ) { // Initialize the meta['subscription'] array if it doesn't exist if (!isset($customer['meta']['subscription'])) { $customer['meta']['subscription'] = [ 'vehicles' => [], // Array of vehicle registration numbers 'subscription_total' => 0, // Total price of the subscriptions for the customer 'subscriptions' => [], // Array of subscriptions for the customer (['registration' => ['type' => (int), 'price' => (int), 'distributions' => <(int: departmentId)>[]]) 'subscription_price_department_distribution' => [ // departmentId => price / (number of unique distributions) // This is calculated after the subscriptions have been added // This is used to distribute the subscription price across the transactions ] ]; } // Get the vehicles for the customer $vehicles_o = new customer_vehicles_o(); // Get the vehicles for the customer (as an array of customer_vehicles_o objects) $customer_vehicles = array_map(function ($vehicle) { return (new customer_vehicles_o())->select((int)$vehicle['id']); }, $vehicles_o->getFieldsWhere([ 'customer_id' => (int)$customer['customer_number'], 'wash_subscription' => 1, // Only get vehicles with a wash subscription ], ['id'])); // Add the vehicle registration numbers to the meta['subscription']['vehicles'] array foreach ( $customer_vehicles as $vehicle ) { if ($vehicle instanceof customer_vehicles_o) { $customer['meta']['subscription']['vehicles'][] = $vehicle->reg->value(); } else { $customer['meta']['subscription']['vehicles'][] = 'Unknown Vehicle'; } } // Calculate the total price of the subscriptions for the customer foreach ( $customer_vehicles as $vehicle ) { if ($vehicle instanceof customer_vehicles_o) { // Get the transaction ids covered by the subscription for the vehicle // This is done to be able to distribute the subscription price across the transactions // We only want to get the transactions that are in the customer's transactions array, (this is to avoid getting transactions that are outside the date range) // We also want to make sure that the transactions are for the correct vehicle (reg_1 = vehicle reg), and that the transactions are not deleted (deleted_at is null) $transaction_ids_covered_by_subscription = array_map(function ($transaction) { return (int)$transaction; }, $vehicle->getSubscriptionAppliedTransactionsFromList(array_map( function ($transaction) { return (int)$transaction['id']; }, (new orders_o())->getFieldsWhereIn([ 'id' => array_map(function ($transaction) { return (int)$transaction['id']; }, $customer['transactions']), 'reg_1' => $vehicle->reg->value(), ], ['id']) ))); // Get the subscription details for the vehicle $subscription = [ 'type' => (int)$vehicle->type->value(), 'price' => (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice(), // Get the unique department ids for the transactions covered by the subscription 'distributions' => array_unique(array_map(function ($transaction_id) { // Get the department id for the transaction(s) covered by the subscription $transaction = (new orders_o())->select((int)$transaction_id); return (int)$transaction->department_id->value(); }, $transaction_ids_covered_by_subscription)), ]; $customer['meta']['subscription']['subscriptions'][$vehicle->reg->value()] = $subscription; $customer['meta']['subscription']['subscription_total'] += $subscription['price']; // Add the subscription price per transaction to the meta['subscription']['subscription_price_department_distribution'] array foreach ( $subscription['distributions'] as $department_id ) { if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) { $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0; } // Divide the subscription price by the number of unique distributions $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription['price'] / count($subscription['distributions']); } // If the vehicle has no transactions covered by the subscription, we need to run through the list of fallback options if (count($transaction_ids_covered_by_subscription) === 0) { // Run fallback options self::attemptSubscriptionFallbacks($vehicle, $customer); } } } } // Get the department distribution for all customers (sum of all customers) $collective_results = [ 'total_subscription_price' => 0, 'subscription_price_department_distribution' => [ // departmentId => price ], ]; unset($customer); // Unset the reference to avoid issues foreach ( $customersWithSubscriptions as $customer ) { if (isset($customer['meta']['subscription'])) { $collective_results['total_subscription_price'] += $customer['meta']['subscription']['subscription_total']; foreach ( $customer['meta']['subscription']['subscription_price_department_distribution'] as $department_id => $price ) { if (!isset($collective_results['subscription_price_department_distribution'][$department_id])) { $collective_results['subscription_price_department_distribution'][$department_id] = 0; } $collective_results['subscription_price_department_distribution'][$department_id] += $price; } } } // Parse the department ids to department names $parsed_distribution = []; foreach ( $collective_results['subscription_price_department_distribution'] as $department_id => $price ) { $department_name = (new \objects\departments_o())->select((int)$department_id)->name->value(); if (empty($department_name)) { $department_name = 'Unknown Department (' . $department_id . ')'; } $parsed_distribution[$department_name] = $price; } $collective_results['subscription_price_department_distribution_parsed'] = $parsed_distribution; // Include the collective results in the response $response->add_include('collective_subscription_results', $collective_results); return $customersWithSubscriptions; } /** * Attempt fallback options to find transactions covered by the subscription. * This is used when a vehicle has a subscription, but no transactions in the given date range. * This function processes the following fallback options: * 1. Attempt to divide the subscription price across the departments the customer has subscription transactions in. * 2. Attempt to get the last transaction the vehicle was washed in, and use that department. * 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer. * 4. If no departments are found, assign the subscription to a default department (e.g., department ID 1). * @param customer_vehicles_o $vehicle The vehicle object. * @param array $customer The customer object (by reference). * @retuns bool True if a fallback was applied, false otherwise. * @throws Exception If an error occurs while processing the fallbacks. */ private static function attemptSubscriptionFallbacks(customer_vehicles_o $vehicle, array &$customer): bool { if (self::debug) echo "Attempting fallbacks for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] . " - " . (new users_o())->getCustomerName((int)$customer['customer_number']) . "\n"; // Run the fallback options in order $subscription_price = (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice(); if (self::divideSubscriptionAcrossCustomerDepartments($customer, $subscription_price)) { return true; } if (self::useLastTransactionDepartment($customer, $vehicle, $subscription_price)) { return true; } if (self::useRecentCustomerTransactions($customer, $subscription_price)) { return true; } if (self::useDefaultDepartment($customer, $subscription_price)) { return true; } // If no fallback was applied, return false if (self::debug) echo "All fallbacks failed for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] . "\n"; return false; // No fallback applied } const debug = false; // 1. Attempt to divide the subscription price across the departments the customer has transactions in. private static function divideSubscriptionAcrossCustomerDepartments(array &$customer, int $subscription_price): bool { if (self::debug) echo "Fallback 1: Dividing subscription price across customer departments\n"; // Get the unique department ids for the customer's transactions $department_ids = array_unique(array_map(/** * @throws Exception */ function ($transaction) { $transaction_obj = (new orders_o())->select((int)$transaction['id']); return (int)$transaction_obj->department_id->value(); }, $customer['transactions'])); if (count($department_ids) > 0) { foreach ( $department_ids as $department_id ) { // If the department id is 10 (automatic), skip it if ($department_id === 10) { continue; } if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) { $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0; } // Divide the subscription price by the number of unique departments $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription_price / count($department_ids); } // Debug: if (self::debug) echo "Fallback 1 applied: Divided subscription price across customer departments. Departments: " . implode(', ', $department_ids) . "\n"; return true; // Fallback applied } if (self::debug) echo "Fallback 1 not applied: No departments found in customer's transactions\n"; return false; // No departments found } // 2. Attempt to get the last transaction the vehicle was washed in, and use that department. /** * @throws Exception */ private static function useLastTransactionDepartment(array &$customer, customer_vehicles_o $vehicle, int $subscription_price): bool { if (self::debug) echo "Fallback 2: Using last transactions departments\n"; // Get the last transaction the vehicle was washed in $last_transaction_ids = $vehicle->getLastTransactions(10); // Get the last 10 transactions $distribution_department_ids = []; // Array to hold the department ids from the last transactions, together with their counts (1 = 10%, 2 = 20%, etc.) // Calculate the department distribution from the last transactions foreach ( $last_transaction_ids as $transaction_id ) { $transaction = (new orders_o())->select((int)$transaction_id); if ($transaction instanceof orders_o) { $department_id = (int)$transaction->department_id->value(); // If the department id is 10 (automatic), skip it if ($department_id === 10) { continue; } if (!isset($distribution_department_ids[$department_id])) { $distribution_department_ids[$department_id] = 0; } $distribution_department_ids[$department_id]++; } } // If we have department ids from the last transactions, use them to distribute the subscription price if (count($distribution_department_ids) > 0) { $total_counts = array_sum($distribution_department_ids); foreach ( $distribution_department_ids as $department_id => $count ) { if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) { $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0; } // Distribute the subscription price based on the count of the department in the last transactions $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price; } // Debug: if (self::debug) echo "Fallback 2 applied: Used last transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n"; return true; // Fallback applied } if (self::debug) echo "Fallback 2 not applied: No last transaction found for vehicle\n"; return false; // No last transaction found } // 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer. private static function useRecentCustomerTransactions(array &$customer, int $subscription_price): bool { if (self::debug) echo "Fallback 3: Using recent customer transactions\n"; // Get the last 10 transactions of the customer $recent_transaction_ids = array_slice(array_map(function ($transaction) { return (int)$transaction['id']; }, (new orders_o())->getFieldsWhere([ 'customer_id' => (int)$customer['customer_number'], 'deleted_at' => null, ], ['id', 'created_at'])), 0, 10); $distribution_department_ids = []; // Array to hold the department ids from the recent transactions, together with their counts (1 = 10%, 2 = 20%, etc.) // Calculate the department distribution from the recent transactions foreach ( $recent_transaction_ids as $transaction_id ) { $transaction = (new orders_o())->select((int)$transaction_id); if ($transaction instanceof orders_o) { $department_id = (int)$transaction->department_id->value(); // If the department id is 10 (automatic), skip it if ($department_id === 10) { continue; } if (!isset($distribution_department_ids[$department_id])) { $distribution_department_ids[$department_id] = 0; } $distribution_department_ids[$department_id]++; } } // If we have department ids from the recent transactions, use them to distribute the subscription price if (count($distribution_department_ids) > 0) { $total_counts = array_sum($distribution_department_ids); foreach ( $distribution_department_ids as $department_id => $count ) { // If the department id is 10 (automatic), skip it if ($department_id === 10) { continue; } if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) { $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0; } // Distribute the subscription price based on the count of the department in the recent transactions $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price; } // Debug: if (self::debug) echo "Fallback 3 applied: Used recent customer transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n"; return true; // Fallback applied } if (self::debug) echo "Fallback 3 not applied: No recent transactions found for customer\n"; return false; // No recent transactions found } // 4. If no departments are found, assign the subscription to the customers default department (e.g., department ID 1). private static function useDefaultDepartment(array &$customer, int $subscription_price): bool { if (self::debug) echo "Fallback 4: Using default department\n"; $default_department_id = (new users_o)->getUserByCustomerNumber((int)$customer['customer_number'])->getDefaultDepartment(); if (!empty($default_department_id)) { if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id])) { $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] = 0; } $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] += $subscription_price; // Debug: if (self::debug) echo "Fallback 4 applied: Used default department ID " . $default_department_id . "\n"; return true; // Fallback applied } if (self::debug) echo "Fallback 4 not applied: No default department found for customer\n"; return false; // No default department found } /** * Get the original price for each customer, and transaction in the fixed pricing array. * * @param array $fixed_pricing The fixed pricing array. * @return array The fixed pricing array with the original price added. * @throws Exception */ private static function getOriginalPrice(array $fixed_pricing): array { global $response; foreach ( $fixed_pricing as &$customer ) { if (isset($customer['meta']['fixed_pricing'])) { $original_price = 0; $department_totals = []; // Array to hold totals per department foreach ( $customer['transactions'] as $transaction ) { $transaction_obj = (new orders_o())->select((int)$transaction['id']); $transaction['original_price'] = $transaction_obj->getNetAmountForOrderItemsOriginal(); $original_price += $transaction['original_price']; // Add the transaction department id to the transaction $department_id = (int)$transaction_obj->department_id->value(); // Initialize the department total if it doesn't exist if (!isset($department_totals[$department_id])) { $department_totals[$department_id] = 0; } // Add the transaction amount to the department total $department_totals[$department_id] += $transaction['original_price']; } $customer['meta']['fixed_pricing']['original_price'] = $original_price; $customer['meta']['fixed_pricing']['department_totals'] = $department_totals; // Take the relative price of the department totals in relation to the fixed price // This is done to see how much each department contributes to the fixed price $total = array_sum($department_totals); foreach ( $department_totals as $department_id => $amount ) { if ($total > 0) { $department_totals[$department_id] = ($amount / $total) * $customer['meta']['fixed_pricing']['price']; } else { $department_totals[$department_id] = 0; } } $customer['meta']['fixed_pricing']['department_totals_relative'] = $department_totals; } } // Calculate the total original price for all customers $collective_results = [ 'total_fixed_price' => 0, 'total_original_price' => 0, 'total_department_totals' => [], 'total_department_totals_relative' => [], ]; unset($customer); // Unset the reference to avoid issues foreach ( $fixed_pricing as $customer ) { if (isset($customer['meta']['fixed_pricing'])) { $collective_results['total_fixed_price'] += $customer['meta']['fixed_pricing']['price']; $collective_results['total_original_price'] += $customer['meta']['fixed_pricing']['original_price']; // Sum the department totals foreach ( $customer['meta']['fixed_pricing']['department_totals'] as $department_id => $amount ) { if (!isset($collective_results['total_department_totals'][$department_id])) { $collective_results['total_department_totals'][$department_id] = 0; } $collective_results['total_department_totals'][$department_id] += $amount; } // Sum the relative department totals foreach ( $customer['meta']['fixed_pricing']['department_totals_relative'] as $department_id => $amount ) { if (!isset($collective_results['total_department_totals_relative'][$department_id])) { $collective_results['total_department_totals_relative'][$department_id] = 0; } $collective_results['total_department_totals_relative'][$department_id] += $amount; } } } // Parse the department ids to department names $collective_results = self::parseTheDepartmentIdsToDepartmentNames($collective_results); // Include the collective results in the response $response->add_include('collective_fixed_pricing_results', $collective_results); return $fixed_pricing; } /** * @throws Exception */ private static function getInvoicingPeriod(string $dateFrom, string $dateTo): array { //$customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo) $customersWithTransactions = self::debugGetTime(function () use ($dateFrom, $dateTo) { return self::getCustomersWithTransactions($dateFrom, $dateTo); }, 'customers_with_transactions'); $types = []; // Add the customers with transactions to the types array $types['all'] = $customersWithTransactions; $types['vehicle_subscriptions'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getVehicleSubscriptions($dateFrom, $dateTo, $customersWithTransactions); }, 'vehicle_subscriptions'); $types['fixed_pricing'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getFixedPricing($dateFrom, $dateTo, $customersWithTransactions); }, 'fixed_pricing'); $types['tank_cleaning'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getTankCleaning($dateFrom, $dateTo, $customersWithTransactions); }, 'tank_cleaning'); $types['special_arrangements'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getSpecialArrangements($dateFrom, $dateTo, $customersWithTransactions); }, 'special_arrangements'); $types['invoice_per_order'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getInvoicingPerOrder($dateFrom, $dateTo, $customersWithTransactions); }, 'invoice_per_order'); $types['possible_duplicates'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions) { return self::getPossibleDuplicates($dateFrom, $dateTo, $customersWithTransactions); }, 'possible_duplicates'); return [ 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, 'types' => $types, ]; } /** * Debugging function to measure the time taken by a function. * * @param callable $function The function to debug. * @return mixed The result of the function. */ private static function debugGetTime(callable $function, string $debug_label = ''): mixed { global $response; $start_time = microtime(true) * 1000; // Start time in milliseconds $result = $function(); $end_time = microtime(true) * 1000; // End time in milliseconds $execution_time = $end_time - $start_time; $response->add_include( 'debug_invoicing_period_' . $debug_label, [ 'execution_time' => $execution_time, ] ); return $result; } /** * @throws Exception */ private static function getCustomersWithTransactions(string $dateFrom, string $dateTo): array { // Define the customers with orders in the specified date range $customers = self::debugGetTime(function () use ($dateFrom, $dateTo) { return (new orders_o())->getCustomersWithOrdersInDateRange($dateFrom, $dateTo); }, 'customers_with_orders_in_date_range'); //$customers = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo); /** * // user_id => customer_number, * @example * [ * '123' => '12345678', * '456' => '87654321' * ] */ $customer_numbers = []; $tmp = []; foreach ( $customers as $customer ) { $customer_number = (int)$customer->customer_number->value(); if (empty($customer_number)) { // Skip if the customer number is empty continue; } // Check if the customer number is already in the array // This ensures that we only process each customer number once // We use (int)$customer_number to ensure that the customer number is an integer if (isset($customer_numbers[$customer_number])) { continue; } // Add the customer number to the array $customer_numbers[$customer_number] = $customer->id; } // Get the transactions for the customers in the specified date range /** * @example * [ * '12345678' => [ * orders_o, * orders_o, * ] * ] * @var $customer_number_transactions */ $customer_number_transactions = (new orders_o())->getTransactionsForCustomersInDateRange(array_keys($customer_numbers), $dateFrom, $dateTo); // Calculate the total amount for each transaction, to minimize the number of queries $transaction_ids = []; foreach ( $customer_number_transactions as $customer_number => $transactions ) { foreach ( $transactions as $transaction ) { if ($transaction instanceof orders_o) { $transaction_ids[] = $transaction->id; } } } // Get the total amount for each transaction $transaction_totals = (new orders_o())->getNetAmountForOrders($transaction_ids); // Add the total amount to each transaction foreach ( $customer_number_transactions as $customer_number => $transactions ) { foreach ( $transactions as $transaction ) { if ($transaction instanceof orders_o) { // Set the total amount for the transaction $transaction->setTemporaryNetAmount($transaction_totals[$transaction->id] ?? 0); } } } // Process the customer numbers to ensure they are unique foreach ( $customer_numbers as $customer_number => $user_id ) { if (empty($customer_number)) { // Skip if the customer number is empty continue; } // Check if the customer number is already in the array if (isset($tmp[$customer_number])) { // If the customer number is already in the array, skip it continue; } // If the customer number is not in the array, add it // Construct the customer object with transactions $tmp[] = self::constructCustomerObject( (int)$customer_number, (new \objects\users_o())->getCustomerName((int)$customer_number), $customer_number_transactions[(int)$customer_number] ?? [], false, (int)$user_id, ); } return $tmp; } /** * @param int $customer_number * @param string $customer_name * @param orders_o[] $transactions * @param bool $requires_action * @return array * @throws Exception */ protected static function constructCustomerObject( int $customer_number, string $customer_name, array $transactions = [], bool $requires_action = false, ?int $user_id = null, ?array $meta = null ): array { return [ 'id' => $user_id ?? (new users_o())->getUserByCustomerNumber((int)$customer_number)->id, 'customer_number' => $customer_number, 'customer_name' => $customer_name, 'transactions' => $parsed_transactions = array_map(function ($transaction) { return self::constructTransactionObject($transaction); }, $transactions), 'requires_action' => self::checkRequiresAction($parsed_transactions, $requires_action), 'meta' => $meta ?? [], ]; } /** * @throws Exception */ private static function constructTransactionObject(orders_o $transaction): array { return [ 'id' => $transaction->id, 'date' => $transaction->created_at->value(), 'amount' => $transaction->temporary_net_amount, // Use the temporary net amount set earlier 'booked' => $transaction->isBooked(), ]; } private static function checkRequiresAction(array $parsed_transactions, bool $requires_action): bool { // If requires_action is already set to true, return true if ($requires_action) { return true; } // Check if any transaction is not booked foreach ( $parsed_transactions as $transaction ) { if (!$transaction['booked']) { return true; } } // If all transactions are booked, return false return false; } /** * @throws Exception */ private static function getVehicleSubscriptions(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // If customersWithTransactions is not provided, get all customers with transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } // Since these are monthly subscriptions, we don't need to filter by transactions $customer_numbers = (new \objects\users_o())->getCustomersWithVehicleSubscriptions(); // Get all customers with vehicle subscriptions $subscriptions = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { $subscriptions[] = (self::getCustomerFromList((int)$customer_number, $customersWithTransactions)) ?? self::constructCustomerObject( (int)$customer_number, (new \objects\users_o())->getCustomerName((int)$customer_number) ?? 'Unknown Customer', [], true ); } return $subscriptions; } /** * Get the customer from the list of customers with transactions. * * @param int $customer_number The customer number to search for. * @param array $customersWithTransactions The list of customers with transactions. * @return array|null The customer object if found, null otherwise. */ private static function getCustomerFromList(int $customer_number, array $customersWithTransactions): ?array { // Search for the customer in the list of customers with transactions foreach ( $customersWithTransactions as $customer ) { if ($customer['customer_number'] === $customer_number) { return $customer; } } // If the customer is not found, return null return null; } /** * @throws Exception */ private static function getFixedPricing(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // If customersWithTransactions is not provided, get all customers with transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } // Get all customers with fixed pricing $customer_numbers = (new \objects\users_o())->getCustomersWithFixedPricing(); // Get the customers fixed pricing $tmp_fixed_pricing = array_map(function ($arr) { // Return the customer number and price return [ 'customer_number' => (int)$arr['customer_number'], 'price' => (float)$arr['price'], 'description' => (string)($arr['description'] ?? ''), ]; }, (new \objects\customer_fixed_pricing_o())->getFieldsWhere(['customer_number' => $customer_numbers], ['customer_number', 'price', 'description'])); // Get all customers with fixed pricing $fixed_pricing = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { $fixed_pricing[] = self::getCustomerFromList( (int)$customer_number, $customersWithTransactions ); // Check if the last entry is null, if so, create a new customer object if (end($fixed_pricing) === null) { $fixed_pricing[count($fixed_pricing) - 1] = self::constructCustomerObject( (int)$customer_number, (new \objects\users_o())->getCustomerName((int)$customer_number) ?? (new \objects\users_o())->select((int)$customer_number)->display_name->value(), [], true, ); } // Add the fixed pricing to the customer object $fixed_pricing[count($fixed_pricing) - 1]['meta']['fixed_pricing'] = self::getObjectFromArray( $tmp_fixed_pricing, function ($item) use ($customer_number) { return $item['customer_number'] === $customer_number; } ); } return $fixed_pricing; } /** * Get an object from an array based on a callback function. * * @param array $array The array to search in. * @param callable $callback The callback function to use for searching. * @return mixed|null The found object or null if not found. */ private static function getObjectFromArray(array $array, callable $callback): mixed { // Iterate through the array and apply the callback function to each element foreach ( $array as $item ) { // If the callback returns true, return the item if ($callback($item)) { return $item; } } // If no item matches the callback, return null return null; } /** * @throws Exception */ private static function getTankCleaning(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // If customersWithTransactions is not provided, get all customers with transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } // Filter out customers that do not have any transactions in the specified date range $customer_numbers = (new \objects\users_o())->getCustomersWithTankCleaning(); self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); // Get all customers with tank cleaning $tank_cleaning = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { $tank_cleaning[] = self::getCustomerFromList( (int)$customer_number, $customersWithTransactions ); // Add the tank cleaning to the list if it has transactions } return $tank_cleaning; } /** * Filters the customer numbers based on whether they have transactions in the specified date range. * * @param array $customer_numbers The customer numbers to filter. * @param array $customersWithTransactions The customers with transactions in the specified date range. */ private static function filterCustomersWithTransactions(array &$customer_numbers, array $customersWithTransactions): void { // Filter out customers that do not have any transactions in the specified date range $customer_numbers = array_filter($customer_numbers, function ($customer_number) use ($customersWithTransactions) { // Check if the customer has any transactions in the specified date range foreach ( $customersWithTransactions as $customer ) { if ($customer['customer_number'] === $customer_number) { return true; } } return false; }); } /** * @throws Exception */ private static function getSpecialArrangements(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // If customersWithTransactions is not provided, get all customers with transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } // Get all customers with tank cleaning $customer_numbers = (new \objects\users_o())->getCustomersWithSpecialArrangements(); // Filter out customers that do not have any transactions in the specified date range self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); $special_arrangements = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { $special_arrangements[] = self::getCustomerFromList( (int)$customer_number, $customersWithTransactions ); } return $special_arrangements; } /** * @throws Exception */ private static function getInvoicingPerOrder(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // Get all customers with the invoicing per order attribute $customer_numbers = (new users_o())->getCustomerNumbersWithAttributes(['invoiceAllOrdersIndividually']); // Filter out customers that do not have any transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); $invoicing_per_order = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { // Add the invoicing per order to the list $invoicing_per_order[] = self::getCustomerFromList((int)$customer_number, $customersWithTransactions); } return $invoicing_per_order; } /** * @throws Exception */ private static function getPossibleDuplicates(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { // If customersWithTransactions is not provided, get all customers with transactions in the specified date range if ($customersWithTransactions === null) { $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } // Get orders with the same reg_1, that has been created within 24 hours of each other $orders = (new orders_o())->getOrdersWithPossibleDuplicates($dateFrom, $dateTo); // Get the customer numbers from the orders $tmp_customer_arr = []; // Remove duplicates from the customer numbers $possible_duplicates = []; /** @var int $customer_number */ foreach ( $orders as $order ) { // Get the customer number from the order $customer_number = (int)$order[0]['object']->customer_id->value(); // Check if the customer number is already in the array if (isset($tmp_customer_arr[$customer_number])) { continue; } // Add the customer number to the array $tmp_customer_arr[$customer_number] = true; // Get the customer from the list of customers with transactions $customer = self::getCustomerFromList($customer_number, $customersWithTransactions); // Add the customer to the possible duplicates array $possible_duplicates[] = self::constructCustomerObject( $customer_number, (new \objects\users_o())->getCustomerName($customer_number) ?? 'Unknown Customer', array_map(function ($transaction) { // Construct the transaction object from the order return $transaction['object']; }, $order), false, // Requires action because there are possible duplicates $customer['id'] ?? null // Use the id from the customer object if available ); } return $possible_duplicates; } }