diff --git a/services/nginx/app/cron/Cron.php b/services/nginx/app/cron/Cron.php index f3cf0734..5b2de216 100644 --- a/services/nginx/app/cron/Cron.php +++ b/services/nginx/app/cron/Cron.php @@ -1421,7 +1421,49 @@ function GoalsProgressAlertsCron(): void case Dest::SLACK: $departments = (array)$goal->departments->value(); $sentToDept = false; - if (count($departments) > 0) { + $internalDepartmentIds = []; + try { + $slackConfig = new Slack(); + if (method_exists($slackConfig, 'get_internal_department_ids')) { + $internalDepartmentIds = array_map('intval', (array)$slackConfig->get_internal_department_ids()); + } + } catch (Throwable $slackConfigError) { + // Ignore - falls back to per-department webhooks + $internalDepartmentIds = []; + } + $goalDeptIds = []; + foreach ($departments as $deptId) { + if (is_numeric($deptId)) { + $goalDeptIds[] = (int)$deptId; + } + } + $allInternal = count($goalDeptIds) > 0 + && count(array_diff($goalDeptIds, $internalDepartmentIds)) === 0; + + if ($allInternal) { + // TRU-76: For internal departments (e.g. Taulov/Taastrup DHL daily + // goal), post to the dedicated internal goal progress webhook + // instead of per-department webhooks, which are typically empty + // for internal locations. + $internalWebhook = ''; + try { + $slackInstance = new Slack(); + if (method_exists($slackInstance, 'get_internal_department_goal_progress_webhook_url')) { + $internalWebhook = trim((string)$slackInstance->get_internal_department_goal_progress_webhook_url()); + } + } catch (Throwable $internalWebhookError) { + $internalWebhook = ''; + } + if ($internalWebhook !== '') { + (new Slack())->send_webhook_message((string)goals_progress_alert_renderer::render($criteria), $internalWebhook); + $sentToDept = true; + echo "[" . date('Y-m-d H:i:s') . "][CRON] GoalsProgressAlertsCron: goal #" . $goalId . " sent to internal goal progress webhook (departments: " . implode(',', $goalDeptIds) . ")\n"; + } else { + echo "[" . date('Y-m-d H:i:s') . "][CRON] GoalsProgressAlertsCron: goal #" . $goalId . " has only internal departments but internal_department_goal_progress_webhook_url is empty; falling back to per-department webhooks\n"; + } + } + + if (!$sentToDept && count($departments) > 0) { foreach ($departments as $deptId) { if (!is_numeric($deptId)) { continue; } $dept = (new departments_o())->select((int)$deptId); diff --git a/services/nginx/app/tests/Unit/Cron/GoalsProgressAlertsInternalWebhookTest.php b/services/nginx/app/tests/Unit/Cron/GoalsProgressAlertsInternalWebhookTest.php new file mode 100644 index 00000000..252eced8 --- /dev/null +++ b/services/nginx/app/tests/Unit/Cron/GoalsProgressAlertsInternalWebhookTest.php @@ -0,0 +1,68 @@ +not->toBeFalse(); + expect($content)->toContain("get_internal_department_ids"); + expect($content)->toContain("get_internal_department_goal_progress_webhook_url"); +}); + +it('posts to the dedicated internal goal progress webhook when all goal departments are internal', function (): void { + $content = file_get_contents(app_path('cron/Cron.php')); + + expect($content)->not->toBeFalse(); + // The new branch should be guarded by an "all internal" check. + expect($content)->toContain('$allInternal'); + expect($content)->toContain('count(array_diff($goalDeptIds, $internalDepartmentIds)) === 0'); + // It should call send_webhook_message with the dedicated internal URL. + expect($content)->toContain("send_webhook_message((string)goals_progress_alert_renderer::render(\$criteria), \$internalWebhook)"); + // It should log a confirmation line referencing the goal id and the + // department list so an operator can verify the message actually went + // somewhere. + expect($content)->toContain('internal goal progress webhook'); + expect($content)->toContain('departments: '); +}); + +it('logs a diagnostic and falls back to per-department webhooks when the internal goal progress webhook is empty', function (): void { + $content = file_get_contents(app_path('cron/Cron.php')); + + expect($content)->not->toBeFalse(); + expect($content)->toContain('internal_department_goal_progress_webhook_url is empty'); + expect($content)->toContain('falling back to per-department webhooks'); + // The per-department fallback should still run after the internal-webhook + // branch is skipped. + expect($content)->toContain('$dept->slack_webhook->value()'); +}); + +it('skips the internal goal progress webhook for goals that include any non-internal department', function (): void { + $content = file_get_contents(app_path('cron/Cron.php')); + + expect($content)->not->toBeFalse(); + // The internal-webhook branch must be guarded by the all-internal check; + // otherwise external customers' goal alerts would be silently redirected + // to the internal Slack channel. + expect($content)->toContain('if ($allInternal) {'); + expect($content)->toContain('send_webhook_message((string)goals_progress_alert_renderer::render($criteria), $internalWebhook)'); + // The per-department loop must still be reachable for mixed/external goals. + expect($content)->toContain('$sentToDept = false;'); + expect($content)->toContain('$dept->slack_webhook->value()'); +});