buildDepartmentOptionMap($ids); } public function departmentPrompt(array $map): string { return $this->buildDepartmentPromptText($map); } public function gatePrompt(string $name): string { return $this->buildGateTypePromptText($name); } public function selectDepartment(array $map, string $digit): ?int { return $this->resolveDepartmentIdByDigit($map, $digit); } public function selectGateType(string $digit): ?string { return $this->resolveGateTypeByDigit($digit); } 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 stateForCaller(array $state, int $countryCode, int $phone): bool { return $this->isStateForCaller($state, $countryCode, $phone); } public function stateKey(string $callId): string { return $this->ivrStateKey($callId); } protected function getDepartmentNameById(int $departmentId): string { return $this->departmentNames[$departmentId] ?? ('Afdeling ' . $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 and caps to 9 options', function (): void { $route = new BirdVoiceWebhookRouteTestDouble(); $route->departmentNames = [11 => 'Nord', 22 => 'Syd', 33 => 'Vest']; $map = $route->mapDepartments([11, 22, 0, -5, 33, 44, 55, 66, 77, 88, 99]); expect($map)->toHaveCount(9); expect(array_keys($map))->toBe([1, 2, 3, 4, 5, 6, 7, 8, 9]); expect($map['1']['department_id'])->toBe(11); expect($map['2']['department_id'])->toBe(22); expect($map['3']['department_id'])->toBe(33); expect($map['1']['department_name'])->toBe('Nord'); expect($map['2']['department_name'])->toBe('Syd'); expect($map['3']['department_name'])->toBe('Vest'); }); 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, '9'))->toBeNull(); expect($route->selectDepartment($map, ''))->toBeNull(); }); it('resolves gate type digit to entrance or exit', function (): void { $route = new BirdVoiceWebhookRouteTestDouble(); expect($route->selectGateType('1'))->toBe('entrance'); expect($route->selectGateType('2'))->toBe('exit'); expect($route->selectGateType('0'))->toBeNull(); expect($route->selectGateType('x'))->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']], 'country_code' => 45, 'phone' => 12345678, ]; $route->storeState($callId, $state); $key = $route->stateKey($callId); expect($route->loadState($callId))->toBe($state); expect($route->ttls[$key] ?? null)->toBe(600); $route->removeState($callId); expect($route->loadState($callId))->toBeNull(); }); it('validates state caller fingerprint matching', function (): void { $route = new BirdVoiceWebhookRouteTestDouble(); $state = ['country_code' => 45, 'phone' => 12345678]; expect($route->stateForCaller($state, 45, 12345678))->toBeTrue(); expect($route->stateForCaller($state, 46, 12345678))->toBeFalse(); expect($route->stateForCaller($state, 45, 87654321))->toBeFalse(); }); it('builds department and gate prompts', function (): void { $route = new BirdVoiceWebhookRouteTestDouble(); $prompt = $route->departmentPrompt([ '1' => ['department_id' => 1, 'department_name' => 'Nord'], '2' => ['department_id' => 2, 'department_name' => 'Syd'], ]); expect($prompt)->toContain('Tast 1 for Nord'); expect($prompt)->toContain('Tast 2 for Syd'); $gatePrompt = $route->gatePrompt('Nord'); expect($gatePrompt)->toContain('Du valgte Nord.'); expect($gatePrompt)->toContain('Tast 1 for indgang. Tast 2 for udgang.'); });