Enhance orders_o, order_items_o, and InvoicingPeriodRoute: add transaction and calculation methods, implement debug execution timing, refine invoicing period handling, optimize order retrieval, improve caching, and expand database query logic.
This commit is contained in:
@@ -274,9 +274,16 @@ class collected_order_invoices_o extends db
|
||||
{
|
||||
// Require the invoice collection to be selected
|
||||
self::requireSelected();
|
||||
// Check if the booked state is cached
|
||||
$cached = self::getCached('isBooked', $this->id);
|
||||
if ($cached !== null) {
|
||||
return (bool)$cached;
|
||||
}
|
||||
// Get the invoice booked id from the external id
|
||||
try {
|
||||
self::getInvoiceBookedId();
|
||||
self::cache('isBooked', true, $this->id);
|
||||
self::setCachedExpiration('isBooked', self::$isBookedCacheExpiration, $this->id);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
@@ -294,17 +301,19 @@ class collected_order_invoices_o extends db
|
||||
// Require the invoice collection to be selected
|
||||
self::requireSelected();
|
||||
// Check if the invoice_booked_id is already set (We don't want to make a request to E-conomic if we already have the id - Since this is slow.)
|
||||
if (!empty($this->booked_invoice_id->value())) {
|
||||
return (int)$this->booked_invoice_id->value();
|
||||
$booked_invoice_id = $this->booked_invoice_id->value();
|
||||
if (!empty($booked_invoice_id)) {
|
||||
return (int)$booked_invoice_id;
|
||||
}
|
||||
$external_id = $this->external_id->value();
|
||||
// If the external id is empty, the invoice booked does not exist
|
||||
if ($this->external_id->value() === null) {
|
||||
if (empty($external_id)) {
|
||||
throw new Exception('Invoice booked does not exist');
|
||||
}
|
||||
// Create an economic object
|
||||
$economic = new economic();
|
||||
// Get the invoice booked id from the external id
|
||||
$invoice_booked_id = $economic->invoices->booked->get_from_external_id($this->external_id->value());
|
||||
$invoice_booked_id = $economic->invoices->booked->get_from_external_id($external_id);
|
||||
$this->booked_invoice_id->set($invoice_booked_id);
|
||||
return (int)$invoice_booked_id;
|
||||
}
|
||||
|
||||
@@ -236,17 +236,22 @@ class order_items_o extends db
|
||||
];
|
||||
}
|
||||
|
||||
public function getAllItemsAsArray(int $orderId): array
|
||||
public function getAllItemsAsArray(int $orderId, ?array $onlySpecificColumns = null): array
|
||||
{
|
||||
global $db;
|
||||
$sql = "SELECT * FROM $this->table WHERE order_id = $orderId AND deleted_at IS NULL";
|
||||
// Get all items for the order
|
||||
if ($onlySpecificColumns) {
|
||||
$columns = implode(', ', $onlySpecificColumns);
|
||||
} else {
|
||||
$columns = '*';
|
||||
}
|
||||
$sql = "SELECT $columns FROM $this->table WHERE order_id = $orderId AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
// Circumvent the repeated instantiation of the object, by just selecting the fields
|
||||
$items = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$items[] = $row;
|
||||
}
|
||||
// Fetch all rows
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use classes\response;
|
||||
use Exception;
|
||||
use helpers\xlvask_usage_log;
|
||||
use helpers\xlvask_wash_item;
|
||||
use routes\InvoicingPeriodRoute;
|
||||
use traits\db_object_t;
|
||||
|
||||
class orders_o extends db
|
||||
@@ -34,6 +35,14 @@ class orders_o extends db
|
||||
public object_property $wash_id; // The XL Vask Wash ID, if any
|
||||
public object_property $lane; // The lane used for the order, if any
|
||||
|
||||
/**
|
||||
* Temporary keys
|
||||
* These are used for calculations and should not be stored in the database.
|
||||
* @var float $temporary_net_amount
|
||||
* @see InvoicingPeriodRoute::getInvoicingPeriod()
|
||||
*/
|
||||
public float $temporary_net_amount = 0.0; // Temporary net amount for the order, used for calculations
|
||||
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
@@ -441,7 +450,9 @@ class orders_o extends db
|
||||
{
|
||||
self::requireSelected();
|
||||
$order_items = new order_items_o();
|
||||
$items = $order_items->getAllItemsAsArray($this->id);
|
||||
$items = $order_items->getAllItemsAsArray($this->id, [
|
||||
'include_in_invoice',
|
||||
]);
|
||||
$count = 0;
|
||||
foreach ( $items as $item ) {
|
||||
if ($item['include_in_invoice']) {
|
||||
@@ -529,7 +540,11 @@ class orders_o extends db
|
||||
// Get the order items object
|
||||
$order_items = new order_items_o();
|
||||
// Get the price, quantity of the order items
|
||||
$items = $order_items->getAllItemsAsArray($this->id);
|
||||
$items = $order_items->getAllItemsAsArray($this->id, [
|
||||
'include_in_invoice',
|
||||
'price',
|
||||
'quantity',
|
||||
]);
|
||||
// Loop through the items and get the net amount
|
||||
foreach ( $items as $item ) {
|
||||
// Check if the item is included in the invoice
|
||||
@@ -544,6 +559,34 @@ class orders_o extends db
|
||||
return $net;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the net amount of the orders
|
||||
* @param int[] $order_ids An array of order IDs to get the net amount for
|
||||
* @return array An array with the net amount for each order [ order_id => net_amount ]
|
||||
* @throws Exception If the order is not selected
|
||||
*/
|
||||
public function getNetAmountForOrders(array $order_ids): array
|
||||
{
|
||||
$order_items = new order_items_o();
|
||||
$tmp = $order_items->getFieldsWhere(
|
||||
[
|
||||
'order_id' => $order_ids,
|
||||
'include_in_invoice' => true,
|
||||
],
|
||||
[
|
||||
'price',
|
||||
'quantity',
|
||||
'include_in_invoice',
|
||||
'order_id'
|
||||
]
|
||||
);
|
||||
$net_amounts = [];
|
||||
foreach ( $tmp as $item ) {
|
||||
$net_amounts[$item['order_id']] = (float)(($net_amounts[$item['order_id']] ?? 0) + ((int)$item['price'] * (int)$item['quantity']));
|
||||
}
|
||||
return $net_amounts;
|
||||
}
|
||||
|
||||
public function includeIncludes(): orders_o
|
||||
{
|
||||
global /** @var response $response */
|
||||
@@ -825,15 +868,17 @@ class orders_o extends db
|
||||
$dateTo = $db->escape_string($dateTo);
|
||||
$sql = "SELECT DISTINCT customer_id FROM $this->table WHERE created_at BETWEEN '$dateFrom' AND '$dateTo' AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
$customer_numbers = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$customer_numbers[] = (int)trim($row['customer_id']);
|
||||
}
|
||||
if (empty($customer_numbers)) {
|
||||
if ($result->num_rows === 0) {
|
||||
return []; // No customers found in the date range
|
||||
}
|
||||
// Fetch all
|
||||
$result = $db->fetch_all($result);
|
||||
// Get the customers by their customer numbers
|
||||
return (new users_o())->getUsersByCustomerNumbers($customer_numbers);
|
||||
return (new users_o())->getUsersByCustomerNumbers(
|
||||
array_map(function ($row) {
|
||||
return (int)$row['customer_id'];
|
||||
}, $result)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -871,6 +916,41 @@ class orders_o extends db
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transactions for customers in a date range
|
||||
* @param int[] $customers An array of customer numbers to filter by
|
||||
* @param string $dateFrom The start date of the date range (inclusive) "Y-m-d H:i:s" format
|
||||
* @param string $dateTo The end date of the date range (inclusive) "Y-m-d H:i:s" format
|
||||
* @return array[customer_number => orders_o[]] The transactions for each customer within the specified date range
|
||||
*/
|
||||
public function getTransactionsForCustomersInDateRange(array $customers, string $dateFrom, string $dateTo): array
|
||||
{
|
||||
global $db;
|
||||
// Validate the date range
|
||||
if (strtotime($dateFrom) === false || strtotime($dateTo) === false) {
|
||||
throw new Exception('Invalid date range provided');
|
||||
}
|
||||
if (strtotime($dateFrom) > strtotime($dateTo)) {
|
||||
throw new Exception('The start date cannot be after the end date');
|
||||
}
|
||||
// Prepare the SQL query to find transactions for the customers in the date range
|
||||
$dateFrom = $db->escape_string($dateFrom);
|
||||
$dateTo = $db->escape_string($dateTo);
|
||||
$customerNumbers = implode(',', array_map('intval', $customers));
|
||||
$sql = "SELECT id, customer_id FROM $this->table WHERE customer_id IN ($customerNumbers) AND created_at BETWEEN '$dateFrom' AND '$dateTo' AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
if ($result->num_rows === 0) {
|
||||
return []; // No transactions found for the customers in the date range
|
||||
}
|
||||
$transactions = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$order = new orders_o();
|
||||
$order->select((int)$row['id']);
|
||||
$transactions[(int)$row['customer_id']][] = $order;
|
||||
}
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -940,4 +1020,10 @@ class orders_o extends db
|
||||
}
|
||||
return $resultOrders;
|
||||
}
|
||||
|
||||
public function setTemporaryNetAmount(float $amount): void
|
||||
{
|
||||
// Set a temporary net amount for the order
|
||||
$this->temporary_net_amount = $amount;
|
||||
}
|
||||
}
|
||||
@@ -55,48 +55,148 @@ class InvoicingPeriodRoute
|
||||
*/
|
||||
private static function getInvoicingPeriod(string $dateFrom, string $dateTo): array
|
||||
{
|
||||
$customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo);
|
||||
//$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' => [
|
||||
'vehicle_subscriptions' => self::getVehicleSubscriptions($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'fixed_pricing' => self::getFixedPricing($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'tank_cleaning' => self::getTankCleaning($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'special_arrangements' => self::getSpecialArrangements($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'invoice_per_order' => self::getInvoicingPerOrder($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'possible_duplicates' => self::getPossibleDuplicates($dateFrom, $dateTo, $customersWithTransactions),
|
||||
'all' => $customersWithTransactions,
|
||||
],
|
||||
'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 = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo);
|
||||
$customer_numbers_processed = [];
|
||||
$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 ) {
|
||||
if (in_array((int)$customer->customer_number->value(), $customer_numbers_processed)) {
|
||||
// Skip if the customer has already been processed
|
||||
$customer_number = (int)$customer->customer_number->value();
|
||||
if (empty($customer_number)) {
|
||||
// Skip if the customer number is empty
|
||||
continue;
|
||||
}
|
||||
$customer_numbers_processed[] = (int)$customer->customer_number->value();
|
||||
// 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->customer_number->value(),
|
||||
(new \objects\users_o())->getCustomerName((int)$customer->customer_number->value()),
|
||||
(new \objects\orders_o())->getTransactionsForCustomer(
|
||||
(int)$customer->customer_number->value(),
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
),
|
||||
(int)$customer_number,
|
||||
(new \objects\users_o())->getCustomerName((int)$customer_number),
|
||||
$customer_number_transactions[(int)$customer_number] ?? [],
|
||||
false,
|
||||
(int)$customer->id
|
||||
(int)$user_id,
|
||||
);
|
||||
}
|
||||
return $tmp;
|
||||
@@ -137,7 +237,7 @@ class InvoicingPeriodRoute
|
||||
return [
|
||||
'id' => $transaction->id,
|
||||
'date' => $transaction->created_at->value(),
|
||||
'amount' => $transaction->getNetAmount(),
|
||||
'amount' => $transaction->temporary_net_amount, // Use the temporary net amount set earlier
|
||||
'booked' => $transaction->isBooked(),
|
||||
];
|
||||
}
|
||||
@@ -205,44 +305,33 @@ class InvoicingPeriodRoute
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getFixedPricing(string $dateFrom, string $dateTo): array
|
||||
private static function getFixedPricing(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array
|
||||
{
|
||||
// Get all customers with fixed pricing
|
||||
// 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())->getCustomersWithFixedPricing();
|
||||
//self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);
|
||||
|
||||
// Get all customers with fixed pricing
|
||||
$fixed_pricing = [];
|
||||
/** @var int $customer_number */
|
||||
foreach ( $customer_numbers as $customer_number ) {
|
||||
$tmp_fixed_pricing = [
|
||||
'customer_number' => $customer_number,
|
||||
'customer_name' => (new \objects\users_o())->getCustomerName($customer_number) ?? 'Unknown Customer',
|
||||
'transactions' => [],
|
||||
'requires_action' => true,
|
||||
];
|
||||
// Check if the customer has a fixed pricing order within the date range
|
||||
$orders = (new \objects\orders_o())->getFixedPricingTransactions($customer_number, false, $dateFrom, $dateTo);
|
||||
/** @var orders_o $order */
|
||||
foreach ( $orders as $order ) {
|
||||
// Add the order to the fixed pricing transactions
|
||||
$tmp_fixed_pricing['transactions'][] = [
|
||||
'id' => $order->id,
|
||||
];
|
||||
$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) ?? 'Unknown Customer',
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
// If there are transactions, set requires_action to false
|
||||
if (count($tmp_fixed_pricing['transactions']) > 0) {
|
||||
// Check if there's any transaction that has not been booked yet
|
||||
$requires_action = false;
|
||||
foreach ( $tmp_fixed_pricing['transactions'] as $transaction ) {
|
||||
$order = (new orders_o())->select((int)$transaction['id']);
|
||||
if (!$order->isBooked()) {
|
||||
$requires_action = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Set requires_action based on the transactions
|
||||
$tmp_fixed_pricing['requires_action'] = $requires_action;
|
||||
}
|
||||
// Add the fixed pricing to the list if it has transactions
|
||||
$fixed_pricing[] = $tmp_fixed_pricing;
|
||||
}
|
||||
return $fixed_pricing;
|
||||
}
|
||||
@@ -308,15 +397,9 @@ class InvoicingPeriodRoute
|
||||
$special_arrangements = [];
|
||||
/** @var int $customer_number */
|
||||
foreach ( $customer_numbers as $customer_number ) {
|
||||
$special_arrangements[] = self::constructCustomerObject(
|
||||
$special_arrangements[] = self::getCustomerFromList(
|
||||
(int)$customer_number,
|
||||
(new \objects\users_o())->getCustomerName((int)$customer_number) ?? 'Unknown Customer',
|
||||
(new orders_o())->getTransactionsForCustomer(
|
||||
(int)$customer_number,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
),
|
||||
false,
|
||||
$customersWithTransactions
|
||||
);
|
||||
}
|
||||
return $special_arrangements;
|
||||
|
||||
@@ -45,6 +45,7 @@ use objects\users_o;
|
||||
trait db_object_t
|
||||
{
|
||||
public static int $asArrayCacheExpiration = 600; // The id of the object in the database
|
||||
public static int $isBookedCacheExpiration = 60; // The booked / not booked cache expiration time, in seconds. Default is 1 minute (60 seconds).
|
||||
public int $id; // The table of the objects in the database (e.g. users)
|
||||
private string $table; // The fields to search in the database (e.g. ['name', 'email']). If empty, all fields will be searched
|
||||
private array $searchableFields = []; // The where clauses to add to the pagination query
|
||||
|
||||
Reference in New Issue
Block a user