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; } }