Files
api/services/nginx/app/tests/Unit/Bird/BirdVoiceWebhooksRouteIvrTest.php
T

232 lines
8.1 KiB
PHP

<?php
$_SERVER['REQUEST_URI'] = $_SERVER['REQUEST_URI'] ?? '/';
app_require('routes/birdVoiceWebhooksRoute.php');
final class BirdVoiceWebhookRouteTestDouble extends \routes\birdVoiceWebhooksRoute
{
public array $departmentNames = [];
public array $rawState = [];
public array $ttls = [];
public function mapDepartments(array $ids): array
{
return $this->buildDepartmentOptionMap($ids);
}
public function departmentPrompt(array $map): string
{
return $this->buildDepartmentPromptText($map);
}
public function gatePrompt(string $name): string
{
return $this->buildGateTypePromptText($name);
}
public function gatePromptForOptions(string $name, array $options): string
{
return $this->buildGateTypePromptText($name, $options);
}
public function selectDepartment(array $map, string $digit): ?int
{
return $this->resolveDepartmentIdByDigit($map, $digit);
}
public function selectGateType(string $digit, array $options = []): ?string
{
return $this->resolveGateTypeByDigit($digit, $options);
}
public function mapGateOptions(array $types): array
{
return $this->buildGateOptionMap($types);
}
public function normalizeMenuDigit(?string $input): ?string
{
return $this->normalizeMenuDigitInput($input);
}
public function storeState(string $callId, array $state): void
{
$this->saveIvrState($callId, $state);
}
public function loadState(string $callId): ?array
{
return $this->readIvrState($callId);
}
public function removeState(string $callId): void
{
$this->clearIvrState($callId);
}
public function stateKey(string $callId): string
{
return $this->ivrStateKey($callId);
}
protected function getDepartmentNameById(int $departmentId): string
{
return $this->departmentNames[$departmentId] ?? ('Department ' . $departmentId);
}
protected function writeIvrStateRaw(string $key, string $value, int $ttlSeconds): void
{
$this->rawState[$key] = $value;
$this->ttls[$key] = $ttlSeconds;
}
protected function readIvrStateRaw(string $key): ?string
{
return $this->rawState[$key] ?? null;
}
protected function deleteIvrStateRaw(string $key): void
{
unset($this->rawState[$key], $this->ttls[$key]);
}
}
it('builds deterministic department digit map without capping options', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
$route->departmentNames = [
11 => 'Nord',
22 => 'Syd',
33 => 'Vest',
44 => 'Ost',
55 => 'Midt',
66 => 'Aalborg',
77 => 'Odense',
88 => 'Esbjerg',
99 => 'Kolding',
111 => 'Roskilde',
];
$map = $route->mapDepartments([
['department_id' => 11, 'department_name' => 'Nord', 'has_entrance_gate' => true, 'has_exit_gate' => false],
['department_id' => 22, 'department_name' => 'Syd', 'has_entrance_gate' => false, 'has_exit_gate' => true],
0,
-5,
['department_id' => 33, 'department_name' => 'Vest', 'has_entrance_gate' => true, 'has_exit_gate' => true],
44,
55,
66,
77,
88,
99,
111,
]);
expect($map)->toHaveCount(10);
expect(array_keys($map))->toBe([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect($map['1']['department_id'])->toBe(11);
expect($map['2']['department_id'])->toBe(22);
expect($map['3']['department_id'])->toBe(33);
expect($map['10']['department_id'])->toBe(111);
expect($map['1']['department_name'])->toBe('Nord');
expect($map['2']['department_name'])->toBe('Syd');
expect($map['3']['department_name'])->toBe('Vest');
expect($map['10']['department_name'])->toBe('Roskilde');
expect($map['1']['has_entrance_gate'])->toBeTrue();
expect($map['1']['has_exit_gate'])->toBeFalse();
expect($map['2']['has_entrance_gate'])->toBeFalse();
expect($map['2']['has_exit_gate'])->toBeTrue();
});
it('resolves department ids by selected digit', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
$map = [
'1' => ['department_id' => 101, 'department_name' => 'A'],
'2' => ['department_id' => 202, 'department_name' => 'B'],
];
expect($route->selectDepartment($map, '1'))->toBe(101);
expect($route->selectDepartment($map, '2'))->toBe(202);
expect($route->selectDepartment($map + ['10' => ['department_id' => 1001, 'department_name' => 'J']], '10'))->toBe(1001);
expect($route->selectDepartment($map, '9'))->toBeNull();
expect($route->selectDepartment($map, ''))->toBeNull();
});
it('builds compact gate option maps and resolves selected gate type by digit', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
$both = $route->mapGateOptions(['entrance', 'exit']);
$exitOnly = $route->mapGateOptions(['exit']);
$entranceOnly = $route->mapGateOptions(['entrance']);
expect($both)->toBe(['1' => 'entrance', '2' => 'exit']);
expect($exitOnly)->toBe(['1' => 'exit']);
expect($entranceOnly)->toBe(['1' => 'entrance']);
expect($route->selectGateType('1', $both))->toBe('entrance');
expect($route->selectGateType('2', $both))->toBe('exit');
expect($route->selectGateType('1', $exitOnly))->toBe('exit');
expect($route->selectGateType('2', $exitOnly))->toBeNull();
expect($route->selectGateType('0', $both))->toBeNull();
expect($route->selectGateType('x', $both))->toBeNull();
});
it('stores, loads and clears ivr state with ttl', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
$callId = 'abc-123';
$state = [
'stage' => 'department_select',
'department_options' => ['1' => ['department_id' => 5, 'department_name' => 'North']],
'gate_options' => ['1' => 'entrance'],
'gather_template' => ['timeout' => 30],
];
$route->storeState($callId, $state);
$key = $route->stateKey($callId);
expect($route->loadState($callId))->toBe($state);
expect($route->ttls[$key] ?? null)->toBe(1200);
$route->removeState($callId);
expect($route->loadState($callId))->toBeNull();
});
it('builds department prompts with multi-digit guidance and compact gate prompts', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
$prompt = $route->departmentPrompt([
'1' => ['department_id' => 1, 'department_name' => 'Nord'],
'2' => ['department_id' => 2, 'department_name' => 'Syd'],
'3' => ['department_id' => 3, 'department_name' => 'Vest'],
'4' => ['department_id' => 4, 'department_name' => 'Ost'],
'5' => ['department_id' => 5, 'department_name' => 'Midt'],
'6' => ['department_id' => 6, 'department_name' => 'Aalborg'],
'7' => ['department_id' => 7, 'department_name' => 'Odense'],
'8' => ['department_id' => 8, 'department_name' => 'Esbjerg'],
'9' => ['department_id' => 9, 'department_name' => 'Kolding'],
'10' => ['department_id' => 10, 'department_name' => 'Roskilde'],
]);
expect($prompt)->toContain('Choose department.');
expect($prompt)->toContain('Enter the option number followed by pound.');
expect($prompt)->toContain('Press 1 for Nord');
expect($prompt)->toContain('Press 10 for Roskilde');
$gatePrompt = $route->gatePromptForOptions('Nord', ['1' => 'exit']);
expect($gatePrompt)->toContain('You selected Nord.');
expect($gatePrompt)->toContain('Press 1 for exit.');
expect($gatePrompt)->not->toContain('Press 2 for exit.');
});
it('normalizes menu digit input from Bird dtmf payload values', function (): void {
$route = new BirdVoiceWebhookRouteTestDouble();
expect($route->normalizeMenuDigit('1'))->toBe('1');
expect($route->normalizeMenuDigit('1#'))->toBe('1');
expect($route->normalizeMenuDigit(' 2## '))->toBe('2');
expect($route->normalizeMenuDigit('10#'))->toBe('10');
expect($route->normalizeMenuDigit('12#'))->toBe('12');
expect($route->normalizeMenuDigit('09'))->toBeNull();
expect($route->normalizeMenuDigit('*'))->toBeNull();
expect($route->normalizeMenuDigit(null))->toBeNull();
});