Refactor Bird API client to extract reusable executeCallAndHangupWhenAccepted method, update callGateAndHangupWhenAccepted and test outbound call logic to use shared implementation, and enhance timeout handling.

This commit is contained in:
Jeppe Bundgaard
2026-03-04 14:24:11 +01:00
parent da8a01479b
commit 72a4c91e5f
2 changed files with 44 additions and 20 deletions
+36 -16
View File
@@ -382,6 +382,15 @@ class bird
}
public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array
{
return $this->executeCallAndHangupWhenAccepted($workspaceId, $channelId, $options, 'BIRD_TEST_OUTBOUND_CALL');
}
/**
* Internal method to place a call, wait for it to be accepted/ongoing, and then hang up.
* Used by both test outbound calls and actual gate calls.
*/
private function executeCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options, string $logPrefix): array
{
$pollIntervalSeconds = max(1, (int)($options['pollIntervalSeconds'] ?? 2));
$maxPollSeconds = max(5, (int)($options['maxPollSeconds'] ?? 30));
@@ -396,15 +405,20 @@ class bird
}
$payload['to'] = $targetNumber;
// Ensure Bird terminates an unanswered call after we've stopped polling for it.
if (!isset($payload['timeout'])) {
$payload['timeout'] = $maxPollSeconds;
}
$this->logBirdAction(
'BIRD_TEST_OUTBOUND_CALL_START',
$logPrefix . '_START',
'workspace=' . $workspaceId . ' channel=' . $channelId . ' to=' . $targetNumber
);
$createResponse = $this->createVoiceCall($workspaceId, $channelId, $payload);
$callId = $this->extractId($createResponse);
if ($callId === null) {
$this->logBirdAction('BIRD_TEST_OUTBOUND_CALL_NO_ID', 'Call accepted but no call ID returned', 0);
$this->logBirdAction($logPrefix . '_NO_ID', 'Call accepted but no call ID returned', 0);
return [
'to' => $targetNumber,
'to_e164' => $targetNumber,
@@ -422,7 +436,7 @@ class bird
$status = $this->extractStatus($current);
$this->logBirdAction(
'BIRD_TEST_OUTBOUND_CALL_POLL',
$logPrefix . '_POLL',
'call=' . $callId . ' attempt=' . $attempt . '/' . $maxAttempts . ' status=' . ($status ?? 'unknown')
);
@@ -435,7 +449,7 @@ class bird
}
}
$hangupResponse = $this->hangupVoiceCall($workspaceId, $channelId, $callId, $hangupPayload);
$this->logBirdAction('BIRD_TEST_OUTBOUND_CALL_HANGUP_SENT', 'call=' . $callId . ' status=' . $status);
$this->logBirdAction($logPrefix . '_HANGUP_SENT', 'call=' . $callId . ' status=' . $status);
return [
'to' => $targetNumber,
'to_e164' => $targetNumber,
@@ -455,7 +469,7 @@ class bird
$lastStatus = $this->extractStatus($lastCall);
$this->logBirdAction(
'BIRD_TEST_OUTBOUND_CALL_TIMEOUT',
$logPrefix . '_TIMEOUT',
'call=' . $callId . ' last_status=' . ($lastStatus ?? 'unknown'),
0
);
@@ -598,17 +612,23 @@ class bird
public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout)
{
$this->logBirdAction('callGateAndHangupWhenAccepted', "Calling gate with country code $countryCode and phone $phone");
$this->logBirdAction('callGateAndHangupWhenAccepted', "Timeout set to $timeout seconds");
$voiceResponse = $this->createVoiceCall(
$this->config->workplaceId->getVariableValue(),
$this->config->channelId->getVariableValue(),
// Call the gate with the provided phone number, and a short timeout to minimize potential disturbance.
[
'to' => '+' . $countryCode . $phone,
'timeout' => max(5, min(60, (int)$timeout)),
]
);
$ws = $this->config->workplaceId->getVariableValue();
$ch = $this->config->channelId->getVariableValue();
$options = [
'to' => '+' . $countryCode . $phone,
'maxPollSeconds' => (int)$timeout,
];
$result = $this->executeCallAndHangupWhenAccepted($ws, $ch, $options, 'BIRD_GATE_CALL');
if (!($result['hangup_sent'] ?? false)) {
$msg = $result['message'] ?? 'Failed to call gate and hangup when accepted';
if ($result['timed_out_waiting_for_accepted'] ?? false) {
$msg = 'Timed out waiting for gate to accept call';
}
throw new Exception("Failed to call gate (+{$countryCode}{$phone}): " . $msg);
}
}
}
@@ -205,13 +205,17 @@ class birdVoiceWebhooksRoute
return false;
}
[$countryCode, $phone] = $normalized;
// Use bird voice call to call the phone number, and then hang up after 5 seconds to trigger the gate
// Use the configured threshold or default to 10 seconds.
$timeout = (int)($config['call_duration_threshold'] ?? 10);
// Use bird voice call to call the phone number, and then hang up after it is accepted to trigger the gate.
$client = new bird();
try {
$client->callGateAndHangupWhenAccepted(
$countryCode,
$phone,
5,
(int)$countryCode,
(int)$phone,
$timeout,
);
return true;
} catch (\Throwable $e) {