Remove redundant methods and IVR handling logic from Bird voice call webhook route, focusing on streamlined call flow and improved maintainability.

This commit is contained in:
Jeppe Bundgaard
2026-03-27 11:15:27 +01:00
parent 0dc1581eb4
commit 368e03ce48
@@ -28,7 +28,6 @@ class birdVoiceWebhooksRoute
$this->post('/bird/voice/calls/webhook/inbound', function () {
global $response;
//self::requirePermission('modules_bird_voice_call_webhooks_trigger');
$client = new bird();
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
@@ -47,204 +46,11 @@ class birdVoiceWebhooksRoute
$client->sayMessage($ws, $ch, $ci, [
'text' => 'Hej! Tak for dit opkald. Vi vil nu guide dig gennem en menu for at aabne en port. Afslut hvert valg med firkantstasten.',
]);
// Tell options to the caller
}, [
'modules_bird_voice_call_webhooks_trigger' => 'Trigger a voice call via Bird webhooks',
]);
}
public static function listDepartmentOptions(): array
{
$department_gates = array_unique((new department_gates_o())->getFields(['department']));
$departments = array_map(function ($row) {
return (new departments_o())->select((int)($row['department'] ?? 0))['name'] ?? 'Afdeling ' . (string)($row['department'] ?? '');
}, $department_gates);
$options = [];
foreach ($departments as $department) {
$options[] = [
'input' => 'text',
'value' => $department,
'label' => $department,
];
}
return $options;
}
protected function handleGatherResponse(
bird $client,
string $ws,
string $ch,
string $callId,
string $digits,
int $countryCode,
int $phone,
array $departments
): void {
$state = $this->readIvrState($callId);
if ($state === null || !$this->isStateForCaller($state, $countryCode, $phone)) {
$this->promptForDepartmentSelection($client, $ws, $ch, $callId, $departments, $countryCode, $phone);
return;
}
$stage = (string)($state['stage'] ?? '');
if ($stage === self::IVR_STAGE_DEPARTMENT_SELECT) {
$this->handleDepartmentSelectionDigit($client, $ws, $ch, $callId, $digits, $state, $countryCode, $phone);
return;
}
if ($stage === self::IVR_STAGE_GATE_TYPE_SELECT) {
$this->handleGateTypeSelectionDigit($client, $ws, $ch, $callId, $digits, $state);
return;
}
$this->clearIvrState($callId);
$this->promptForDepartmentSelection($client, $ws, $ch, $callId, $departments, $countryCode, $phone);
}
protected function handleDepartmentSelectionDigit(
bird $client,
string $ws,
string $ch,
string $callId,
string $digits,
array $state,
int $countryCode,
int $phone
): void {
$optionMap = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
$selected = $this->resolveDepartmentIdByDigit($optionMap, $digits);
if ($selected === null) {
$this->promptForDepartmentRetry($client, $ws, $ch, $callId, $state);
return;
}
$state['stage'] = self::IVR_STAGE_GATE_TYPE_SELECT;
$state['selected_department'] = $selected;
$state['country_code'] = $countryCode;
$state['phone'] = $phone;
$state['selected_department_name'] = $this->getDepartmentNameById($selected);
$this->saveIvrState($callId, $state);
$this->promptForGateTypeSelection($client, $ws, $ch, $callId, $selected, (string)$state['selected_department_name']);
}
protected function handleGateTypeSelectionDigit(
bird $client,
string $ws,
string $ch,
string $callId,
string $digits,
array $state
): void {
$departmentId = (int)($state['selected_department'] ?? 0);
if ($departmentId <= 0) {
$this->clearIvrState($callId);
$this->say($client, $ws, $ch, $callId, 'Sessionen er udloeebet. Ring venligst op igen.', true);
return;
}
$gateType = $this->resolveGateTypeByDigit($digits);
if ($gateType === null) {
$departmentName = (string)($state['selected_department_name'] ?? $this->getDepartmentNameById($departmentId));
$this->promptForGateTypeRetry($client, $ws, $ch, $callId, $departmentId, $departmentName);
return;
}
$gate = $gateType === self::IVR_GATE_TYPE_ENTRANCE
? (new department_gates_o())->getEntranceGate($departmentId)
: (new department_gates_o())->getExitGate($departmentId);
$this->clearIvrState($callId);
if ($gate === null || !$gate->exists()) {
$directionLabel = $gateType === self::IVR_GATE_TYPE_ENTRANCE ? 'indgang' : 'udgang';
$this->say($client, $ws, $ch, $callId, 'Vi kunne ikke finde en ' . $directionLabel . 'sport for den valgte afdeling.', true);
return;
}
try {
$gate->openGate();
$this->say($client, $ws, $ch, $callId, 'Aabner port: ' . $gate->name->value(), true);
return;
} catch (\Throwable) {
$this->say($client, $ws, $ch, $callId, 'Kunne ikke aktivere relaeet for ' . $gate->name->value() . '. Kontakt venligst support.', true);
}
}
protected function getCallerDepartments(int $countryCode, int $phone): array
{
// Return all visible departments.
$tmp = (new departments_o())->getFieldsWhere([
'visible' => 1,
], ['id']);
return array_map(function ($row) {
return (int)$row['id'];
}, $tmp);
}
protected function promptForDepartmentSelection(
bird $client,
string $ws,
string $ch,
string $callId,
array $departments,
int $countryCode,
int $phone
): void {
$optionMap = $this->buildDepartmentOptionMap($departments);
if (empty($optionMap)) {
$this->say($client, $ws, $ch, $callId, 'Der er ingen valgbare afdelinger lige nu. Kontakt venligst support.', true);
return;
}
$state = [
'stage' => self::IVR_STAGE_DEPARTMENT_SELECT,
'department_options' => $optionMap,
'selected_department' => null,
'country_code' => $countryCode,
'phone' => $phone,
];
$this->saveIvrState($callId, $state);
$text = $this->buildDepartmentPromptText($optionMap);
$this->gather($client, $ws, $ch, $callId, $text);
}
protected function promptForDepartmentRetry(bird $client, string $ws, string $ch, string $callId, array $state): void
{
$optionMap = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
if (empty($optionMap)) {
$this->clearIvrState($callId);
$this->say($client, $ws, $ch, $callId, 'Sessionen er udloeebet. Ring venligst op igen.', true);
return;
}
$text = 'Ugyldigt valg. ' . $this->buildDepartmentPromptText($optionMap);
$this->gather($client, $ws, $ch, $callId, $text);
}
protected function promptForGateTypeSelection(
bird $client,
string $ws,
string $ch,
string $callId,
int $departmentId,
string $departmentName
): void {
$text = $this->buildGateTypePromptText($departmentName);
$this->gather($client, $ws, $ch, $callId, $text);
}
protected function promptForGateTypeRetry(
bird $client,
string $ws,
string $ch,
string $callId,
int $departmentId,
string $departmentName
): void {
$text = 'Ugyldigt valg. ' . $this->buildGateTypePromptText($departmentName);
$this->gather($client, $ws, $ch, $callId, $text);
}
protected function buildDepartmentOptionMap(array $departmentIds): array
{
$map = [];
@@ -425,37 +231,6 @@ class birdVoiceWebhooksRoute
}
}
protected function extractCallerPhone(array $payload): ?array
{
$candidates = [
$payload['phone_number'] ?? null,
$payload['source'] ?? null,
$payload['from'] ?? null,
];
foreach ($candidates as $candidate) {
$normalized = department_gates_o::normalizePhoneCandidate($candidate);
if ($normalized !== null) {
return $normalized;
}
}
return null;
}
protected function isRegisteredCaller(int $countryCode, int $phone): bool
{
$userRows = (new users_o())->getFieldsWhere([
'phone_country_code' => $countryCode,
'phone' => $phone,
], ['id']);
if (!empty($userRows)) {
return true;
}
return (new subusers_o())->getSubuserByPhone($countryCode, $phone) !== null;
}
protected function say(bird $client, string $ws, string $ch, string $callId, string $text, bool $hangup = false): void
{
if ($callId !== '') {