Add customer mass import service with API route, test coverage, and e-conomic integration
This commit is contained in:
@@ -124,7 +124,7 @@ class edge_gateway_manager
|
||||
'status' => self::INSTALL_SESSION_STATUS_PENDING,
|
||||
'step' => self::INSTALL_SESSION_STATUS_PENDING,
|
||||
'message' => 'Installer command generated. Run it on the gateway host.',
|
||||
], strtotime($expiresAt) - self::INSTALL_TOKEN_TTL_SECONDS),
|
||||
], (self::parseApplicationDateTime($expiresAt) ?? time()) - self::INSTALL_TOKEN_TTL_SECONDS),
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -1735,8 +1735,10 @@ BASH;
|
||||
throw new Exception('Shell session is closed');
|
||||
}
|
||||
|
||||
$expiresAt = $session->expires_at->value() === null ? null : strtotime((string)$session->expires_at->value());
|
||||
if ($expiresAt !== null && $expiresAt !== false && $expiresAt <= time()) {
|
||||
$expiresAt = self::parseApplicationDateTime(
|
||||
$session->expires_at->value() === null ? null : (string)$session->expires_at->value()
|
||||
);
|
||||
if ($expiresAt !== null && $expiresAt <= time()) {
|
||||
$session->status->set('EXPIRED');
|
||||
$session->closed_at->set($this->now());
|
||||
throw new Exception('Shell session expired');
|
||||
@@ -2071,7 +2073,11 @@ BASH;
|
||||
'events' => self::sanitizeInstallSessionEvents($session['events'] ?? []),
|
||||
];
|
||||
$status = (string)$normalized['status'];
|
||||
if (!self::installSessionStatusIsTerminal($status) && $expiresAt !== null && strtotime($expiresAt) < ($now ?? time())) {
|
||||
if (
|
||||
!self::installSessionStatusIsTerminal($status)
|
||||
&& $expiresAt !== null
|
||||
&& (self::parseApplicationDateTime($expiresAt) ?? PHP_INT_MAX) < ($now ?? time())
|
||||
) {
|
||||
$status = self::INSTALL_SESSION_STATUS_EXPIRED;
|
||||
$normalized['status'] = $status;
|
||||
$normalized['message'] = $normalized['message'] ?: 'Installer token expired before the gateway claimed successfully.';
|
||||
@@ -2257,7 +2263,16 @@ BASH;
|
||||
$host .= ':' . $port;
|
||||
}
|
||||
|
||||
return $scheme . '://' . $host;
|
||||
$forwardedPrefix = $this->firstForwardedHeaderValue($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? null);
|
||||
if ($forwardedPrefix !== null) {
|
||||
$normalizedForwardedPrefix = '/' . trim($forwardedPrefix, '/');
|
||||
$basePath = $normalizedForwardedPrefix === '/' ? '' : $normalizedForwardedPrefix;
|
||||
} else {
|
||||
$requestPath = parse_url((string)($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);
|
||||
$basePath = (is_string($requestPath) && preg_match('#^/api(?:/|$)#', $requestPath) === 1) ? '/api' : '';
|
||||
}
|
||||
|
||||
return $scheme . '://' . $host . $basePath;
|
||||
}
|
||||
|
||||
private function detectForwardedScheme(): ?string
|
||||
@@ -2383,7 +2398,7 @@ BASH;
|
||||
if (!$claimToken->exists()) {
|
||||
throw new Exception('Invalid install token');
|
||||
}
|
||||
if (strtotime((string)$claimToken->expires_at->value()) < time()) {
|
||||
if ((self::parseApplicationDateTime((string)$claimToken->expires_at->value()) ?? 0) < time()) {
|
||||
throw new Exception('Install token has expired');
|
||||
}
|
||||
|
||||
@@ -2597,7 +2612,7 @@ BASH;
|
||||
|
||||
try {
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT id
|
||||
'SELECT id, delivery_json
|
||||
FROM edge_gateway_command_jobs
|
||||
WHERE gateway_id = :gateway_id
|
||||
AND deleted_at IS NULL
|
||||
@@ -2626,16 +2641,25 @@ BASH;
|
||||
return null;
|
||||
}
|
||||
|
||||
$delivery = isset($row['delivery_json']) && is_string($row['delivery_json'])
|
||||
? json_decode($row['delivery_json'], true)
|
||||
: [];
|
||||
if (!is_array($delivery)) {
|
||||
$delivery = [];
|
||||
}
|
||||
$delivery['delivery_channel'] = self::DELIVERY_CHANNEL_API;
|
||||
$delivery['attempt_count'] = ((int)($delivery['attempt_count'] ?? 0)) + 1;
|
||||
$delivery['last_dispatch_error'] = null;
|
||||
$encodedDelivery = json_encode($delivery, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($encodedDelivery)) {
|
||||
$encodedDelivery = '{}';
|
||||
}
|
||||
|
||||
$update = $pdo->prepare(
|
||||
'UPDATE edge_gateway_command_jobs
|
||||
SET status = :status,
|
||||
response_json = :response_json,
|
||||
delivery_json = JSON_SET(
|
||||
COALESCE(delivery_json, JSON_OBJECT()),
|
||||
\'$.delivery_channel\', :delivery_channel,
|
||||
\'$.attempt_count\', COALESCE(JSON_EXTRACT(COALESCE(delivery_json, JSON_OBJECT()), \'$.attempt_count\'), 0) + 1,
|
||||
\'$.last_dispatch_error\', CAST(NULL AS JSON)
|
||||
),
|
||||
delivery_json = :delivery_json,
|
||||
error_message = NULL,
|
||||
completed_at = NULL
|
||||
WHERE id = :id'
|
||||
@@ -2643,7 +2667,7 @@ BASH;
|
||||
$update->execute([
|
||||
':status' => 'DISPATCHING',
|
||||
':response_json' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
':delivery_channel' => json_encode(self::DELIVERY_CHANNEL_API, JSON_UNESCAPED_UNICODE),
|
||||
':delivery_json' => $encodedDelivery,
|
||||
':id' => (int)$row['id'],
|
||||
]);
|
||||
|
||||
@@ -2655,6 +2679,7 @@ BASH;
|
||||
throw $throwable;
|
||||
}
|
||||
|
||||
$this->clearObjectPropertyCache('edge_gateway_command_jobs', (int)$row['id']);
|
||||
return (new edge_gateway_command_jobs_o())->select((int)$row['id']);
|
||||
}
|
||||
|
||||
@@ -4441,8 +4466,8 @@ BASH;
|
||||
? (array)$gateway['active_operation']
|
||||
: null;
|
||||
if ($activeOperation !== null && !empty($activeOperation['started_at'])) {
|
||||
$startedAt = strtotime((string)$activeOperation['started_at']);
|
||||
if ($startedAt !== false && (($now ?? time()) - $startedAt) >= edge_gateway_operation_service::OPERATION_TIMEOUT_SECONDS) {
|
||||
$startedAt = self::parseApplicationDateTime((string)$activeOperation['started_at']);
|
||||
if ($startedAt !== null && (($now ?? time()) - $startedAt) >= edge_gateway_operation_service::OPERATION_TIMEOUT_SECONDS) {
|
||||
$diagnostics[] = [
|
||||
'code' => edge_gateway_operation_service::ERROR_OPERATION_TIMEOUT,
|
||||
'severity' => 'warning',
|
||||
@@ -4594,8 +4619,8 @@ BASH;
|
||||
continue;
|
||||
}
|
||||
|
||||
$epoch = strtotime($timestamp);
|
||||
if ($epoch === false) {
|
||||
$epoch = self::parseApplicationDateTime($timestamp);
|
||||
if ($epoch === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4622,6 +4647,16 @@ BASH;
|
||||
return self::RELAY_FALLBACK_PREFER_LOCAL;
|
||||
}
|
||||
|
||||
private function clearObjectPropertyCache(string $table, int $id): void
|
||||
{
|
||||
if ($id <= 0 || !defined('redis')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$normalizedTable = trim($table, " `\t\n\r\0\x0B");
|
||||
redis->clear_keys('obj_prop:' . $normalizedTable . ':' . $id . ':*');
|
||||
}
|
||||
|
||||
private static function resolveDeviceFreshnessState(?array $device, ?int $ageSeconds): string
|
||||
{
|
||||
if ($device === null) {
|
||||
@@ -4692,8 +4727,8 @@ BASH;
|
||||
return null;
|
||||
}
|
||||
|
||||
$heartbeatTimestamp = strtotime($lastHeartbeatAt);
|
||||
if ($heartbeatTimestamp === false) {
|
||||
$heartbeatTimestamp = self::parseApplicationDateTime($lastHeartbeatAt);
|
||||
if ($heartbeatTimestamp === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4706,8 +4741,7 @@ BASH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$heartbeatTimestamp = strtotime($lastHeartbeatAt);
|
||||
return $heartbeatTimestamp === false ? 0 : $heartbeatTimestamp;
|
||||
return self::parseApplicationDateTime($lastHeartbeatAt) ?? 0;
|
||||
}
|
||||
|
||||
private static function statusPriority(string $status): int
|
||||
@@ -4822,14 +4856,55 @@ BASH;
|
||||
return hash('sha256', $plainToken);
|
||||
}
|
||||
|
||||
public static function parseApplicationDateTime(?string $value): ?int
|
||||
{
|
||||
$normalized = trim((string)$value);
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timezone = self::applicationTimeZone();
|
||||
$dateTime = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $normalized, $timezone);
|
||||
if ($dateTime instanceof \DateTimeImmutable) {
|
||||
return $dateTime->getTimestamp();
|
||||
}
|
||||
|
||||
try {
|
||||
return (new \DateTimeImmutable($normalized, $timezone))->getTimestamp();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function formatApplicationDateTime(int $timestamp): string
|
||||
{
|
||||
return (new \DateTimeImmutable('@' . $timestamp))
|
||||
->setTimezone(self::applicationTimeZone())
|
||||
->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
private static function applicationTimeZone(): \DateTimeZone
|
||||
{
|
||||
$timezone = trim((string)($_ENV['CONFIG_TIMEZONE'] ?? getenv('CONFIG_TIMEZONE') ?: 'Europe/Copenhagen'));
|
||||
if ($timezone === '') {
|
||||
$timezone = 'Europe/Copenhagen';
|
||||
}
|
||||
|
||||
try {
|
||||
return new \DateTimeZone($timezone);
|
||||
} catch (\Throwable) {
|
||||
return new \DateTimeZone('Europe/Copenhagen');
|
||||
}
|
||||
}
|
||||
|
||||
private function now(): string
|
||||
{
|
||||
return date('Y-m-d H:i:s');
|
||||
return self::formatApplicationDateTime(time());
|
||||
}
|
||||
|
||||
private function formatDateTime(int $timestamp): string
|
||||
{
|
||||
return date('Y-m-d H:i:s', $timestamp);
|
||||
return self::formatApplicationDateTime($timestamp);
|
||||
}
|
||||
|
||||
private function remoteIp(): ?string
|
||||
|
||||
Reference in New Issue
Block a user