Fix XLVask usage-log import metadata and period-scoped Selvvask automation.
305 lines
12 KiB
PHP
305 lines
12 KiB
PHP
<?php
|
|
|
|
namespace objects;
|
|
|
|
use classes\db;
|
|
use classes\object_property;
|
|
use classes\xlvask;
|
|
use classes\xlvask_usage_logs_schema_bootstrap;
|
|
use Exception;
|
|
use helpers\xlvask_customer;
|
|
use helpers\xlvask_usage_log;
|
|
use helpers\xlvask_vehicle;
|
|
use traits\db_object_t;
|
|
|
|
class xlvask_usage_logs_o extends db
|
|
{
|
|
use db_object_t;
|
|
|
|
public object_property $WashId;
|
|
public object_property $CustomerId;
|
|
public object_property $Customer;
|
|
public object_property $VatNumber;
|
|
public object_property $Location;
|
|
public object_property $Hall;
|
|
public object_property $HallId;
|
|
public object_property $StartTime;
|
|
public object_property $FinishTime;
|
|
public object_property $RegistrationNumber;
|
|
public object_property $VehicleType;
|
|
public object_property $IdentificationType;
|
|
public object_property $IdentificationId;
|
|
public object_property $Info;
|
|
public object_property $Updated;
|
|
public object_property $Prepaid;
|
|
public object_property $FinishStatus;
|
|
public object_property $CustomerGuid;
|
|
public object_property $VehicleId;
|
|
public object_property $WashItems;
|
|
public object_property $ignored_at;
|
|
public object_property $ignored_by;
|
|
public object_property $ignored_reason;
|
|
|
|
public function structure(): void
|
|
{
|
|
xlvask_usage_logs_schema_bootstrap::ensureTables();
|
|
$this->setTable('xlvask_usage_logs');
|
|
}
|
|
|
|
/**
|
|
* Add a new XL Vask customer
|
|
* @param array $data The properties
|
|
* @returns void
|
|
* @throws Exception If the object was not created successfully
|
|
*/
|
|
public function add(array $data): void
|
|
{
|
|
$tmp_id = self::add_object($data);
|
|
$this->id = $tmp_id;
|
|
self::getObjectProperties();
|
|
self::objectChanged();
|
|
}
|
|
|
|
public function getObjectProperties(): void
|
|
{
|
|
$this->WashId = new object_property($this->table, $this->id, 'WashId', 'string', false);
|
|
$this->CustomerId = new object_property($this->table, $this->id, 'CustomerId', 'string', false);
|
|
$this->Customer = new object_property($this->table, $this->id, 'Customer', 'string', false);
|
|
$this->VatNumber = new object_property($this->table, $this->id, 'VatNumber', 'string', false);
|
|
$this->Location = new object_property($this->table, $this->id, 'Location', 'string', false);
|
|
$this->Hall = new object_property($this->table, $this->id, 'Hall', 'string', false);
|
|
$this->HallId = new object_property($this->table, $this->id, 'HallId', 'string', false);
|
|
$this->StartTime = new object_property($this->table, $this->id, 'StartTime', 'string', false);
|
|
$this->FinishTime = new object_property($this->table, $this->id, 'FinishTime', 'string', false);
|
|
$this->RegistrationNumber = new object_property($this->table, $this->id, 'RegistrationNumber', 'string', false);
|
|
$this->VehicleType = new object_property($this->table, $this->id, 'VehicleType', 'string', false);
|
|
$this->IdentificationType = new object_property($this->table, $this->id, 'IdentificationType', 'string', false);
|
|
$this->IdentificationId = new object_property($this->table, $this->id, 'IdentificationId', 'string', false);
|
|
$this->Info = new object_property($this->table, $this->id, 'Info', 'string', false);
|
|
$this->Updated = new object_property($this->table, $this->id, 'Updated', 'string', false);
|
|
$this->Prepaid = new object_property($this->table, $this->id, 'Prepaid', 'string', false);
|
|
$this->FinishStatus = new object_property($this->table, $this->id, 'FinishStatus', 'string', false);
|
|
$this->CustomerGuid = new object_property($this->table, $this->id, 'CustomerGuid', 'string', false);
|
|
$this->VehicleId = new object_property($this->table, $this->id, 'VehicleId', 'string', false);
|
|
$this->WashItems = new object_property($this->table, $this->id, 'WashItems', 'string', false);
|
|
$this->ignored_at = new object_property($this->table, $this->id, 'ignored_at', 'datetime', false);
|
|
$this->ignored_by = new object_property($this->table, $this->id, 'ignored_by', 'int', false);
|
|
$this->ignored_reason = new object_property($this->table, $this->id, 'ignored_reason', 'string', false);
|
|
}
|
|
|
|
public function objectChanged(): void
|
|
{
|
|
//TODO: Add cache invalidation
|
|
}
|
|
|
|
public function getCachedAmountSummaryFromRow(array $row): array
|
|
{
|
|
$cached_amount = self::normalizeMoneyValue($row['cached_total_net_amount'] ?? null);
|
|
$cached_at = trim((string)($row['cached_amount_at'] ?? ''));
|
|
|
|
if ($cached_amount !== null && $cached_at !== '') {
|
|
return [
|
|
'total_net_amount' => $cached_amount,
|
|
'primary_product_name' => (string)($row['cached_primary_product_name'] ?? ''),
|
|
'cached' => true,
|
|
];
|
|
}
|
|
|
|
$summary = self::calculateAmountSummaryFromWashItems($row['WashItems'] ?? []);
|
|
$id = (int)($row['id'] ?? 0);
|
|
if ($id > 0) {
|
|
self::cacheAmountSummary($id, $summary);
|
|
}
|
|
|
|
return [
|
|
...$summary,
|
|
'cached' => false,
|
|
];
|
|
}
|
|
|
|
public static function calculateAmountSummaryFromWashItems(array|string|null $washItems): array
|
|
{
|
|
if (is_string($washItems)) {
|
|
$decoded = json_decode($washItems, true);
|
|
$washItems = is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
$total = 0.0;
|
|
$primaryProductName = '';
|
|
foreach (is_array($washItems) ? $washItems : [] as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
if ($primaryProductName === '' && isset($item['OriginalProductName'])) {
|
|
$primaryProductName = trim((string)$item['OriginalProductName']);
|
|
}
|
|
|
|
$priceIncVat = self::normalizeMoneyValue($item['PriceIncVat'] ?? null);
|
|
$vat = self::normalizeMoneyValue($item['Vat'] ?? 0.0) ?? 0.0;
|
|
if ($priceIncVat === null) {
|
|
continue;
|
|
}
|
|
|
|
$total += $priceIncVat - $vat;
|
|
}
|
|
|
|
return [
|
|
'total_net_amount' => round($total, 2),
|
|
'primary_product_name' => $primaryProductName,
|
|
];
|
|
}
|
|
|
|
private static function cacheAmountSummary(int $id, array $summary): void
|
|
{
|
|
global $db;
|
|
|
|
if (!isset($db) || !is_object($db) || !method_exists($db, 'query')) {
|
|
return;
|
|
}
|
|
|
|
$amount = number_format((float)($summary['total_net_amount'] ?? 0.0), 2, '.', '');
|
|
$primaryProductName = $db->escape_string((string)($summary['primary_product_name'] ?? ''));
|
|
$db->query(
|
|
"UPDATE xlvask_usage_logs
|
|
SET cached_total_net_amount = {$amount},
|
|
cached_primary_product_name = " . ($primaryProductName === '' ? 'NULL' : "'{$primaryProductName}'") . ",
|
|
cached_amount_at = NOW()
|
|
WHERE id = {$id}"
|
|
);
|
|
}
|
|
|
|
private static function normalizeMoneyValue(mixed $value): ?float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_int($value) || is_float($value)) {
|
|
return (float)$value;
|
|
}
|
|
|
|
$normalized = preg_replace('/[^\d,.\-]/', '', (string)$value);
|
|
if ($normalized === null || $normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_contains($normalized, ',') && !str_contains($normalized, '.')) {
|
|
$normalized = str_replace(',', '.', $normalized);
|
|
} else {
|
|
$normalized = str_replace(',', '', $normalized);
|
|
}
|
|
|
|
return is_numeric($normalized) ? (float)$normalized : null;
|
|
}
|
|
|
|
/**
|
|
* Import the usage logs from XL Vask
|
|
* @param string|null $dateFrom Optional import start date or date-time modifier. Defaults to '-7 days'.
|
|
* @param string|null $dateTo Optional inclusive import end date.
|
|
* @throws Exception If the objects were not successfully added.
|
|
* @returns void
|
|
*/
|
|
public function importUsageLogs(?string $dateFrom = null, ?string $dateTo = null): void
|
|
{
|
|
if (!empty($this->id)) {
|
|
throw new Exception('To prevent issues, having a selected object is not allowed.');
|
|
}
|
|
$usage_logs = $this->getUsageLogsFromXLVask(
|
|
self::formatImportDateFrom($dateFrom) // Example: '2025-05-01T00:00:00.000'
|
|
);
|
|
$usage_logs = self::filterUsageLogsUntil($usage_logs, $dateTo);
|
|
/** @var string[] $known_usage_logIds The XL Vask usage logIds currently known */
|
|
$known_usage_logIds = array_map(function ($log) {
|
|
return $log['WashId'];
|
|
},
|
|
self::getFieldsWhere(
|
|
['WashId' => array_column($usage_logs, 'WashId')],
|
|
['WashId']
|
|
));
|
|
/** The XL Vask usage logs without a matching WashId in the database */
|
|
$new_usage_logs = array_filter($usage_logs, function ($log) use ($known_usage_logIds) {
|
|
return !in_array($log->WashId, $known_usage_logIds);
|
|
});
|
|
unset($known_usage_logIds);
|
|
unset($usage_logs);
|
|
/** Adding the new usage logs */
|
|
foreach ($new_usage_logs as $log) {
|
|
if (!$log->isValid()) {
|
|
echo $log->formattedDetails();
|
|
throw new Exception('A usage log from XL Vask is not valid, have the structure changed?');
|
|
}
|
|
}
|
|
// Actually save the usage log
|
|
foreach ($new_usage_logs as $log) {
|
|
/** @var xlvask_usage_log $log */
|
|
$this->add($log->toArray());
|
|
}
|
|
unset($new_usage_logs);
|
|
}
|
|
|
|
private static function formatImportDateFrom(?string $dateFrom): string
|
|
{
|
|
$dateFrom = trim((string)($dateFrom ?? ''));
|
|
$timestamp = strtotime($dateFrom === '' ? '-7 days' : $dateFrom);
|
|
|
|
if ($timestamp === false) {
|
|
throw new Exception('Invalid XL Vask usage import dateFrom');
|
|
}
|
|
|
|
return date('Y-m-d\TH:i:s.000', $timestamp);
|
|
}
|
|
|
|
/**
|
|
* @param xlvask_usage_log[] $usageLogs
|
|
* @return xlvask_usage_log[]
|
|
* @throws Exception
|
|
*/
|
|
private static function filterUsageLogsUntil(array $usageLogs, ?string $dateTo): array
|
|
{
|
|
$dateTo = trim((string)($dateTo ?? ''));
|
|
if ($dateTo === '') {
|
|
return $usageLogs;
|
|
}
|
|
|
|
$dateToTimestamp = strtotime($dateTo);
|
|
if ($dateToTimestamp === false) {
|
|
throw new Exception('Invalid XL Vask usage import dateTo');
|
|
}
|
|
|
|
$inclusiveEndTimestamp = strtotime(date('Y-m-d 23:59:59', $dateToTimestamp));
|
|
if ($inclusiveEndTimestamp === false) {
|
|
throw new Exception('Invalid XL Vask usage import dateTo');
|
|
}
|
|
|
|
return array_values(array_filter($usageLogs, function (xlvask_usage_log $log) use ($inclusiveEndTimestamp) {
|
|
$startTimestamp = strtotime((string)$log->StartTime);
|
|
return $startTimestamp !== false && $startTimestamp <= $inclusiveEndTimestamp;
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* This function retrieves the usage logs from XL Vask
|
|
* @param string $fromDate The date from which to retrieve the usage logs, in ISO 8601 format (e.g., '2025-05-01T00:00:00.000')
|
|
* @returns xlvask_usage_log[]
|
|
* @throws Exception
|
|
*/
|
|
public function getUsageLogsFromXLVask(string $fromDate): array
|
|
{
|
|
// Validate the date format
|
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/', $fromDate)) {
|
|
throw new Exception('Invalid date format. Expected format: YYYY-MM-DDTHH:MM:SS.SSS (e.g., 2025-05-01T00:00:00.000)');
|
|
}
|
|
// Set the memory limit to 512MB, as the import can be quite large
|
|
ini_set('memory_limit', '512M');
|
|
/** Get the vehicles from XL Vask */
|
|
$xlvask = new xlvask();
|
|
$vehicle_class = $xlvask->new($xlvask->helpers->xlvask_usage_log);
|
|
/** @var xlvask_usage_log[] $vehicles */
|
|
$vehicles = $vehicle_class->toObjects($xlvask->getUsageLog(
|
|
$fromDate, // Example: '2025-05-01T00:00:00.000'
|
|
));
|
|
return $vehicles;
|
|
}
|
|
}
|