Files
api/services/nginx/app/tests/Unit/Workfeed/DepartmentWeatherWorkfeedHoursTest.php
T
Jeppe Bundgaard 7d450e285e Remove outdated edge gateway object classes, add new agent implementation
Transitioned from obsolete gateway object classes (`edge_gateway_shell_action_jobs_o`, `edge_gateway_shell_events_o`, `edge_gateway_shell_sessions_o`, `edge_gateway_update_jobs_o`) to the new agent implementation (`edge-gateway-agent/agent.php`).
2026-04-21 14:13:17 +02:00

259 lines
8.7 KiB
PHP

<?php
app_require('routes/moduleWeatherAPIRoute.php');
use routes\moduleWeatherAPIRoute;
function weather_route_invoke_private(moduleWeatherAPIRoute $route, string $method, array $args = []): mixed
{
$reflection = new ReflectionClass($route);
$target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs($route, $args);
}
beforeEach(function (): void {
$_SERVER['REQUEST_URI'] = '/departments/weather';
});
it('calculates workfeed employee hours for the hour slot based on overlap', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-24T13:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-24T13:00:00+00:00',
'end' => '2026-03-24T14:00:00+00:00',
],
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-24T13:30:00+00:00',
'end' => '2026-03-24T15:00:00+00:00',
],
(object)[
'departmentID' => 'dep_other',
'start' => '2026-03-24T13:00:00+00:00',
'end' => '2026-03-24T14:00:00+00:00',
],
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-24T14:00:00+00:00',
'end' => '2026-03-24T13:00:00+00:00',
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);
expect($hours)->toBe(1.5);
});
it('extracts department id from supported workfeed shift shapes', function (): void {
$route = new moduleWeatherAPIRoute();
$idFromNested = weather_route_invoke_private($route, 'extractWorkfeedDepartmentId', [
(object)[
'department' => (object)['id' => 'dep_nested'],
'start' => '2026-03-24T12:45:00+00:00',
'end' => '2026-03-24T13:15:00+00:00',
],
]);
$idFromCamelCase = weather_route_invoke_private($route, 'extractWorkfeedDepartmentId', [[
'departmentId' => 'dep_camel',
'start' => '2026-03-24T12:45:00+00:00',
'end' => '2026-03-24T13:15:00+00:00',
]]);
expect($idFromNested)->toBe('dep_nested');
expect($idFromCamelCase)->toBe('dep_camel');
});
it('normalizes wrapped workfeed collections from common response keys', function (): void {
$route = new moduleWeatherAPIRoute();
$wrapped = (object)[
'data' => [
(object)['id' => 'a'],
(object)['id' => 'b'],
],
];
$items = weather_route_invoke_private($route, 'normalizeWorkfeedCollection', [$wrapped]);
expect($items)->toHaveCount(2);
expect($items[0]->id ?? null)->toBe('a');
expect($items[1]->id ?? null)->toBe('b');
});
it('calculates workfeed employee hours across multiple departments for one hour slot', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-24T13:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-24T13:00:00+00:00',
'end' => '2026-03-24T14:00:00+00:00',
],
(object)[
'departmentID' => 'dep_2',
'start' => '2026-03-24T13:00:00+00:00',
'end' => '2026-03-24T14:00:00+00:00',
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, ['dep_1', 'dep_2'], $slot]);
expect($hours)->toBe(2.0);
});
it('counts overtime minutes when a saved shift end extends past the approved original end', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-23T18:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-23T10:00:00+00:00',
'end' => '2026-03-23T18:17:00+00:00',
'approval' => (object)[
'originalEnd' => '2026-03-23T18:00:00+00:00',
],
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);
expect($hours)->toBe(0.28);
});
it('does not count removed approved time when a saved shift end is shortened', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-23T17:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-23T10:00:00+00:00',
'end' => '2026-03-23T17:00:00+00:00',
'approval' => (object)[
'originalEnd' => '2026-03-23T18:00:00+00:00',
],
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);
expect($hours)->toBe(0.0);
});
it('counts unapproved overtime from a bounded shift updateTime fallback', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-23T17:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-23T09:00:00+00:00',
'end' => '2026-03-23T17:00:00+00:00',
'approval' => null,
'updateTime' => '2026-03-23T17:15:00+00:00',
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);
expect($hours)->toBe(0.25);
});
it('does not treat late unapproved administrative edits as overtime', function (): void {
$route = new moduleWeatherAPIRoute();
$slot = new DateTime('2026-03-23T17:00:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-23T09:00:00+00:00',
'end' => '2026-03-23T17:00:00+00:00',
'approval' => null,
'updateTime' => '2026-03-24T02:30:00+00:00',
],
];
$hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);
expect($hours)->toBe(0.0);
});
it('caps current-slot hours to elapsed minutes and zeroes future slots', function (): void {
$route = new moduleWeatherAPIRoute();
$currentSlot = new DateTime('2026-03-24T13:00:00+00:00');
$futureSlot = new DateTime('2026-03-24T14:00:00+00:00');
$occurredUntil = new DateTime('2026-03-24T13:15:00+00:00');
$shifts = [
(object)[
'departmentID' => 'dep_1',
'start' => '2026-03-24T13:00:00+00:00',
'end' => '2026-03-24T15:00:00+00:00',
],
];
$currentHours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $currentSlot, $occurredUntil]);
$futureHours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $futureSlot, $occurredUntil]);
expect($currentHours)->toBe(0.25);
expect($futureHours)->toBe(0.0);
});
it('normalizes department id input from scalar csv and nested array values', function (): void {
$route = new moduleWeatherAPIRoute();
$single = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', ['7']);
$csv = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', ['1, 2,3']);
$nested = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', [[1, '2,3', [4, '5']]]);
expect($single)->toBe(['7']);
expect($csv)->toBe(['1', '2', '3']);
expect($nested)->toBe([1, '2', '3', 4, '5']);
});
it('normalizes batched wash count rows into hourly slot totals', function (): void {
$route = new moduleWeatherAPIRoute();
$rows = [
[
'department_id' => 1,
'hour_bucket' => '2026-03-24 08:00:00',
'wash_count' => 3,
],
[
'department_id' => 2,
'hour_bucket' => '2026-03-24 08:15:00',
'wash_count' => 2,
],
[
'department_id' => 2,
'hour_bucket' => '2026-03-24 09:00:00',
'wash_count' => 4,
],
];
$counts = weather_route_invoke_private($route, 'normalizeDepartmentWashCountRows', [$rows]);
expect($counts)->toBe([
'2026-03-24 08:00' => 5,
'2026-03-24 09:00' => 4,
]);
});
it('wires department weather route to use batched wash aggregation', function (): void {
$routeContent = (string)file_get_contents(app_path('routes/moduleWeatherAPIRoute.php'));
$ordersContent = (string)file_get_contents(app_path('objects/orders_o.php'));
expect($routeContent)->toContain('loadDepartmentWashCountsBySlot');
expect($routeContent)->toContain('countWashesByHourForDepartments');
expect($ordersContent)->toContain('public function countWashesByHourForDepartments');
expect($ordersContent)->toContain('GROUP BY o.department_id');
});