Add unit tests for advanced target duration parsing, goal range calculations, and Workfeed query normalization. Extend OpenAPI spec with updated schemas and examples.

This commit is contained in:
Jeppe Bundgaard
2026-03-24 11:35:13 +01:00
parent 8c9388546b
commit 28b571e2e6
13 changed files with 877 additions and 332 deletions
+97 -30
View File
@@ -30,15 +30,9 @@ class workfeed implements workfeed_i
/**
* @throws Exception
*/
public function listEmployees(array $filters = []): object
public function listEmployees(array $filters = []): array|object
{
return $this->sendApiRequest('GET', '/employees', $this->filterAllowed($filters, [
'search',
'departmentId',
'includeInactive',
'limit',
'cursor',
]));
return $this->sendApiRequest('GET', '/employees');
}
/**
@@ -54,17 +48,11 @@ class workfeed implements workfeed_i
/**
* @throws Exception
*/
public function listShifts(array $filters = []): object
public function listShifts(array $filters = []): array|object
{
return $this->sendApiRequest('GET', '/shifts', $this->filterAllowed($filters, [
'from',
'to',
'departmentId',
'employeeId',
'status',
'limit',
'cursor',
]));
$normalizedFilters = $this->normalizeShiftFilters($filters);
return $this->sendApiRequest('GET', '/shifts', $normalizedFilters);
}
/**
@@ -80,28 +68,29 @@ class workfeed implements workfeed_i
/**
* @throws Exception
*/
public function listDepartments(array $filters = []): object
public function listDepartments(array $filters = []): array|object
{
return $this->sendApiRequest('GET', '/departments', $this->filterAllowed($filters, [
'search',
'limit',
'cursor',
]));
return $this->sendApiRequest('GET', '/departments');
}
/**
* @throws Exception
*/
private function sendApiRequest(string $method, string $path, array $query = []): object
private function sendApiRequest(string $method, string $path, array $query = []): array|object
{
$this->requireModuleEnabled();
$this->requireConfiguredApiUrl();
$this->requireConfiguredApiKey();
$companyId = $this->requireConfiguredCompanyId();
$url = $this->buildUrl($this->config->api_url->getVariableValue(), $path, $query);
$url = $this->buildUrl(
$this->config->api_url->getVariableValue(),
'/companies/' . rawurlencode($companyId) . '/' . ltrim($path, '/'),
$query
);
$headers = [
'Accept: application/json',
'Authorization: Bearer ' . trim((string)$this->config->api_key->getVariableValue()),
'Authorization: ' . trim((string)$this->config->api_key->getVariableValue()),
];
return $this->executeJsonRequest($method, $url, $headers);
@@ -110,7 +99,7 @@ class workfeed implements workfeed_i
/**
* @throws Exception
*/
private function executeJsonRequest(string $method, string $url, array $headers): object
private function executeJsonRequest(string $method, string $url, array $headers): array|object
{
$response = $this->executeRequest($method, $url, $headers);
$decoded = json_decode($response['body']);
@@ -123,12 +112,12 @@ class workfeed implements workfeed_i
throw new Exception($this->extractErrorMessage($decoded, $response['status'], 'Workfeed API request failed'));
}
if (is_object($decoded)) {
if (is_object($decoded) || is_array($decoded)) {
return $decoded;
}
return (object)[
'data' => $decoded,
'value' => $decoded,
];
}
@@ -208,6 +197,19 @@ class workfeed implements workfeed_i
}
}
/**
* @throws Exception
*/
private function requireConfiguredCompanyId(): string
{
$companyId = trim((string)$this->config->company_id->getVariableValue());
if ($companyId === '') {
throw new Exception('Invalid Workfeed CompanyID configured.');
}
return $companyId;
}
private function normalizeUrlForValidation(string $url): string
{
if (preg_match('#^https?://#i', $url)) {
@@ -241,6 +243,71 @@ class workfeed implements workfeed_i
return $filtered;
}
/**
* @throws Exception
*/
private function normalizeShiftFilters(array $filters): array
{
$filtered = $this->filterAllowed($filters, [
'startFrom',
'startTo',
'from',
'to',
'employeeID',
'employeeId',
'released',
]);
if (!isset($filtered['startFrom']) && isset($filtered['from'])) {
$filtered['startFrom'] = $filtered['from'];
}
if (!isset($filtered['startTo']) && isset($filtered['to'])) {
$filtered['startTo'] = $filtered['to'];
}
if (!isset($filtered['employeeID']) && isset($filtered['employeeId'])) {
$filtered['employeeID'] = $filtered['employeeId'];
}
unset($filtered['from'], $filtered['to'], $filtered['employeeId']);
if (!isset($filtered['startFrom']) || trim((string)$filtered['startFrom']) === '') {
throw new Exception('Workfeed shift query requires startFrom.');
}
if (!isset($filtered['startTo']) || trim((string)$filtered['startTo']) === '') {
throw new Exception('Workfeed shift query requires startTo.');
}
if (isset($filtered['released'])) {
$filtered['released'] = $this->normalizeBooleanQueryValue($filtered['released']);
}
return $filtered;
}
private function normalizeBooleanQueryValue(mixed $value): string
{
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) {
return 'true';
}
if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) {
return 'false';
}
}
if (is_int($value)) {
return $value === 1 ? 'true' : 'false';
}
return (string)$value;
}
private function extractErrorMessage(mixed $decoded, int $statusCode, string $fallback): string
{
if (is_object($decoded)) {