Files
api/services/nginx/app/routes/InvoicingPeriodRoute.php
T
Jeppe Bundgaard fbbcf7e85d Add collective subscription price aggregation and department distribution parsing
- Enhanced `getTransactionsWithItemsNotIncludedInInvoices` to aggregate total subscription prices and calculate department-based distribution.
- Added processing to parse department IDs into human-readable names.
- Updated response to include aggregated subscription results under `collective_subscription_results`.
2025-10-01 19:13:18 +02:00

1018 lines
51 KiB
PHP

<?php
namespace routes;
use classes\authentication;
use Exception;
use objects\customer_vehicles_o;
use objects\logs_o;
use objects\orders_o;
use objects\products_o;
use objects\users_o;
use traits\route_t;
class InvoicingPeriodRoute
{
use route_t;
/**
* @param array $collective_results
* @return array
* @throws Exception
*/
private static function parseTheDepartmentIdsToDepartmentNames(array $collective_results): array
{
foreach ( $collective_results['total_department_totals'] as $department_id => $amount ) {
$department_name = (new \objects\departments_o())->select((int)$department_id)->name->value();
if (empty($department_name)) {
$department_name = 'Unknown Department (' . $department_id . ')';
}
$collective_results['total_department_totals_parsed'][$department_name] = $amount;
}
foreach ( $collective_results['total_department_totals_relative'] as $department_id => $amount ) {
$department_name = (new \objects\departments_o())->select((int)$department_id)->name->value();
if (empty($department_name)) {
$department_name = 'Unknown Department (' . $department_id . ')';
}
$collective_results['total_department_totals_relative_parsed'][$department_name] = $amount;
}
return $collective_results;
}
public function run(): void
{
$this->get('/superuser/invoicing/period', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
self::requireParameters([
'dateFrom',
'dateTo',
]);
$dateFrom = $this->getParameter('dateFrom');
$dateTo = $this->getParameter('dateTo');
// Require the dateFrom and dateTo parameters to be valid dates
self::requireDateFormat($dateFrom, 'Y-m-d');
self::requireDateFormat($dateTo, 'Y-m-d');
// Add a day to the dateTo parameter to include the end date in the range
$dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
// Get the invoicing period for the user
$response->success([...self::getInvoicingPeriod($dateFrom, $dateTo)]);
} else {
// Log the incident
(new logs_o())->add('vehicles', 'global', 1, 0, 'LIST_OWN_VEHICLES', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'superuser_invoicing_period' => 'Get the invoicing period for superusers',
]
);
$this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () {
// Require the user to be logged in
global $response;
//$this->requirePermission('superuser_invoicing_period');
self::requireParameters([
'dateFrom',
'dateTo',
]);
$dateFrom = $this->getParameter('dateFrom');
$dateTo = $this->getParameter('dateTo');
// Require the dateFrom and dateTo parameters to be valid dates
self::requireDateFormat($dateFrom, 'Y-m-d');
self::requireDateFormat($dateTo, 'Y-m-d');
// Add a day to the dateTo parameter to include the end date in the range
$dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
$response->success([...self::getOriginalPrice(self::getFixedPricing($dateFrom, $dateTo))]);
},
[
'superuser_invoicing_period' => 'Get the invoicing period for superusers',
]
);
$this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () {
// Require the user to be logged in
global $response;
//$this->requirePermission('superuser_invoicing_period');
self::requireParameters([
'dateFrom',
'dateTo',
]);
$dateFrom = $this->getParameter('dateFrom');
$dateTo = $this->getParameter('dateTo');
// Require the dateFrom and dateTo parameters to be valid dates
self::requireDateFormat($dateFrom, 'Y-m-d');
self::requireDateFormat($dateTo, 'Y-m-d');
// Add a day to the dateTo parameter to include the end date in the range
$dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day'));
$response->success([...self::getTransactionsWithItemsNotIncludedInInvoices(self::getVehicleSubscriptions($dateFrom, $dateTo))]);
},
[
'superuser_invoicing_period' => 'Get the invoicing period for superusers',
]
);
}
/**
* 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;
}
}