Refactor employee name handling to utilize workfeed_employee_name_formatter for improved name resolution and fallback logic
This commit is contained in:
@@ -892,6 +892,7 @@ class economic_v2_distribution_service
|
||||
|
||||
$weight_total = array_sum($eligible_weights);
|
||||
if (abs($weight_total) <= self::EPSILON) {
|
||||
$fallback_department_id = $this->getFallbackDistributionDepartmentId();
|
||||
$warnings[] = 'Booked department 75 '
|
||||
. $source_category
|
||||
. ' amount for customer '
|
||||
@@ -900,11 +901,15 @@ class economic_v2_distribution_service
|
||||
. $month_key
|
||||
. ' on invoice(s) '
|
||||
. $invoice_ids
|
||||
. ' has no redistribution basis and remains undistributed.';
|
||||
. ' has no redistribution basis and was assigned to fallback department '
|
||||
. $fallback_department_id
|
||||
. '.';
|
||||
|
||||
return [
|
||||
'department_distribution' => [],
|
||||
'undistributed_net_amount' => $booked_amount,
|
||||
'department_distribution' => [
|
||||
$fallback_department_id => $booked_amount,
|
||||
],
|
||||
'undistributed_net_amount' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1061,6 +1066,9 @@ class economic_v2_distribution_service
|
||||
} elseif (!$this->isOrderEligible($order)) {
|
||||
continue;
|
||||
}
|
||||
$distribution_department_id = $system_order_fallback
|
||||
? $this->getFallbackDistributionDepartmentId()
|
||||
: $department_id;
|
||||
|
||||
$fixed_version = $this->versioning->resolveFixedPricingVersionAt($customer_number, $created_at);
|
||||
if ($fixed_version === null) {
|
||||
@@ -1090,14 +1098,14 @@ class economic_v2_distribution_service
|
||||
$order_original_price = $this->calculateOrderOriginalPrice(
|
||||
$order_items_by_order_id[$order_id] ?? [],
|
||||
$customer_number,
|
||||
$department_id,
|
||||
$distribution_department_id,
|
||||
$created_at
|
||||
);
|
||||
$groups[$group_key]['original_price'] += $order_original_price;
|
||||
if (!isset($groups[$group_key]['department_totals'][$department_id])) {
|
||||
$groups[$group_key]['department_totals'][$department_id] = 0.0;
|
||||
if (!isset($groups[$group_key]['department_totals'][$distribution_department_id])) {
|
||||
$groups[$group_key]['department_totals'][$distribution_department_id] = 0.0;
|
||||
}
|
||||
$groups[$group_key]['department_totals'][$department_id] += $order_original_price;
|
||||
$groups[$group_key]['department_totals'][$distribution_department_id] += $order_original_price;
|
||||
$groups[$group_key]['order_ids'][] = $order_id;
|
||||
|
||||
if (!isset($customer_transactions[$customer_number][$order_id])) {
|
||||
@@ -1141,12 +1149,15 @@ class economic_v2_distribution_service
|
||||
} elseif (!$this->isOrderEligible($order)) {
|
||||
continue;
|
||||
}
|
||||
$distribution_department_id = $system_order_fallback
|
||||
? $this->getFallbackDistributionDepartmentId()
|
||||
: $department_id;
|
||||
|
||||
$month_key = substr($created_at, 0, 7);
|
||||
if (!isset($customer_department_month_map[$customer_number][$month_key][$department_id])) {
|
||||
$customer_department_month_map[$customer_number][$month_key][$department_id] = 0;
|
||||
if (!isset($customer_department_month_map[$customer_number][$month_key][$distribution_department_id])) {
|
||||
$customer_department_month_map[$customer_number][$month_key][$distribution_department_id] = 0;
|
||||
}
|
||||
$customer_department_month_map[$customer_number][$month_key][$department_id]++;
|
||||
$customer_department_month_map[$customer_number][$month_key][$distribution_department_id]++;
|
||||
|
||||
$candidates = $this->buildWashSubscriptionCandidates(
|
||||
$order,
|
||||
@@ -1193,10 +1204,10 @@ class economic_v2_distribution_service
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($groups[$group_key]['distribution'][$department_id])) {
|
||||
$groups[$group_key]['distribution'][$department_id] = 0;
|
||||
if (!isset($groups[$group_key]['distribution'][$distribution_department_id])) {
|
||||
$groups[$group_key]['distribution'][$distribution_department_id] = 0;
|
||||
}
|
||||
$groups[$group_key]['distribution'][$department_id]++;
|
||||
$groups[$group_key]['distribution'][$distribution_department_id]++;
|
||||
$groups[$group_key]['order_ids'][] = $order_id;
|
||||
$matched_order = true;
|
||||
}
|
||||
@@ -1394,6 +1405,9 @@ class economic_v2_distribution_service
|
||||
): array {
|
||||
$distribution = [];
|
||||
$department_counts = $customer_department_month_map[$customer_number][$month_key] ?? [];
|
||||
if (!empty($department_counts)) {
|
||||
$department_counts = $this->normalizeFallbackDepartmentCounts($department_counts);
|
||||
}
|
||||
if (!empty($department_counts)) {
|
||||
$total = (float)array_sum($department_counts);
|
||||
foreach ($department_counts as $department_id => $count) {
|
||||
@@ -1402,20 +1416,61 @@ class economic_v2_distribution_service
|
||||
return $distribution;
|
||||
}
|
||||
|
||||
$default_department = 1;
|
||||
try {
|
||||
$default = (new users_o())->getUserByCustomerNumber($customer_number)->getDefaultDepartment();
|
||||
if (!empty($default)) {
|
||||
$default_department = (int)$default;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// fall back to department 1
|
||||
}
|
||||
$customer_default_department_id = $this->getCustomerDefaultDepartmentId($customer_number);
|
||||
$default_department = $customer_default_department_id !== null && $customer_default_department_id > 0
|
||||
? $customer_default_department_id
|
||||
: $this->getFallbackDistributionDepartmentId();
|
||||
|
||||
$distribution[$default_department] = $monthly_price;
|
||||
return $distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string,int|float> $department_counts
|
||||
* @return array<int,int|float>
|
||||
*/
|
||||
private function normalizeFallbackDepartmentCounts(array $department_counts): array
|
||||
{
|
||||
$normalized = [];
|
||||
foreach ($department_counts as $department_id => $count) {
|
||||
$department_id = (int)$department_id;
|
||||
if ($department_id === self::SYSTEM_ORDER_DEPARTMENT_ID || $department_id <= 0) {
|
||||
$department_id = $this->getFallbackDistributionDepartmentId();
|
||||
}
|
||||
if (!isset($normalized[$department_id])) {
|
||||
$normalized[$department_id] = 0;
|
||||
}
|
||||
$normalized[$department_id] += $count;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
protected function getCustomerDefaultDepartmentId(int $customer_number): ?int
|
||||
{
|
||||
try {
|
||||
$default = (new users_o())->getUserByCustomerNumber($customer_number)->getDefaultDepartment();
|
||||
return !empty($default) ? (int)$default : null;
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFallbackDistributionDepartmentId(): int
|
||||
{
|
||||
try {
|
||||
$department_id = (new economic())->getDefaultDistributionDepartmentId();
|
||||
} catch (\Throwable $e) {
|
||||
$department_id = economic::DEFAULT_DISTRIBUTION_DEPARTMENT_ID;
|
||||
}
|
||||
|
||||
if ($department_id <= 0 || $department_id === self::SYSTEM_ORDER_DEPARTMENT_ID) {
|
||||
return economic::DEFAULT_DISTRIBUTION_DEPARTMENT_ID;
|
||||
}
|
||||
|
||||
return $department_id;
|
||||
}
|
||||
|
||||
private function normalizeSubscriptionGroupAllocation(array $distribution, float $monthly_price): array
|
||||
{
|
||||
if (empty($distribution)) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
final class workfeed_employee_name_formatter
|
||||
{
|
||||
/**
|
||||
* @param array<int,string> $firstNamePaths
|
||||
* @param array<int,string> $lastNamePaths
|
||||
* @param array<int,string> $fallbackNamePaths
|
||||
*/
|
||||
public static function fromRecord(
|
||||
mixed $record,
|
||||
array $firstNamePaths,
|
||||
array $lastNamePaths,
|
||||
array $fallbackNamePaths = [],
|
||||
?string $employeeId = null
|
||||
): ?string {
|
||||
$firstName = self::firstTextValueByPath($record, $firstNamePaths);
|
||||
$lastName = self::firstTextValueByPath($record, $lastNamePaths);
|
||||
|
||||
$schemaName = self::joinNameParts($firstName, $lastName);
|
||||
if ($schemaName !== null) {
|
||||
return $schemaName;
|
||||
}
|
||||
|
||||
foreach ($fallbackNamePaths as $path) {
|
||||
$name = self::normalizeTextValue(self::valueByPath($record, $path));
|
||||
if ($name !== null && !self::isMissingDisplayName($name, $employeeId)) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function isMissingDisplayName(?string $employeeName, ?string $employeeId = null): bool
|
||||
{
|
||||
if ($employeeName === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($employeeId !== null && strcasecmp($employeeName, 'Employee ' . $employeeId) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return strcasecmp($employeeName, 'Unknown employee') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $paths
|
||||
*/
|
||||
private static function firstTextValueByPath(mixed $record, array $paths): ?string
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$value = self::normalizeTextValue(self::valueByPath($record, $path));
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function joinNameParts(?string $firstName, ?string $lastName): ?string
|
||||
{
|
||||
$name = trim((string)($firstName ?? '') . ' ' . (string)($lastName ?? ''));
|
||||
|
||||
return $name !== '' ? $name : null;
|
||||
}
|
||||
|
||||
private static function valueByPath(mixed $record, string $path): mixed
|
||||
{
|
||||
$segments = explode('.', $path);
|
||||
$value = $record;
|
||||
foreach ($segments as $segment) {
|
||||
if (is_array($value) && array_key_exists($segment, $value)) {
|
||||
$value = $value[$segment];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object($value) && isset($value->{$segment})) {
|
||||
$value = $value->{$segment};
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function normalizeTextValue(mixed $value): ?string
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = trim((string)$value);
|
||||
|
||||
return $normalized !== '' ? $normalized : null;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use classes\system_search_document_index;
|
||||
use classes\system_search_economic_customer_index;
|
||||
use classes\system_search_registry;
|
||||
use classes\workfeed;
|
||||
use classes\workfeed_employee_name_formatter;
|
||||
use classes\xlvask;
|
||||
use classes\slack as Slack;
|
||||
use classes\email as Email;
|
||||
@@ -32,6 +33,7 @@ use routes\moduleWeatherAPIRoute;
|
||||
require_once __DIR__ . '/../classes/economic_transfer_executor.php';
|
||||
require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';
|
||||
require_once __DIR__ . '/../classes/economic_transfer_queue.php';
|
||||
require_once __DIR__ . '/../classes/workfeed_employee_name_formatter.php';
|
||||
|
||||
if (!defined('WD')) {
|
||||
exit;
|
||||
@@ -301,8 +303,27 @@ function extractWorkfeedEmployeeWarmupIdentity(mixed $employee): array
|
||||
|
||||
$employeeId = $employeeIds[0] ?? null;
|
||||
|
||||
$employeeName = null;
|
||||
foreach ([
|
||||
$employeeName = workfeed_employee_name_formatter::fromRecord($record, [
|
||||
'firstname',
|
||||
'firstName',
|
||||
'first_name',
|
||||
'employee.firstname',
|
||||
'employee.firstName',
|
||||
'employee.first_name',
|
||||
'user.firstname',
|
||||
'user.firstName',
|
||||
'user.first_name',
|
||||
], [
|
||||
'lastname',
|
||||
'lastName',
|
||||
'last_name',
|
||||
'employee.lastname',
|
||||
'employee.lastName',
|
||||
'employee.last_name',
|
||||
'user.lastname',
|
||||
'user.lastName',
|
||||
'user.last_name',
|
||||
], [
|
||||
'employeeName',
|
||||
'employee.name',
|
||||
'employee.fullName',
|
||||
@@ -319,36 +340,7 @@ function extractWorkfeedEmployeeWarmupIdentity(mixed $employee): array
|
||||
'user.full_name',
|
||||
'user.displayName',
|
||||
'user.display_name',
|
||||
] as $path) {
|
||||
$employeeName = normalizeWarmupTextValue(getWarmupRecordValueByPath($record, $path));
|
||||
if ($employeeName !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($employeeName === null) {
|
||||
$firstName = normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'firstname'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'firstName'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'first_name'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.firstname'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.firstName'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.first_name'));
|
||||
$lastName = normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'lastname'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'lastName'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'last_name'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.lastname'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.lastName'))
|
||||
?? normalizeWarmupTextValue(getWarmupRecordValueByPath($record, 'employee.last_name'));
|
||||
|
||||
$parts = array_values(array_filter([$firstName, $lastName], static fn (?string $value): bool => $value !== null));
|
||||
if ($parts !== []) {
|
||||
$employeeName = implode(' ', $parts);
|
||||
}
|
||||
}
|
||||
|
||||
if ($employeeName !== null && strcasecmp($employeeName, 'Unknown employee') === 0) {
|
||||
$employeeName = null;
|
||||
}
|
||||
], $employeeId);
|
||||
|
||||
return [
|
||||
'id' => $employeeId,
|
||||
|
||||
@@ -8,6 +8,7 @@ use classes\response;
|
||||
use classes\router;
|
||||
use classes\weatherapi;
|
||||
use classes\workfeed;
|
||||
use classes\workfeed_employee_name_formatter;
|
||||
use classes\workfeed_shift_time_resolver;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
@@ -1484,14 +1485,14 @@ class moduleWeatherAPIRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeDepartmentName(string $name): string
|
||||
private static function normalizeDepartmentName(string $name): string
|
||||
{
|
||||
$collapsed = preg_replace('/\s+/', ' ', trim($name));
|
||||
|
||||
return strtolower($collapsed ?? trim($name));
|
||||
}
|
||||
|
||||
private function normalizeWorkfeedCollection(array|object $payload): array
|
||||
private static function normalizeWorkfeedCollection(array|object $payload): array
|
||||
{
|
||||
if (is_array($payload)) {
|
||||
return $payload;
|
||||
@@ -1514,7 +1515,7 @@ class moduleWeatherAPIRoute
|
||||
return [];
|
||||
}
|
||||
|
||||
private function normalizeWorkfeedRecord(mixed $record): array
|
||||
private static function normalizeWorkfeedRecord(mixed $record): array
|
||||
{
|
||||
if (is_array($record)) {
|
||||
return $record;
|
||||
@@ -1526,7 +1527,7 @@ class moduleWeatherAPIRoute
|
||||
return [];
|
||||
}
|
||||
|
||||
private function extractWorkfeedDepartmentId(mixed $shift): ?string
|
||||
private static function extractWorkfeedDepartmentId(mixed $shift): ?string
|
||||
{
|
||||
$record = self::normalizeWorkfeedRecord($shift);
|
||||
|
||||
@@ -1545,7 +1546,7 @@ class moduleWeatherAPIRoute
|
||||
return $normalized === '' ? null : $normalized;
|
||||
}
|
||||
|
||||
private function parseDateTimeValue(mixed $value): ?DateTime
|
||||
private static function parseDateTimeValue(mixed $value): ?DateTime
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$normalized = trim($value);
|
||||
@@ -1599,7 +1600,7 @@ class moduleWeatherAPIRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getNestedRecordValue(array $record, string $path): mixed
|
||||
private static function getNestedRecordValue(array $record, string $path): mixed
|
||||
{
|
||||
$segments = explode('.', $path);
|
||||
$current = $record;
|
||||
@@ -1627,7 +1628,7 @@ class moduleWeatherAPIRoute
|
||||
return $current;
|
||||
}
|
||||
|
||||
private function firstShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
|
||||
private static function firstShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$value = self::getNestedRecordValue($record, $path);
|
||||
@@ -1640,7 +1641,7 @@ class moduleWeatherAPIRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function lastShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
|
||||
private static function lastShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
|
||||
{
|
||||
$latest = null;
|
||||
|
||||
@@ -1659,7 +1660,7 @@ class moduleWeatherAPIRoute
|
||||
return $latest;
|
||||
}
|
||||
|
||||
private function hasShiftApproval(array $record): bool
|
||||
private static function hasShiftApproval(array $record): bool
|
||||
{
|
||||
if (!array_key_exists('approval', $record)) {
|
||||
return false;
|
||||
@@ -1680,7 +1681,7 @@ class moduleWeatherAPIRoute
|
||||
return true;
|
||||
}
|
||||
|
||||
private function resolveEffectiveShiftEnd(array $record, DateTime $shift_start, DateTime $shift_end): DateTime
|
||||
private static function resolveEffectiveShiftEnd(array $record, DateTime $shift_start, DateTime $shift_end): DateTime
|
||||
{
|
||||
if (self::hasShiftApproval($record)) {
|
||||
return $shift_end;
|
||||
@@ -1711,7 +1712,7 @@ class moduleWeatherAPIRoute
|
||||
return $update_time;
|
||||
}
|
||||
|
||||
private function calculateWorkfeedEmployeeHoursForHour(
|
||||
private static function calculateWorkfeedEmployeeHoursForHour(
|
||||
array $shifts,
|
||||
string|array $workfeed_department_ids,
|
||||
DateTime $slot_start,
|
||||
@@ -1775,8 +1776,8 @@ class moduleWeatherAPIRoute
|
||||
if (defined('redis')) {
|
||||
try {
|
||||
$employeeNameCache = new redis();
|
||||
} catch (Exception) {
|
||||
$employeeNameCache = null;
|
||||
} catch (Exception $e) {
|
||||
// Redis is unavailable, proceed without caching.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,7 +1823,8 @@ class moduleWeatherAPIRoute
|
||||
$employees = $this->resolveMissingWorkfeedEmployeeNames(
|
||||
$employees,
|
||||
$employeeNameCache,
|
||||
$resolved_employee_names_by_id
|
||||
$resolved_employee_names_by_id,
|
||||
$workfeed
|
||||
);
|
||||
if ($employees === []) {
|
||||
continue;
|
||||
@@ -1851,7 +1853,7 @@ class moduleWeatherAPIRoute
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateWorkfeedEmployeeHoursForHourByEmployee(
|
||||
private static function calculateWorkfeedEmployeeHoursForHourByEmployee(
|
||||
array $shifts,
|
||||
string|array $workfeed_department_ids,
|
||||
DateTime $slot_start,
|
||||
@@ -1902,7 +1904,11 @@ class moduleWeatherAPIRoute
|
||||
|
||||
$employee_identity = self::extractWorkfeedEmployeeIdentity($shift);
|
||||
$employee_id = $employee_identity['id'];
|
||||
$employee_name = self::normalizeShiftTextValue($employee_identity['name'] ?? null) ?? 'Unknown employee';
|
||||
$employee_name = self::normalizeShiftTextValue($employee_identity['name'] ?? null);
|
||||
if ($employee_id === null && $employee_name === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$employee_key = $employee_id ?? ('name:' . strtolower($employee_name));
|
||||
if (!isset($hours_by_employee_key[$employee_key])) {
|
||||
$hours_by_employee_key[$employee_key] = [
|
||||
@@ -1929,8 +1935,14 @@ class moduleWeatherAPIRoute
|
||||
}));
|
||||
}
|
||||
|
||||
private function resolveMissingWorkfeedEmployeeNames(array $employees, ?redis $employee_name_cache, array &$resolved_names_by_employee_id): array
|
||||
private function resolveMissingWorkfeedEmployeeNames(
|
||||
array $employees,
|
||||
?redis $employee_name_cache,
|
||||
array &$resolved_names_by_employee_id,
|
||||
?workfeed $workfeed = null
|
||||
): array
|
||||
{
|
||||
$ids_to_fetch = [];
|
||||
foreach ($employees as &$employee) {
|
||||
$employee_id = self::normalizeShiftTextValue($employee['employee_id'] ?? null);
|
||||
if ($employee_id === null) {
|
||||
@@ -1949,6 +1961,35 @@ class moduleWeatherAPIRoute
|
||||
);
|
||||
}
|
||||
|
||||
if ($resolved_names_by_employee_id[$employee_id] === null) {
|
||||
$ids_to_fetch[$employee_id] = true;
|
||||
}
|
||||
}
|
||||
unset($employee);
|
||||
|
||||
if ($ids_to_fetch !== [] && $workfeed !== null) {
|
||||
$api_names_by_employee_id = $this->fetchWorkfeedEmployeeDisplayNames(
|
||||
$workfeed,
|
||||
array_keys($ids_to_fetch),
|
||||
$employee_name_cache
|
||||
);
|
||||
|
||||
foreach (array_keys($ids_to_fetch) as $employee_id) {
|
||||
$resolved_names_by_employee_id[$employee_id] = $api_names_by_employee_id[$employee_id] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($employees as &$employee) {
|
||||
$employee_id = self::normalizeShiftTextValue($employee['employee_id'] ?? null);
|
||||
if ($employee_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$employee_name = self::normalizeShiftTextValue($employee['employee_name'] ?? null);
|
||||
if (!self::isMissingEmployeeDisplayName($employee_name, $employee_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolved_name = $resolved_names_by_employee_id[$employee_id] ?? null;
|
||||
if ($resolved_name !== null) {
|
||||
$employee['employee_name'] = $resolved_name;
|
||||
@@ -1960,7 +2001,172 @@ class moduleWeatherAPIRoute
|
||||
return strcasecmp((string)($left['employee_name'] ?? ''), (string)($right['employee_name'] ?? ''));
|
||||
});
|
||||
|
||||
return $employees;
|
||||
return array_values(array_filter($employees, static function (array $employee): bool {
|
||||
$employee_id = self::normalizeShiftTextValue($employee['employee_id'] ?? null);
|
||||
$employee_name = self::normalizeShiftTextValue($employee['employee_name'] ?? null);
|
||||
|
||||
return !self::isMissingEmployeeDisplayName($employee_name, $employee_id);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $employee_ids
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function fetchWorkfeedEmployeeDisplayNames(workfeed $workfeed, array $employee_ids, ?redis $employee_name_cache): array
|
||||
{
|
||||
$employee_id_lookup = [];
|
||||
foreach ($employee_ids as $employee_id) {
|
||||
$normalized = self::normalizeShiftTextValue($employee_id);
|
||||
if ($normalized !== null) {
|
||||
$employee_id_lookup[$normalized] = true;
|
||||
}
|
||||
}
|
||||
if ($employee_id_lookup === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names_by_employee_id = [];
|
||||
try {
|
||||
$employees = self::normalizeWorkfeedCollection($workfeed->listEmployees());
|
||||
} catch (Exception) {
|
||||
$employees = [];
|
||||
}
|
||||
|
||||
foreach ($employees as $employee) {
|
||||
$this->appendWorkfeedEmployeeDisplayNames($employee, $employee_id_lookup, $names_by_employee_id, $employee_name_cache);
|
||||
}
|
||||
|
||||
foreach (array_keys($employee_id_lookup) as $employee_id) {
|
||||
if (isset($names_by_employee_id[$employee_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$employee = $workfeed->getEmployee($employee_id);
|
||||
} catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->appendWorkfeedEmployeeDisplayNames($employee, $employee_id_lookup, $names_by_employee_id, $employee_name_cache);
|
||||
}
|
||||
|
||||
return $names_by_employee_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,bool> $employee_id_lookup
|
||||
* @param array<string,string> $names_by_employee_id
|
||||
*/
|
||||
private function appendWorkfeedEmployeeDisplayNames(
|
||||
mixed $employee,
|
||||
array $employee_id_lookup,
|
||||
array &$names_by_employee_id,
|
||||
?redis $employee_name_cache
|
||||
): void {
|
||||
foreach (self::extractWorkfeedEmployeeIds($employee) as $employee_id) {
|
||||
if (!isset($employee_id_lookup[$employee_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$employee_name = self::extractWorkfeedEmployeeDisplayName($employee, $employee_id);
|
||||
if ($employee_name === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$names_by_employee_id[$employee_id] = $employee_name;
|
||||
$this->cacheWorkfeedEmployeeDisplayName($employee_name_cache, $employee_id, $employee_name);
|
||||
}
|
||||
}
|
||||
|
||||
private static function extractWorkfeedEmployeeDisplayName(mixed $employee, ?string $employee_id = null): ?string
|
||||
{
|
||||
return workfeed_employee_name_formatter::fromRecord($employee, [
|
||||
'firstname',
|
||||
'firstName',
|
||||
'first_name',
|
||||
'employee.firstname',
|
||||
'employee.firstName',
|
||||
'employee.first_name',
|
||||
'user.firstname',
|
||||
'user.firstName',
|
||||
'user.first_name',
|
||||
], [
|
||||
'lastname',
|
||||
'lastName',
|
||||
'last_name',
|
||||
'employee.lastname',
|
||||
'employee.lastName',
|
||||
'employee.last_name',
|
||||
'user.lastname',
|
||||
'user.lastName',
|
||||
'user.last_name',
|
||||
], [
|
||||
'employeeName',
|
||||
'employee.name',
|
||||
'employee.fullName',
|
||||
'employee.full_name',
|
||||
'employee.displayName',
|
||||
'employee.display_name',
|
||||
'name',
|
||||
'fullName',
|
||||
'full_name',
|
||||
'displayName',
|
||||
'display_name',
|
||||
'user.name',
|
||||
'user.fullName',
|
||||
'user.full_name',
|
||||
'user.displayName',
|
||||
'user.display_name',
|
||||
], $employee_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private static function extractWorkfeedEmployeeIds(mixed $employee): array
|
||||
{
|
||||
$record = self::normalizeWorkfeedRecord($employee);
|
||||
if ($record === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$employee_ids = [];
|
||||
foreach ([
|
||||
'id',
|
||||
'employeeID',
|
||||
'employeeId',
|
||||
'employee_id',
|
||||
'uuid',
|
||||
'employee.id',
|
||||
'employee.employeeID',
|
||||
'employee.employeeId',
|
||||
'employee.employee_id',
|
||||
'employee.uuid',
|
||||
'employeeUUID',
|
||||
'employee_uuid',
|
||||
'user.id',
|
||||
'userId',
|
||||
] as $path) {
|
||||
$employee_id = self::normalizeShiftTextValue(self::getNestedRecordValue($record, $path));
|
||||
if ($employee_id !== null) {
|
||||
$employee_ids[$employee_id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($employee_ids);
|
||||
}
|
||||
|
||||
private function cacheWorkfeedEmployeeDisplayName(?redis $employee_name_cache, string $employee_id, string $employee_name): void
|
||||
{
|
||||
if ($employee_name_cache === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$employee_name_cache->cache_workfeed_employee_name($employee_id, $employee_name);
|
||||
} catch (Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchCachedWorkfeedEmployeeDisplayName(?redis $employee_name_cache, string $employee_id): ?string
|
||||
@@ -1978,20 +2184,12 @@ class moduleWeatherAPIRoute
|
||||
return self::isMissingEmployeeDisplayName($employee_name, $employee_id) ? null : $employee_name;
|
||||
}
|
||||
|
||||
private function isMissingEmployeeDisplayName(?string $employee_name, ?string $employee_id = null): bool
|
||||
private static function isMissingEmployeeDisplayName(?string $employee_name, ?string $employee_id = null): bool
|
||||
{
|
||||
if ($employee_name === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($employee_id !== null && strcasecmp($employee_name, 'Employee ' . $employee_id) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return strcasecmp($employee_name, 'Unknown employee') === 0;
|
||||
return workfeed_employee_name_formatter::isMissingDisplayName($employee_name, $employee_id);
|
||||
}
|
||||
|
||||
private function extractWorkfeedEmployeeIdentity(mixed $shift): array
|
||||
private static function extractWorkfeedEmployeeIdentity(mixed $shift): array
|
||||
{
|
||||
$record = self::normalizeWorkfeedRecord($shift);
|
||||
|
||||
@@ -2017,8 +2215,27 @@ class moduleWeatherAPIRoute
|
||||
}
|
||||
}
|
||||
|
||||
$employee_name = null;
|
||||
foreach ([
|
||||
$employee_name = workfeed_employee_name_formatter::fromRecord($record, [
|
||||
'employee.firstname',
|
||||
'employee.firstName',
|
||||
'employee.first_name',
|
||||
'firstname',
|
||||
'firstName',
|
||||
'first_name',
|
||||
'user.firstname',
|
||||
'user.firstName',
|
||||
'user.first_name',
|
||||
], [
|
||||
'employee.lastname',
|
||||
'employee.lastName',
|
||||
'employee.last_name',
|
||||
'lastname',
|
||||
'lastName',
|
||||
'last_name',
|
||||
'user.lastname',
|
||||
'user.lastName',
|
||||
'user.last_name',
|
||||
], [
|
||||
'employeeName',
|
||||
'employee.name',
|
||||
'employee.fullName',
|
||||
@@ -2035,37 +2252,7 @@ class moduleWeatherAPIRoute
|
||||
'user.full_name',
|
||||
'user.displayName',
|
||||
'user.display_name',
|
||||
] as $path) {
|
||||
$value = self::normalizeShiftTextValue(self::getNestedRecordValue($record, $path));
|
||||
if ($value !== null) {
|
||||
$employee_name = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($employee_name === null) {
|
||||
$first_name = self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.firstname'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.firstName'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.first_name'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'firstname'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'firstName'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'first_name'));
|
||||
$last_name = self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.lastname'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.lastName'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'employee.last_name'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'lastname'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'lastName'))
|
||||
?? self::normalizeShiftTextValue(self::getNestedRecordValue($record, 'last_name'));
|
||||
|
||||
$combined_name = trim((string)($first_name ?? '') . ' ' . (string)($last_name ?? ''));
|
||||
if ($combined_name !== '') {
|
||||
$employee_name = $combined_name;
|
||||
}
|
||||
}
|
||||
|
||||
if ($employee_name === null) {
|
||||
$employee_name = 'Unknown employee';
|
||||
}
|
||||
], $employee_id);
|
||||
|
||||
return [
|
||||
'id' => $employee_id,
|
||||
@@ -2073,7 +2260,7 @@ class moduleWeatherAPIRoute
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeShiftTextValue(mixed $value): ?string
|
||||
private static function normalizeShiftTextValue(mixed $value): ?string
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return null;
|
||||
|
||||
+24
-5
@@ -53,6 +53,8 @@ if (!class_exists('TestableEconomicV2BookedDepartment75DistributionService')) {
|
||||
public array $subscriptionPrices = [];
|
||||
public array $stubBookedInvoices = [];
|
||||
public array $stubBookedInvoiceLines = [];
|
||||
public int $fallbackDepartmentId = 8;
|
||||
public array $customerDefaultDepartments = [];
|
||||
public ?array $includedCustomers = null;
|
||||
|
||||
public function __construct(?economic_v2_versioning_service $versioning = null)
|
||||
@@ -138,6 +140,16 @@ if (!class_exists('TestableEconomicV2BookedDepartment75DistributionService')) {
|
||||
return in_array($customer_number, $this->includedCustomers, true);
|
||||
}
|
||||
|
||||
protected function getCustomerDefaultDepartmentId(int $customer_number): ?int
|
||||
{
|
||||
return $this->customerDefaultDepartments[$customer_number] ?? null;
|
||||
}
|
||||
|
||||
protected function getFallbackDistributionDepartmentId(): int
|
||||
{
|
||||
return $this->fallbackDepartmentId;
|
||||
}
|
||||
|
||||
protected function parseDepartmentMap(array $department_map): array
|
||||
{
|
||||
$parsed = [];
|
||||
@@ -303,7 +315,7 @@ it('redistributes booked department 75 net amounts using fixed pricing and wash
|
||||
expect($result['collective_results']['department_distribution']['2'])->toBe(138.46154);
|
||||
});
|
||||
|
||||
it('keeps classified booked department 75 amounts undistributed when no monthly basis exists', function (): void {
|
||||
it('assigns classified booked department 75 amounts to fallback when no monthly basis exists', function (): void {
|
||||
$service = new TestableEconomicV2BookedDepartment75DistributionService(new FakeEconomicV2BookedDepartment75VersioningService());
|
||||
$service->stubBookedInvoices = [[
|
||||
'bookedInvoiceNumber' => 7002,
|
||||
@@ -336,11 +348,18 @@ it('keeps classified booked department 75 amounts undistributed when no monthly
|
||||
$result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31');
|
||||
|
||||
expect($result['customers'])->toHaveCount(1);
|
||||
expect($result['customers'][0]['meta']['booked_department_75']['booked_groups'][0]['source_category'])->toBe('fixed_pricing');
|
||||
expect($result['customers'][0]['meta']['booked_department_75']['distributed_net_amount'])->toBe(0.0);
|
||||
expect($result['customers'][0]['meta']['booked_department_75']['undistributed_net_amount'])->toBe(60.0);
|
||||
expect($result['collective_results']['undistributed_net_amount'])->toBe(60.0);
|
||||
$meta = $result['customers'][0]['meta']['booked_department_75'];
|
||||
expect($meta['booked_groups'][0]['source_category'])->toBe('fixed_pricing');
|
||||
expect($meta['distributed_net_amount'])->toBe(60.0);
|
||||
expect($meta['undistributed_net_amount'])->toBe(0.0);
|
||||
expect($meta['department_distribution']['8'])->toBe(60.0);
|
||||
expect($meta['booked_groups'][0]['department_distribution']['8'])->toBe(60.0);
|
||||
expect($meta['booked_groups'][0]['undistributed_net_amount'])->toBe(0.0);
|
||||
expect($result['collective_results']['distributed_net_amount'])->toBe(60.0);
|
||||
expect($result['collective_results']['undistributed_net_amount'])->toBe(0.0);
|
||||
expect($result['collective_results']['department_distribution']['8'])->toBe(60.0);
|
||||
expect(implode("\n", $result['warnings']))->toContain('has no redistribution basis');
|
||||
expect(implode("\n", $result['warnings']))->toContain('assigned to fallback department 8');
|
||||
});
|
||||
|
||||
it('keeps unclassified booked department 75 lines undistributed with warnings', function (): void {
|
||||
|
||||
+48
-3
@@ -54,6 +54,8 @@ if (!class_exists('TestableEconomicV2DistributionService')) {
|
||||
public array $stubVersionRows = [];
|
||||
public array $subscriptionPrices = [];
|
||||
public array $versionTableState = [];
|
||||
public int $fallbackDepartmentId = 8;
|
||||
public array $customerDefaultDepartments = [];
|
||||
public ?array $includedCustomers = null;
|
||||
|
||||
public function __construct(?economic_v2_versioning_service $versioning = null)
|
||||
@@ -138,6 +140,16 @@ if (!class_exists('TestableEconomicV2DistributionService')) {
|
||||
return in_array($customer_number, $this->includedCustomers, true);
|
||||
}
|
||||
|
||||
protected function getCustomerDefaultDepartmentId(int $customer_number): ?int
|
||||
{
|
||||
return $this->customerDefaultDepartments[$customer_number] ?? null;
|
||||
}
|
||||
|
||||
protected function getFallbackDistributionDepartmentId(): int
|
||||
{
|
||||
return $this->fallbackDepartmentId;
|
||||
}
|
||||
|
||||
protected function parseDepartmentMap(array $department_map): array
|
||||
{
|
||||
$parsed = [];
|
||||
@@ -198,7 +210,7 @@ it('runs best-effort backfill when fixed pricing version history is empty', func
|
||||
expect($result['customers'])->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('falls back to system orders for fixed pricing when regular orders yield no customers', function (): void {
|
||||
it('falls back to system orders for fixed pricing using the configured fallback department', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
$versioning->fixedVersion = [
|
||||
'id' => 91,
|
||||
@@ -235,11 +247,14 @@ it('falls back to system orders for fixed pricing when regular orders yield no c
|
||||
expect($result['customers'][0]['customer_number'])->toBe(12345);
|
||||
expect($result['customers'][0]['meta']['fixed_pricing']['price'])->toBe(499.95);
|
||||
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['order_ids'])->toBe([501]);
|
||||
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals']['8'])->toBe(499.95);
|
||||
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals'])->not->toHaveKey('10');
|
||||
expect($result['collective_results']['total_department_totals']['8'])->toBe(499.95);
|
||||
expect($result['collective_results']['total_fixed_price'])->toBe(499.95);
|
||||
expect($result['warnings'])->toContain('System order fallback used for fixed pricing (department 10).');
|
||||
});
|
||||
|
||||
it('falls back to system orders for wash subscriptions when regular orders yield no customers', function (): void {
|
||||
it('falls back to system orders for wash subscriptions using the configured fallback department', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
$versioning->subscriptionVersions = [[
|
||||
'id' => 42,
|
||||
@@ -277,11 +292,41 @@ it('falls back to system orders for wash subscriptions when regular orders yield
|
||||
expect($result['customers'][0]['customer_number'])->toBe(12345);
|
||||
expect($result['customers'][0]['meta']['subscription']['subscription_total'])->toBe(299.0);
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['reg'])->toBe('AB12345');
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['10'])->toBe(299.0);
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0);
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('10');
|
||||
expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0);
|
||||
expect($result['collective_results']['total_subscription_price'])->toBe(299.0);
|
||||
expect($result['warnings'])->toContain('System order fallback used for wash subscriptions (department 10).');
|
||||
});
|
||||
|
||||
it('uses the configured fallback department when a subscription has no customer department basis', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
|
||||
$service = new TestableEconomicV2DistributionService($versioning);
|
||||
$service->subscriptionPrices = [
|
||||
77 => 299.0,
|
||||
];
|
||||
$service->stubVersionRows = [[
|
||||
'id' => 42,
|
||||
'customer_number' => 12345,
|
||||
'reg' => 'AB12345',
|
||||
'vehicle_type' => 77,
|
||||
'wash_subscription' => 1,
|
||||
'source' => 'test.version',
|
||||
'confidence' => 1.0,
|
||||
'inferred' => false,
|
||||
'effective_from' => '2026-01-01 00:00:00',
|
||||
'effective_to' => null,
|
||||
]];
|
||||
|
||||
$result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31');
|
||||
|
||||
expect($result['customers'])->toHaveCount(1);
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0);
|
||||
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('1');
|
||||
expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0);
|
||||
});
|
||||
|
||||
it('excludes orphaned customer traces from fixed pricing and customer price distributions', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
$versioning->fixedVersion = [
|
||||
|
||||
@@ -38,10 +38,11 @@ it('registers and implements cron warming for workfeed employee name cache', fun
|
||||
expect($cronContent)->toContain('extractWorkfeedEmployeeWarmupIdentity');
|
||||
expect($cronContent)->toContain('extractWorkfeedEmployeeWarmupIds');
|
||||
expect($cronContent)->toContain('foreach ($employeeIds as $employeeId)');
|
||||
expect($cronContent)->toContain('getWarmupRecordValueByPath($record, \'firstname\')');
|
||||
expect($cronContent)->toContain('getWarmupRecordValueByPath($record, \'lastname\')');
|
||||
expect($cronContent)->toContain('getWarmupRecordValueByPath($record, \'employee.firstname\')');
|
||||
expect($cronContent)->toContain('getWarmupRecordValueByPath($record, \'employee.lastname\')');
|
||||
expect($cronContent)->toContain('workfeed_employee_name_formatter::fromRecord');
|
||||
expect($cronContent)->toContain("'firstname'");
|
||||
expect($cronContent)->toContain("'lastname'");
|
||||
expect($cronContent)->toContain("'employee.firstname'");
|
||||
expect($cronContent)->toContain("'employee.lastname'");
|
||||
|
||||
expect($routeContent)->toContain('fetchCachedWorkfeedEmployeeDisplayName');
|
||||
expect($routeContent)->toContain('get_workfeed_employee_name');
|
||||
|
||||
@@ -182,7 +182,46 @@ it('extracts weather employee identity from supported shift payload shapes', fun
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses an unknown employee label when no employee name is available', function (): void {
|
||||
it('prefers canonical workfeed employee schema fields over generic employee names', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
$identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
|
||||
(object)[
|
||||
'employee' => (object)[
|
||||
'id' => 'emp_4',
|
||||
'firstname' => 'Dana',
|
||||
'lastname' => 'Scully',
|
||||
'name' => 'Wrong Name',
|
||||
],
|
||||
'employeeName' => 'Also Wrong',
|
||||
],
|
||||
]);
|
||||
|
||||
expect($identity)->toBe([
|
||||
'id' => 'emp_4',
|
||||
'name' => 'Dana Scully',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not use workfeed schema name fields as employee ids', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
$identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
|
||||
(object)[
|
||||
'employee' => (object)[
|
||||
'firstname' => 'Fox',
|
||||
'lastname' => 'Mulder',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
expect($identity)->toBe([
|
||||
'id' => null,
|
||||
'name' => 'Fox Mulder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a null employee name when no workfeed employee name is available', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
$identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
|
||||
@@ -193,10 +232,66 @@ it('uses an unknown employee label when no employee name is available', function
|
||||
|
||||
expect($identity)->toBe([
|
||||
'id' => 'emp_99',
|
||||
'name' => 'Unknown employee',
|
||||
'name' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves missing workfeed employee names from the employees endpoint', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
$workfeed = new class extends workfeed {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function listEmployees(array $filters = []): array|object
|
||||
{
|
||||
return [
|
||||
(object)[
|
||||
'id' => 'emp_42',
|
||||
'firstname' => 'Jane',
|
||||
'lastname' => 'Doe',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getEmployee(string $id): object
|
||||
{
|
||||
return (object)[];
|
||||
}
|
||||
};
|
||||
|
||||
$resolvedNames = [];
|
||||
$employees = weather_route_invoke_private($route, 'resolveMissingWorkfeedEmployeeNames', [[
|
||||
[
|
||||
'employee_id' => 'emp_42',
|
||||
'employee_name' => null,
|
||||
'hours' => 1.0,
|
||||
],
|
||||
], null, &$resolvedNames, $workfeed]);
|
||||
|
||||
expect($employees)->toBe([[
|
||||
'employee_id' => 'emp_42',
|
||||
'employee_name' => 'Jane Doe',
|
||||
'hours' => 1.0,
|
||||
]]);
|
||||
});
|
||||
|
||||
it('filters employee hour rows that cannot be resolved to a workfeed schema name', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
$resolvedNames = [];
|
||||
$employees = weather_route_invoke_private($route, 'resolveMissingWorkfeedEmployeeNames', [[
|
||||
[
|
||||
'employee_id' => 'emp_missing',
|
||||
'employee_name' => null,
|
||||
'hours' => 1.0,
|
||||
],
|
||||
], null, &$resolvedNames, null]);
|
||||
|
||||
expect($employees)->toBe([]);
|
||||
});
|
||||
|
||||
it('resolves employee display name from cache when shift payload lacks a name', function (): void {
|
||||
$route = new moduleWeatherAPIRoute();
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
app_require('classes/workfeed_employee_name_formatter.php');
|
||||
|
||||
use classes\workfeed_employee_name_formatter;
|
||||
|
||||
function workfeed_employee_name_from_record(mixed $record): ?string
|
||||
{
|
||||
return workfeed_employee_name_formatter::fromRecord($record, [
|
||||
'firstname',
|
||||
'firstName',
|
||||
'first_name',
|
||||
'employee.firstname',
|
||||
'employee.firstName',
|
||||
'employee.first_name',
|
||||
], [
|
||||
'lastname',
|
||||
'lastName',
|
||||
'last_name',
|
||||
'employee.lastname',
|
||||
'employee.lastName',
|
||||
'employee.last_name',
|
||||
], [
|
||||
'employeeName',
|
||||
'employee.name',
|
||||
'name',
|
||||
'displayName',
|
||||
]);
|
||||
}
|
||||
|
||||
it('formats workfeed employee display names from the documented firstname lastname schema', function (): void {
|
||||
$name = workfeed_employee_name_from_record((object)[
|
||||
'id' => 'employee_1',
|
||||
'firstname' => 'API 2',
|
||||
'lastname' => 'Test 2',
|
||||
]);
|
||||
|
||||
expect($name)->toBe('API 2 Test 2');
|
||||
});
|
||||
|
||||
it('prefers workfeed schema names over generic display name fields', function (): void {
|
||||
$name = workfeed_employee_name_from_record((object)[
|
||||
'employee' => (object)[
|
||||
'id' => 'employee_2',
|
||||
'firstname' => 'Jane',
|
||||
'lastname' => 'Doe',
|
||||
'name' => 'Wrong Name',
|
||||
],
|
||||
'employeeName' => 'Also Wrong',
|
||||
]);
|
||||
|
||||
expect($name)->toBe('Jane Doe');
|
||||
});
|
||||
|
||||
it('falls back to legacy display name fields when schema names are absent', function (): void {
|
||||
$name = workfeed_employee_name_from_record((object)[
|
||||
'employeeID' => 'employee_3',
|
||||
'employeeName' => 'Legacy Name',
|
||||
]);
|
||||
|
||||
expect($name)->toBe('Legacy Name');
|
||||
});
|
||||
|
||||
it('ignores placeholder workfeed display names', function (): void {
|
||||
$name = workfeed_employee_name_from_record((object)[
|
||||
'employeeID' => 'employee_4',
|
||||
'employeeName' => 'Unknown employee',
|
||||
]);
|
||||
|
||||
expect($name)->toBeNull();
|
||||
});
|
||||
|
||||
it('ignores employee id placeholder display names when the id is known', function (): void {
|
||||
$name = workfeed_employee_name_formatter::fromRecord((object)[
|
||||
'employeeID' => 'employee_5',
|
||||
'employeeName' => 'Employee employee_5',
|
||||
], [
|
||||
'firstname',
|
||||
], [
|
||||
'lastname',
|
||||
], [
|
||||
'employeeName',
|
||||
], 'employee_5');
|
||||
|
||||
expect($name)->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user