Resolve backend Qodana critical and high findings (#314)

Resolve recommended-profile Critical and High findings, retain narrow analyzer exceptions, and update the edge-broker WebSocket dependency to a non-vulnerable release.
This commit is contained in:
Jeppe B
2026-07-17 05:44:16 +02:00
committed by GitHub
parent 6566027746
commit 2a6a86c9c3
108 changed files with 234 additions and 1283 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ $MINIO = [
'access_key' => '', // Minio access 'access_key' => '', // Minio access
'secret_key' => '' // Minio secret key 'secret_key' => '' // Minio secret key
]; ];
$SLACK_DEFAULT_WEBHOOK = ''; // Default Slack webhook URL e.g. https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX $SLACK_DEFAULT_WEBHOOK = ''; // Set through SLACK_DEFAULT_WEBHOOK; never commit a production webhook URL.
$REDIS_CONFIG = [ $REDIS_CONFIG = [
'host' => '', // Redis host (IP address) 'host' => '', // Redis host (IP address)
'user' => '', // Redis user 'user' => '', // Redis user
+21
View File
@@ -16,6 +16,26 @@ bootstrap: |+
npm --prefix services/edge-broker ci --ignore-scripts npm --prefix services/edge-broker ci --ignore-scripts
exclude: exclude:
# This application is intentionally Composer-classmapped and keeps legacy snake_case
# classes plus multiple local test doubles in single files; PSR path rules do not apply.
- name: PhpIllegalPsrClassPathInspection
paths:
- services/nginx/app
# Unit-test doubles intentionally bypass integration-heavy parent constructors.
- name: PhpMissingParentConstructorInspection
paths:
- services/nginx/app/tests
# These focused tests configure doubles through public fields before invoking behavior.
- name: PhpObjectFieldsAreOnlyWrittenInspection
paths:
- services/nginx/app/tests/Unit/Bird/BirdGateCallFlowTest.php
- services/nginx/app/tests/Unit/Invoicing/EconomicCustomersDiscountFallbackTest.php
- services/nginx/app/tests/Unit/Selfserve/SelfserveCustomerLaneAccessTest.php
# API coverage markers are intentional statement-style calls in the Pest DSL.
# Their return value is irrelevant; the call records route/scenario coverage.
- name: PhpExpressionResultUnusedInspection
paths:
- services/nginx/app/tests/Api
- name: All - name: All
paths: paths:
- services/nginx/app/vendor - services/nginx/app/vendor
@@ -30,5 +50,6 @@ exclude:
- documentation/topics/generated - documentation/topics/generated
- documentation/_build - documentation/_build
- documentation/_site_rebuild_20260317 - documentation/_site_rebuild_20260317
- docs_bird_voice_calls.html
- .tmp - .tmp
- .openclaw - .openclaw
+1 -1
View File
@@ -144,7 +144,7 @@ function requestJson({ method = "GET", port, path: requestPath, body = null, hea
raw += chunk; raw += chunk;
}); });
response.on("end", () => { response.on("end", () => {
let decoded = {}; let decoded;
try { try {
decoded = raw.trim() === "" ? {} : JSON.parse(raw); decoded = raw.trim() === "" ? {} : JSON.parse(raw);
} catch { } catch {
+6 -9
View File
@@ -95,7 +95,7 @@ function directCaddyBaseUrl(baseUrl) {
} }
function isLocalHost(hostname) { function isLocalHost(hostname) {
const normalized = String(hostname || "").toLowerCase().replace(/^\[|\]$/g, ""); const normalized = String(hostname || "").toLowerCase().replace(/^\x5b|\x5d$/g, "");
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1"; return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
} }
@@ -227,11 +227,7 @@ async function connectCurrentContainerToComposeNetwork(rootDir, composeProject)
return true; return true;
} }
if (/already exists|already connected/i.test(stderr)) { return /already exists|already connected/i.test(stderr);
return true;
}
return false;
} }
async function disconnectCurrentContainerFromComposeNetwork(rootDir, composeProject) { async function disconnectCurrentContainerFromComposeNetwork(rootDir, composeProject) {
@@ -833,7 +829,7 @@ async function main() {
{ timeoutMs: 180_000, message: "Gateway operation never completed through the live agent." } { timeoutMs: 180_000, message: "Gateway operation never completed through the live agent." }
); );
} catch (error) { } catch (error) {
let operationSnapshot = null; let operationSnapshot;
try { try {
const operations = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/operations`, { const operations = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/operations`, {
token: authToken, token: authToken,
@@ -959,9 +955,10 @@ async function main() {
allowFailure: true, allowFailure: true,
}).catch(() => {}); }).catch(() => {});
if (gatewayId !== null && fixture?.auth_token) { const fixtureAuthToken = fixture?.auth_token;
if (gatewayId !== null && fixtureAuthToken) {
await apiRequest(baseUrl, "DELETE", `/edge-gateways/${gatewayId}`, { await apiRequest(baseUrl, "DELETE", `/edge-gateways/${gatewayId}`, {
token: String(fixture.auth_token), token: String(fixtureAuthToken),
}).catch(() => {}); }).catch(() => {});
} }
+4 -4
View File
@@ -6,13 +6,13 @@
"": { "": {
"name": "truckwash-edge-broker", "name": "truckwash-edge-broker",
"dependencies": { "dependencies": {
"ws": "^8.18.0" "ws": "^8.21.1"
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.20.0", "version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
+1 -1
View File
@@ -7,6 +7,6 @@
"test:live": "node --test live/live-smoke.mjs" "test:live": "node --test live/live-smoke.mjs"
}, },
"dependencies": { "dependencies": {
"ws": "^8.18.0" "ws": "^8.21.1"
} }
} }
+2 -2
View File
@@ -49,7 +49,7 @@ function resolveManagerUrl(options = {}) {
return trimTrailingSlash(options.managerUrl || process.env.EDGE_MANAGER_URL || process.env.EDGE_PUBLIC_API_URL || ""); return trimTrailingSlash(options.managerUrl || process.env.EDGE_MANAGER_URL || process.env.EDGE_PUBLIC_API_URL || "");
} }
function resolveAuthMode(options = {}, managerUrl = "") { function resolveAuthMode(options = {}) {
if (options.authMode) { if (options.authMode) {
return options.authMode; return options.authMode;
} }
@@ -179,7 +179,7 @@ function rejectUpgrade(socket, statusCode, errorCode, message, details = {}) {
export function createBrokerServer(options = {}) { export function createBrokerServer(options = {}) {
const sharedSecret = resolveSharedSecret(options); const sharedSecret = resolveSharedSecret(options);
const managerUrl = resolveManagerUrl(options); const managerUrl = resolveManagerUrl(options);
const authMode = resolveAuthMode(options, managerUrl); const authMode = resolveAuthMode(options);
const commandTimeoutMs = options.commandTimeoutMs ?? 10000; const commandTimeoutMs = options.commandTimeoutMs ?? 10000;
const shellOpenTimeoutMs = options.shellOpenTimeoutMs ?? DEFAULT_SHELL_OPEN_TIMEOUT_MS; const shellOpenTimeoutMs = options.shellOpenTimeoutMs ?? DEFAULT_SHELL_OPEN_TIMEOUT_MS;
+10 -10
View File
@@ -39,8 +39,8 @@ test("traefik does not expose a dedicated public edge broker port", () => {
test("base docker compose routes edge broker traffic through traefik", () => { test("base docker compose routes edge broker traffic through traefik", () => {
const serviceBlock = readComposeServiceBlock(baseComposeSource, "edge-broker"); const serviceBlock = readComposeServiceBlock(baseComposeSource, "edge-broker");
assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/);
assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\{EDGE_AUTH_MODE:-strict\}/); assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\x7bEDGE_AUTH_MODE:-strict\x7d/);
assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\{EDGE_MANAGER_URL:-http:\/\/caddy\}/); assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\x7bEDGE_MANAGER_URL:-http:\/\/caddy\x7d/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.priority=200/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.priority=200/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.priority=200/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.priority=200/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/);
@@ -55,8 +55,8 @@ test("base docker compose routes edge broker traffic through traefik", () => {
test("example docker compose routes edge broker traffic through traefik", () => { test("example docker compose routes edge broker traffic through traefik", () => {
const serviceBlock = readComposeServiceBlock(exampleComposeSource, "edge-broker"); const serviceBlock = readComposeServiceBlock(exampleComposeSource, "edge-broker");
assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/);
assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\{EDGE_AUTH_MODE:-strict\}/); assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\x7bEDGE_AUTH_MODE:-strict\x7d/);
assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\{EDGE_MANAGER_URL:-http:\/\/caddy\}/); assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\x7bEDGE_MANAGER_URL:-http:\/\/caddy\x7d/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.example\.com`\) && PathPrefix\(`\/edge-broker`\)/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.example\.com`\) && PathPrefix\(`\/edge-broker`\)/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.rule=Host\(`localhost`\) && PathPrefix\(`\/api\/edge-broker`\)/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.rule=Host\(`localhost`\) && PathPrefix\(`\/api\/edge-broker`\)/);
assert.match(serviceBlock, /traefik\.http\.services\.edge-broker\.loadbalancer\.server\.port=4300/); assert.match(serviceBlock, /traefik\.http\.services\.edge-broker\.loadbalancer\.server\.port=4300/);
@@ -65,8 +65,8 @@ test("example docker compose routes edge broker traffic through traefik", () =>
test("standalone production compose routes edge broker traffic through traefik", () => { test("standalone production compose routes edge broker traffic through traefik", () => {
const serviceBlock = readComposeServiceBlock(standaloneProdComposeSource, "edge-broker"); const serviceBlock = readComposeServiceBlock(standaloneProdComposeSource, "edge-broker");
assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/);
assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\{EDGE_AUTH_MODE:-manager\}/); assert.match(serviceBlock, /EDGE_AUTH_MODE:\s*\$\x7bEDGE_AUTH_MODE:-manager\x7d/);
assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\{EDGE_MANAGER_URL:-http:\/\/caddy\}/); assert.match(serviceBlock, /EDGE_MANAGER_URL:\s*\$\x7bEDGE_MANAGER_URL:-http:\/\/caddy\x7d/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.priority=200/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.priority=200/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.priority=200/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.priority=200/);
assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/); assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/);
@@ -78,8 +78,8 @@ test("standalone production compose routes edge broker traffic through traefik",
test("compose config does not provide insecure broker secret defaults", () => { test("compose config does not provide insecure broker secret defaults", () => {
for (const composeSource of [baseComposeSource, exampleComposeSource]) { for (const composeSource of [baseComposeSource, exampleComposeSource]) {
assert.match(composeSource, /EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/); assert.match(composeSource, /EDGE_BROKER_URL:\s*\$\x7bEDGE_BROKER_URL:-http:\/\/edge-broker:4300\x7d/);
assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:\?set EDGE_BROKER_SHARED_SECRET in \.env\}/); assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\x7bEDGE_BROKER_SHARED_SECRET:\?set EDGE_BROKER_SHARED_SECRET in \.env\x7d/);
} }
}); });
@@ -87,7 +87,7 @@ test("base docker compose wires the broker into each php worker", () => {
for (const serviceName of ["php1", "php2", "php3", "php4", "php5", "php-staging", "php-cron"]) { for (const serviceName of ["php1", "php2", "php3", "php4", "php5", "php-staging", "php-cron"]) {
const serviceBlock = readComposeServiceBlock(baseComposeSource, serviceName); const serviceBlock = readComposeServiceBlock(baseComposeSource, serviceName);
assert.match(serviceBlock, /\n\s+depends_on:\s*\n[\s\S]*?\n\s+- edge-broker/); assert.match(serviceBlock, /\n\s+depends_on:\s*\n[\s\S]*?\n\s+- edge-broker/);
assert.match(serviceBlock, /EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/); assert.match(serviceBlock, /EDGE_BROKER_URL:\s*\$\x7bEDGE_BROKER_URL:-http:\/\/edge-broker:4300\x7d/);
assert.match(serviceBlock, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:\?set EDGE_BROKER_SHARED_SECRET in \.env\}/); assert.match(serviceBlock, /EDGE_BROKER_SHARED_SECRET:\s*\$\x7bEDGE_BROKER_SHARED_SECRET:\?set EDGE_BROKER_SHARED_SECRET in \.env\x7d/);
} }
}); });
+3 -2
View File
@@ -162,6 +162,7 @@ class cron_worker
$errorSql = $this->nullableSql($error); $errorSql = $this->nullableSql($error);
$loopStarted = $this->nullableSql($loopStartedAt); $loopStarted = $this->nullableSql($loopStartedAt);
$stoppedAt = $stopped ? $this->sql($now) : 'NULL'; $stoppedAt = $stopped ? $this->sql($now) : 'NULL';
$nowSql = $this->sql($now);
$this->query( $this->query(
"INSERT INTO cron_worker_state ( "INSERT INTO cron_worker_state (
@@ -172,8 +173,8 @@ class cron_worker
) VALUES ( ) VALUES (
$workerId, $name, $hostname, $pid, $source, $statusSql, $releaseChannelId, $releaseTargetId, $workerId, $name, $hostname, $pid, $source, $statusSql, $releaseChannelId, $releaseTargetId,
$resourceUuid, $resourceType, $commitSha, $this->poll_seconds, $runCount, $resourceUuid, $resourceType, $commitSha, $this->poll_seconds, $runCount,
$staleRunCount, $errorSql, $this->sql($now), $this->sql($now), $loopStarted, $staleRunCount, $errorSql, $nowSql, $nowSql, $loopStarted,
$this->sql($now), $stoppedAt $nowSql, $stoppedAt
) )
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
name = VALUES(name), name = VALUES(name),
@@ -396,7 +396,7 @@ class customer_rule_product_restriction_service
return $ids; return $ids;
} }
/** @return list<array{id:?int,name:string,sort_order:int,product_ids:list<int>}> */ /** @return list<array{id: ?int, name: string, sort_order: int, product_ids: list<int>}> */
private function validateCollections(string $attribute, mixed $value): array private function validateCollections(string $attribute, mixed $value): array
{ {
if (!is_array($value)) { if (!is_array($value)) {
@@ -302,10 +302,10 @@ class department_outside_hours_statistics_service
* @param array<int,array<string,mixed>> $opening_hours_by_department_id * @param array<int,array<string,mixed>> $opening_hours_by_department_id
* @param array<string,array<int,bool>>|null $missing_lookup_by_day * @param array<string,array<int,bool>>|null $missing_lookup_by_day
* @return array{ * @return array{
* counted:bool, * counted: bool,
* reason:string, * reason: string,
* candidate_date:?string, * candidate_date: ?string,
* department_id:int * department_id: int
* } * }
*/ */
public function classifyCandidateAgainstOpeningHours( public function classifyCandidateAgainstOpeningHours(
@@ -756,11 +756,14 @@ class economic_transfer_queue
$normalized_payload['requested_by'] = max(0, (int)$normalized_payload['requested_by']); $normalized_payload['requested_by'] = max(0, (int)$normalized_payload['requested_by']);
} }
return match ($transfer_type) { if ($transfer_type === self::TYPE_COLLECTED_INVOICE_EXPORT) {
self::TYPE_ORDER_DRAFT_EXPORT, self::TYPE_ORDER_INVOICE_EXPORT => $this->normalizeOrderPayload($normalized_payload, $created_by), return $this->normalizeCollectedInvoicePayload($normalized_payload, $created_by);
self::TYPE_COLLECTED_INVOICE_EXPORT => $this->normalizeCollectedInvoicePayload($normalized_payload, $created_by), }
default => $this->rejectPayload($created_by, 'Unsupported transfer type payload: ' . $transfer_type), if (!in_array($transfer_type, [self::TYPE_ORDER_DRAFT_EXPORT, self::TYPE_ORDER_INVOICE_EXPORT], true)) {
}; $this->rejectPayload($created_by, 'Unsupported transfer type payload: ' . $transfer_type);
}
return $this->normalizeOrderPayload($normalized_payload, $created_by);
} }
/** /**
@@ -770,7 +773,7 @@ class economic_transfer_queue
{ {
$order_id = $payload['order_id'] ?? null; $order_id = $payload['order_id'] ?? null;
if ($order_id === null || !is_numeric($order_id) || (int)$order_id < 1) { if ($order_id === null || !is_numeric($order_id) || (int)$order_id < 1) {
return $this->rejectPayload($created_by, 'order_id is required and must be a positive number'); $this->rejectPayload($created_by, 'order_id is required and must be a positive number');
} }
$payload['order_id'] = (int)$order_id; $payload['order_id'] = (int)$order_id;
return $payload; return $payload;
@@ -783,7 +786,7 @@ class economic_transfer_queue
{ {
$collected_invoice_id = $payload['collected_invoice_id'] ?? null; $collected_invoice_id = $payload['collected_invoice_id'] ?? null;
if ($collected_invoice_id === null || !is_numeric($collected_invoice_id) || (int)$collected_invoice_id < 1) { if ($collected_invoice_id === null || !is_numeric($collected_invoice_id) || (int)$collected_invoice_id < 1) {
return $this->rejectPayload($created_by, 'collected_invoice_id is required and must be a positive number'); $this->rejectPayload($created_by, 'collected_invoice_id is required and must be a positive number');
} }
$payload['collected_invoice_id'] = (int)$collected_invoice_id; $payload['collected_invoice_id'] = (int)$collected_invoice_id;
@@ -804,17 +807,17 @@ class economic_transfer_queue
if ($numeric === 0 || $numeric === 1) { if ($numeric === 0 || $numeric === 1) {
return $numeric === 1; return $numeric === 1;
} }
return $this->rejectPayload($created_by, $field_name . ' must be a boolean'); $this->rejectPayload($created_by, $field_name . ' must be a boolean');
} }
if (is_string($value)) { if (is_string($value)) {
$normalized = strtolower(trim($value)); $normalized = strtolower(trim($value));
if (in_array($normalized, ['true', 'false', '1', '0'], true)) { if (in_array($normalized, ['true', 'false', '1', '0'], true)) {
return in_array($normalized, ['true', '1'], true); return in_array($normalized, ['true', '1'], true);
} }
return $this->rejectPayload($created_by, $field_name . ' must be a boolean'); $this->rejectPayload($created_by, $field_name . ' must be a boolean');
} }
return $this->rejectPayload($created_by, $field_name . ' must be a boolean'); $this->rejectPayload($created_by, $field_name . ' must be a boolean');
} }
private function findActiveJobByTarget(string $transfer_type, array $payload, int $created_by): ?array private function findActiveJobByTarget(string $transfer_type, array $payload, int $created_by): ?array
@@ -10,7 +10,7 @@ class economic_v2_revenue_statistics_service
private economic $economic; private economic $economic;
/** @var array<int, array{customer_number:int,name:?string,barred:?bool,status:string}> */ /** @var array<int, array{customer_number: int, name: ?string, barred: ?bool, status: string}> */
private array $customer_cache = []; private array $customer_cache = [];
public function __construct(?economic $economic = null) public function __construct(?economic $economic = null)
@@ -44,7 +44,6 @@ class economic_v2_revenue_statistics_service
$summary = [ $summary = [
'invoice_count' => 0, 'invoice_count' => 0,
'line_count' => 0, 'line_count' => 0,
'unique_customers' => 0,
'net_amount' => 0.0, 'net_amount' => 0.0,
'vat_amount' => 0.0, 'vat_amount' => 0.0,
'gross_amount' => 0.0, 'gross_amount' => 0.0,
@@ -398,7 +397,7 @@ class economic_v2_revenue_statistics_service
} }
/** /**
* @return array{customer_number:int,name:?string,barred:?bool,status:string} * @return array{customer_number: int, name: ?string, barred: ?bool, status: string}
*/ */
private function resolveCustomerSnapshot(int $customer_number, array &$warnings): array private function resolveCustomerSnapshot(int $customer_number, array &$warnings): array
{ {
@@ -500,4 +499,3 @@ class economic_v2_revenue_statistics_service
return $data; return $data;
} }
} }
+1 -31
View File
@@ -9,41 +9,11 @@ class encrypt implements encrypt_i
public function encrypt(string $data): string public function encrypt(string $data): string
{ {
// Debug:
return $data; return $data;
// Encrypt data
global $ENCRYPTION_KEY;
// Use AES 256 encryption
$cipher = "aes-256-cbc";
// Use the encryption key
$options = 0;
// Get the initialization vector
$iv_length = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($iv_length);
// Use the first 16 bytes of the initialization vector
$iv = substr($iv, 0, 16);
// Encrypt the data
$encrypted = openssl_encrypt($data, $cipher, $ENCRYPTION_KEY, $options, $iv);
// Save the initialization vector for decryption
return $iv . $encrypted;
} }
public function decrypt(string $data): string public function decrypt(string $data): string
{ {
// Debug:
return $data; return $data;
// Decrypt data
global $ENCRYPTION_KEY;
// Use AES 256 encryption
$cipher = "aes-256-cbc";
// Use the encryption key and initialization vector
$options = 0;
// Get the initialization vector
$iv_length = openssl_cipher_iv_length($cipher);
$iv = substr($data, 0, $iv_length);
// Get the encrypted data
$encrypted = substr($data, $iv_length);
// Decrypt the data
return openssl_decrypt($encrypted, $cipher, $ENCRYPTION_KEY, $options, $iv);
} }
} }
+75 -25
View File
@@ -5,9 +5,6 @@ namespace classes;
require_once WD . '/modules/entra/entra_c.php'; require_once WD . '/modules/entra/entra_c.php';
use entra\entra_c; use entra\entra_c;
use Microsoft\Graph\GraphServiceClient;
use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext;
use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContextBuilder;
class entra class entra
@@ -23,42 +20,95 @@ class entra
$this->config = new entra_c(); $this->config = new entra_c();
} }
public function get_users($array = false): array|object public function get_users(bool $array = false): array
{ {
$graphClient = $this->getGraphClient(); $accessToken = $this->requestAccessToken();
$usersResponse = $this->requestJson(
$users = $graphClient->users() 'https://graph.microsoft.com/v1.0/users?$select=id,displayName,mail,userPrincipalName',
->get() ['Authorization: Bearer ' . $accessToken]
->wait() );
->getValue(); $users = is_array($usersResponse['value'] ?? null) ? $usersResponse['value'] : [];
if (!$array) { if (!$array) {
return $users; return $users;
} }
$result = []; $result = [];
foreach ( $users as $user ) { foreach ($users as $user) {
if (!is_array($user)) {
continue;
}
$result[] = [ $result[] = [
'id' => $user->getId(), 'id' => $user['id'] ?? null,
'displayName' => $user->getDisplayName(), 'displayName' => $user['displayName'] ?? null,
'mail' => $user->getMail(), 'mail' => $user['mail'] ?? null,
'userPrincipalName' => $user->getUserPrincipalName(), 'userPrincipalName' => $user['userPrincipalName'] ?? null,
]; ];
} }
return $result; return $result;
} }
public function getGraphClient(): GraphServiceClient private function requestAccessToken(): string
{ {
return new GraphServiceClient( $tenantId = trim((string)$this->config->tenant_id->getVariableValue());
$this->getTokenRequestContext(), $response = $this->requestJson(
'https://login.microsoftonline.com/' . rawurlencode($tenantId) . '/oauth2/v2.0/token',
['Content-Type: application/x-www-form-urlencoded'],
http_build_query([
'client_id' => (string)$this->config->client_id->getVariableValue(),
'client_secret' => (string)$this->config->client_secret->getVariableValue(),
'scope' => 'https://graph.microsoft.com/.default',
'grant_type' => 'client_credentials',
])
); );
$token = trim((string)($response['access_token'] ?? ''));
if ($token === '') {
throw new \RuntimeException('Microsoft Entra token response did not contain an access token.');
}
return $token;
} }
public function getTokenRequestContext(): ClientCredentialContext /**
* @param list<string> $headers
* @return array<string,mixed>
*/
private function requestJson(string $url, array $headers, ?string $postFields = null): array
{ {
return new ClientCredentialContext( $curl = curl_init($url);
$this->config->tenant_id->getVariableValue(), if ($curl === false) {
$this->config->client_id->getVariableValue(), throw new \RuntimeException('Unable to initialize Microsoft Entra request.');
$this->config->client_secret->getVariableValue() }
);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => $headers,
]);
if ($postFields !== null) {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $postFields);
}
try {
$body = curl_exec($curl);
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($body === false) {
throw new \RuntimeException('Microsoft Entra request failed: ' . curl_error($curl));
}
} finally {
curl_close($curl);
}
$decoded = json_decode((string)$body, true);
if ($status < 200 || $status >= 300 || !is_array($decoded)) {
$message = is_array($decoded)
? (string)($decoded['error_description'] ?? $decoded['error']['message'] ?? 'Unexpected response')
: 'Invalid JSON response';
throw new \RuntimeException('Microsoft Entra request failed with HTTP ' . $status . ': ' . $message);
}
return $decoded;
} }
} }
-1
View File
@@ -10,7 +10,6 @@ require_once WD . '/modules/forms/form_helper_c.php';
use Exception; use Exception;
use forms\form_helper_c; use forms\form_helper_c;
use forms\objects\book_interior_wash_f;
use forms\objects\book_wash_f; use forms\objects\book_wash_f;
use objects\form_submissions_o; use objects\form_submissions_o;
use traits\form_t; use traits\form_t;
@@ -137,7 +137,7 @@ class licenseplaterecognizer implements licenseplaterecognizer_i
$cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key); $cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key);
if ($cached_result !== null) { if ($cached_result !== null) {
$this->last_timings['cache_hit'] = 1; $this->last_timings['cache_hit'] = 1;
return $cached_result; return $this->completeRecognition($started_at, $cached_result);
} }
} }
} }
@@ -202,7 +202,7 @@ class licenseplaterecognizer implements licenseplaterecognizer_i
$this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result); $this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);
return $recognized_result; return $this->completeRecognition($started_at, $recognized_result);
} }
$recognized_result = [ $recognized_result = [
@@ -211,12 +211,19 @@ class licenseplaterecognizer implements licenseplaterecognizer_i
]; ];
$this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result); $this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);
return $recognized_result; return $this->completeRecognition($started_at, $recognized_result);
} finally { } catch (\Throwable $exception) {
$this->last_timings['total'] = $this->elapsedMs($started_at); $this->last_timings['total'] = $this->elapsedMs($started_at);
throw $exception;
} }
} }
private function completeRecognition(float $started_at, array $result): array
{
$this->last_timings['total'] = $this->elapsedMs($started_at);
return $result;
}
private static function clientDisconnectAbortCallback(): callable private static function clientDisconnectAbortCallback(): callable
{ {
return static function (): int { return static function (): int {
@@ -8,7 +8,6 @@ class object_property
private string $table; // The id of the object in the database private string $table; // The id of the object in the database
private string $column; // The column name of the field in the database table (e.g. id, name, email) private string $column; // The column name of the field in the database table (e.g. id, name, email)
private string $type; // The data type of the field in the database table (e.g. int, varchar, text) private string $type; // The data type of the field in the database table (e.g. int, varchar, text)
private bool $required; // Whether the field is required or not
private mixed $default; // The default value of the field private mixed $default; // The default value of the field
private mixed $fake_value; // The fake value of the field, used for testing purposes (When the object id is -1) private mixed $fake_value; // The fake value of the field, used for testing purposes (When the object id is -1)
@@ -18,7 +17,7 @@ class object_property
$this->id = $id; $this->id = $id;
$this->column = $column; $this->column = $column;
$this->type = $type; $this->type = $type;
$this->required = $required; unset($required); // Retained in the constructor for compatibility with existing object definitions.
$this->default = $default; $this->default = $default;
} }
+2 -3
View File
@@ -8,12 +8,11 @@ use objects\ratelimit_o;
class ratelimit implements ratelimit_i class ratelimit implements ratelimit_i
{ {
private int $limit; // The number of requests allowed in the time period private int $limit; // The number of requests allowed in the time period
private int $time; // The time period in seconds
public function __construct(int $defaultLimit, int $defaultTime) public function __construct(int $defaultLimit, int $defaultTime)
{ {
$this->limit = $defaultLimit; $this->limit = $defaultLimit;
$this->time = $defaultTime; unset($defaultTime); // The reset interval is managed by the rate-limit maintenance task.
} }
public function enforceIP(string $ip): bool public function enforceIP(string $ip): bool
@@ -26,4 +25,4 @@ class ratelimit implements ratelimit_i
$ratelimit->increment($ratelimit->id, 1); $ratelimit->increment($ratelimit->id, 1);
return true; return true;
} }
} }
+2 -2
View File
@@ -339,10 +339,10 @@ class redis implements redis_i
/** /**
* Cache auth session payload for a token with TTL * Cache auth session payload for a token with TTL
*/ */
public function cache_auth_session(string $token, array $data, int $ttl = 60): self public function cache_auth_session(string $token, array $session, int $ttl = 60): self
{ {
$key = 'auth_session_' . $token; $key = 'auth_session_' . $token;
$this->set_array($key, $data); $this->set_array($key, $session);
$this->expire($key, $ttl); $this->expire($key, $ttl);
return $this; return $this;
} }
@@ -5680,7 +5680,7 @@ class release_manager
$channelId = $this->nullablePositiveInt($input['channel_id'] ?? null); $channelId = $this->nullablePositiveInt($input['channel_id'] ?? null);
$channelSlug = self::safeSlug((string)($input['channel_slug'] ?? $input['channel'] ?? '')); $channelSlug = self::safeSlug((string)($input['channel_slug'] ?? $input['channel'] ?? ''));
if ($channelId === null && $channelSlug !== '') { if ($channelId === null && $channelSlug !== '') {
$channel = $this->channelBySlug($channelSlug); $channel = $this->findChannelBySlug($channelSlug);
if ($channel === null) { if ($channel === null) {
throw new RuntimeException('Release channel was not found for Coolify cleanup.'); throw new RuntimeException('Release channel was not found for Coolify cleanup.');
} }
@@ -9346,7 +9346,7 @@ class release_manager
} }
$raw = trim($raw); $raw = trim($raw);
$raw = preg_replace('#[/\s].*$#', '', $raw) ?? ''; $raw = preg_replace('#[/\s].*$#', '', $raw) ?? '';
if (str_contains($raw, ':') && preg_match('/^\[[^\]]+\]:(\d+)$/', $raw) !== 1) { if (str_contains($raw, ':') && preg_match('/^\x5b[^\x5d]+\x5d:(\d+)$/', $raw) !== 1) {
$parts = parse_url('https://' . $raw); $parts = parse_url('https://' . $raw);
if (is_array($parts) && !empty($parts['host'])) { if (is_array($parts) && !empty($parts['host'])) {
$raw = (string)$parts['host']; $raw = (string)$parts['host'];
@@ -526,7 +526,6 @@ class superuser_system_status_service
'enabled' => $enabled, 'enabled' => $enabled,
'configured' => $configured, 'configured' => $configured,
'probe_supported' => isset($descriptor['probe']), 'probe_supported' => isset($descriptor['probe']),
'status' => 'configured',
'status_reason' => null, 'status_reason' => null,
'status_reason_key' => null, 'status_reason_key' => null,
'status_reason_params' => [], 'status_reason_params' => [],
@@ -671,7 +671,7 @@ class system_search_service
/** /**
* @param array<int, mixed> $entityIds * @param array<int, mixed> $entityIds
* @return array<string, array{name:?string,created_at:?string,closed_at:?string}> * @return array<string, array{name: ?string, created_at: ?string, closed_at: ?string}>
*/ */
private function loadInvoiceTitleContexts(array $entityIds): array private function loadInvoiceTitleContexts(array $entityIds): array
{ {
@@ -2283,7 +2283,7 @@ class system_search_service
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return array{title:string,description:string,customer_number:?int,department_id:?int,payload:array<string,mixed>} * @return array{title: string, description: string, customer_number: ?int, department_id: ?int, payload: array<string,mixed>}
*/ */
private function resolveObjectSearchContext(array $row): array private function resolveObjectSearchContext(array $row): array
{ {
@@ -2296,7 +2296,7 @@ class system_search_service
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return array{title:string,description:string,customer_number:?int,department_id:?int,payload:array<string,mixed>} * @return array{title: string, description: string, customer_number: ?int, department_id: ?int, payload: array<string,mixed>}
*/ */
private function resolveOrderObjectSearchContext(array $row): array private function resolveOrderObjectSearchContext(array $row): array
{ {
@@ -2336,7 +2336,7 @@ class system_search_service
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return array{title:string,description:string,customer_number:?int,department_id:?int,payload:array<string,mixed>} * @return array{title: string, description: string, customer_number: ?int, department_id: ?int, payload: array<string,mixed>}
*/ */
private function resolveTaskObjectSearchContext(array $row): array private function resolveTaskObjectSearchContext(array $row): array
{ {
@@ -2379,7 +2379,7 @@ class system_search_service
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return array{title:string,description:string,customer_number:?int,department_id:?int,payload:array<string,mixed>} * @return array{title: string, description: string, customer_number: ?int, department_id: ?int, payload: array<string,mixed>}
*/ */
private function resolveGenericObjectSearchContext(array $row): array private function resolveGenericObjectSearchContext(array $row): array
{ {
-1
View File
@@ -96,7 +96,6 @@ class webauthn
$pk_hex = bin2hex($this->b64urlDecode((string)$passkey->public_key->value())); $pk_hex = bin2hex($this->b64urlDecode((string)$passkey->public_key->value()));
error_log("WebAuthn verification failed: " . $e->getMessage() . " (PK Hex: $pk_hex)"); error_log("WebAuthn verification failed: " . $e->getMessage() . " (PK Hex: $pk_hex)");
throw new Exception("WebAuthn verification failed: " . $e->getMessage() . " (PK Hex: $pk_hex)"); throw new Exception("WebAuthn verification failed: " . $e->getMessage() . " (PK Hex: $pk_hex)");
return false;
} catch (ExceptionInterface $e) { } catch (ExceptionInterface $e) {
throw new Exception('Serialization error: ' . $e->getMessage()); throw new Exception('Serialization error: ' . $e->getMessage());
} }
@@ -81,10 +81,7 @@ class wordpress_bookings_remote implements wordpress_bookings_remote_i
} else { } else {
$booking = $booking["data"]["booking"]; $booking = $booking["data"]["booking"];
} }
} else if (isset($booking["id"])) { } else if (!isset($booking["id"])) {
// Check if the booking property is set
} else {
return null; return null;
} }
@@ -157,4 +154,4 @@ class wordpress_bookings_remote implements wordpress_bookings_remote_i
// Get the booking cache // Get the booking cache
return $this->booking_cache; return $this->booking_cache;
} }
} }
@@ -820,45 +820,6 @@ class xlvask_automation_service
private function scoreOrderMatch(array $usageItems, array $orderItems): array private function scoreOrderMatch(array $usageItems, array $orderItems): array
{ {
return self::scoreItemMatchForAutomation($usageItems, $orderItems); return self::scoreItemMatchForAutomation($usageItems, $orderItems);
$usageSignature = $this->itemSignatureParts($usageItems);
$orderSignature = $this->itemSignatureParts($orderItems);
$usageTotal = $this->itemsTotal($usageItems);
$orderTotal = $this->itemsTotal($orderItems);
if ($usageSignature === $orderSignature && $usageTotal === $orderTotal) {
return [
'confidence' => 0.95,
'source' => self::SOURCE_DETERMINISTIC,
'reason' => 'Produkterne og prisen matcher en ordre fra samme dag.',
];
}
$usagePrimary = (int)($usageItems[0]['product_id'] ?? 0);
$orderPrimary = (int)($orderItems[0]['product_id'] ?? 0);
if ($usagePrimary < 1 || $usagePrimary !== $orderPrimary) {
return ['confidence' => 0.0, 'source' => self::SOURCE_DETERMINISTIC, 'reason' => ''];
}
$overlap = $this->productOverlap($usageItems, $orderItems);
$totalDiff = abs($usageTotal - $orderTotal);
if ($overlap >= 0.70 && $totalDiff <= 50) {
return [
'confidence' => 0.93,
'source' => self::SOURCE_FUZZY,
'reason' => 'Samme primære produkt og relaterede tillæg matcher en ordre fra samme dag.',
];
}
if ($overlap >= 0.50 && $totalDiff <= 150) {
return [
'confidence' => 0.80,
'source' => self::SOURCE_FUZZY,
'reason' => 'Vasken ligner en ordre fra samme dag, men kræver manuel godkendelse.',
];
}
return ['confidence' => 0.0, 'source' => self::SOURCE_DETERMINISTIC, 'reason' => ''];
} }
private function findSameDayCandidateOrders(xlvask_usage_log $log, array $proposedOrder): array private function findSameDayCandidateOrders(xlvask_usage_log $log, array $proposedOrder): array
+1 -10
View File
@@ -381,7 +381,7 @@ function normalizeWorkfeedEmployeeWarmupCollection(mixed $raw): array
} }
/** /**
* @return array{id:?string,name:?string} * @return array{id: ?string, name: ?string}
*/ */
function extractWorkfeedEmployeeWarmupIdentity(mixed $employee): array function extractWorkfeedEmployeeWarmupIdentity(mixed $employee): array
{ {
@@ -515,15 +515,6 @@ function normalizeWarmupTextValue(mixed $value): ?string
return null; return null;
} }
function checkUnfulfilledBookings(): void
{
// This is deactivated for now, as it is not wanted.
// I'm saving this for later, as it is a good idea to have this in place.
return;
$bookings_o = new bookings_o();
$bookings_o->checkUnfulfilledBookings();
}
function syncBookings(): void function syncBookings(): void
{ {
$bookings_o = new bookings_o(); $bookings_o = new bookings_o();
@@ -17,23 +17,13 @@ $start = microtime(true);
// Load the XL Vask module // Load the XL Vask module
$xlvask = new xlvask; $xlvask = new xlvask;
try { try {
// Check if the module is enabled if ($xlvask->config->enabled->isTrue() && $xlvask->config->synchronization_enabled->isTrue()) {
if ($xlvask->config->enabled->isTrue()) { $xlvask->getTasks()->runCronTasks();
// Check if synchronization is enabled // TODO: Add synchronization for usage logs and vehicles.
if ($xlvask->config->synchronization_enabled->isTrue()) {
$xlvask->getTasks()->runCronTasks();
// TODO: Add synchronization for:
// - usageLogs
// - vehicles
} else {
// Synchronization is not enabled, do nothing
}
} else {
// The module is not enabled, do nothing
} }
} catch (Exception $e) { } catch (Exception $e) {
// This is automatically running, so we don't need to log the error // This is automatically running, so we don't need to log the error
} }
$end = microtime(true); $end = microtime(true);
//$slack = new \classes\slack(); //$slack = new \classes\slack();
//$slack->send_message("The booking sync script has finished. It took " . round($end - $start, 2) . " seconds to run."); //$slack->send_message("The booking sync script has finished. It took " . round($end - $start, 2) . " seconds to run.");
+2
View File
@@ -184,6 +184,7 @@ spl_autoload_register(function (string $class): void {
use classes\application_write_freeze; use classes\application_write_freeze;
use classes\db; use classes\db;
use classes\replication_bootstrap_config;
use classes\replication_manager; use classes\replication_manager;
use classes\release_manager; use classes\release_manager;
use classes\redis; use classes\redis;
@@ -311,4 +312,5 @@ $load_enabled_module_routes = static function (): void {
$load_enabled_module_routes(); $load_enabled_module_routes();
// Autoload all the routes // Autoload all the routes
/** @var router $router */
$router->auto_load_routes(WD . '/routes'); $router->auto_load_routes(WD . '/routes');
@@ -349,10 +349,6 @@ trait dynamicimages_image_t
if ($this->image instanceof \Imagick) { if ($this->image instanceof \Imagick) {
$img = clone $this->image; $img = clone $this->image;
$img->setImageFormat('png'); $img->setImageFormat('png');
// Quality influences compression for PNG differently; keep as hint
if ($format !== null && strtolower($format) !== 'png') {
// For now we only support PNG for composed images as requested
}
// Strip metadata to reduce size // Strip metadata to reduce size
$img->stripImage(); $img->stripImage();
$blob = $img->getImageBlob(); $blob = $img->getImageBlob();
@@ -205,7 +205,6 @@ class economic_tasks
break; break;
default: default:
throw new Exception('Unknown error message: ' . $error_message); throw new Exception('Unknown error message: ' . $error_message);
break;
} }
} }
return; return;
@@ -4967,7 +4967,6 @@ BASH;
'recent_command_timeouts' => 0, 'recent_command_timeouts' => 0,
'recent_unknown_gate_outcomes' => 0, 'recent_unknown_gate_outcomes' => 0,
'recent_relay_commands' => 0, 'recent_relay_commands' => 0,
'recent_relay_failure_rate' => 0.0,
'recent_command_avg_latency_seconds' => null, 'recent_command_avg_latency_seconds' => null,
'last_successful_command_at' => null, 'last_successful_command_at' => null,
'last_successful_discovery_at' => null, 'last_successful_discovery_at' => null,
@@ -5278,7 +5277,7 @@ BASH;
} }
/** /**
* @return array{url:?string,error:?string} * @return array{url: ?string, error: ?string}
*/ */
private function normalizeBrokerDiagnosticBaseUrl(mixed $value): array private function normalizeBrokerDiagnosticBaseUrl(mixed $value): array
{ {
@@ -5307,7 +5306,7 @@ BASH;
} }
/** /**
* @param array{url:?string,error:?string} $baseUrl * @param array{url: ?string, error: ?string} $baseUrl
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function diagnoseBrokerHttpEndpoint(array $baseUrl, string $label): array private function diagnoseBrokerHttpEndpoint(array $baseUrl, string $label): array
@@ -5353,7 +5352,7 @@ BASH;
} }
/** /**
* @param array{url:?string,error:?string} $baseUrl * @param array{url: ?string, error: ?string} $baseUrl
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function diagnoseBrokerSharedSecret(array $baseUrl, string $sharedSecret): array private function diagnoseBrokerSharedSecret(array $baseUrl, string $sharedSecret): array
@@ -5413,7 +5412,7 @@ BASH;
} }
/** /**
* @param array{url:?string,error:?string} $baseUrl * @param array{url: ?string, error: ?string} $baseUrl
* @param array<int,string> $headers * @param array<int,string> $headers
* @return array<string,mixed> * @return array<string,mixed>
*/ */
@@ -5464,7 +5463,7 @@ BASH;
} }
/** /**
* @param array{url:?string,error:?string} $baseUrl * @param array{url: ?string, error: ?string} $baseUrl
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function brokerDiagnosticUrlFailure(array $baseUrl, string $label): array private function brokerDiagnosticUrlFailure(array $baseUrl, string $label): array
@@ -35,7 +35,6 @@ class edgeGatewayConfigRoute
if (!$user) { if (!$user) {
(new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_CONFIG', 'No user found, or invalid session'); (new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
(new logs_o())->add('edgegateway_config', 'global', 1, $user->id, 'EDGEGATEWAY_CONFIG', 'Successfully fetched edge gateway config'); (new logs_o())->add('edgegateway_config', 'global', 1, $user->id, 'EDGEGATEWAY_CONFIG', 'Successfully fetched edge gateway config');
@@ -59,7 +58,6 @@ class edgeGatewayConfigRoute
if (!$user) { if (!$user) {
(new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_CONFIG', 'No user found, or invalid session'); (new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
(new logs_o())->add('edgegateway_config', 'global', 1, $user->id, 'EDGEGATEWAY_CONFIG', 'Successfully updated edge gateway config'); (new logs_o())->add('edgegateway_config', 'global', 1, $user->id, 'EDGEGATEWAY_CONFIG', 'Successfully updated edge gateway config');
@@ -75,7 +73,6 @@ class edgeGatewayConfigRoute
if (!$user) { if (!$user) {
(new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_BROKER_DIAGNOSTICS', 'No user found, or invalid session'); (new logs_o())->add('edgegateway_config', 'global', 1, 0, 'EDGEGATEWAY_BROKER_DIAGNOSTICS', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$payload = self::getParametersAsArray(); $payload = self::getParametersAsArray();
@@ -42,74 +42,17 @@ class book_wash_f extends form_helper_c
public function beforeSave(): void public function beforeSave(): void
{ {
throw new \Exception('Deprecated: Please refresh the page, and use the new booking page instead.'); throw new \Exception('Deprecated: Please refresh the page, and use the new booking page instead.');
// Set the department id to the department id of the user
self::setDepartmentId(self::getSanitizedData('department_id'));
// Set the customer number to the customer number of the user
self::setCustomerNumber(self::getSanitizedData('customer_number'));
} }
/** /**
* @inheritDoc * @inheritDoc
* @throws \Exception * @throws \Exception
* @throws ClientExceptionInterface
*/ */
public function afterSubmit(): void public function afterSubmit(): void
{ {
$email = new email();
// Get the user from the customer number
$user = (new users_o())->getUserByCustomerNumber(self::getSanitizedData('customer_number'));
// Get the department name from the department id
$department = (new departments_o())->selectId((int)self::getSanitizedData('department_id'));
$department->getObjectProperties();
// Get the bookings_new object
throw new \Exception('Deprecated: Use bookings_o instead of bookings_new_o'); throw new \Exception('Deprecated: Use bookings_o instead of bookings_new_o');
$bookings = new bookings_o();
// Check if the wash type contains the interior wash
$wants_wash_certificate = (bool)self::getSanitizedData('wants_wash_certificate');
if (in_array(3, self::getSanitizedData('wash_type'))) {
if ($wants_wash_certificate) {
$wash_certificate_email = self::getSanitizedData('wants_wash_certificate_email');
} else {
$wash_certificate_email = '';
}
} else {
$wash_certificate_email = '';
$wants_wash_certificate = false;
}
// Create a new booking
$bookings->add(
self::getCustomerNumber(),
self::getFormIdentifier(),
self::getSanitizedData('contact_email'),
self::getSanitizedData('reference'),
strtoupper(self::getSanitizedData('registration_number_tractor')),
strtoupper(self::getSanitizedData('registration_number_trailer')),
$wash_certificate_email ?? '',
self::getSanitizedData('date'),
$department->id,
(bool)self::getSanitizedData('wants_pickup'),
self::getSanitizedData('notes'),
($wants_wash_certificate ? 'pending' : 'cancelled'),
'',
'pending',
self::getSanitizedData('wash_type'),
);
// Ad
// Validate the booking actually exists
if (!$bookings->exists()) {
throw new \Exception('Booking could not be created');
}
// Send the booking confirmation email
$email->sendBookingConfirmationEmail(
$bookings->id,
);
$this->booking_object = $bookings;
// TODO: Implement afterSubmit() method.
} }
/**
* @inheritDoc
*/
public function setup(): void public function setup(): void
{ {
self::setFormIdentifier('BOOK_WASH'); self::setFormIdentifier('BOOK_WASH');
@@ -254,4 +197,4 @@ class book_wash_f extends form_helper_c
3 => 'Indvendig trailer vask', 3 => 'Indvendig trailer vask',
]; ];
} }
} }
@@ -1047,7 +1047,7 @@ class goals_criteria implements goals_criteria_i
{ {
$immutable = $date instanceof \DateTimeImmutable $immutable = $date instanceof \DateTimeImmutable
? $date ? $date
: \DateTimeImmutable::createFromMutable($date); : \DateTimeImmutable::createFromInterface($date);
if ($timezone !== null) { if ($timezone !== null) {
$immutable = $immutable->setTimezone($timezone); $immutable = $immutable->setTimezone($timezone);
} }
@@ -117,7 +117,6 @@ class goals_progress_alert_renderer
$ref = new \ReflectionClass($criteria); $ref = new \ReflectionClass($criteria);
if ($ref->hasMethod('getProgress')) { if ($ref->hasMethod('getProgress')) {
$m = $ref->getMethod('getProgress'); $m = $ref->getMethod('getProgress');
$m->setAccessible(true);
/** @var int $val */ /** @var int $val */
$val = $m->invoke($criteria); $val = $m->invoke($criteria);
return (int)$val; return (int)$val;
@@ -836,7 +836,8 @@ class selfserve_studio_graph
$customerNumber, $customerNumber,
$configSource, $configSource,
$versionId, $versionId,
$hardwareMode $hardwareMode,
$confirmationRows
): void { ): void {
if ($progressCallback === null) { if ($progressCallback === null) {
return; return;
@@ -93,7 +93,7 @@ class selfserve_task_attachment_payloads
} }
/** /**
* @return array{image:?string,document:?string,relation:mixed,other:mixed} * @return array{image: ?string, document: ?string, relation: mixed, other: mixed}
*/ */
private function contentPayload(mixed $content): array private function contentPayload(mixed $content): array
{ {
@@ -3337,7 +3337,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
} }
/** /**
* @return array{driver:string,key:string,token:?string} * @return array{driver: string, key: string, token: ?string}
*/ */
protected function acquireSessionMutationLock(string $lockKey): array protected function acquireSessionMutationLock(string $lockKey): array
{ {
@@ -3369,7 +3369,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
} }
/** /**
* @param array{driver:string,key:string,token:?string} $lock * @param array{driver: string, key: string, token: ?string} $lock
*/ */
protected function releaseSessionMutationLock(array $lock): void protected function releaseSessionMutationLock(array $lock): void
{ {
@@ -145,8 +145,7 @@ trait selfserve_lane_command_t
} }
try { try {
$active_wash = new selfserve_wash_flow(); $this->addVehicleTypeProductToLastInvoiceOrder();
$active_wash->addVehicleTypeProductToInvoiceForLane($this->id);
} catch (\Throwable) { } catch (\Throwable) {
// Best effort only; invoice correction can be handled manually if needed. // Best effort only; invoice correction can be handled manually if needed.
} }
@@ -82,66 +82,3 @@ if (isset($_GET['justDownload'])) {
http_response_code(410); http_response_code(410);
echo 'Booking completion must be completed through POS desktop or mobile steps.'; echo 'Booking completion must be completed through POS desktop or mobile steps.';
exit; exit;
// Require the $_GET variables sealOrPlumber, safetySeal, performedBy, and bookingId, regNumber, and regNumberTrailer to be set
if (!isset($_GET['sealOrPlumber']) || !isset($_GET['performedBy']) || !isset($_GET['bookingId']) || !isset($_GET['regNumber']) || !isset($_GET['regNumberTrailer']) || !isset($_GET['department'])) {
// We are missing some required fields in the query string
echo 'Missing required fields';
exit;
}
// Check if the certificate already exists in the bucket
$wash_certificate_store = new wash_certificate_store();
if ($wash_certificate_store->washCertificateExists($_GET['bookingId'])) {
// Return the certificate url
echo $wash_certificate_store->getWashCertificateDownload($_GET['bookingId']);
// Exit the script
exit;
} else {
// Check if the certificate exists in the filesystem (legacy system)
if (file_exists(dirname(__FILE__) . "/output/certificates/wash_certificate_" . $_GET['bookingId'] . ".pdf")) {
// Upload the certificate to the bucket
$success = $wash_certificate_store->uploadFile("wash_certificate_" . $_GET['bookingId'] . ".pdf", dirname(__FILE__) . "/output/certificates/wash_certificate_" . $_GET['bookingId'] . ".pdf");
// If the certificate was uploaded successfully, delete the local copy
if ($success) {
unlink(dirname(__FILE__) . "/output/certificates/wash_certificate_" . $_GET['bookingId'] . ".pdf");
// Return the certificate url
echo $wash_certificate_store->getWashCertificateDownload($_GET['bookingId']);
exit;
}
// If the certificate was not uploaded successfully, return an error
echo 'Failed to upload the certificate';
// Exit the script
exit;
}
}
// Usage example
$template = "templates/template2024julv3.xlsx";
$generator = new WashCertificateGenerator($template, $_GET['department']);
$generator->generateCertificate(dirname(__FILE__) . "/output/certificates/wash_certificate_" . $_GET['bookingId'] . ".pdf", $_GET['sealOrPlumber'], $_GET['regNumber'], $_GET['regNumberTrailer'], $_GET['performedBy']);
// Determine the generated certificate name
$generatedCertificateName = "wash_certificate_" . $_GET['bookingId'] . ".pdf";
// Return the generated certificate path
$generatedCertificatePath = "output/certificates/wash_certificate_" . $_GET['bookingId'] . ".pdf";
// Upload the certificate to the bucket
$wash_certificate_store->uploadFile($generatedCertificateName, dirname(__FILE__) . '/' . $generatedCertificatePath);
// Delete the local copy of the certificate
unlink(dirname(__FILE__) . '/' . $generatedCertificatePath);
// Set the status of the booking to completed
$booking = new bookings_o();
$booking->id = $_GET['bookingId'];
$booking->getObjectProperties();
$booking->status->set('completed');
$booking->washCertificateUrl->set('Protected URL');
$booking->washCertificateStatus->set('completed');
// Return the generated certificate object download URL
echo $wash_certificate_store->getWashCertificateDownload($_GET['bookingId']);
// Exit the script
exit;
@@ -13,10 +13,9 @@ class xlvask_create_customer extends xlvask_helper
* Creates a customer in the XLVask system based on the provided user object. * Creates a customer in the XLVask system based on the provided user object.
* *
* @param users_o $user The user object containing customer information. * @param users_o $user The user object containing customer information.
* @return xlvask_customer The created XLVask customer object.
* @throws Exception If the user does not have a customer number or if the customer already exists. * @throws Exception If the user does not have a customer number or if the customer already exists.
*/ */
public static function createCustomer(users_o $user): xlvask_customer public static function createCustomer(users_o $user): never
{ {
// Validate user object // Validate user object
$user->requireSelected(); $user->requireSelected();
@@ -30,55 +29,8 @@ class xlvask_create_customer extends xlvask_helper
if ($user->hasXLVaskCustomerAccount()) { if ($user->hasXLVaskCustomerAccount()) {
throw new Exception('User already has an XLVask customer account'); throw new Exception('User already has an XLVask customer account');
} }
// Generate a unique customer GUID
$customer_id = self::generateCustomerId();
// TODO: FINISH THE CUSTOMER CREATION, ONCE THE XLVASK API IS READY // TODO: FINISH THE CUSTOMER CREATION, ONCE THE XLVASK API IS READY
throw new Exception('Customer creation is not implemented yet, delayed until the XLVask API is ready'); throw new Exception('Customer creation is not implemented yet, delayed until the XLVask API is ready');
// Create the customer
$tmp_result = $xlvask->sendRequest($xlvask->config->api_url . '/Customers', 'POST', [
'customerId' => $customer_id,
'name' => $user->getCustomerName((int)$user->customer_number->value()),
'vendorId' => $xlvask->config->vendor_id,
'customerTypeId' => null,
'discount' => 1,
'phone' => null,
'email' => null,
'address' => null,
'address2' => null,
'zip' => null,
'city' => null,
'userId' => null,
'vatnumber' => null,
'excludeFromAutoInvoice' => true,
'country' => null,
'createDate' => null,
'active' => true,
'language' => null,
'note' => null,
'updated' => null,
'externId' => (string)$user->customer_number->value()
], [
$xlvask->getAuthHeader()
]);
print_r($tmp_result);
} }
/** }
* Generates a unique customer ID for the XLVask system.
*
* @return string The generated customer ID. (UUID format)
*/
private static function generateCustomerId(): string
{
// Generate a UUID for the customer ID
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}
@@ -204,303 +204,17 @@ class xlvask_tasks
$user = $users[$customer->externId]; $user = $users[$customer->externId];
// Cache the customer // Cache the customer
$cache->setCustomerCache((int)$customer->externId, $customer); $cache->setCustomerCache((int)$customer->externId, $customer);
} else {
//echo 'MISSING USER: ' . $customer->name . ' (' . $customer->externId . ')' . PHP_EOL;
} }
} }
return $with_external_id; // Return the synchronized customers return $with_external_id; // Return the synchronized customers
} }
/** /**
* Run the sync usage task * Usage synchronization is intentionally disabled until the XL Vask integration is completed.
* This task is used to synchronize the usage of the XL Vask module.
* It fetches the usage data from XL Vask and adds it to the applicable users.
* The time format XL vask uses is "2025-05-01T00:00:00.000" - this is the ISO 8601 format.
* @return array|null
* @throws Exception
*/ */
public function runSyncUsage(string $dateFrom = null, string $dateTo = null): array|null public function runSyncUsage(?string $dateFrom = null, ?string $dateTo = null): void
{ {
// TODO: Remove this, this is just for testing purposes unset($dateFrom, $dateTo);
return null;
// Define the XL Vask object
$xlvask = new \classes\xlvask();
// Require the module to be enabled
$xlvask->requireModuleEnabled();
// Check if synchronization is enabled
if (!$xlvask->config->synchronization_enabled->isTrue()) {
throw new Exception('XL Vask synchronization is not enabled.');
}
// TODO: Remove this, this is just for testing purposes:
$dateFrom = "2025-06-01 00:00:00";
$dateTo = "2025-07-01 00:00:00";
// Set the date from which to fetch the usage data
// Since this is a cron task, we will fetch the usage data from the last 24 hours
$minutes = 24 * 60; // 24 hours in minutes
// TODO: Remove this, this is just for testing purposes:
$minutes = $minutes * 20;
// If dateFrom is not set, set it to 24 hours ago
if (empty($dateFrom)) {
$dateFrom = date('Y-m-d\TH:i:s.000', strtotime('-' . $minutes . ' minutes'));
} else {
// If dateFrom is set, make sure it is in the correct format
$dateFrom = date('Y-m-d\TH:i:s.000', strtotime($dateFrom));
}
// If dateTo is not set, set it to now
if (empty($dateTo)) {
$dateTo = date('Y-m-d\TH:i:s.000'); // Current time in ISO 8601 format
} else {
// If dateTo is set, make sure it is in the correct format
$dateTo = date('Y-m-d\TH:i:s.000', strtotime($dateTo));
}
echo 'Fetching usage data from: ' . $dateFrom . ' to: ' . $dateTo . PHP_EOL;
//echo $dateFrom; (E.g. 2025-06-11T12:13:16.000)
// If there are no customers, get them. TODO: Remove this in production, this is just for testing
$xlvask->getTasks()->runSyncUsers(true);
// Get the cached customers
$customers = $xlvask->getCache()->getAllCachedCustomers();
// Get all the usage logs from XL Vask
$debug_usage_logs = $xlvask->getUsageLog(
$dateFrom,
null, // regNr
null, // vehicleId
null // customerId
);
$debug_usage_logs = self::formatUsageLogs($debug_usage_logs);
// Filter out the logs that are after the dateTo
$debug_usage_logs = array_filter($debug_usage_logs, function ($log) use ($dateTo) {
/** @var xlvask_usage_log $log */
return strtotime($log->getFormattedDate()) <= strtotime($dateTo);
});
echo 'Found ' . count($debug_usage_logs) . ' usage logs in the date range from ' . $dateFrom . ' to ' . $dateTo . PHP_EOL;
echo "Prepaid: " . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return $log->isPrepaid();
})) . PHP_EOL;
echo "Finished: " . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return $log->isCompleted();
})) . PHP_EOL;
echo 'Not finished (skipped): ' . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return !$log->isCompleted();
})) . PHP_EOL;
echo 'Billed to default customer (skipped): ' . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return $log->hasDefaultCustomer();
})) . PHP_EOL;
echo 'Unique customers: ' . count(array_unique(array_map(function ($log) {
/** @var xlvask_usage_log $log */
return $log->CustomerId;
}, $debug_usage_logs))) . PHP_EOL;
$linked_orders = (new orders_o())->getFieldsWhere([
'wash_id' => array_map(function ($log) {
/** @var xlvask_usage_log $log */
return $log->WashId;
}, $debug_usage_logs),
'deleted_at' => null,
], [
'id',
'wash_id',
]);
echo "Linked to orders: " . count($linked_orders) . PHP_EOL;
$eligible_for_automatic_continuance = array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return $log->isEligibleForAutomaticContinuance(true);
});
$eligible_for_automatic_continuance_without_prepaid = array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return $log->isEligibleForAutomaticContinuance(false);
});
echo "Eligible for automatic continuance: " . count($eligible_for_automatic_continuance) . " (" . count($eligible_for_automatic_continuance_without_prepaid) . " without prepaid)" . PHP_EOL;
// Print a list of customers that do not have an external ID
$customers_without_external_id = array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return !$log->hasExternalId() && !$log->hasDefaultCustomer();
});
echo "Without billable customer: " . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return !$log->hasBillableCustomer();
})) . " (" . count(array_filter($debug_usage_logs, function ($log) {
/** @var xlvask_usage_log $log */
return !$log->hasBillableCustomer() && !$log->hasDefaultCustomer() && !$log->hasExternalId();
})) . " without external ID)" . PHP_EOL;
foreach ( self::sortLogsByDate($customers_without_external_id) as $customer ) {
/** @var xlvask_usage_log $customer */
echo ' - ' . $customer->getFormattedDate() . ' - ' . $customer->Customer . ' - ' . $customer->getDepartment()->name->value() . ', ' . $customer->getLane() . ' - ' . $customer->getTotalPrice() . ' DKK' . ' ( ' . ($customer->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($customer->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ' )' . PHP_EOL;
}
// Get all unique external IDs from the customers
$unique_external_customer_ids = array_unique(array_map(function ($customer) {
/** @var xlvask_usage_log $customer */
return $customer->CustomerId;
}, $debug_usage_logs));
// Remove the external IDs that are not numeric or empty
$unique_external_customer_ids = array_filter($unique_external_customer_ids, function ($id) {
return !empty($id) && is_numeric($id);
});
echo "Unique external customer IDs: " . count($unique_external_customer_ids) . PHP_EOL;
foreach ( $unique_external_customer_ids as $customer_id ) {
$doesCustomerExistInArray = array_filter($customers, function ($customer) use ($customer_id) {
/** @var xlvask_customer $customer */
return $customer->externId === $customer_id;
});
$tmp_does_customer_exist = count($doesCustomerExistInArray) > 0;
if (!$tmp_does_customer_exist) {
echo ' - ' . $customer_id . ' does not exist in the system (skipped)' . PHP_EOL;
continue; // Skip customers that do not exist in the system
}
$tmp_customer_object = reset($doesCustomerExistInArray); // Get the first customer object that matches the external ID
/** @var xlvask_customer $tmp_customer_object */
echo ' - ' . $tmp_customer_object->name . ' (' . $tmp_customer_object->externId . ') - ' . $tmp_customer_object->customerId . PHP_EOL;
echo " - # Washes: " . count(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) {
/** @var xlvask_usage_log $log */
return $log->CustomerId === $tmp_customer_object->externId;
})) . PHP_EOL;
// Echo the eligible for automatic continuance logs
foreach ( self::sortLogsByDate(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) {
/** @var xlvask_usage_log $log */
return $log->CustomerId === $tmp_customer_object->externId && $log->isEligibleForAutomaticContinuance(false);
})) as $log ) {
/** @var xlvask_usage_log $log */
$tmp_linked_to_order = array_filter($linked_orders, function ($order) use ($log) {
/** @var array $order */
return $order['wash_id'] === $log->WashId;
});
echo ' - - ' . $log->getFormattedDate() . ' - ' . $log->getDepartment()->name->value() . ', ' . $log->getLane() . ' - ' . $log->getTotalPrice() . ' DKK' . ' ( ' . ($log->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($log->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ', ' . ($tmp_linked_to_order ? 'Linked to order' : 'Not linked to order') . ' )' . PHP_EOL;
}
// Echo the ineligible for automatic continuance logs
echo " - # Not eligible for automatic continuance: " . count(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) {
/** @var xlvask_usage_log $log */
return $log->CustomerId === $tmp_customer_object->externId && !$log->isEligibleForAutomaticContinuance(false);
})) . PHP_EOL;
foreach ( self::sortLogsByDate(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) {
/** @var xlvask_usage_log $log */
return $log->CustomerId === $tmp_customer_object->externId && !$log->isEligibleForAutomaticContinuance(false);
})) as $log ) {
/** @var xlvask_usage_log $log */
echo ' - - ' . $log->getFormattedDate() . ' - ' . $log->getDepartment()->name->value() . ', ' . $log->getLane() . ' - ' . $log->getTotalPrice() . ' DKK' . ' ( ' . ($log->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($log->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ' )' . PHP_EOL;
}
}
echo 'Customers:';
print_r($customers);
echo 'Eligible for automatic continuance:';
print_r($eligible_for_automatic_continuance);
exit;
// Loop through the customers and check if there's any new usage data
foreach ( $customers as $customer ) {
/** @var xlvask_customer $customer */
// Check if the customer has an externId
if (empty($customer->externId)) {
continue; // Skip customers without an externId
}
//echo 'Checking customer: ' . $customer->name . ' (' . $customer->externId . ')' . PHP_EOL;
$tmp_usage_logs = self::formatUsageLogs($xlvask->getUsageLog(
$dateFrom,
null, // regNr
null, // vehicleId
$customer->customerId // customerId
));
foreach ( $tmp_usage_logs as $log ) {
/** @var xlvask_usage_log $log */
// Check if the wash was completed
if (!$log->isCompleted()) {
//echo 'A log is not completed: ' . $log->getFormattedDate() . ' - ' . $log->CustomerId . ' - ' . $log->VehicleId . PHP_EOL;
continue; // Skip logs that are not completed
}
// echo PHP_EOL;
// echo '### ' . $log->getFormattedDate() . ' - ' . $customer->name . ' (' . $customer->externId . ') - ' . $log->getTotalPrice() . PHP_EOL;
// echo '# License Plate: ' . $log->RegistrationNumber . PHP_EOL;
// echo '# Vehicle ID: ' . $log->VehicleId . PHP_EOL;
// echo '# Wash ID: ' . $log->WashId . PHP_EOL;
// echo '# Hall: ' . $log->Hall . PHP_EOL;
// echo '# Hall ID: ' . $log->HallId . PHP_EOL;
// echo '# Department: ' . $log->getDepartment()->id . ' ( ' . $log->getDepartment()->name->value() . ' )' . PHP_EOL;
// echo '# Track / Lane: ' . $log->getLane() . PHP_EOL;
// echo '# Linked to order: ' . ($log->isLinkedToOrder() ? 'Yes' : 'No') . PHP_EOL;
// echo '# Items: ' . PHP_EOL;
$tmp_skipped_items = [];
foreach ( $log->WashItems as $item ) {
/** @var xlvask_wash_item $item */
// Skip items that have the id 64, since these are not relevant to anyone.
$tmp_item_id = $item->getProduct($log)->id;
if ($tmp_item_id === 64 || !$item->isCountAboveZero()) {
$tmp_skipped_items[] = $item;
continue; // Skip items with id 64 (Or items simply not relevant)
}
//echo '## ' . $item->Count . ' x ' . $item->OriginalProductName . ' @ ' . $item->getPriceExVat() . ' as ' . $tmp_item_id . ' ( ' . $item->getProduct($log)->name->value() . ' )' . PHP_EOL;
}
//echo '### Total: ' . $log->getTotalPrice() . PHP_EOL;
//echo '### Skipped items: ' . count($tmp_skipped_items) . PHP_EOL;
if (count($tmp_skipped_items) > 0) {
//echo '### Skipped items details: ' . PHP_EOL;
foreach ( $tmp_skipped_items as $skipped_item ) {
/** @var xlvask_wash_item $skipped_item */
//echo '## ' . $skipped_item->Count . ' x ' . $skipped_item->OriginalProductName . ' @ ' . $skipped_item->getPriceExVat() . ' as ' . $skipped_item->getProduct($log)->id . ' ( ' . $skipped_item->getProduct($log)->name->value() . ' )' . PHP_EOL;
}
}
//echo '#' . PHP_EOL;
if (!$log->isLinkedToOrder()) {
//echo '# Checking for orders that might be addressing this wash.' . PHP_EOL;
// Check if an order that matches this wash exists.
if ($log->getPotentialOrder() !== null) {
$xlvask_potential_order_matches_o = new xlvask_potential_order_matches_o();
// Check if the potential order match is already in the database (Prevent duplicates)
if (!$xlvask_potential_order_matches_o->doesWashPotentialOrderMatchExist(
$log->WashId,
)) {
// There's no match in the database, so we will add it.
//echo '# Adding potential order match for wash: ' . $log->WashId . ' - Order: ' . $log->getPotentialOrder()->id . ' - Customer: ' . $customer->customerId . ' - Customer Number: ' . (int)$customer->getUser()->customer_number->value() . ' - Department: ' . $log->getDepartment()->id . PHP_EOL;
$xlvask_potential_order_matches_o->add(
(string)$log->WashId,
(int)$log->getPotentialOrder()->id,
(string)$customer->customerId,
(int)$customer->getUser()->customer_number->value(),
(int)$log->getDepartment()->id,
);
}
//echo '# This wash might be associated with an order: ' . $log->getPotentialOrder()->id . PHP_EOL;
} else {
// If no order is found, we will generate a new order.
//echo '# No potential order found for this wash.' . PHP_EOL;
// Verify the customer object
if (!$customer instanceof xlvask_customer) {
//echo '# The customer object is not an instance of xlvask_customer.' . PHP_EOL;
continue; // Skip this wash
}
if (!$tmp_order = $this->createOrderFromWash($log, $customer)) {
//echo '# Failed to create an order from this wash.' . PHP_EOL;
} else {
//echo '# Order created successfully.' . PHP_EOL;
//echo '# Order ID: ' . $tmp_order->id . PHP_EOL;
//echo '# Order total: ' . $tmp_order->getNetAmount() . PHP_EOL;
}
}
//echo '# This wash is not linked to an order, generating a new order.' . PHP_EOL;
}
//echo '# ------------------------' . PHP_EOL;
}
//print_r($tmp_usage_logs);
// Check if the customer has any usage data in the last 24 hours
// This is a placeholder, you might want to implement a method to check this
// if (!$xlvask->hasRecentUsageData($customer->externId, $dateFrom)) {
// continue; // Skip customers without recent usage data
// }
}
// Process the usage data and update the users accordingly
//$xlvask->processUsageData($usage_data);
return [];
} }
private static function formatUsageLogs(array $getUsageLog): array private static function formatUsageLogs(array $getUsageLog): array
@@ -297,12 +297,7 @@ class xlvask_usage_log extends xlvask_helper
} }
// If the washItems property is set, ensure it is an array // If the washItems property is set, ensure it is an array
if (isset($data['WashItems']) && is_array($data['WashItems'])) { if (isset($data['WashItems']) && is_array($data['WashItems'])) {
//echo 'Setting WashItems with ' . count($this->WashItems) . ' items.' . PHP_EOL;
$this->WashItems = self::generateWashItems($data['WashItems']); $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 // After setting all properties, nullify nullable properties
$this->unsetNullifiableProperties(); $this->unsetNullifiableProperties();
@@ -106,11 +106,6 @@ class collected_order_invoices_o extends db
{ {
// Require the invoice collection to be selected // Require the invoice collection to be selected
self::requireSelected(); self::requireSelected();
// Check if the object is cached
$cached = self::getCached('asArray', $this->id);
if ($cached !== null) {
//return (array)$cached;
}
$tmp = [ $tmp = [
'id' => (int)$this->id, 'id' => (int)$this->id,
'customer_number' => (int)$this->customer_number->value(), 'customer_number' => (int)$this->customer_number->value(),
@@ -1154,10 +1149,8 @@ class collected_order_invoices_o extends db
break; break;
case 2: case 2:
throw new Exception('Stripe invoice collections cannot be split'); throw new Exception('Stripe invoice collections cannot be split');
break;
case 3: case 3:
throw new Exception('Due to the stateless nature of the processor, invoice collections cannot be split. Please contact technical support for assistance.'); throw new Exception('Due to the stateless nature of the processor, invoice collections cannot be split. Please contact technical support for assistance.');
break;
default: default:
throw new Exception('Invalid processor type'); throw new Exception('Invalid processor type');
} }
@@ -1734,11 +1727,6 @@ class collected_order_invoices_o extends db
self::requireSelected(); self::requireSelected();
// Get the orders in the invoice collection // Get the orders in the invoice collection
$orders = self::getOrders(); $orders = self::getOrders();
// Check if there are any orders in the invoice collection
if (empty($orders)) {
// This has been removed as it is valid to have an invoice collection with no orders, if the customer only has a fixed price agreement
// throw new Exception('No orders in invoice collection'); DON'T RE-ADD THIS.
}
self::removeVehicleSubscriptionsTransactions(); self::removeVehicleSubscriptionsTransactions();
// Create the fixed prices transaction // Create the fixed prices transaction
$transaction = new orders_o(); $transaction = new orders_o();
@@ -2,6 +2,8 @@
namespace objects; namespace objects;
use Psr\Http\Client\ClientExceptionInterface;
use attachments\helpers\attachment_content; use attachments\helpers\attachment_content;
use classes\db; use classes\db;
use classes\email; use classes\email;
@@ -175,7 +175,6 @@ class subusers_o extends db
return $this; return $this;
} catch (Exception $e) { } catch (Exception $e) {
$response->error($e->getMessage()); $response->error($e->getMessage());
throw $e;
} }
} }
@@ -366,10 +366,6 @@ final class BrokerWebSocketClient
'lastDisconnectedAt' => null, 'lastDisconnectedAt' => null,
]; ];
public function __construct(private readonly Logger $logger)
{
}
public function configure(?string $brokerUrl, ?int $gatewayId, ?string $agentToken, ?string $agentInstanceId): void public function configure(?string $brokerUrl, ?int $gatewayId, ?string $agentToken, ?string $agentInstanceId): void
{ {
$normalizedUrl = $this->normalizeBrokerBaseUrl($brokerUrl); $normalizedUrl = $this->normalizeBrokerBaseUrl($brokerUrl);
@@ -1061,7 +1057,7 @@ final class TruckwashEdgeAgent
$this->controlPlaneStatusPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'control-plane-status.json'; $this->controlPlaneStatusPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'control-plane-status.json';
$this->stagedUpdatePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'staged-update.json'; $this->stagedUpdatePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'staged-update.json';
$this->agentInstanceId = $this->ensureAgentInstanceId(); $this->agentInstanceId = $this->ensureAgentInstanceId();
$this->brokerClient = new BrokerWebSocketClient($this->logger); $this->brokerClient = new BrokerWebSocketClient();
$this->shellBridge = new AgentShellBridge($this->installDir); $this->shellBridge = new AgentShellBridge($this->installDir);
$this->logger->setSink(fn(string $level, string $message): bool => $this->emitLogFrame($level, $message)); $this->logger->setSink(fn(string $level, string $message): bool => $this->emitLogFrame($level, $message));
$this->configureBrokerClient(); $this->configureBrokerClient();
@@ -122,11 +122,6 @@ class InvoicingPeriodRoute
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
$response->error($e->getMessage(), 400); $response->error($e->getMessage(), 400);
} }
return [
'dateFrom' => '',
'dateTo' => '',
];
} }
/** /**
@@ -855,7 +850,6 @@ class InvoicingPeriodRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
try { try {
@@ -881,7 +875,6 @@ class InvoicingPeriodRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$id = (int)($this->fromRoute('id') ?? 0); $id = (int)($this->fromRoute('id') ?? 0);
@@ -910,7 +903,6 @@ class InvoicingPeriodRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
try { try {
+2 -12
View File
@@ -2,12 +2,8 @@
namespace routes; namespace routes;
use attachments\helpers\attachment;
use attachments\helpers\attachment_content;
use classes\attachment_store; use classes\attachment_store;
use classes\attachments;
use classes\response; use classes\response;
use objects\orders_o;
use traits\route_t; use traits\route_t;
class attachmentsRoute class attachmentsRoute
@@ -16,14 +12,8 @@ class attachmentsRoute
public function run(): void public function run(): void
{ {
$this->get('/attachments/example', function () { $this->get('/attachments/example', function (): never {
global $response;
throw new \Exception('EXAMPLE ROUTE, SHOULD BE IMPLEMENTED IN THE INDIVIDUAL OBJECT ROUTES'); throw new \Exception('EXAMPLE ROUTE, SHOULD BE IMPLEMENTED IN THE INDIVIDUAL OBJECT ROUTES');
$orders_o = new orders_o();
$orders_o->select(22636);
// Debug: Create an attachment
$orders_o->addAttachment((new attachment_content())->setOther('Hello World!'));
$response->success($orders_o->listAttachments());
}); });
$this->post('/attachments/upload', function () { $this->post('/attachments/upload', function () {
global $response; global $response;
@@ -41,4 +31,4 @@ class attachmentsRoute
$response->success(['object_name' => $object_name]); $response->success(['object_name' => $object_name]);
}); });
} }
} }
-3
View File
@@ -532,7 +532,6 @@ class authRoute
'message' => $exception->getMessage(), 'message' => $exception->getMessage(),
]); ]);
$response->error('CVR could not be verified. Please check the CVR number and try again.', 400); $response->error('CVR could not be verified. Please check the CVR number and try again.', 400);
return;
} }
$name = trim((string)($companyInformation->name ?? '')); $name = trim((string)($companyInformation->name ?? ''));
@@ -543,7 +542,6 @@ class authRoute
'requestedCustomerNumber' => $companyPhone, 'requestedCustomerNumber' => $companyPhone,
]); ]);
$response->error('CVR could not be verified. Please check the CVR number and try again.', 400); $response->error('CVR could not be verified. Please check the CVR number and try again.', 400);
return;
} }
if ($localUserExists) { if ($localUserExists) {
@@ -1019,7 +1017,6 @@ class authRoute
'customerNumber' => $customerNumber, 'customerNumber' => $customerNumber,
]); ]);
$response->error('Customer was created in e-conomic but could not be imported locally.', 500); $response->error('Customer was created in e-conomic but could not be imported locally.', 500);
throw new Exception('Customer was created in e-conomic but could not be imported locally.');
} }
/** /**
@@ -462,33 +462,9 @@ class bookingsRoute
); );
$this->post('/admin/bookings/completeWashWithoutWashCertificate', function () { $this->post('/admin/bookings/completeWashWithoutWashCertificate', function () {
// Require the user to be logged in
global /** @var response $response */ global /** @var response $response */
$response; $response;
$response->error('Booking completion must be completed through POS desktop or mobile steps.', 410); $response->error('Booking completion must be completed through POS desktop or mobile steps.', 410);
$this->requirePermission('complete_wash_without_wash_certificate');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if (!$user->exists()) {
$response->error('User not found', 400);
}
// Check if the required fields are set
$id = $response->getRequestParameter('id');
// Make sure the id is a number
if (!is_numeric($id)) {
$response->error('id parameter must be a number got: ' . $id, 400);
}
// Make sure the user is allowed to complete the wash without a wash certificate
if (!$user->hasAccessToBooking($id)) {
$response->error('You are not allowed to complete this wash without a wash certificate', 400);
}
// Complete the wash without a wash certificate
(new bookings_o())->completeWashWithoutWashCertificate($id);
// Return success
$response->success(
["message" => "Wash completed without wash certificate"]
);
}, },
[ [
'complete_wash_without_wash_certificate' => 'Complete a wash without a wash certificate' 'complete_wash_without_wash_certificate' => 'Complete a wash without a wash certificate'
@@ -268,7 +268,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'CREATE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'CREATE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters([ self::requireParameters([
@@ -303,19 +302,16 @@ class departmentDailyReportsRoute
$description = trim((string)self::getParameter('description')); $description = trim((string)self::getParameter('description'));
if ($description === '') { if ($description === '') {
$response->error('Description is required', 400); $response->error('Description is required', 400);
return;
} }
$wash_date = $this->requireComplaintWashDateParameter('wash_date'); $wash_date = $this->requireComplaintWashDateParameter('wash_date');
if ($wash_date === null) { if ($wash_date === null) {
$response->error('Wash date is required', 400); $response->error('Wash date is required', 400);
return;
} }
$category = $this->requireComplaintCategoryParameter('category'); $category = $this->requireComplaintCategoryParameter('category');
if ($category === null) { if ($category === null) {
$response->error('Category is required', 400); $response->error('Category is required', 400);
return;
} }
$customer_number = null; $customer_number = null;
@@ -337,7 +333,6 @@ class departmentDailyReportsRoute
$customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number); $customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number);
if ($customer === false || !$customer->exists()) { if ($customer === false || !$customer->exists()) {
$response->error('Customer not found', 400); $response->error('Customer not found', 400);
return;
} }
} }
@@ -371,7 +366,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINT_CUSTOMERS', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINT_CUSTOMERS', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$has_create_permission = $this->hasPermission('create_department_daily_report_complaints'); $has_create_permission = $this->hasPermission('create_department_daily_report_complaints');
@@ -393,7 +387,6 @@ class departmentDailyReportsRoute
$search = trim((string)self::getParameter('search')); $search = trim((string)self::getParameter('search'));
if (mb_strlen($search) < 2) { if (mb_strlen($search) < 2) {
$response->error('Search must be at least 2 characters', 400); $response->error('Search must be at least 2 characters', 400);
return;
} }
$limit = 10; $limit = 10;
@@ -437,7 +430,6 @@ class departmentDailyReportsRoute
'message' => 'Failed to fetch complaint customers from e-conomic', 'message' => 'Failed to fetch complaint customers from e-conomic',
'upstream_message' => $upstream_message, 'upstream_message' => $upstream_message,
], 502); ], 502);
return;
} }
$matches = array_values(array_filter(array_map( $matches = array_values(array_filter(array_map(
@@ -485,7 +477,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$repository = $this->dailyReportComplaintsRepository(); $repository = $this->dailyReportComplaintsRepository();
@@ -503,7 +494,6 @@ class departmentDailyReportsRoute
$complaint = $repository->select((int)self::getParameter('id')); $complaint = $repository->select((int)self::getParameter('id'));
if (!$complaint->exists()) { if (!$complaint->exists()) {
$response->error('Complaint not found', 404); $response->error('Complaint not found', 404);
return;
} }
self::requireDepartmentAccess((int)$complaint->department_id->value()); self::requireDepartmentAccess((int)$complaint->department_id->value());
@@ -511,7 +501,6 @@ class departmentDailyReportsRoute
(new logs_o())->add('departments', 'global', 1, $user->id, 'GET_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully retrieved department daily report complaint'); (new logs_o())->add('departments', 'global', 1, $user->id, 'GET_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully retrieved department daily report complaint');
$response->success($repository->parseComplaint($complaint->asArray())); $response->success($repository->parseComplaint($complaint->asArray()));
return;
} }
(new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'Successfully listed department daily report complaints'); (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'Successfully listed department daily report complaints');
@@ -549,7 +538,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'EDIT_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'EDIT_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['id']); self::requireParameters(['id']);
@@ -565,7 +553,6 @@ class departmentDailyReportsRoute
$complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id')); $complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id'));
if (!$complaint->exists()) { if (!$complaint->exists()) {
$response->error('Complaint not found', 404); $response->error('Complaint not found', 404);
return;
} }
self::requireDepartmentAccess((int)$complaint->department_id->value()); self::requireDepartmentAccess((int)$complaint->department_id->value());
@@ -585,7 +572,6 @@ class departmentDailyReportsRoute
$department = (new departments_o())->select((int)self::getParameter('department_id')); $department = (new departments_o())->select((int)self::getParameter('department_id'));
if (!$department->exists()) { if (!$department->exists()) {
$response->error('Department not found', 400); $response->error('Department not found', 400);
return;
} }
self::requireDepartmentAccess((int)self::getParameter('department_id')); self::requireDepartmentAccess((int)self::getParameter('department_id'));
@@ -610,7 +596,6 @@ class departmentDailyReportsRoute
$description = trim((string)self::getParameter('description')); $description = trim((string)self::getParameter('description'));
if ($description === '') { if ($description === '') {
$response->error('Description is required', 400); $response->error('Description is required', 400);
return;
} }
$updates['description'] = $description; $updates['description'] = $description;
@@ -620,7 +605,6 @@ class departmentDailyReportsRoute
$wash_date = $this->requireComplaintWashDateParameter('wash_date'); $wash_date = $this->requireComplaintWashDateParameter('wash_date');
if ($wash_date === null) { if ($wash_date === null) {
$response->error('Wash date is required', 400); $response->error('Wash date is required', 400);
return;
} }
$updates['wash_date'] = $wash_date; $updates['wash_date'] = $wash_date;
@@ -630,7 +614,6 @@ class departmentDailyReportsRoute
$category = $this->requireComplaintCategoryParameter('category'); $category = $this->requireComplaintCategoryParameter('category');
if ($category === null) { if ($category === null) {
$response->error('Category is required', 400); $response->error('Category is required', 400);
return;
} }
$updates['category'] = $category; $updates['category'] = $category;
@@ -655,7 +638,6 @@ class departmentDailyReportsRoute
$customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number); $customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number);
if ($customer === false || !$customer->exists()) { if ($customer === false || !$customer->exists()) {
$response->error('Customer not found', 400); $response->error('Customer not found', 400);
return;
} }
$updates['customer_number'] = $customer_number; $updates['customer_number'] = $customer_number;
@@ -666,7 +648,6 @@ class departmentDailyReportsRoute
$response->success( $response->success(
$this->dailyReportComplaintsRepository()->parseComplaint($complaint->asArray()) $this->dailyReportComplaintsRepository()->parseComplaint($complaint->asArray())
); );
return;
} }
$complaint->update($updates); $complaint->update($updates);
@@ -690,7 +671,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'DELETE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'DELETE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['id']); self::requireParameters(['id']);
@@ -706,7 +686,6 @@ class departmentDailyReportsRoute
$complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id')); $complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id'));
if (!$complaint->exists()) { if (!$complaint->exists()) {
$response->error('Complaint not found', 404); $response->error('Complaint not found', 404);
return;
} }
self::requireDepartmentAccess((int)$complaint->department_id->value()); self::requireDepartmentAccess((int)$complaint->department_id->value());
@@ -823,13 +802,11 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$department_id_param = (string)($this->fromRoute('id') ?? ''); $department_id_param = (string)($this->fromRoute('id') ?? '');
if (!ctype_digit($department_id_param) || (int)$department_id_param <= 0) { if (!ctype_digit($department_id_param) || (int)$department_id_param <= 0) {
$response->error('Parameter id must be a positive integer', 400); $response->error('Parameter id must be a positive integer', 400);
return;
} }
self::requireParameters([ self::requireParameters([
@@ -843,7 +820,6 @@ class departmentDailyReportsRoute
if (!$department->exists()) { if (!$department->exists()) {
$response->error('Department not found', 404); $response->error('Department not found', 404);
return;
} }
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'Successfully loaded superuser department overview'); (new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'Successfully loaded superuser department overview');
@@ -872,7 +848,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OVERVIEW', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OVERVIEW', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters([ self::requireParameters([
@@ -886,7 +861,6 @@ class departmentDailyReportsRoute
if ($department_ids === []) { if ($department_ids === []) {
$response->error('At least one department_id must be provided', 400); $response->error('At least one department_id must be provided', 400);
return;
} }
foreach ($department_ids as $department_id) { foreach ($department_ids as $department_id) {
@@ -919,7 +893,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters([ self::requireParameters([
@@ -931,13 +904,11 @@ class departmentDailyReportsRoute
$department_id = (int)self::getParameter('department_id'); $department_id = (int)self::getParameter('department_id');
if ($department_id <= 0) { if ($department_id <= 0) {
$response->error('Parameter department_id must be a positive integer', 400); $response->error('Parameter department_id must be a positive integer', 400);
return;
} }
$department = (new departments_o())->select($department_id); $department = (new departments_o())->select($department_id);
if (!$department->exists()) { if (!$department->exists()) {
$response->error('Department not found', 404); $response->error('Department not found', 404);
return;
} }
self::requireDepartmentAccess($department_id); self::requireDepartmentAccess($department_id);
@@ -945,13 +916,11 @@ class departmentDailyReportsRoute
$product_id = (int)self::getParameter('product_id'); $product_id = (int)self::getParameter('product_id');
if (!$this->isDailyReportProductId($product_id)) { if (!$this->isDailyReportProductId($product_id)) {
$response->error('Invalid daily report product_id', 400); $response->error('Invalid daily report product_id', 400);
return;
} }
$parsed_target = $this->parseDailyReportProductTargetPercentage(self::getParameter('target_percentage')); $parsed_target = $this->parseDailyReportProductTargetPercentage(self::getParameter('target_percentage'));
if (!$parsed_target['valid']) { if (!$parsed_target['valid']) {
$response->error($parsed_target['message'], 400); $response->error($parsed_target['message'], 400);
return;
} }
$target_percentage = $parsed_target['value']; $target_percentage = $parsed_target['value'];
@@ -1191,7 +1160,6 @@ class departmentDailyReportsRoute
if (!$user) { if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OUTSIDE_HOURS_TREND', 'No user found, or invalid session'); (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OUTSIDE_HOURS_TREND', 'No user found, or invalid session');
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters([ self::requireParameters([
@@ -1209,7 +1177,6 @@ class departmentDailyReportsRoute
$department_ids = $this->normalizeDepartmentIdsParameter(self::getParameter('department_ids')); $department_ids = $this->normalizeDepartmentIdsParameter(self::getParameter('department_ids'));
if ($department_ids === []) { if ($department_ids === []) {
$response->error('At least one department_id must be provided', 400); $response->error('At least one department_id must be provided', 400);
return;
} }
foreach ($department_ids as $department_id) { foreach ($department_ids as $department_id) {
@@ -1602,7 +1569,7 @@ class departmentDailyReportsRoute
} }
/** /**
* @return array{valid:bool,value:?float,message:string} * @return array{valid: bool, value: ?float, message: string}
*/ */
private function parseDailyReportProductTargetPercentage(mixed $target_percentage): array private function parseDailyReportProductTargetPercentage(mixed $target_percentage): array
{ {
@@ -1698,37 +1665,6 @@ class departmentDailyReportsRoute
return $this->outsideHoursStatisticsService()->toOverviewMetric( return $this->outsideHoursStatisticsService()->toOverviewMetric(
$this->outsideHoursStatisticsService()->getSummary($date, $department_ids, $date_to) $this->outsideHoursStatisticsService()->getSummary($date, $department_ids, $date_to)
); );
foreach ($department_ids as $department_id) {
if (!isset($opening_hours_by_department_id[$department_id])) {
return $this->metricPayload(
null,
null,
'unavailable',
'Døgnvask kræver åbningstider for alle valgte afdelinger.'
);
}
}
$night_wash_count = 0;
foreach ($wash_transactions as $wash_transaction) {
$department_id = (int)($wash_transaction['department_id'] ?? 0);
$created_at = (string)($wash_transaction['created_at'] ?? '');
if ($created_at === '') {
continue;
}
$opening_hours = $opening_hours_by_department_id[$department_id] ?? null;
if (!is_array($opening_hours)) {
continue;
}
if ($this->isOutsideOpeningHours($created_at, $opening_hours)) {
$night_wash_count++;
}
}
return $this->metricPayload($night_wash_count);
} }
/** /**
@@ -72,7 +72,6 @@ class economicInvoiceRoute
'mode' => 'synchronous_fallback', 'mode' => 'synchronous_fallback',
'result' => $result, 'result' => $result,
]); ]);
return;
} }
$queue = new economic_transfer_queue(); $queue = new economic_transfer_queue();
@@ -197,7 +196,6 @@ class economicInvoiceRoute
'mode' => 'synchronous_fallback', 'mode' => 'synchronous_fallback',
'result' => $result, 'result' => $result,
]); ]);
return;
} }
$queue = new economic_transfer_queue(); $queue = new economic_transfer_queue();
+3 -123
View File
@@ -28,136 +28,17 @@ class exampleRoute
}); });
$this->get('/tmp-send-email', function () { $this->get('/tmp-send-email', function () {
global $response; global $response;
$response->error(['message' => 'This route is deprecated.']); $response->error(['message' => 'This route is deprecated.']);
$emailAddress = "my@truckwash.dk";
$customer_name = "John Doe";
/**
* Tillykke med din nye kredit konto!
*
* Du kan nu vaske i alle vores afdelinger.
* Vedhæftet finder du en liste over vores afdelinger, som kan viderebringes til dine chauffører.
*
* Vi har oprettet en kredit konto til dig, med følgende informationer:
*
* Virksomhed: AT Kloakservice ApS
* Tlf: 77302200
* CVR: 40650717
*/
$email = new email();
$email->sendWelcomeEmailToCustomer($customer_number = 43632122, $emailAddress);
$response->success(['message' => 'This route is deprecated.']);
}); });
$this->get('/tmp-washes-in-time', function () { $this->get('/tmp-washes-in-time', function () {
global $response; global $response;
$response->error(['message' => 'This route is deprecated.']); $response->error(['message' => 'This route is deprecated.']);
$date_from = date('2026-01-01 00:00:00');
$date_to = date('2026-01-31 23:59:59');
// Set the hours on the date range to be from 17:00-23:59
$daily_start_time = '17:00:00';
$daily_end_time = '23:59:59';
// Loop through each day in the date range, and get the washes that were done in the time range
$washes = [];
$current_date = $date_from;
while (strtotime($current_date) <= strtotime($date_to)) {
$daily_start = date('Y-m-d', strtotime($current_date)) . ' ' . $daily_start_time;
$daily_end = date('Y-m-d', strtotime($current_date)) . ' ' . $daily_end_time;
$daily_washes = (new orders_o())->getWashesInTimeRange($daily_start, $daily_end, ['department_id' => 7]);
$washes = array_merge($washes, $daily_washes);
$current_date = date('Y-m-d H:i:s', strtotime($current_date . ' +1 day'));
}
$wash_arrays = array_map(function ($wash) {
return $wash->asArray();
}, $washes);
// Create a CSV file from the washes $csv = "Order ID,Customer ID,Department ID,Created At\n";
foreach ($wash_arrays as $wash) {
$csv .= "{$wash['id']},{$wash['customer_id']},{$wash['department_id']},{$wash['created_at']}\n";
}
// Output the CSV file
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="washes_in_time_range.csv"');
echo $csv;
exit;
}); });
$this->get('/tmp-customer-list-overcharged', function () { $this->get('/tmp-customer-list-overcharged', function () {
global $response; global $response;
$response->error(['message' => 'This route is deprecated.']); $response->error(['message' => 'This route is deprecated.']);
/**
* Steps:
* 1. Get a list of all collected order invoices in january
* 2. Extract the customers, and make sure they haven't been charged more than once for product ID: 78.
* 3. Get a list of the amount of times, at what price and for what order IDs they have been charged.
* 4. If they have been charged more than once, add them to a list of overcharged customers.
* 5. Return the list of overcharged customers.
*/
// Step 1: Get a list of all collected order invoices in january
$invoices = [];
$product = (new products_o())->select(78);
$all_invoices = (new collected_order_invoices_o())->getObjectsWhereClause("closed_at BETWEEN '2026-01-31 00:00:00' AND '2026-02-01 23:59:59'");
// Filter out empty invoices
foreach ($all_invoices as $invoice) {
if ($invoice->isEmpty()) {
continue;
}
$invoices[] = $invoice;
}
// Get all orders related to the invoices
$overcharged_customers = [];
$order_ids = (new orders_o())->getFieldsWhereIn([
'deleted_at' => null,
'invoice_collection_id' => array_map(function ($invoice) {
return $invoice->id;
}, $invoices)],
['id', 'customer_id']);
$order_id_to_customer_id = array_column($order_ids, 'customer_id', 'id');
$order_ids = array_map(function ($order) {
return (int)$order['id'];
}, $order_ids);
// Get all the products in the orders
$order_items = (new order_items_o())->getFieldsWhereIn(['order_id' => $order_ids, 'product_id' => [$product->id], 'deleted_at' => null], ['price', 'quantity', 'order_id', 'id']);
// Define the customers => items map
$customer_items_map = [];
foreach ($order_items as $order_item) {
$customer_items_map[(string)$order_id_to_customer_id[(string)$order_item['order_id']]][] = $order_item;
}
// Ignore the first item for each customer (as that is correct)
foreach ($customer_items_map as $customer_id => $items) {
array_shift($customer_items_map[$customer_id]);
}
// Create a total per customer number of items and price map
foreach ($customer_items_map as $customer_id => $items) {
$total_quantity = 0;
$total_price = 0.0;
foreach ($items as $item) {
$total_quantity += (int)$item['quantity'];
$total_price += (float)$item['price'] * (int)$item['quantity'];
}
// If the total quantity is more than 1, add to overcharged customers
if ($total_quantity > 1) {
$overcharged_customers[$customer_id] = [
'total_quantity' => $total_quantity,
'total_price' => $total_price,
'items' => $items,
];
}
}
// Format message
foreach ($overcharged_customers as $customer_id => $data) {
$customer = (new users_o())->getUserByCustomerNumber((int)$customer_id);
$message = "Customer number: {$customer->customer_number->value()} - x{$data['total_quantity']} items for a total of {$data['total_price']} DKK\n";
//$message .= "Items:\n";
foreach ($data['items'] as $item) {
//$message .= "- Order Item ID: {$item['id']}, Order ID: {$item['order_id']}, Price: {$item['price']}, Quantity: {$item['quantity']}\n";
}
echo $message . "\n";
}
$response->success(['message' => 'Customer list overcharged', 'inv_count' => count($invoices), 'ord_count' => count($order_ids), 'order_ids' => $order_ids, 'order_items' => count($order_items), 'customer_items_map' => $customer_items_map, 'overcharged_customers' => $overcharged_customers]);
}); });
$this->get('/debug', function () { $this->get('/debug', function () {
global $response; global $response;
//$response->success(['message' => 'Debugging route!']); //$response->success(['message' => 'Debugging route!']);
@@ -171,7 +52,6 @@ class exampleRoute
$machine_1->setup(); $machine_1->setup();
$machine_1->servePicture(); $machine_1->servePicture();
exit; exit;
$response->success(['message' => 'Debugging route!', 'base64_image' => $machine_1->exportAsBase64()]);
}); });
} }
} }
@@ -242,7 +242,6 @@ class moduleSelfServeRoute
'customer' => null, 'customer' => null,
'vehicle' => null, 'vehicle' => null,
], $customer_scope)); ], $customer_scope));
return;
} }
$runtime_customer_number = (int)$lane->getCustomerNumber(); $runtime_customer_number = (int)$lane->getCustomerNumber();
@@ -281,7 +280,6 @@ class moduleSelfServeRoute
'subuser' => null, 'subuser' => null,
'vehicle' => $vehicle, 'vehicle' => $vehicle,
], $customer_scope)); ], $customer_scope));
return;
} }
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value(); $customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
@@ -1505,7 +1503,7 @@ class moduleSelfServeRoute
} }
/** /**
* @return array{customer_number:?int,subuser_id:?int} * @return array{customer_number: ?int, subuser_id: ?int}
*/ */
private function normalizeCustomerScope(mixed $customer_scope): array private function normalizeCustomerScope(mixed $customer_scope): array
{ {
@@ -1567,7 +1565,7 @@ class moduleSelfServeRoute
} }
/** /**
* @return array{customer_number:int,subuser_id:?int} * @return array{customer_number: int, subuser_id: ?int}
*/ */
private function requireMyActiveWashPrincipalScope(): array private function requireMyActiveWashPrincipalScope(): array
{ {
@@ -51,7 +51,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['q']); self::requireParameters(['q']);
@@ -69,7 +68,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['q']); self::requireParameters(['q']);
@@ -88,7 +86,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['q']); self::requireParameters(['q']);
@@ -106,7 +103,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$department_ids = self::parseDepartmentIdsFromRequest(); $department_ids = self::parseDepartmentIdsFromRequest();
@@ -163,7 +159,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$department_ids = self::parseDepartmentIdsFromRequest(); $department_ids = self::parseDepartmentIdsFromRequest();
@@ -199,7 +194,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$department_ids = self::parseDepartmentIdsFromRequest(); $department_ids = self::parseDepartmentIdsFromRequest();
@@ -230,7 +224,6 @@ class moduleWeatherAPIRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
self::requireParameters(['department_id', 'degraded_threshold', 'healthy_threshold']); self::requireParameters(['department_id', 'degraded_threshold', 'healthy_threshold']);
@@ -894,7 +894,6 @@ class orderInvoicesRoute
'mode' => 'synchronous_fallback', 'mode' => 'synchronous_fallback',
'result' => $result, 'result' => $result,
]); ]);
return;
} }
$queue = new economic_transfer_queue(); $queue = new economic_transfer_queue();
@@ -1422,7 +1421,6 @@ class orderInvoicesRoute
'mode' => 'synchronous_fallback', 'mode' => 'synchronous_fallback',
'result' => $result, 'result' => $result,
]); ]);
return;
} }
$queue = new economic_transfer_queue(); $queue = new economic_transfer_queue();
+1 -1
View File
@@ -2,6 +2,7 @@
namespace routes; namespace routes;
use Exception;
use attachments\helpers\attachment_content; use attachments\helpers\attachment_content;
use classes\attachment_store; use classes\attachment_store;
use classes\attachments; use classes\attachments;
@@ -1104,7 +1105,6 @@ class ordersRoute
foreach ( $data as $key => $value ) { foreach ( $data as $key => $value ) {
if (!in_array($key, $allowed_to_edit)) { if (!in_array($key, $allowed_to_edit)) {
$response->error('You do not have permission to edit this order field (key: ' . $key . ')', 400); $response->error('You do not have permission to edit this order field (key: ' . $key . ')', 400);
break;
} }
}; };
$shouldRefreshAttachedWashCertificate = false; $shouldRefreshAttachedWashCertificate = false;
@@ -6,7 +6,6 @@ use classes\pdf_generator;
use classes\pdf_store; use classes\pdf_store;
use classes\response; use classes\response;
use classes\router; use classes\router;
use objects\logs_o;
use traits\route_t; use traits\route_t;
class pdfGeneratorRoute class pdfGeneratorRoute
@@ -22,7 +21,6 @@ class pdfGeneratorRoute
/** PDF Generator > GET */ /** PDF Generator > GET */
$this->get('/modules/pdf-generator/test', function () { $this->get('/modules/pdf-generator/test', function () {
global $response; global $response;
if (true) {
// Check if the required parameters are present // Check if the required parameters are present
self::requireParameters([ self::requireParameters([
'id', 'id',
@@ -118,10 +116,6 @@ class pdfGeneratorRoute
$pdf_path $pdf_path
), ),
]); ]);
} else {
(new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES', 'User tried to access the list of collected order invoices without a valid session');
$response->error('Invalid session', 400);
}
}, },
[ [
'list_collected_invoices' => 'List ALL collected order invoices. This is a superuser-only route.' 'list_collected_invoices' => 'List ALL collected order invoices. This is a superuser-only route.'
@@ -160,4 +154,4 @@ class pdfGeneratorRoute
]); ]);
}); });
} }
} }
@@ -22,6 +22,7 @@ class permissionsRoute
/** Permissions > List */ /** Permissions > List */
$this->get('/permissions', function () { $this->get('/permissions', function () {
global $response, $router; global $response, $router;
/** @var router $router */
$this->requirePermission('permissions_list'); $this->requirePermission('permissions_list');
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if ($user) { if ($user) {
@@ -54,4 +55,4 @@ class permissionsRoute
] ]
); );
} }
} }
@@ -50,7 +50,6 @@ class releaseManagerRoute
$manager = new release_manager(); $manager = new release_manager();
if (!$manager->verifyReleaseGateToken($this->releaseGateToken())) { if (!$manager->verifyReleaseGateToken($this->releaseGateToken())) {
$response->error(['message' => 'Invalid release gate token.'], 401); $response->error(['message' => 'Invalid release gate token.'], 401);
return;
} }
try { try {
@@ -93,7 +93,6 @@ class subusersRoute
} }
$response->error('Unauthorized', 401); $response->error('Unauthorized', 401);
return 0;
} }
private function parsePermissionsPayload(mixed $raw, ?array $default = null): ?array private function parsePermissionsPayload(mixed $raw, ?array $default = null): ?array
@@ -145,7 +144,6 @@ class subusersRoute
$response->error($exception->getMessage(), 400); $response->error($exception->getMessage(), 400);
} }
return null;
} }
private function normalizeOptionalString(mixed $value): ?string private function normalizeOptionalString(mixed $value): ?string
+5 -168
View File
@@ -40,89 +40,8 @@ class workerRoute
$response->success(['message' => 'Version update functionality is not yet implemented.']); $response->success(['message' => 'Version update functionality is not yet implemented.']);
}); });
$this->get('/worker/test', function () { $this->get('/worker/test', function () {
global /** @var router $router */ global $response;
$response, $router;
$response->error('This endpoint is disabled for security reasons.', 403); $response->error('This endpoint is disabled for security reasons.', 403);
$department = (new departments_o())->select((int)6);
$days = 7;
// The time should be from 00:00:00 of the start date to 23:59:59 of the end date
$date_end = date('Y-m-d 23:59:59', strtotime("-1 days")); // Yesterday (Sunday) at 23:59:59
$date_start = date('Y-m-d 00:00:00', strtotime("-$days days")); // $days ago at 00:00:00
$department->sendSlackInternalStatisticNotification($date_start, $date_end);
$response->success(['message' => 'Test message sent to Slack (not really, this is a placeholder).' ]);
exit;
// Configuration
$department_id = 6;
/**
* Weekly results for Roskilde
*
* Period: 29.01.2026 - 04.02.2026
* Washes: 67
* Fælg Flex: 55%
* Spot Free: 22%
* Special Sæbe: 11%
* 10 min ekstra: 15%
* Undervognsskyl: 44%
* Voks: 66%
*/
$max_addons = [];
$sold_addons = [];
$percentages = [];
$department = (new departments_o())->select($department_id);
echo "Testing addon sales calculation for department ID: $department_id from $date_start to $date_end\n";
$tmp = "*Weekly results for {$department->name->value()}*\n";
$tmp .= "Period: " . date('d.m.Y', strtotime($date_start)) . " - " . date('d.m.Y', strtotime($date_end)) . "\n";
// Washes
$wash_count = (new orders_o())->countWashesInDateRange($date_start, $date_end, $department_id);
$tmp .= "Washes: $wash_count\n";
// Get the percentage of addons sold out of max
foreach ($product_ids as $product_id) {
// Handle merged products
if (is_array($product_id)) {
$addon_sold_count = 0;
$addon_max_count = 0;
foreach ($product_id as $pid) {
$pid = (int)$pid;
$addon_sold_count += (new product_options_o())->countSoldAddonsInDateRange($pid, $date_start, $date_end, $department_id);
$addon_max_count += (new product_options_o())->getMaxAddonsInDateRange($pid, $date_start, $date_end, $department_id);
}
} else {
$pid = $product_id;
$addon_sold_count = (new product_options_o())->countSoldAddonsInDateRange($pid, $date_start, $date_end, $department_id);
$addon_max_count = (new product_options_o())->getMaxAddonsInDateRange($pid, $date_start, $date_end, $department_id);
}
// Prevent division by zero
if ($addon_max_count === 0) {
$addon_percentage_sold = 0;
} else {
$addon_percentage_sold = ($addon_sold_count / $addon_max_count) * 100;
}
$max_addons[is_array($product_id) ? implode('_', $product_id) : $product_id] = $addon_max_count;
$sold_addons[is_array($product_id) ? implode('_', $product_id) : $product_id] = $addon_sold_count;
$percentages[is_array($product_id) ? implode('_', $product_id) : $product_id] = number_format($addon_percentage_sold, 2);
}
foreach ($product_ids as $product_id) {
if (is_array($product_id)) {
// Merged product names
$product_names = [];
foreach ($product_id as $pid) {
$product_names[] = (new products_o())->select($pid)->name->value();
}
// Switch to joined names
$product_name = match (true) {
in_array(23, $product_id) && in_array(24, $product_id) => 'Spot Free',
default => implode(' + ', $product_names),
};
} else {
$product_name = (new products_o())->select($product_id)->name->value();
}
$tmp .= "{$product_name}: {$percentages[is_array($product_id) ? implode('_', $product_id) : $product_id]}%\n";
}
// Send test message to Slack
$slack = new slack();
$department = (new departments_o())->select($department_id);
$slack->send_message($tmp);
$response->success(['message' => 'Test message sent to Slack (not really, this is a placeholder).' ]);
}); });
$this->get('/worker/status', function () { $this->get('/worker/status', function () {
global /** @var router $router */ global /** @var router $router */
@@ -153,102 +72,20 @@ class workerRoute
]); ]);
}); });
$this->get('/worker/debug', function () { $this->get('/worker/debug', function () {
global /** @var router $router */ global $response;
$response, $router;
$response->error('This endpoint is disabled for security reasons.', 403); $response->error('This endpoint is disabled for security reasons.', 403);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
$parameters = new shelly_request_body_get_states();
$parameters->ids = ['e4b323243f90'];
$parameters->select = ['status'];
$result = $shelly->sendPostRequest('/v2/devices/api/get', (array)$parameters);
// Format the result
$result = array_map(function ($device) {
return (new shelly_device_switch())->populate($device);
}, $result);
//TODO: fetch statuses
$response->success($result);
}); });
$this->get('/worker/debug/on', function () { $this->get('/worker/debug/on', function () {
global /** @var router $router */ global $response;
$response, $router;
$response->error('This endpoint is disabled for security reasons.', 403); $response->error('This endpoint is disabled for security reasons.', 403);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
$parameters = new shelly_request_body_get_states();
$parameters->ids = ['e4b323243f90'];
$parameters->select = ['status'];
$result = $shelly->sendPostRequest('/v2/devices/api/get', (array)$parameters);
// Wait 1 second
sleep(1);
// Format the result
$result = array_map(function ($device) {
return (new shelly_device_switch())->populate($device);
}, $result);
// Open the switch
foreach ($result as $device) {
$device->switch(true);
}
//TODO: fetch statuses
$response->success($result);
}); });
$this->get('/worker/debug/off', function () { $this->get('/worker/debug/off', function () {
global /** @var router $router */ global $response;
$response, $router;
$response->error('This endpoint is disabled for security reasons.', 403); $response->error('This endpoint is disabled for security reasons.', 403);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
$parameters = new shelly_request_body_get_states();
$parameters->ids = ['e4b323243f90'];
$parameters->select = ['status'];
$result = $shelly->sendPostRequest('/v2/devices/api/get', (array)$parameters);
// Wait 1 second
sleep(1);
// Format the result
$result = array_map(function ($device) {
return (new shelly_device_switch())->populate($device);
}, $result);
// Open the switch
foreach ($result as $device) {
$device->switch(false);
}
//TODO: fetch statuses
$response->success($result);
}); });
$this->get('/worker/licenseplates', function () { $this->get('/worker/licenseplates', function () {
global /** @var router $router */ global $response;
$response, $db;
$response->error('This endpoint is disabled for security reasons.', 403); $response->error('This endpoint is disabled for security reasons.', 403);
$counted_plates = []; // Array to hold counted license plates
// This is used to fetch all UNIQUE license plates from the database tables:
// 'customer_vehicles' -> 'reg' column
// 'orders' -> 'reg_1', 'reg_2', 'reg_3' columns
// 'bookings' -> 'regNrTraekker', 'regNrTrailer' columns
// 'plate_scans' -> 'plate' column
$tables_and_columns = [
'customer_vehicles' => ['reg'],
'orders' => ['reg_1', 'reg_2', 'reg_3'],
'bookings' => ['regNrTraekker', 'regNrTrailer'],
'plate_scans' => ['plate'],
];
foreach ($tables_and_columns as $table => $columns) {
foreach ($columns as $column) {
$results = $db->query("SELECT DISTINCT $column FROM $table WHERE $column IS NOT NULL AND $column != ''");
foreach ($results as $row) {
$formatted_plate = $this->FORMAT_LICENSE_PLATE($row[$column]);
if ($formatted_plate !== '') {
if (!isset($counted_plates[$formatted_plate])) {
$counted_plates[$formatted_plate] = 0;
}
$counted_plates[$formatted_plate]++;
}
}
}
}
$response->success(['counted_license_plates' => $counted_plates, 'total_unique_plates' => count($counted_plates)]);
}); });
$this->get('/economic/doesCustomerExist', function () { $this->get('/economic/doesCustomerExist', function () {
global $response; global $response;
@@ -170,13 +170,11 @@ class xlvaskUsageLogsRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$id = (int)($this->fromRoute('id') ?? 0); $id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) { if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400); $response->error('Invalid XL Vask usage log id', 400);
return;
} }
$reason = $this->isParametersSet(['reason']) ? trim((string)$this->getParameter('reason')) : null; $reason = $this->isParametersSet(['reason']) ? trim((string)$this->getParameter('reason')) : null;
@@ -210,7 +208,6 @@ class xlvaskUsageLogsRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$ids = $this->isParametersSet(['ids']) ? $this->getParameter('ids') : []; $ids = $this->isParametersSet(['ids']) ? $this->getParameter('ids') : [];
@@ -238,13 +235,11 @@ class xlvaskUsageLogsRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$id = (int)($this->fromRoute('id') ?? 0); $id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) { if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400); $response->error('Invalid XL Vask usage log id', 400);
return;
} }
$response->success( $response->success(
@@ -263,13 +258,11 @@ class xlvaskUsageLogsRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$id = (int)($this->fromRoute('id') ?? 0); $id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) { if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400); $response->error('Invalid XL Vask usage log id', 400);
return;
} }
$suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null; $suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null;
@@ -291,13 +284,11 @@ class xlvaskUsageLogsRoute
$user = (new authentication())->get_user(); $user = (new authentication())->get_user();
if (!$user) { if (!$user) {
$response->error('Invalid session', 400); $response->error('Invalid session', 400);
return;
} }
$id = (int)($this->fromRoute('id') ?? 0); $id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) { if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400); $response->error('Invalid XL Vask usage log id', 400);
return;
} }
$suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null; $suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null;
@@ -56,7 +56,7 @@ it('keeps the OpenAPI manifest entries aligned with the API spec', function ():
$operationsInSpec = []; $operationsInSpec = [];
$currentPath = null; $currentPath = null;
foreach ($lines as $line) { foreach ($lines as $line) {
if (preg_match('/^ (\/[^:]+):\s*$/', $line, $pathMatch) === 1) { if (preg_match('/^ {2}(\/[^:]+):\s*$/', $line, $pathMatch) === 1) {
$currentPath = $pathMatch[1]; $currentPath = $pathMatch[1];
continue; continue;
} }
@@ -65,7 +65,7 @@ it('keeps the OpenAPI manifest entries aligned with the API spec', function ():
continue; continue;
} }
if (preg_match('/^ ([a-z]+):\s*$/', $line, $methodMatch) === 1) { if (preg_match('/^ {4}([a-z]+):\s*$/', $line, $methodMatch) === 1) {
$operationsInSpec[] = strtoupper($methodMatch[1]) . ' ' . $currentPath; $operationsInSpec[] = strtoupper($methodMatch[1]) . ' ' . $currentPath;
} }
} }
@@ -40,9 +40,11 @@ function usesApiSuite(): void
// The API lifecycle is bound via Tests\Support\Api\ApiTestCase in tests/Pest.php. // The API lifecycle is bound via Tests\Support\Api\ApiTestCase in tests/Pest.php.
} }
function api_test_covers(string $operation, string $kind = 'happy'): bool function api_test_covers(string $operation, string $kind = 'happy'): void
{ {
return $operation !== '' && $kind !== ''; if ($operation === '' || $kind === '') {
throw new InvalidArgumentException('API coverage markers require an operation and coverage kind.');
}
} }
function assert_api_envelope(ApiResponse $response): ApiResponse function assert_api_envelope(ApiResponse $response): ApiResponse
@@ -12,7 +12,7 @@ it('unregisters legacy booking completion forms', function (): void {
expect($code)->not->toContain('GENERATE_BOOKING_WASH_CERTIFICATE'); expect($code)->not->toContain('GENERATE_BOOKING_WASH_CERTIFICATE');
}); });
it('keeps legacy wash certificate downloads but disables generation and completion', function (): void { it('keeps legacy wash certificate downloads but removes disabled generation and completion code', function (): void {
$code = (string)file_get_contents(app_path('modules/washcertificates/index.php')); $code = (string)file_get_contents(app_path('modules/washcertificates/index.php'));
$downloadPosition = strpos($code, "isset(\$_GET['justDownload'])"); $downloadPosition = strpos($code, "isset(\$_GET['justDownload'])");
@@ -21,9 +21,8 @@ it('keeps legacy wash certificate downloads but disables generation and completi
expect($downloadPosition)->not->toBeFalse(); expect($downloadPosition)->not->toBeFalse();
expect($disabledPosition)->not->toBeFalse(); expect($disabledPosition)->not->toBeFalse();
expect($completionPosition)->not->toBeFalse(); expect($completionPosition)->toBeFalse();
expect($downloadPosition)->toBeLessThan($disabledPosition); expect($downloadPosition)->toBeLessThan($disabledPosition);
expect($disabledPosition)->toBeLessThan($completionPosition); expect($code)->not->toContain('new WashCertificateGenerator');
expect($code)->toContain('Booking completion must be completed through POS desktop or mobile steps.'); expect($code)->toContain('Booking completion must be completed through POS desktop or mobile steps.');
}); });
@@ -163,7 +163,6 @@ it('allows failed Coolify replica targets to be removed after the service disapp
it('retries Coolify maintenance while linked replication provisioning is still incomplete', function (): void { it('retries Coolify maintenance while linked replication provisioning is still incomplete', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'replicationHostStillNeedsProvisioning'); $method = new ReflectionMethod(coolify_manager::class, 'replicationHostStillNeedsProvisioning');
$method->setAccessible(true);
expect($method->invoke(null, [ expect($method->invoke(null, [
'role' => 'replica', 'role' => 'replica',
@@ -189,7 +188,6 @@ it('retries Coolify maintenance while linked replication provisioning is still i
it('plans Hetzner load balancer target and service drift without mutating state', function (): void { it('plans Hetzner load balancer target and service drift without mutating state', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile'); $method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [ $plan = $method->invoke($manager, [
'targets' => [ 'targets' => [
@@ -219,7 +217,6 @@ it('plans Hetzner load balancer target and service drift without mutating state'
it('plans Hetzner load balancer service health check drift updates', function (): void { it('plans Hetzner load balancer service health check drift updates', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile'); $method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [ $plan = $method->invoke($manager, [
'targets' => [ 'targets' => [
@@ -272,7 +269,6 @@ it('plans Hetzner load balancer service health check drift updates', function ()
it('does not plan removal of the last Hetzner load balancer target', function (): void { it('does not plan removal of the last Hetzner load balancer target', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile'); $method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [ $plan = $method->invoke($manager, [
'targets' => [ 'targets' => [
@@ -294,7 +290,6 @@ it('does not plan removal of the last Hetzner load balancer target', function ()
it('plans removal only for disabled or deleted Hetzner load balancer targets', function (): void { it('plans removal only for disabled or deleted Hetzner load balancer targets', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile'); $method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [ $plan = $method->invoke($manager, [
'targets' => [ 'targets' => [
@@ -320,9 +315,7 @@ it('plans removal only for disabled or deleted Hetzner load balancer targets', f
it('builds gateway API auto-provision context for connected Coolify servers', function (): void { it('builds gateway API auto-provision context for connected Coolify servers', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext'); $contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext');
$contextMethod->setAccessible(true);
$ipMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteServerPublicIp'); $ipMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteServerPublicIp');
$ipMethod->setAccessible(true);
$server = [ $server = [
'uuid' => 'server-node1', 'uuid' => 'server-node1',
@@ -376,7 +369,6 @@ it('builds gateway API auto-provision context for connected Coolify servers', fu
it('builds gateway frontend auto-provision context with the release Dockerfile', function (): void { it('builds gateway frontend auto-provision context with the release Dockerfile', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext'); $contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext');
$contextMethod->setAccessible(true);
$server = [ $server = [
'uuid' => 'server-node3', 'uuid' => 'server-node3',
@@ -421,9 +413,7 @@ it('builds gateway frontend auto-provision context with the release Dockerfile',
it('adds explicit Coolify application route labels for gateway API domains', function (): void { it('adds explicit Coolify application route labels for gateway API domains', function (): void {
$payloadMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteApplicationPayload'); $payloadMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteApplicationPayload');
$payloadMethod->setAccessible(true);
$publicUrlMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteTargetPublicUrl'); $publicUrlMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteTargetPublicUrl');
$publicUrlMethod->setAccessible(true);
$payload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io', 'api-app-uuid', 8080, base64_encode(implode("\n", [ $payload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io', 'api-app-uuid', 8080, base64_encode(implode("\n", [
'custom.keep=true', 'custom.keep=true',
@@ -479,7 +469,6 @@ it('adds explicit Coolify application route labels for gateway API domains', fun
it('isolates and restores Hetzner load balancer IP targets for gateway certificate bootstrap', function (): void { it('isolates and restores Hetzner load balancer IP targets for gateway certificate bootstrap', function (): void {
$manager = new coolify_manager(); $manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'setLoadBalancerIpTargets'); $method = new ReflectionMethod(coolify_manager::class, 'setLoadBalancerIpTargets');
$method->setAccessible(true);
$client = new CoolifyManagerHetznerTargetSetFake([ $client = new CoolifyManagerHetznerTargetSetFake([
'94.130.142.41', '94.130.142.41',
'65.21.214.30', '65.21.214.30',
@@ -508,7 +497,6 @@ it('isolates and restores Hetzner load balancer IP targets for gateway certifica
it('requires gateway ping probes to return the API ping contract', function (): void { it('requires gateway ping probes to return the API ping contract', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'gatewayProbePingContract'); $method = new ReflectionMethod(coolify_manager::class, 'gatewayProbePingContract');
$method->setAccessible(true);
expect($method->invoke(null, json_encode([ expect($method->invoke(null, json_encode([
'success' => true, 'success' => true,
@@ -523,7 +511,6 @@ it('requires gateway ping probes to return the API ping contract', function ():
it('normalizes gateway probe paths for release gateway health checks', function (): void { it('normalizes gateway probe paths for release gateway health checks', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'normalizeGatewayProbePath'); $method = new ReflectionMethod(coolify_manager::class, 'normalizeGatewayProbePath');
$method->setAccessible(true);
expect($method->invoke(null, 'internal/api/ping'))->toBe('/internal/api/ping') expect($method->invoke(null, 'internal/api/ping'))->toBe('/internal/api/ping')
->and($method->invoke(null, '//internal//api//ping//'))->toBe('/internal/api/ping') ->and($method->invoke(null, '//internal//api//ping//'))->toBe('/internal/api/ping')
@@ -533,7 +520,6 @@ it('normalizes gateway probe paths for release gateway health checks', function
it('returns structured errors for failed gateway certificate bootstrap and verification', function (): void { it('returns structured errors for failed gateway certificate bootstrap and verification', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'gatewayRouteHealthErrors'); $method = new ReflectionMethod(coolify_manager::class, 'gatewayRouteHealthErrors');
$method->setAccessible(true);
$errors = $method->invoke(null, [ $errors = $method->invoke(null, [
'ok' => false, 'ok' => false,
@@ -10,7 +10,6 @@ function department_daily_reports_route_invoke_private(object $route, string $me
{ {
$reflection = new ReflectionClass(departmentDailyReportsRoute::class); $reflection = new ReflectionClass(departmentDailyReportsRoute::class);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -49,7 +49,6 @@ if (!class_exists('EconomicMLegacyGrantFallbackProbe')) {
private function setPrivateTokenField(string $fieldName, string $value): void private function setPrivateTokenField(string $fieldName, string $value): void
{ {
$reflection = new ReflectionProperty(economic_m::class, $fieldName); $reflection = new ReflectionProperty(economic_m::class, $fieldName);
$reflection->setAccessible(true);
$reflection->setValue($this, $value); $reflection->setValue($this, $value);
} }
} }
@@ -11,7 +11,6 @@ function economic_customer_helper_from_payload(object $payload): economic_custom
$customer = $reflection->newInstanceWithoutConstructor(); $customer = $reflection->newInstanceWithoutConstructor();
$property = $reflection->getProperty('customer_data_object'); $property = $reflection->getProperty('customer_data_object');
$property->setAccessible(true);
$property->setValue($customer, $payload); $property->setValue($customer, $payload);
return $customer; return $customer;
@@ -8,7 +8,6 @@ function economic_payment_terms_route_extract_collection(mixed $response): array
{ {
$reflection = new ReflectionClass(economicPaymentTermsRoute::class); $reflection = new ReflectionClass(economicPaymentTermsRoute::class);
$target = $reflection->getMethod('extractPaymentTermsCollection'); $target = $reflection->getMethod('extractPaymentTermsCollection');
$target->setAccessible(true);
return $target->invokeArgs(null, [$response]); return $target->invokeArgs(null, [$response]);
} }
@@ -8,7 +8,6 @@ function economic_transfer_queue_invoke_private(economic_transfer_queue $queue,
{ {
$reflection = new ReflectionClass($queue); $reflection = new ReflectionClass($queue);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($queue, $args); return $target->invokeArgs($queue, $args);
} }
@@ -17,7 +17,6 @@ function invoice_period_flag_service_invoke(string $method, array $args = []): m
$service = invoice_period_flag_service_instance(); $service = invoice_period_flag_service_instance();
$reflection = new ReflectionClass(invoice_period_flag_service::class); $reflection = new ReflectionClass(invoice_period_flag_service::class);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($service, $args); return $target->invokeArgs($service, $args);
} }
@@ -35,7 +34,6 @@ it('canonicalizes invoice period redis keys for bare dates and normalized API ti
$reflection = new ReflectionClass(\classes\redis::class); $reflection = new ReflectionClass(\classes\redis::class);
$redis = $reflection->newInstanceWithoutConstructor(); $redis = $reflection->newInstanceWithoutConstructor();
$key = $reflection->getMethod('invoicePeriodCacheKey'); $key = $reflection->getMethod('invoicePeriodCacheKey');
$key->setAccessible(true);
expect($key->invoke($redis, 'invoice_period_automatic_flags', '2025-03-01', '2025-03-31')) expect($key->invoke($redis, 'invoice_period_automatic_flags', '2025-03-01', '2025-03-31'))
->toBe($key->invoke( ->toBe($key->invoke(
@@ -682,15 +680,12 @@ it('uses a preloaded e-conomic global discount in expected price breakdowns', fu
$reflection = new ReflectionClass(invoice_period_flag_service::class); $reflection = new ReflectionClass(invoice_period_flag_service::class);
$cache = $reflection->getProperty('economicCustomerDiscountCache'); $cache = $reflection->getProperty('economicCustomerDiscountCache');
$cache->setAccessible(true);
$cache->setValue($service, [ $cache->setValue($service, [
35131752 => 18, 35131752 => 18,
]); ]);
$calculate = $reflection->getMethod('calculateExpectedPrice'); $calculate = $reflection->getMethod('calculateExpectedPrice');
$calculate->setAccessible(true);
$breakdownMethod = $reflection->getMethod('priceBreakdown'); $breakdownMethod = $reflection->getMethod('priceBreakdown');
$breakdownMethod->setAccessible(true);
$row = [ $row = [
'customer_number' => 35131752, 'customer_number' => 35131752,
@@ -813,9 +808,7 @@ it('seeds order item preview cache from period rows', function (): void {
$reflection = new ReflectionClass(invoice_period_flag_service::class); $reflection = new ReflectionClass(invoice_period_flag_service::class);
$seed = $reflection->getMethod('seedOrderItemsPreviewCacheFromRows'); $seed = $reflection->getMethod('seedOrderItemsPreviewCacheFromRows');
$seed->setAccessible(true);
$preview = $reflection->getMethod('getOrderItemsForPreview'); $preview = $reflection->getMethod('getOrderItemsForPreview');
$preview->setAccessible(true);
$seed->invoke($service, [ $seed->invoke($service, [
[ [
@@ -8,7 +8,6 @@ function invoicing_period_draft_overlay_invoke(string $method, array $args = [])
{ {
$reflection = new ReflectionClass(InvoicingPeriodRoute::class); $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs(null, $args); return $target->invokeArgs(null, $args);
} }
@@ -47,7 +46,6 @@ function invoicing_period_draft_overlay_reset_deleted_at_column_cache(): void
{ {
$reflection = new ReflectionClass(InvoicingPeriodRoute::class); $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$property = $reflection->getProperty('collectedOrderInvoicesHasDeletedAtColumn'); $property = $reflection->getProperty('collectedOrderInvoicesHasDeletedAtColumn');
$property->setAccessible(true);
$property->setValue(null, null); $property->setValue(null, null);
} }
@@ -10,7 +10,6 @@ function invoicing_period_pagination_invoke(string $method, array $args = []): m
{ {
$reflection = new ReflectionClass(InvoicingPeriodRoute::class); $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs(null, $args); return $target->invokeArgs(null, $args);
} }
@@ -8,7 +8,6 @@ function invoicing_period_queue_overlay_invoke(string $method, array $args = [])
{ {
$reflection = new ReflectionClass(InvoicingPeriodRoute::class); $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs(null, $args); return $target->invokeArgs(null, $args);
} }
@@ -8,7 +8,6 @@ function invoicing_period_route_invoke_private(InvoicingPeriodRoute $route, stri
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -68,7 +68,6 @@ it('only includes invoice period flags when the list permission is granted', fun
it('maps batched period transaction rows to the legacy transaction response shape', function (): void { it('maps batched period transaction rows to the legacy transaction response shape', function (): void {
$reflection = new ReflectionClass(InvoicingPeriodRoute::class); $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$method = $reflection->getMethod('constructTransactionObjectFromPeriodRow'); $method = $reflection->getMethod('constructTransactionObjectFromPeriodRow');
$method->setAccessible(true);
$transaction = $method->invokeArgs(null, [[ $transaction = $method->invokeArgs(null, [[
'id' => '42', 'id' => '42',
@@ -8,7 +8,6 @@ function n8n_route_invoke_private(moduleN8nRoute $route, string $method, array $
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -7,7 +7,7 @@ it('restricts absolute webhook URLs to configured n8n webhook host', function ()
expect($content)->not->toBeFalse(); expect($content)->not->toBeFalse();
expect($content)->toContain('isAllowedWebhookAbsoluteUrl'); expect($content)->toContain('isAllowedWebhookAbsoluteUrl');
expect($content)->toContain('Webhook URL must use the configured n8n webhook host.'); expect($content)->toContain('Webhook URL must use the configured n8n webhook host.');
expect($content)->toContain("$targetHost !== $baseHost"); expect($content)->toContain('$targetHost !== $baseHost');
expect($content)->toContain("$targetScheme !== $baseScheme"); expect($content)->toContain('$targetScheme !== $baseScheme');
expect($content)->toContain('return $targetPort === $basePort;'); expect($content)->toContain('return $targetPort === $basePort;');
}); });
@@ -25,7 +25,7 @@ if (!class_exists('OrdersAutoWashCertificateCompletionDouble')) {
{ {
public bool $containsWashCertificate = false; public bool $containsWashCertificate = false;
public bool $washCertificateAttached = false; public bool $washCertificateAttached = false;
/** @var array<int, array{seal_number:?string,operator:?string,date:mixed}> */ /** @var array<int, array{seal_number: ?string, operator: ?string, date: mixed}> */
public array $generatedCertificates = []; public array $generatedCertificates = [];
public ?order_bookings_o $linkedBooking = null; public ?order_bookings_o $linkedBooking = null;
@@ -13,9 +13,9 @@ class RedisAtomicReservationTestClient extends PredisClient
{ {
} }
public function mget(array $keys): array public function mget(array $keyOrKeys): array
{ {
$this->calls[] = ['mget', $keys]; $this->calls[] = ['mget', $keyOrKeys];
return $this->returnValue; return $this->returnValue;
} }
@@ -34,7 +34,6 @@ function redis_test_inject_client(redis $redis, PredisClient $client): void
{ {
$reflection = new ReflectionClass($redis); $reflection = new ReflectionClass($redis);
$property = $reflection->getProperty('redis'); $property = $reflection->getProperty('redis');
$property->setAccessible(true);
$property->setValue($redis, $client); $property->setValue($redis, $client);
} }
@@ -8,7 +8,6 @@ function releaseStatusOverviewForTest(array $summary): array
{ {
$manager = new release_manager(); $manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'releaseStatusOverview'); $method = new ReflectionMethod(release_manager::class, 'releaseStatusOverview');
$method->setAccessible(true);
return $method->invoke($manager, array_replace([ return $method->invoke($manager, array_replace([
'generated_at' => '2026-05-20T10:00:00+00:00', 'generated_at' => '2026-05-20T10:00:00+00:00',
@@ -89,9 +89,7 @@ it('verifies CI release gate bearer tokens from dedicated release credentials',
it('normalizes app-specific release gate auto-sync metadata', function (): void { it('normalizes app-specific release gate auto-sync metadata', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$normalizeGate = new ReflectionMethod(release_manager::class, 'normalizeReleaseGateInput'); $normalizeGate = new ReflectionMethod(release_manager::class, 'normalizeReleaseGateInput');
$normalizeGate->setAccessible(true);
$appMatches = new ReflectionMethod(release_manager::class, 'releaseGateAppMatches'); $appMatches = new ReflectionMethod(release_manager::class, 'releaseGateAppMatches');
$appMatches->setAccessible(true);
$gate = $normalizeGate->invoke($manager, [ $gate = $normalizeGate->invoke($manager, [
'channel_slug' => 'stable', 'channel_slug' => 'stable',
@@ -129,7 +127,6 @@ it('normalizes app-specific release gate auto-sync metadata', function (): void
it('requires non-empty release gate checks before auto-sync can proceed', function (): void { it('requires non-empty release gate checks before auto-sync can proceed', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'releaseGateAutoSyncValidationSteps'); $method = new ReflectionMethod(release_manager::class, 'releaseGateAutoSyncValidationSteps');
$method->setAccessible(true);
$failed = $method->invoke($manager, [ $failed = $method->invoke($manager, [
'channel_slug' => 'stable', 'channel_slug' => 'stable',
@@ -167,7 +164,6 @@ it('requires non-empty release gate checks before auto-sync can proceed', functi
it('extracts API ping commit metadata for release gate verification', function (): void { it('extracts API ping commit metadata for release gate verification', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'releaseGateApiPayloadCommitSha'); $method = new ReflectionMethod(release_manager::class, 'releaseGateApiPayloadCommitSha');
$method->setAccessible(true);
expect($method->invoke($manager, [ expect($method->invoke($manager, [
'success' => true, 'success' => true,
@@ -194,7 +190,6 @@ it('normalizes GitHub repository identifiers for private repository access check
it('keeps GitHub commit timestamps in public release manager commit payloads', function (): void { it('keeps GitHub commit timestamps in public release manager commit payloads', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'publicGithubCommit'); $method = new ReflectionMethod(release_manager::class, 'publicGithubCommit');
$method->setAccessible(true);
$commit = $method->invoke($manager, [ $commit = $method->invoke($manager, [
'sha' => 'feedface00000000000000000000000000000000', 'sha' => 'feedface00000000000000000000000000000000',
@@ -248,7 +243,6 @@ it('summarizes failed deployments and blocks promotion until a deployment succee
it('uses the selected Coolify project and resolves server UUID from the instance default', function (): void { it('uses the selected Coolify project and resolves server UUID from the instance default', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload'); $method = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
$method->setAccessible(true);
$payload = $method->invoke($manager, [ $payload = $method->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -276,11 +270,9 @@ it('supports isolated stack mode and names new Coolify services explicitly', fun
$manager = new release_manager(); $manager = new release_manager();
$normalizeMode = new ReflectionMethod(release_manager::class, 'normalizeServiceSetMode'); $normalizeMode = new ReflectionMethod(release_manager::class, 'normalizeServiceSetMode');
$normalizeMode->setAccessible(true);
expect($normalizeMode->invoke($manager, ' isolated_stack '))->toBe('isolated_stack'); expect($normalizeMode->invoke($manager, ' isolated_stack '))->toBe('isolated_stack');
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
'app' => 'frontend', 'app' => 'frontend',
@@ -306,7 +298,6 @@ it('supports isolated stack mode and names new Coolify services explicitly', fun
it('creates frontend Coolify GitHub App application payloads with the release Dockerfile', function (): void { it('creates frontend Coolify GitHub App application payloads with the release Dockerfile', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'canary', 'channel_slug' => 'canary',
@@ -346,7 +337,6 @@ it('creates frontend Coolify GitHub App application payloads with the release Do
it('uses the self-contained Coolify API Dockerfile for API applications', function (): void { it('uses the self-contained Coolify API Dockerfile for API applications', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -378,7 +368,6 @@ it('uses the self-contained Coolify API Dockerfile for API applications', functi
it('creates private Coolify application payloads for cron workers', function (): void { it('creates private Coolify application payloads for cron workers', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -411,7 +400,6 @@ it('creates private Coolify application payloads for cron workers', function ():
it('derives cron worker deployment context from the API target without public routing', function (): void { it('derives cron worker deployment context from the API target without public routing', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$contextMethod = new ReflectionMethod(release_manager::class, 'cronWorkerDeployContext'); $contextMethod = new ReflectionMethod(release_manager::class, 'cronWorkerDeployContext');
$contextMethod->setAccessible(true);
$context = $contextMethod->invoke($manager, [ $context = $contextMethod->invoke($manager, [
'id' => 17, 'id' => 17,
@@ -446,8 +434,6 @@ it('requires Coolify cron worker autoprovisioning for API deployments by default
$manager = new release_manager(); $manager = new release_manager();
$enabledMethod = new ReflectionMethod(release_manager::class, 'cronWorkerAutoprovisionEnabled'); $enabledMethod = new ReflectionMethod(release_manager::class, 'cronWorkerAutoprovisionEnabled');
$requiredMethod = new ReflectionMethod(release_manager::class, 'cronWorkerAutoprovisionRequired'); $requiredMethod = new ReflectionMethod(release_manager::class, 'cronWorkerAutoprovisionRequired');
$enabledMethod->setAccessible(true);
$requiredMethod->setAccessible(true);
expect($enabledMethod->invoke($manager, ['deploy_context_json' => null]))->toBeTrue(); expect($enabledMethod->invoke($manager, ['deploy_context_json' => null]))->toBeTrue();
expect($requiredMethod->invoke($manager, ['deploy_context_json' => null]))->toBeTrue(); expect($requiredMethod->invoke($manager, ['deploy_context_json' => null]))->toBeTrue();
@@ -475,7 +461,6 @@ it('requires Coolify cron worker autoprovisioning for API deployments by default
it('classifies cron worker deployment and heartbeat lifecycle states', function (): void { it('classifies cron worker deployment and heartbeat lifecycle states', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$healthMethod = new ReflectionMethod(release_manager::class, 'cronWorkerHealth'); $healthMethod = new ReflectionMethod(release_manager::class, 'cronWorkerHealth');
$healthMethod->setAccessible(true);
expect($healthMethod->invoke($manager, null, null, [], ['running' => 0, 'stale' => 0, 'failed' => 0], null)['state']) expect($healthMethod->invoke($manager, null, null, [], ['running' => 0, 'stale' => 0, 'failed' => 0], null)['state'])
->toBe('needs_deploy'); ->toBe('needs_deploy');
@@ -503,7 +488,6 @@ it('classifies cron worker deployment and heartbeat lifecycle states', function
it('extracts Coolify cron deployment operation identifiers from provider payloads', function (): void { it('extracts Coolify cron deployment operation identifiers from provider payloads', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$operationMethod = new ReflectionMethod(release_manager::class, 'coolifyDeploymentOperationId'); $operationMethod = new ReflectionMethod(release_manager::class, 'coolifyDeploymentOperationId');
$operationMethod->setAccessible(true);
expect($operationMethod->invoke($manager, ['deployment' => ['deployment_uuid' => 'deployment-123']])) expect($operationMethod->invoke($manager, ['deployment' => ['deployment_uuid' => 'deployment-123']]))
->toBe('deployment-123'); ->toBe('deployment-123');
@@ -515,7 +499,6 @@ it('extracts Coolify cron deployment operation identifiers from provider payload
it('detects missing Coolify cron worker resources from provider errors', function (): void { it('detects missing Coolify cron worker resources from provider errors', function (): void {
$method = new ReflectionMethod(release_manager::class, 'coolifyResourceMissing'); $method = new ReflectionMethod(release_manager::class, 'coolifyResourceMissing');
$method->setAccessible(true);
expect($method->invoke(null, new RuntimeException('Coolify API request failed: HTTP 404')))->toBeTrue(); expect($method->invoke(null, new RuntimeException('Coolify API request failed: HTTP 404')))->toBeTrue();
expect($method->invoke(null, new RuntimeException('Application not found')))->toBeTrue(); expect($method->invoke(null, new RuntimeException('Application not found')))->toBeTrue();
@@ -525,7 +508,6 @@ it('detects missing Coolify cron worker resources from provider errors', functio
it('classifies missing Coolify cron worker resources as repairable', function (): void { it('classifies missing Coolify cron worker resources as repairable', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness'); $readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness');
$readiness->setAccessible(true);
$result = $readiness->invoke($manager, [ $result = $readiness->invoke($manager, [
'id' => 17, 'id' => 17,
@@ -552,7 +534,6 @@ it('classifies missing Coolify cron worker resources as repairable', function ()
it('repairs from an existing cron target when the API target is absent', function (): void { it('repairs from an existing cron target when the API target is absent', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness'); $readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness');
$readiness->setAccessible(true);
$result = $readiness->invoke($manager, null, [ $result = $readiness->invoke($manager, null, [
'id' => 71, 'id' => 71,
@@ -574,7 +555,6 @@ it('repairs from an existing cron target when the API target is absent', functio
it('blocks cron worker deployment without an API target or deployable cron context', function (): void { it('blocks cron worker deployment without an API target or deployable cron context', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness'); $readiness = new ReflectionMethod(release_manager::class, 'cronWorkerDeploymentReadiness');
$readiness->setAccessible(true);
$result = $readiness->invoke($manager, null, [ $result = $readiness->invoke($manager, null, [
'id' => 71, 'id' => 71,
@@ -596,7 +576,6 @@ it('blocks cron worker deployment without an API target or deployable cron conte
it('builds explicit Coolify application route labels for release API targets', function (): void { it('builds explicit Coolify application route labels for release API targets', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationRoutePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationRoutePayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -622,7 +601,6 @@ it('builds explicit Coolify application route labels for release API targets', f
it('updates existing frontend Coolify applications away from legacy Nixpacks detection', function (): void { it('updates existing frontend Coolify applications away from legacy Nixpacks detection', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationUpdatePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationUpdatePayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -650,7 +628,6 @@ it('updates existing frontend Coolify applications away from legacy Nixpacks det
it('can use the Coolify instance default GitHub App when source targets do not store it yet', function (): void { it('can use the Coolify instance default GitHub App when source targets do not store it yet', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload');
$payloadMethod->setAccessible(true);
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -676,7 +653,6 @@ it('can use the Coolify instance default GitHub App when source targets do not s
it('does not treat an existing Coolify service as an application just because a GitHub App UUID is stored', function (): void { it('does not treat an existing Coolify service as an application just because a GitHub App UUID is stored', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$resourceTypeMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyResourceType'); $resourceTypeMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyResourceType');
$resourceTypeMethod->setAccessible(true);
expect($resourceTypeMethod->invoke($manager, [ expect($resourceTypeMethod->invoke($manager, [
'coolify_github_app_uuid' => 'github-app-copenhagentruckwash-github', 'coolify_github_app_uuid' => 'github-app-copenhagentruckwash-github',
@@ -695,7 +671,6 @@ it('does not treat an existing Coolify service as an application just because a
it('auto-prepares path-routed release targets for Coolify application creation', function (): void { it('auto-prepares path-routed release targets for Coolify application creation', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$needsApplication = new ReflectionMethod(release_manager::class, 'releaseTargetNeedsApplicationAutoCreate'); $needsApplication = new ReflectionMethod(release_manager::class, 'releaseTargetNeedsApplicationAutoCreate');
$needsApplication->setAccessible(true);
$target = [ $target = [
'id' => 42, 'id' => 42,
@@ -720,7 +695,6 @@ it('auto-prepares path-routed release targets for Coolify application creation',
it('offers application target preparation for missing Coolify service creation failures', function (): void { it('offers application target preparation for missing Coolify service creation failures', function (): void {
$needsApplicationAction = new ReflectionMethod(release_manager::class, 'releaseStatusIssueNeedsApplicationTarget'); $needsApplicationAction = new ReflectionMethod(release_manager::class, 'releaseStatusIssueNeedsApplicationTarget');
$needsApplicationAction->setAccessible(true);
expect($needsApplicationAction->invoke(null, [ expect($needsApplicationAction->invoke(null, [
'message' => 'API deployment failed before activation.', 'message' => 'API deployment failed before activation.',
@@ -762,7 +736,6 @@ it('builds API Coolify runtime environment from allowed process variables', func
$manager = new release_manager(); $manager = new release_manager();
$runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv'); $runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv');
$runtimeEnv->setAccessible(true);
$env = $runtimeEnv->invoke($manager, [ $env = $runtimeEnv->invoke($manager, [
'app' => 'api', 'app' => 'api',
@@ -853,7 +826,6 @@ it('resolves backend commit sha from API runtime environment in priority order',
it('forces selected API commit into generated Coolify runtime env keys', function (): void { it('forces selected API commit into generated Coolify runtime env keys', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv'); $runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv');
$runtimeEnv->setAccessible(true);
$selectedCommit = '1111111111111111111111111111111111111111'; $selectedCommit = '1111111111111111111111111111111111111111';
$explicitCommit = '2222222222222222222222222222222222222222'; $explicitCommit = '2222222222222222222222222222222222222222';
@@ -893,7 +865,6 @@ it('builds cron worker runtime environment from API runtime keys and cron contex
$manager = new release_manager(); $manager = new release_manager();
$runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv'); $runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv');
$runtimeEnv->setAccessible(true);
$selectedCommit = '4444444444444444444444444444444444444444'; $selectedCommit = '4444444444444444444444444444444444444444';
$env = $runtimeEnv->invoke($manager, [ $env = $runtimeEnv->invoke($manager, [
@@ -932,7 +903,6 @@ it('builds cron worker runtime environment from API runtime keys and cron contex
it('uses the selected deployment commit before stale Coolify context commits', function (): void { it('uses the selected deployment commit before stale Coolify context commits', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$gitCommitSha = new ReflectionMethod(release_manager::class, 'releaseCoolifyGitCommitSha'); $gitCommitSha = new ReflectionMethod(release_manager::class, 'releaseCoolifyGitCommitSha');
$gitCommitSha->setAccessible(true);
$selectedCommit = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; $selectedCommit = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$staleCommit = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; $staleCommit = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
@@ -948,7 +918,6 @@ it('uses the selected deployment commit before stale Coolify context commits', f
it('injects selected frontend commit into Coolify runtime env for manifest builds', function (): void { it('injects selected frontend commit into Coolify runtime env for manifest builds', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv'); $runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv');
$runtimeEnv->setAccessible(true);
$selectedCommit = '3333333333333333333333333333333333333333'; $selectedCommit = '3333333333333333333333333333333333333333';
@@ -987,7 +956,6 @@ it('keeps beta API runtime environment on production database target', function
$manager = new release_manager(); $manager = new release_manager();
$runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv'); $runtimeEnv = new ReflectionMethod(release_manager::class, 'releaseCoolifyRuntimeEnv');
$runtimeEnv->setAccessible(true);
$env = $runtimeEnv->invoke($manager, [ $env = $runtimeEnv->invoke($manager, [
'app' => 'api', 'app' => 'api',
@@ -1015,7 +983,6 @@ it('keeps beta API runtime environment on production database target', function
it('treats attach-existing service sets without data target ids as production-shared ready', function (): void { it('treats attach-existing service sets without data target ids as production-shared ready', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$status = new ReflectionMethod(release_manager::class, 'serviceSetStatus'); $status = new ReflectionMethod(release_manager::class, 'serviceSetStatus');
$status->setAccessible(true);
$dataTargets = ['database' => null, 'redis' => null, 'minio' => null]; $dataTargets = ['database' => null, 'redis' => null, 'minio' => null];
expect($status->invoke($manager, 'attach_existing', 10, 11, $dataTargets))->toBe('ready'); expect($status->invoke($manager, 'attach_existing', 10, 11, $dataTargets))->toBe('ready');
@@ -1027,7 +994,6 @@ it('treats attach-existing service sets without data target ids as production-sh
it('detects explicit data target ids so beta service sets can stay data-only', function (): void { it('detects explicit data target ids so beta service sets can stay data-only', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$hasExplicitDataTargets = new ReflectionMethod(release_manager::class, 'serviceSetInputHasExplicitDataTargets'); $hasExplicitDataTargets = new ReflectionMethod(release_manager::class, 'serviceSetInputHasExplicitDataTargets');
$hasExplicitDataTargets->setAccessible(true);
expect($hasExplicitDataTargets->invoke($manager, [ expect($hasExplicitDataTargets->invoke($manager, [
'mode' => 'attach_existing', 'mode' => 'attach_existing',
@@ -1046,7 +1012,6 @@ it('detects explicit data target ids so beta service sets can stay data-only', f
it('allows beta production-service bundles only when data services stay production-shared', function (): void { it('allows beta production-service bundles only when data services stay production-shared', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$assert = new ReflectionMethod(release_manager::class, 'assertBetaProductionDataPolicy'); $assert = new ReflectionMethod(release_manager::class, 'assertBetaProductionDataPolicy');
$assert->setAccessible(true);
$betaChannel = ['id' => 2, 'slug' => 'beta']; $betaChannel = ['id' => 2, 'slug' => 'beta'];
expect($assert->invoke($manager, $betaChannel, ['mode' => 'attach_existing']))->toBeNull(); expect($assert->invoke($manager, $betaChannel, ['mode' => 'attach_existing']))->toBeNull();
@@ -1060,7 +1025,6 @@ it('allows beta production-service bundles only when data services stay producti
it('keeps release branch services out of the production Coolify environment', function (): void { it('keeps release branch services out of the production Coolify environment', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
$payloadMethod->setAccessible(true);
$canaryPayload = $payloadMethod->invoke($manager, [ $canaryPayload = $payloadMethod->invoke($manager, [
'channel_slug' => 'canary', 'channel_slug' => 'canary',
@@ -1103,7 +1067,6 @@ it('keeps release branch services out of the production Coolify environment', fu
it('does not invent GHCR images for Coolify service payloads', function (): void { it('does not invent GHCR images for Coolify service payloads', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
$payloadMethod->setAccessible(true);
$payloadMethod->invoke($manager, [ $payloadMethod->invoke($manager, [
'channel_slug' => 'internal', 'channel_slug' => 'internal',
@@ -1123,7 +1086,6 @@ it('does not invent GHCR images for Coolify service payloads', function (): void
it('creates Coolify service payloads from raw compose without a service type', function (): void { it('creates Coolify service payloads from raw compose without a service type', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload'); $payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
$payloadMethod->setAccessible(true);
$compose = "services:\n app:\n image: ghcr.io/copenhagentruckwash/api:test"; $compose = "services:\n app:\n image: ghcr.io/copenhagentruckwash/api:test";
$payload = $payloadMethod->invoke($manager, [ $payload = $payloadMethod->invoke($manager, [
@@ -1485,7 +1447,6 @@ it('uses master as the public route slug for the stable release channel', functi
it('ignores explicit runtime selection for channels outside the principal channel set', function (): void { it('ignores explicit runtime selection for channels outside the principal channel set', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$chooseRuntimeChannel = new ReflectionMethod(release_manager::class, 'chooseRuntimeChannel'); $chooseRuntimeChannel = new ReflectionMethod(release_manager::class, 'chooseRuntimeChannel');
$chooseRuntimeChannel->setAccessible(true);
$stable = [ $stable = [
'id' => 1, 'id' => 1,
@@ -1508,15 +1469,10 @@ it('ignores explicit runtime selection for channels outside the principal channe
it('requires non-default release channel runtime URLs and preserves load balancer paths', function (): void { it('requires non-default release channel runtime URLs and preserves load balancer paths', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$availability = new ReflectionMethod(release_manager::class, 'channelAvailability'); $availability = new ReflectionMethod(release_manager::class, 'channelAvailability');
$availability->setAccessible(true);
$runtimeUrls = new ReflectionMethod(release_manager::class, 'releaseRuntimeUrls'); $runtimeUrls = new ReflectionMethod(release_manager::class, 'releaseRuntimeUrls');
$runtimeUrls->setAccessible(true);
$publicUrl = new ReflectionMethod(release_manager::class, 'releaseCoolifyPublicUrl'); $publicUrl = new ReflectionMethod(release_manager::class, 'releaseCoolifyPublicUrl');
$publicUrl->setAccessible(true);
$targetPublicBaseUrl = new ReflectionMethod(release_manager::class, 'releaseTargetPublicBaseUrl'); $targetPublicBaseUrl = new ReflectionMethod(release_manager::class, 'releaseTargetPublicBaseUrl');
$targetPublicBaseUrl->setAccessible(true);
$applicationLabels = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationLabels'); $applicationLabels = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationLabels');
$applicationLabels->setAccessible(true);
expect($availability->invoke($manager, [ expect($availability->invoke($manager, [
'id' => 1, 'id' => 1,
@@ -1628,11 +1584,8 @@ it('requires non-default release channel runtime URLs and preserves load balance
it('resolves release deployment endpoints from manual overrides, URLs, health checks, and gateway defaults', function (): void { it('resolves release deployment endpoints from manual overrides, URLs, health checks, and gateway defaults', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$endpoint = new ReflectionMethod(release_manager::class, 'releaseDeploymentEndpoint'); $endpoint = new ReflectionMethod(release_manager::class, 'releaseDeploymentEndpoint');
$endpoint->setAccessible(true);
$publicUrl = new ReflectionMethod(release_manager::class, 'releaseCoolifyPublicUrl'); $publicUrl = new ReflectionMethod(release_manager::class, 'releaseCoolifyPublicUrl');
$publicUrl->setAccessible(true);
$targetPublicBaseUrl = new ReflectionMethod(release_manager::class, 'releaseTargetPublicBaseUrl'); $targetPublicBaseUrl = new ReflectionMethod(release_manager::class, 'releaseTargetPublicBaseUrl');
$targetPublicBaseUrl->setAccessible(true);
$manual = $endpoint->invoke($manager, [ $manual = $endpoint->invoke($manager, [
'app' => 'api', 'app' => 'api',
@@ -1746,7 +1699,6 @@ it('resolves release deployment endpoints from manual overrides, URLs, health ch
it('collects previous Coolify application UUIDs for stale route cleanup', function (): void { it('collects previous Coolify application UUIDs for stale route cleanup', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$previousApplications = new ReflectionMethod(release_manager::class, 'releaseCoolifyPreviousApplicationUuids'); $previousApplications = new ReflectionMethod(release_manager::class, 'releaseCoolifyPreviousApplicationUuids');
$previousApplications->setAccessible(true);
expect($previousApplications->invoke($manager, [ expect($previousApplications->invoke($manager, [
'coolify_previous_artifact_app_uuid' => 'old-artifact-app', 'coolify_previous_artifact_app_uuid' => 'old-artifact-app',
@@ -1767,7 +1719,6 @@ it('collects previous Coolify application UUIDs for stale route cleanup', functi
it('deletes only generated API commit env rows before Coolify application env updates', function (): void { it('deletes only generated API commit env rows before Coolify application env updates', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$deleteCommitEnvs = new ReflectionMethod(release_manager::class, 'deleteCoolifyGeneratedCommitEnvs'); $deleteCommitEnvs = new ReflectionMethod(release_manager::class, 'deleteCoolifyGeneratedCommitEnvs');
$deleteCommitEnvs->setAccessible(true);
$client = new ReleaseManagerCoolifyEnvFake([ $client = new ReleaseManagerCoolifyEnvFake([
['uuid' => 'api-commit', 'key' => 'API_COMMIT_SHA'], ['uuid' => 'api-commit', 'key' => 'API_COMMIT_SHA'],
['uuid' => 'commit', 'key' => 'COMMIT_SHA'], ['uuid' => 'commit', 'key' => 'COMMIT_SHA'],
@@ -1797,7 +1748,6 @@ it('waits for an in-flight automatic sync instead of passing the retry immediate
it('redacts GitHub access metadata from public release versions', function (): void { it('redacts GitHub access metadata from public release versions', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$publicVersion = new ReflectionMethod(release_manager::class, 'publicVersion'); $publicVersion = new ReflectionMethod(release_manager::class, 'publicVersion');
$publicVersion->setAccessible(true);
$version = $publicVersion->invoke($manager, [ $version = $publicVersion->invoke($manager, [
'id' => 12, 'id' => 12,
@@ -1841,7 +1791,6 @@ it('redacts GitHub access metadata from public release versions', function (): v
it('only chooses requested runtime channels from channels available to the principal', function (): void { it('only chooses requested runtime channels from channels available to the principal', function (): void {
$manager = new release_manager(); $manager = new release_manager();
$choose = new ReflectionMethod(release_manager::class, 'chooseRuntimeChannel'); $choose = new ReflectionMethod(release_manager::class, 'chooseRuntimeChannel');
$choose->setAccessible(true);
$stable = [ $stable = [
'id' => 1, 'id' => 1,
@@ -1905,13 +1854,9 @@ it('restricts release gate fetches to Truckwash release hosts and relative paths
$manager = new release_manager(); $manager = new release_manager();
$joinUrl = new ReflectionMethod(release_manager::class, 'releaseGateJoinUrl'); $joinUrl = new ReflectionMethod(release_manager::class, 'releaseGateJoinUrl');
$joinUrl->setAccessible(true);
$hostAllowed = new ReflectionMethod(release_manager::class, 'releaseGateFetchHostAllowed'); $hostAllowed = new ReflectionMethod(release_manager::class, 'releaseGateFetchHostAllowed');
$hostAllowed->setAccessible(true);
$publicIpAllowed = new ReflectionMethod(release_manager::class, 'releaseGatePublicIpAllowed'); $publicIpAllowed = new ReflectionMethod(release_manager::class, 'releaseGatePublicIpAllowed');
$publicIpAllowed->setAccessible(true);
$stringArray = new ReflectionMethod(release_manager::class, 'releaseGateStringArray'); $stringArray = new ReflectionMethod(release_manager::class, 'releaseGateStringArray');
$stringArray->setAccessible(true);
expect($joinUrl->invoke($manager, 'https://api-v2.truckwash.io', '/master/api/ping')) expect($joinUrl->invoke($manager, 'https://api-v2.truckwash.io', '/master/api/ping'))
->toBe('https://api-v2.truckwash.io/master/api/ping') ->toBe('https://api-v2.truckwash.io/master/api/ping')
@@ -436,7 +436,6 @@ it('allows the MinIO client binary to be configured explicitly', function (): vo
try { try {
$method = new ReflectionMethod(replication_manager::class, 'minioClientBinary'); $method = new ReflectionMethod(replication_manager::class, 'minioClientBinary');
$method->setAccessible(true);
expect($method->invoke(null))->toBe('/opt/minio/mc'); expect($method->invoke(null))->toBe('/opt/minio/mc');
} finally { } finally {
@@ -460,15 +459,10 @@ it('supports MinIO client runtime fallback configuration', function (): void {
try { try {
$downloadUrl = new ReflectionMethod(replication_manager::class, 'minioClientDownloadUrl'); $downloadUrl = new ReflectionMethod(replication_manager::class, 'minioClientDownloadUrl');
$downloadUrl->setAccessible(true);
$autoInstall = new ReflectionMethod(replication_manager::class, 'minioClientAutoInstallEnabled'); $autoInstall = new ReflectionMethod(replication_manager::class, 'minioClientAutoInstallEnabled');
$autoInstall->setAccessible(true);
$commandTimeout = new ReflectionMethod(replication_manager::class, 'minioClientCommandTimeoutSeconds'); $commandTimeout = new ReflectionMethod(replication_manager::class, 'minioClientCommandTimeoutSeconds');
$commandTimeout->setAccessible(true);
$downloadTimeout = new ReflectionMethod(replication_manager::class, 'minioClientDownloadTimeoutSeconds'); $downloadTimeout = new ReflectionMethod(replication_manager::class, 'minioClientDownloadTimeoutSeconds');
$downloadTimeout->setAccessible(true);
$commandLabel = new ReflectionMethod(replication_manager::class, 'minioClientCommandLabel'); $commandLabel = new ReflectionMethod(replication_manager::class, 'minioClientCommandLabel');
$commandLabel->setAccessible(true);
expect($downloadUrl->invoke(null))->toBe('https://example.test/mc'); expect($downloadUrl->invoke(null))->toBe('https://example.test/mc');
expect($autoInstall->invoke(null))->toBeFalse(); expect($autoInstall->invoke(null))->toBeFalse();
@@ -632,11 +626,8 @@ it('creates the generated replication user on the primary during provisioning',
it('extracts host-specific MariaDB replication account denials', function (): void { it('extracts host-specific MariaDB replication account denials', function (): void {
$extract = new ReflectionMethod(replication_manager::class, 'databaseDeniedAccountHostsFromText'); $extract = new ReflectionMethod(replication_manager::class, 'databaseDeniedAccountHostsFromText');
$extract->setAccessible(true);
$normalize = new ReflectionMethod(replication_manager::class, 'normalizeDatabaseAccountHost'); $normalize = new ReflectionMethod(replication_manager::class, 'normalizeDatabaseAccountHost');
$normalize->setAccessible(true);
$candidates = new ReflectionMethod(replication_manager::class, 'databaseAccountHostGrantCandidates'); $candidates = new ReflectionMethod(replication_manager::class, 'databaseAccountHostGrantCandidates');
$candidates->setAccessible(true);
expect($extract->invoke(null, "Access denied for user 'replication'@'10.0.1.13' (using password: YES)")) expect($extract->invoke(null, "Access denied for user 'replication'@'10.0.1.13' (using password: YES)"))
->toBe(['10.0.1.13']); ->toBe(['10.0.1.13']);
@@ -650,7 +641,6 @@ it('extracts host-specific MariaDB replication account denials', function (): vo
it('identifies stopped database replication threads as a restartable status', function (): void { it('identifies stopped database replication threads as a restartable status', function (): void {
$onlyThreadBlockers = new ReflectionMethod(replication_manager::class, 'databaseOnlyReplicationThreadBlockers'); $onlyThreadBlockers = new ReflectionMethod(replication_manager::class, 'databaseOnlyReplicationThreadBlockers');
$onlyThreadBlockers->setAccessible(true);
expect($onlyThreadBlockers->invoke(null, [ expect($onlyThreadBlockers->invoke(null, [
'Database replication IO and SQL threads must both be running.', 'Database replication IO and SQL threads must both be running.',
@@ -427,7 +427,7 @@ it('uses a short exact-image result cache before calling Plate Recognizer', func
expect($source)->toContain('RESULT_CACHE_TTL_SECONDS = 10'); expect($source)->toContain('RESULT_CACHE_TTL_SECONDS = 10');
expect($source)->toContain('RESULT_CACHE_REDIS_KEY_PREFIX'); expect($source)->toContain('RESULT_CACHE_REDIS_KEY_PREFIX');
expect($source)->toContain('$cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key);'); expect($source)->toContain('$cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key);');
expect($source)->toContain('return $cached_result;'); expect($source)->toContain('return $this->completeRecognition($started_at, $cached_result);');
expect($source)->toContain('$this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);'); expect($source)->toContain('$this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);');
expect($source)->toContain('$this->last_timings[\'cache\']'); expect($source)->toContain('$this->last_timings[\'cache\']');
expect($source)->toContain('$this->last_timings[\'cache_hit\'] = 1;'); expect($source)->toContain('$this->last_timings[\'cache_hit\'] = 1;');
@@ -13,7 +13,6 @@ function system_search_entity_coverage_invoke_private(object $instance, string $
{ {
$reflection = new ReflectionClass($instance); $reflection = new ReflectionClass($instance);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return (array)$target->invoke($instance); return (array)$target->invoke($instance);
} }
@@ -8,7 +8,6 @@ function system_search_route_invoke_private(systemSearchRoute $route, string $me
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -141,7 +141,6 @@ if (!function_exists('system_search_service_invoke_private')) {
function system_search_service_invoke_private(object $instance, string $method, array $args = []): mixed function system_search_service_invoke_private(object $instance, string $method, array $args = []): mixed
{ {
$reflection = new ReflectionMethod($instance, $method); $reflection = new ReflectionMethod($instance, $method);
$reflection->setAccessible(true);
return $reflection->invokeArgs($instance, $args); return $reflection->invokeArgs($instance, $args);
} }
} }
@@ -87,7 +87,7 @@ it('resolves relay bindings without requiring a primary department gateway first
expect($managerSource)->not->toBeFalse(); expect($managerSource)->not->toBeFalse();
preg_match( preg_match(
'/public function resolveRelayBinding\(int \$departmentId, string \$logicalRelayId\): array\s*\{(?P<body>.*?)\n \}\n\n \/\*\*/s', '/public function resolveRelayBinding\(int \$departmentId, string \$logicalRelayId\): array\s*\{(?P<body>.*?)\n\x20{4}\x7d\n\n\x20{4}\/\*\*/s',
(string)$managerSource, (string)$managerSource,
$matches $matches
); );
@@ -109,7 +109,7 @@ it('reactivates soft-deleted relay bindings before inserting replacements', func
expect($managerSource)->not->toBeFalse(); expect($managerSource)->not->toBeFalse();
preg_match( preg_match(
'/public function setRelayBindings\(int \$gatewayId, array \$bindings, \?int \$userId = null\): array\s*\{(?P<body>.*?)\n \}\n\n \/\*\*/s', '/public function setRelayBindings\(int \$gatewayId, array \$bindings, \?int \$userId = null\): array\s*\{(?P<body>.*?)\n\x20{4}\x7d\n\n\x20{4}\/\*\*/s',
(string)$managerSource, (string)$managerSource,
$matches $matches
); );
@@ -57,7 +57,6 @@ function with_edge_gateway_server_state(array $server, callable $callback): void
function invoke_edge_gateway_private(object $instance, string $method, mixed ...$arguments): mixed function invoke_edge_gateway_private(object $instance, string $method, mixed ...$arguments): mixed
{ {
$reflection = new ReflectionMethod($instance, $method); $reflection = new ReflectionMethod($instance, $method);
$reflection->setAccessible(true);
return $reflection->invokeArgs($instance, $arguments); return $reflection->invokeArgs($instance, $arguments);
} }
@@ -424,7 +424,6 @@ it('keeps runtime on published v2 configs and leaves draft JSON as the studio ed
it('upserts path editor answers into generated condition and task config rows', function (): void { it('upserts path editor answers into generated condition and task config rows', function (): void {
$service = selfserve_studio_graph_without_constructor(); $service = selfserve_studio_graph_without_constructor();
$method = new ReflectionMethod(selfserve_studio_graph::class, 'applyConfigOperation'); $method = new ReflectionMethod(selfserve_studio_graph::class, 'applyConfigOperation');
$method->setAccessible(true);
$config = [ $config = [
'schema_version' => 2, 'schema_version' => 2,
'questions' => [ 'questions' => [
@@ -35,7 +35,6 @@ it('infers legacy-defaulted always task gates from condition_id at runtime', fun
$reflection = new ReflectionClass(selfserve_wash_flow::class); $reflection = new ReflectionClass(selfserve_wash_flow::class);
$flow = $reflection->newInstanceWithoutConstructor(); $flow = $reflection->newInstanceWithoutConstructor();
$method = $reflection->getMethod('resolveTaskGate'); $method = $reflection->getMethod('resolveTaskGate');
$method->setAccessible(true);
$conditionGate = $method->invoke($flow, [ $conditionGate = $method->invoke($flow, [
'condition_id' => 22, 'condition_id' => 22,
@@ -99,7 +99,6 @@ it('marks non-injected local and gateway overrides as local-only gateway transpo
foreach (['local', 'gateway'] as $override) { foreach (['local', 'gateway'] as $override) {
$transport = $resolver->resolveForDepartment(17, $override); $transport = $resolver->resolveForDepartment(17, $override);
$localOnly = new ReflectionProperty($transport, 'localOnly'); $localOnly = new ReflectionProperty($transport, 'localOnly');
$localOnly->setAccessible(true);
expect($transport)->toBeInstanceOf(gateway_shelly_transport::class); expect($transport)->toBeInstanceOf(gateway_shelly_transport::class);
expect($localOnly->getValue($transport))->toBeTrue(); expect($localOnly->getValue($transport))->toBeTrue();
@@ -8,7 +8,6 @@ function weather_cache_invoke_private(moduleWeatherAPIRoute $route, string $meth
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -189,7 +189,6 @@ function department_weather_runtime_invoke_private(moduleWeatherAPIRoute $route,
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -8,7 +8,6 @@ function department_weather_fallback_invoke_private(moduleWeatherAPIRoute $route
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }
@@ -8,7 +8,6 @@ function weather_status_targets_invoke_private(moduleWeatherAPIRoute $route, str
{ {
$reflection = new ReflectionClass($route); $reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method); $target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args); return $target->invokeArgs($route, $args);
} }

Some files were not shown because too many files have changed in this diff Show More