Files
api/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php
T

555 lines
22 KiB
PHP

<?php
namespace helpers;
use classes\xlvask;
use Exception;
use objects\departments_o;
use objects\orders_o;
use objects\xlvask_potential_order_matches_o;
class xlvask_usage_log extends xlvask_helper
{
/**
* Example of a usage log object:
* [WashId] => 8cb0b5a7-ab4b-4dd5-b805-869733c23e54
* [CustomerId] => 59440200
* [Customer] => DITOBUS EXCURSIONS A/S
* [VatNumber] => 31171520
* [Location] => Hvidovre
* [Hall] => Hvidovre_1
* [HallId] => 845d29a1-a7d2-4e3b-bbc3-2b13242d744a
* [StartTime] => 2025-06-02T15:20:14.950
* [FinishTime] => 2025-06-02T15:25:55.377
* [RegistrationNumber] => CZ92915
* [VehicleType] => Bus/autocamper, M
* [IdentificationType] => LPR
* [IdentificationId] => CZ92915
* [Info] => CZ92915
* [Updated] =>
* [Prepaid] =>
* [FinishStatus] => 1
* [CustomerGuid] => 21ba156a-b2d2-44be-8398-4b67d66003d6
* [VehicleId] => e6679209-e52f-4376-9660-d56cb46f523b
* [WashItems] => Array[washItem] => Array
*/
/**
* The wash ID of the usage log entry
* @var int|string|null $WashId
*/
public int|string|null $WashId;
/**
* The ID of the customer (XLVask ID)
* @var int|string|null $CustomerId
*/
public int|string|null $CustomerId;
/**
* The name of the customer
* @var int|string|null $Customer
*/
public int|string|null $Customer;
/**
* The VAT number of the customer
* @var int|string|null $VatNumber
*/
public int|string|null $VatNumber;
/**
* The location of the wash
* @var int|string|null $Location
*/
public int|string|null $Location;
/**
* The hall where the wash took place
* @var int|string|null $Hall
*/
public int|string|null $Hall;
/**
* The ID of the hall where the wash took place
* @var int|string|null $HallId
*/
public int|string|null $HallId;
/**
* The start time of the wash
* @var string|int|null $StartTime
*/
public string|int|null $StartTime;
/**
* The finish time of the wash
* @var string|int|null $FinishTime
*/
public string|int|null $FinishTime;
/**
* The registration number of the vehicle
* @var int|string|null $RegistrationNumber
*/
public int|string|null $RegistrationNumber;
/**
* The type of the vehicle
* @var string|int|null $VehicleType
*/
public string|int|null $VehicleType;
/**
* The type of identification used for the wash
* @var string|int|null $IdentificationType
*/
public string|int|null $IdentificationType;
/**
* The ID of the identification used for the wash
* @var string|int|null $IdentificationId
*/
public string|int|null $IdentificationId;
/**
* Additional information about the wash
* @var string|int|null $Info
*/
public string|int|null $Info;
/**
* The last updated time of the wash entry
* @var string|int|null $Updated
*/
public string|int|null $Updated;
/**
* Indicates if the wash was prepaid
* @var int|string|null $Prepaid
*/
public int|string|null $Prepaid;
/**
* The finish status of the wash
* @var int|string|null $FinishStatus
*/
public int|string|null $FinishStatus;
/**
* The GUID of the customer in XL Vask
* @var string|int|null $CustomerGuid
*/
public string|int|null $CustomerGuid;
/**
* The ID of the vehicle associated with the wash
* @var string|int|null $vehicleId
*/
public string|int|null $VehicleId;
/**
* The wash items associated with the wash
* @var array[washItem] $washItems
* @see xlvask_wash_item
*/
public array $WashItems;
protected string $default_string = 'DEFAULT_STRING_1';
protected string $default_int = 'DEFAULT_INT_1';
protected string $default_bool = 'DEFAULT_BOOL_1';
protected string $default_int_nullable = 'DEFAULT_INT_NULLABLE_1';
protected string $default_string_nullable = 'DEFAULT_STRING_NULLABLE_1';
protected string $default_bool_nullable = 'DEFAULT_BOOL_NULLABLE_1';
/**
* Constructor to initialize the customer object with default values
* @throws Exception
*/
public function __construct(array $data = [])
{
$this->default_string = 'DEFAULT_STRING_1';
$this->default_int = 'DEFAULT_INT_1';
$this->default_bool = 'DEFAULT_BOOL_1';
$this->default_int_nullable = 'DEFAULT_INT_NULLABLE_1';
$this->default_string_nullable = 'DEFAULT_STRING_NULLABLE_1';
$this->default_bool_nullable = 'DEFAULT_BOOL_NULLABLE_1';
// Initialize the customer object with default values
$this::reset();
$this::setProperties($data);
}
/**
* Reset this object to its default values.
* This method clears all properties of the usage log object,
* effectively resetting it to a clean state.
* @return void
*/
private function reset(): void
{
$this->WashId = $this->default_string;
$this->CustomerId = $this->default_string;
$this->Customer = $this->default_string;
$this->VatNumber = $this->default_string;
$this->Location = $this->default_string;
$this->Hall = $this->default_string;
$this->HallId = $this->default_string;
$this->StartTime = $this->default_string;
$this->FinishTime = $this->default_string;
$this->RegistrationNumber = $this->default_string;
$this->VehicleType = $this->default_string;
$this->IdentificationType = $this->default_string;
$this->IdentificationId = $this->default_string;
$this->Info = $this->default_string;
$this->Updated = $this->default_string_nullable;
$this->Prepaid = $this->default_bool;
$this->FinishStatus = $this->default_int;
$this->CustomerGuid = $this->default_string;
$this->VehicleId = $this->default_string;
$this->WashItems = []; // Initialize as an empty array
}
/**
* Set the properties of the object based on the provided data.
* @note This method DOES NOT reset the object before setting the properties.
* @param array $data
* @return self
* @throws Exception
* @see reset()
*/
public function setProperties(array $data): self
{
$default_property_values = [
'WashId' => $this->default_string,
'CustomerId' => $this->default_string,
'Customer' => $this->default_string,
'VatNumber' => $this->default_string,
'Location' => $this->default_string,
'Hall' => $this->default_string,
'HallId' => $this->default_string,
'StartTime' => $this->default_string,
'FinishTime' => $this->default_string,
'RegistrationNumber' => $this->default_string,
'VehicleType' => $this->default_string,
'IdentificationType' => $this->default_string,
'IdentificationId' => $this->default_string,
'Info' => $this->default_string,
'Updated' => $this->default_string_nullable,
'Prepaid' => $this->default_bool,
'FinishStatus' => $this->default_int,
'CustomerGuid' => $this->default_string,
'VehicleId' => $this->default_string,
];
foreach ( $data as $key => $value ) {
if (property_exists(self::class, $key)) {
switch ($default_property_values[$key] ?? null) {
case $this->default_string:
case $this->default_string_nullable:
$nullable = $default_property_values[$key] === $this->default_string_nullable;
// Check if the value is a string or can be converted to a string
if (is_string($value) || is_numeric($value)) {
$value = (string)$value; // Ensure the value is a string
} elseif ($nullable) {
$value = null; // If nullable, set to null
} else {
$value = ''; // If not nullable, set to empty string
}
break;
case $this->default_int:
case $this->default_int_nullable:
$nullable = $default_property_values[$key] === $this->default_int_nullable;
// Check if the value is numeric and convert it to an integer or null
$value = is_numeric($value) ? (int)$value : ($nullable ? null : 0); // Ensure the value is an integer or null
break;
case $this->default_bool:
case $this->default_bool_nullable:
$nullable = $default_property_values[$key] === $this->default_bool_nullable;
// Check if the value is a boolean or can be converted to a boolean
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($value === null && !$nullable) {
$value = false; // If the value is not a boolean and not nullable, set it to false
}
break;
case null:
break;
default:
// If the value is not a default string, keep it as is
echo "No conversion needed for property '$key' with value '$value'." . PHP_EOL;
break;
}
// Set the property value
$this->{$key} = $value;
} else {
// If the property does not exist, throw an exception
// Debug print all properties of the class
//$properties = get_class_vars(self::class);
//echo "Available properties in " . self::class . ": " . implode(', ', array_keys($properties)) . PHP_EOL;
throw new Exception("Property '$key' does not exist in " . self::class);
}
}
// If the washItems property is set, ensure it is an array
if (isset($data['WashItems']) && is_array($data['WashItems'])) {
//echo 'Setting WashItems with ' . count($this->WashItems) . ' items.' . PHP_EOL;
$this->WashItems = self::generateWashItems($data['WashItems']);
//print_r($this->WashItems);
} else {
//echo 'No washItems provided or not an array. Initializing as empty array.' . PHP_EOL;
//print_r($data);
}
// After setting all properties, nullify nullable properties
$this->unsetNullifiableProperties();
// Return the populated object
return $this;
}
/**
* Generate wash items from the provided array.
* This method takes an array of wash items and converts them into instances of xlvask_wash_item.
* @throws Exception
*/
private static function generateWashItems(array $washItems): array
{
// Generate wash items from the provided array
$generatedItems = [];
foreach ( $washItems as $item ) {
if (is_array($item)) {
$generatedItems[] = new xlvask_wash_item($item);
} elseif ($item instanceof xlvask_wash_item) {
$generatedItems[] = $item; // If it's already an instance, add it
} else {
throw new Exception("Invalid wash item type: " . gettype($item));
}
}
// Sort the generated items with the sortWashItems method
return self::sortWashItems($generatedItems);
}
private static function sortWashItems(array $generatedItems): array
{
// Make sure the primary wash item is always first
$primaryItems = [
'Stor bil',
'Lille bil',
'Vask udført',
];
usort($generatedItems, function ($a, $b) use ($primaryItems) {
// Check if the item is a primary item
$aIsPrimary = in_array($a->OriginalProductName, $primaryItems, true);
$bIsPrimary = in_array($b->OriginalProductName, $primaryItems, true);
// If both are primary items, keep their order
if ($aIsPrimary && $bIsPrimary) {
return 0;
}
// If only one is a primary item, it should come first
return $aIsPrimary ? -1 : 1;
});
// Return the sorted array of wash items
return $generatedItems;
}
private function unsetNullifiableProperties(): void
{
// Unset properties that are null or empty strings
$properties = [
'WashId', 'CustomerId', 'Customer', 'VatNumber', 'Location',
'Hall', 'HallId', 'StartTime', 'FinishTime', 'RegistrationNumber',
'VehicleType', 'IdentificationType', 'IdentificationId', 'Info',
'Updated', 'Prepaid', 'FinishStatus', 'CustomerGuid', 'VehicleId'
];
foreach ( $properties as $property ) {
if ($this->isEmptyOrDefault($this->{$property})) {
$tmp_value = $this->{$property};
if ($tmp_value === $this->default_string || $tmp_value === $this->default_string_nullable) {
$this->{$property} = ''; // Set to null if it matches the default string
} elseif ($tmp_value === $this->default_int || $tmp_value === $this->default_int_nullable) {
if ($tmp_value === $this->default_int_nullable) {
$this->{$property} = null; // Set to null if it matches the default int nullable
} else {
$this->{$property} = 0; // Set to 0 if it matches the default int
}
} elseif ($tmp_value === $this->default_bool || $tmp_value === $this->default_bool_nullable) {
$this->{$property} = false; // Set to null if it matches the default bool
} else {
$this->{$property} = null; // Otherwise, set to null
}
}
}
}
private function isEmptyOrDefault($param): bool
{
// Check if the parameter is empty or matches the default values
return $param === null || $param === '' || $param === $this->default_string || $param === $this->default_int || $param === $this->default_bool || $param === $this->default_int_nullable || $param === $this->default_string_nullable || $param === $this->default_bool_nullable;
}
/**
* Get the formatted date of the wash start time.
* @return string The formatted date of the wash start time. YYYY-MM-DD HH:MM:SS format.
* @note Returns an empty string if StartTime is not set.
*/
public function getFormattedDate(): string
{
// Format the date in a human-readable format
if ($this->StartTime) {
return date('Y-m-d H:i:s', strtotime($this->StartTime));
}
return '';
}
/**
* Get the formatted finish date of the wash.
* @return string The formatted finish date of the wash. YYYY-MM-DD HH:MM:SS format.
* @note Returns an empty string if FinishTime is not set.
*/
public function getFormattedFinishDate(): string
{
// Format the finish date in a human-readable format
if ($this->FinishTime) {
return date('Y-m-d H:i:s', strtotime($this->FinishTime));
}
return '';
}
/**
* Get the total price of the wash.
* This method calculates the total price of the wash by summing up the prices of all wash items.
* @return float The total price of the wash.
* @throws Exception If any of the wash items do not have a valid price.
*/
public function getTotalPrice(): float
{
// Calculate the total price of the wash by summing up the prices of all wash items
$totalPrice = 0.0;
foreach ( $this->WashItems as $item ) {
/** @var xlvask_wash_item $item */
if (isset($item->PriceIncVat) && is_numeric($item->PriceIncVat)) {
// The reason why we use the PriceIncVat is that it is the final price, and the price without VAT is not always available / correct.
$totalPrice += ((float)$item->PriceIncVat - (float)$item->Vat);
} else {
//print_r($item);
throw new Exception("Invalid price for wash item: " . json_encode($item));
}
}
return $totalPrice;
}
/**
* Check if the wash is completed.
* @note This method checks the FinishStatus property to determine if the wash is completed.
* * A wash is considered completed if the FinishStatus is set to 1.
* @return bool
*/
public function isCompleted(): bool
{
// Check if the wash is completed based on the FinishStatus
return $this->FinishStatus === 1; // Assuming 1 indicates a completed wash
}
/**
* Get the primary wash item from the usage log.
* This method returns the first wash item in the WashItems array as the primary item.
* @throws Exception
*/
public function getPrimaryItem(): xlvask_wash_item
{
// Return the first wash item as the primary item
if (isset($this->WashItems[0]) && $this->WashItems[0] instanceof xlvask_wash_item) {
return $this->WashItems[0];
}
throw new Exception("No primary wash item found in the usage log.");
}
/**
* Is the wash linked to an order?
* @return bool
* @note This method checks if an order exists with the "wash_id" field matching the WashId of this usage log.
* @throws Exception
*/
public function isLinkedToOrder(): bool
{
// Check if the wash is linked to an order by checking the "wash_id" field in the orders table
$order = (new orders_o())->selectByWashId($this->WashId);
return !empty($order);
}
/**
* Get the order associated with this wash.
* @return orders_o|null
* @throws Exception
*/
public function getOrder(): ?orders_o
{
// Get the order associated with this wash by checking the "wash_id" field in the orders table
$order = (new orders_o())->selectByWashId($this->WashId);
if ($order) {
return $order;
}
return null; // Return null if no order is found
}
/**
* Get the department associated with this wash.
* @throws Exception If the department cannot be determined.
*/
public function getDepartment(): ?departments_o
{
// Since the department / hall XLVASK API endpoints are not available,
// We will try to get the department from the hall name.
// These are formatted as "DepartmentName_HallNumber" (E.g. "Hvidovre_1").
if (!empty($this->Hall)) {
$parts = explode('_', $this->Hall);
if (count($parts) > 1) {
$departmentName = $parts[0];
// If there's a capitalized letter in the department name, add a space before it (e.g. "AarhusC" -> "Aarhus C")
$departmentName = preg_replace('/([a-z])([A-Z])/', '$1 $2', $departmentName);
// Remove any trailing underscores or spaces
$departmentName = trim($departmentName, '_ ');
return (new departments_o())->selectByName($departmentName);
}
}
// If the hall is not set or does not contain a department name, return null
return null;
}
public function getLane(): ?int
{
// Since the department / hall XLVASK API endpoints are not available,
// We will try to get the lane from the hall name.
// These are formatted as "DepartmentName_HallNumber" (E.g. "Hvidovre_1").
if (!empty($this->Hall)) {
$parts = explode('_', $this->Hall);
if (count($parts) > 1 && is_numeric($parts[1])) {
return (int)$parts[1]; // Return the lane number as an integer
}
}
// If the hall is not set or does not contain a lane number, return null
return null;
}
/**
* @throws Exception
*/
public function getCustomer(): xlvask_customer
{
$xlvask = new xlvask();
$matches = $xlvask->getCustomers(
$this->CustomerId,
);
if (isset($matches[0]) && $matches[0] instanceof xlvask_customer) {
return $matches[0];
} else {
throw new Exception("No customer found with ID: " . $this->CustomerId);
}
}
/**
* @throws Exception
*/
public function getPotentialOrder(): ?orders_o
{
// To prevent duplicate orders, we will check if an order that looks like it belongs to this wash already exists.
// There will be false positives, but we will try to minimize them.
// Usually, vehicles aren't washed more than once a day, so we will check for orders with the same license plate and within a 24-hour period.
$date = [
'start' => date('Y-m-d H:i:s', strtotime($this->StartTime) - 86400), // 24 hours before the start time
'end' => date('Y-m-d H:i:s', strtotime($this->FinishTime) + 86400), // 24 hours after the finish time
];
//echo 'Looking for potential order for vehicle ' . $this->RegistrationNumber . ' between ' . $date['start'] . ' and ' . $date['end'] . PHP_EOL;
$order = (new orders_o())->selectByRegistrationNumberAndDateRange(
$this->RegistrationNumber,
$date['start'],
$date['end'],
);
if ($order) {
// Make sure the order duplicate is not ignored
if (!(new xlvask_potential_order_matches_o())->shouldIgnoreDuplicate($this->WashId)) {
// If an order is found, return it
return $order;
}
}
// If no order is found, return null
return null;
}
}