Add Edge Agent implementation for gateway connection lifecycle, agent commands, Shelly device discovery, relay control, and WebSocket communication with broker. Include unit tests for critical flows.
This commit is contained in:
+321
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
app_require('classes/department_outside_hours_statistics_service.php');
|
||||
app_require('routes/departmentDailyReportsRoute.php');
|
||||
|
||||
use classes\department_outside_hours_statistics_service;
|
||||
use routes\departmentDailyReportsRoute;
|
||||
|
||||
function department_daily_reports_route_invoke_private(object $route, string $method, array $args = []): mixed
|
||||
{
|
||||
$reflection = new ReflectionClass(departmentDailyReportsRoute::class);
|
||||
$target = $reflection->getMethod($method);
|
||||
$target->setAccessible(true);
|
||||
|
||||
return $target->invokeArgs($route, $args);
|
||||
}
|
||||
|
||||
final class FakeDailyReportValue
|
||||
{
|
||||
public function __construct(private readonly mixed $current)
|
||||
{
|
||||
}
|
||||
|
||||
public function value(): mixed
|
||||
{
|
||||
return $this->current;
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeDailyReportVariables
|
||||
{
|
||||
public function __construct(private readonly array $values = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function getVariable(string $key): mixed
|
||||
{
|
||||
return $this->values[$key] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeDailyReportRepository
|
||||
{
|
||||
public array $transaction_summary = [
|
||||
'quantity' => 0,
|
||||
'products' => 0,
|
||||
'earnings' => 0,
|
||||
'washes' => 0,
|
||||
'water_usage' => 0,
|
||||
];
|
||||
|
||||
public array $booking_summary = [
|
||||
'completed' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
|
||||
public array $product_overview = [];
|
||||
|
||||
public array $wash_transactions = [];
|
||||
|
||||
public function getTransactionSummaryForDepartments(string $date, array $department_ids, string $date_to = null): array
|
||||
{
|
||||
return $this->transaction_summary;
|
||||
}
|
||||
|
||||
public function getBookingSummaryForDepartments(string $date, array $department_ids, string $date_to = null): array
|
||||
{
|
||||
return $this->booking_summary;
|
||||
}
|
||||
|
||||
public function getProductOverviewForDepartments(string $date, array $department_ids, array $product_ids, string $date_to = null): array
|
||||
{
|
||||
return $this->product_overview;
|
||||
}
|
||||
|
||||
public function getWashTransactionsForDepartments(string $date, array $department_ids, string $date_to = null): array
|
||||
{
|
||||
return $this->wash_transactions;
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeDailyReportComplaintsRepository
|
||||
{
|
||||
public int $count = 0;
|
||||
|
||||
public function countForDepartmentsInRange(array $department_ids, string $date, ?string $date_to = null): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeOutsideHoursStatisticsService extends department_outside_hours_statistics_service
|
||||
{
|
||||
public array $summary = [
|
||||
'total' => 0,
|
||||
'by_source' => [
|
||||
'orders' => 0,
|
||||
'xlvask' => 0,
|
||||
'selfserve' => 0,
|
||||
],
|
||||
'has_missing_opening_hours' => false,
|
||||
'missing_department_ids' => [],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getSummary(string $date, array|int|string $department_ids, ?string $date_to = null): array
|
||||
{
|
||||
return [
|
||||
'department_ids' => is_array($department_ids) ? array_values(array_map('intval', $department_ids)) : [(int)$department_ids],
|
||||
'date' => $date,
|
||||
'date_to' => $date_to ?? $date,
|
||||
...$this->summary,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class DepartmentDailyReportsOverviewRouteDouble extends departmentDailyReportsRoute
|
||||
{
|
||||
public object $repository;
|
||||
public object $complaints_repository;
|
||||
public array $opening_hours = [];
|
||||
public array $departments = [];
|
||||
public array $workfeed_departments = [];
|
||||
public array $workfeed_shifts = [];
|
||||
public department_outside_hours_statistics_service $outside_hours_service;
|
||||
|
||||
protected function dailyReportRepository(): object
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function dailyReportComplaintsRepository(): object
|
||||
{
|
||||
return $this->complaints_repository;
|
||||
}
|
||||
|
||||
protected function fetchOpeningHoursByDepartmentId(array $department_ids): array
|
||||
{
|
||||
return $this->opening_hours;
|
||||
}
|
||||
|
||||
protected function fetchDepartmentsByIds(array $department_ids): array
|
||||
{
|
||||
return $this->departments;
|
||||
}
|
||||
|
||||
protected function fetchWorkfeedDepartments(): array
|
||||
{
|
||||
return $this->workfeed_departments;
|
||||
}
|
||||
|
||||
protected function fetchWorkfeedShifts(\DateTime $query_start, \DateTime $range_end_exclusive): array
|
||||
{
|
||||
return $this->workfeed_shifts;
|
||||
}
|
||||
|
||||
protected function outsideHoursStatisticsService(): department_outside_hours_statistics_service
|
||||
{
|
||||
return $this->outside_hours_service;
|
||||
}
|
||||
}
|
||||
|
||||
function fake_daily_report_department(int $id, string $name, array $variables = []): object
|
||||
{
|
||||
return (object)[
|
||||
'id' => $id,
|
||||
'name' => new FakeDailyReportValue($name),
|
||||
'variables' => new FakeDailyReportVariables($variables),
|
||||
];
|
||||
}
|
||||
|
||||
beforeEach(function (): void {
|
||||
$_SERVER['REQUEST_URI'] = '/departments/daily-reports/overview';
|
||||
});
|
||||
|
||||
it('builds the overview payload from batched repository data with deterministic tile states', function (): void {
|
||||
$repository = new FakeDailyReportRepository();
|
||||
$complaints_repository = new FakeDailyReportComplaintsRepository();
|
||||
$repository->transaction_summary = [
|
||||
'quantity' => 12,
|
||||
'products' => 37,
|
||||
'earnings' => 4900,
|
||||
'washes' => 14,
|
||||
'water_usage' => 56,
|
||||
];
|
||||
$repository->booking_summary = [
|
||||
'completed' => 9,
|
||||
'total' => 11,
|
||||
];
|
||||
$repository->product_overview = [
|
||||
24 => ['product_id' => 24, 'quantity' => 3, 'out_of' => 14],
|
||||
25 => ['product_id' => 25, 'quantity' => 2, 'out_of' => 14],
|
||||
];
|
||||
$complaints_repository->count = 4;
|
||||
$route = new DepartmentDailyReportsOverviewRouteDouble();
|
||||
$route->repository = $repository;
|
||||
$route->complaints_repository = $complaints_repository;
|
||||
$route->outside_hours_service = new FakeOutsideHoursStatisticsService();
|
||||
$route->outside_hours_service->summary = [
|
||||
'total' => 1,
|
||||
'by_source' => [
|
||||
'orders' => 0,
|
||||
'xlvask' => 1,
|
||||
'selfserve' => 0,
|
||||
],
|
||||
'has_missing_opening_hours' => false,
|
||||
'missing_department_ids' => [],
|
||||
];
|
||||
$route->departments = [
|
||||
fake_daily_report_department(1, 'North', ['workfeed_department_id' => 'dep_1']),
|
||||
fake_daily_report_department(2, 'South', ['workfeed_department_id' => 'dep_2']),
|
||||
];
|
||||
$route->workfeed_shifts = [
|
||||
(object)[
|
||||
'departmentID' => 'dep_1',
|
||||
'start' => '2026-03-23T09:00:00+00:00',
|
||||
'end' => '2026-03-23T17:00:00+00:00',
|
||||
'approval' => (object)['originalEnd' => '2026-03-23T17:30:00+00:00'],
|
||||
],
|
||||
(object)[
|
||||
'departmentID' => 'dep_2',
|
||||
'start' => '2026-03-23T10:00:00+00:00',
|
||||
'end' => '2026-03-23T18:00:00+00:00',
|
||||
'approval' => null,
|
||||
'updateTime' => '2026-03-23T18:15:00+00:00',
|
||||
],
|
||||
];
|
||||
|
||||
$overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[1, 2], '2026-03-23', '2026-03-23']);
|
||||
|
||||
expect($overview['department_ids'])->toBe([1, 2]);
|
||||
expect($overview['metrics']['transactions']['value'])->toBe(12);
|
||||
expect($overview['metrics']['bookings']['value'])->toBe(9);
|
||||
expect($overview['metrics']['bookings']['out_of'])->toBe(11);
|
||||
expect($overview['metrics']['night_washes']['state'])->toBe('ready');
|
||||
expect($overview['metrics']['night_washes']['value'])->toBe(1);
|
||||
expect($overview['metrics']['night_washes']['by_source'])->toBe([
|
||||
'orders' => 0,
|
||||
'xlvask' => 1,
|
||||
'selfserve' => 0,
|
||||
]);
|
||||
expect($overview['metrics']['overtime']['state'])->toBe('ready');
|
||||
expect($overview['metrics']['overtime']['value'])->toBe(0.75);
|
||||
expect($overview['metrics']['complaints']['state'])->toBe('ready');
|
||||
expect($overview['metrics']['complaints']['value'])->toBe(4);
|
||||
expect(count($overview['products']))->toBe(6);
|
||||
expect($overview['products'][0]['slug'])->toBe('spot-free-lastbil');
|
||||
expect($overview['products'][0]['title'])->toBe('Spot Free (Lastbil)');
|
||||
expect($overview['products'][0]['value'])->toBe(3);
|
||||
expect($overview['products'][1]['title'])->toBe('Fælg flex pr. enhed');
|
||||
expect($overview['products'][1]['value'])->toBe(2);
|
||||
expect(array_column($overview['products'], 'title'))->toBe([
|
||||
'Spot Free (Lastbil)',
|
||||
'Fælg flex pr. enhed',
|
||||
'Ekstraordinær pr. 10 min inkl. kemi',
|
||||
'Højglans - Voksforsegling pr. enhed',
|
||||
'Undervognsskyl pr. enhed',
|
||||
'Tillæg for Specialsæbe - DD',
|
||||
]);
|
||||
});
|
||||
|
||||
it('marks overtime unavailable when not every selected department can be mapped to workfeed', function (): void {
|
||||
$repository = new FakeDailyReportRepository();
|
||||
|
||||
$route = new DepartmentDailyReportsOverviewRouteDouble();
|
||||
$route->repository = $repository;
|
||||
$route->complaints_repository = new FakeDailyReportComplaintsRepository();
|
||||
$route->outside_hours_service = new FakeOutsideHoursStatisticsService();
|
||||
$route->departments = [
|
||||
fake_daily_report_department(1, 'North', ['workfeed_department_id' => 'dep_1']),
|
||||
fake_daily_report_department(2, 'South'),
|
||||
];
|
||||
|
||||
$overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[1, 2], '2026-03-23', '2026-03-23']);
|
||||
|
||||
expect($overview['metrics']['overtime']['state'])->toBe('unavailable');
|
||||
expect($overview['metrics']['overtime']['message'])->toContain('Workfeed');
|
||||
});
|
||||
|
||||
it('normalizes department id input from csv strings and nested values', function (): void {
|
||||
$route = new DepartmentDailyReportsOverviewRouteDouble();
|
||||
$route->repository = new FakeDailyReportRepository();
|
||||
$route->complaints_repository = new FakeDailyReportComplaintsRepository();
|
||||
$route->outside_hours_service = new FakeOutsideHoursStatisticsService();
|
||||
|
||||
$normalized = department_daily_reports_route_invoke_private($route, 'normalizeDepartmentIdsParameter', [['1, 2', [3, '4'], 2]]);
|
||||
|
||||
expect($normalized)->toBe([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('limits overtime counting to the selected reporting range', function (): void {
|
||||
$route = new DepartmentDailyReportsOverviewRouteDouble();
|
||||
$route->repository = new FakeDailyReportRepository();
|
||||
$route->complaints_repository = new FakeDailyReportComplaintsRepository();
|
||||
$route->outside_hours_service = new FakeOutsideHoursStatisticsService();
|
||||
|
||||
$hours = department_daily_reports_route_invoke_private($route, 'calculateShiftOvertimeHoursInRange', [[
|
||||
'start' => '2026-03-22T20:00:00+00:00',
|
||||
'end' => '2026-03-22T23:45:00+00:00',
|
||||
'approval' => (object)['originalEnd' => '2026-03-23T00:30:00+00:00'],
|
||||
], new \DateTime('2026-03-23T00:00:00+00:00'), new \DateTime('2026-03-24T00:00:00+00:00')]);
|
||||
|
||||
expect($hours)->toBe(0.5);
|
||||
});
|
||||
|
||||
it('wires the overview route to batched repository methods and overview path', function (): void {
|
||||
$routeContent = (string)file_get_contents(app_path('routes/departmentDailyReportsRoute.php'));
|
||||
$objectContent = (string)file_get_contents(app_path('objects/department_daily_reports_o.php'));
|
||||
|
||||
expect($routeContent)->toContain('/departments/daily-reports/overview');
|
||||
expect($routeContent)->toContain('/departments/daily-reports/complaints');
|
||||
expect($routeContent)->toContain('outsideHoursStatisticsService');
|
||||
expect($routeContent)->toContain('dailyReportComplaintsRepository');
|
||||
expect($routeContent)->toContain('/departments/daily-reports/outside-hours-trend');
|
||||
expect($routeContent)->toContain('getTransactionSummaryForDepartments');
|
||||
expect($routeContent)->toContain('normalizeDepartmentIdsParameter');
|
||||
expect($objectContent)->toContain('public function getBookingSummaryForDepartments');
|
||||
});
|
||||
Reference in New Issue
Block a user