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:
+1
-1
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-4
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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/);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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 +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]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
-1
@@ -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
Reference in New Issue
Block a user