Add Bird gate call flow unit tests, self-serve machine wash minutes config, relay sync improvements, and OpenAPI updates. Refactor Bird call handling with terminal status detection and timeout normalization.
This commit is contained in:
@@ -12,6 +12,8 @@ class bird
|
||||
public const TEST_OUTBOUND_NUMBER_RAW = '+45 42 33 11 28';
|
||||
public const TEST_OUTBOUND_NUMBER_E164 = '+4542331128';
|
||||
private const ALLOWED_HANGUP_CAUSES = ['rejected', 'busy', 'completed'];
|
||||
private const ACCEPTED_CALL_STATUSES = ['accepted', 'ongoing'];
|
||||
private const TERMINAL_GATE_FAILURE_STATUSES = ['rejected', 'busy', 'failed', 'cancelled', 'no-answer', 'completed'];
|
||||
|
||||
/**
|
||||
* Configuration of the Bird module
|
||||
@@ -418,11 +420,7 @@ class bird
|
||||
$targetNumber = self::TEST_OUTBOUND_NUMBER_E164;
|
||||
}
|
||||
$payload['to'] = $targetNumber;
|
||||
|
||||
// Ensure Bird terminates an unanswered call after we've stopped polling for it.
|
||||
if (!isset($payload['timeout'])) {
|
||||
$payload['timeout'] = $maxPollSeconds;
|
||||
}
|
||||
$payload = $this->normalizeCreateVoiceCallPayload($payload, $maxPollSeconds);
|
||||
|
||||
$this->logBirdAction(
|
||||
$logPrefix . '_START',
|
||||
@@ -443,18 +441,18 @@ class bird
|
||||
}
|
||||
|
||||
$lastCall = null;
|
||||
$acceptedStates = ['accepted', 'ongoing'];
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
$current = $this->getVoiceCall($workspaceId, $channelId, $callId);
|
||||
$lastCall = $current;
|
||||
$status = $this->extractStatus($current);
|
||||
$normalizedStatus = $status === null ? null : strtolower($status);
|
||||
|
||||
$this->logBirdAction(
|
||||
$logPrefix . '_POLL',
|
||||
'call=' . $callId . ' attempt=' . $attempt . '/' . $maxAttempts . ' status=' . ($status ?? 'unknown')
|
||||
);
|
||||
|
||||
if ($status !== null && in_array(strtolower($status), $acceptedStates, true)) {
|
||||
if ($normalizedStatus !== null && in_array($normalizedStatus, self::ACCEPTED_CALL_STATUSES, true)) {
|
||||
$hangupPayload = [];
|
||||
if (isset($options['hangupCause']) && is_string($options['hangupCause']) && $options['hangupCause'] !== '') {
|
||||
$normalizedCause = strtolower(trim($options['hangupCause']));
|
||||
@@ -476,8 +474,26 @@ class bird
|
||||
];
|
||||
}
|
||||
|
||||
if ($normalizedStatus !== null && in_array($normalizedStatus, self::TERMINAL_GATE_FAILURE_STATUSES, true)) {
|
||||
$this->logBirdAction(
|
||||
$logPrefix . '_TERMINAL',
|
||||
'call=' . $callId . ' status=' . $status
|
||||
);
|
||||
return [
|
||||
'to' => $targetNumber,
|
||||
'to_e164' => $targetNumber,
|
||||
'call_id' => $callId,
|
||||
'final_status' => $status,
|
||||
'hangup_sent' => false,
|
||||
'terminal_failure' => true,
|
||||
'created_call' => $createResponse,
|
||||
'last_call_snapshot' => $lastCall,
|
||||
'message' => 'Call reached terminal status before acceptance',
|
||||
];
|
||||
}
|
||||
|
||||
if ($attempt < $maxAttempts) {
|
||||
sleep($pollIntervalSeconds);
|
||||
$this->waitForCallPollInterval($pollIntervalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +515,14 @@ class bird
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook point for tests to avoid real waiting during call polling.
|
||||
*/
|
||||
protected function waitForCallPollInterval(int $pollIntervalSeconds): void
|
||||
{
|
||||
sleep($pollIntervalSeconds);
|
||||
}
|
||||
|
||||
private function voiceBase(string $workspaceId, string $channelId): string
|
||||
{
|
||||
return '/workspaces/' . rawurlencode($workspaceId) . '/channels/' . rawurlencode($channelId) . '/calls';
|
||||
@@ -626,12 +650,23 @@ class bird
|
||||
|
||||
public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout)
|
||||
{
|
||||
$ws = $this->config->workplaceId->getVariableValue();
|
||||
$ch = $this->config->channelId->getVariableValue();
|
||||
$ws = $this->getConfiguredWorkspaceId();
|
||||
$ch = $this->getConfiguredChannelId();
|
||||
if ($ws === '') {
|
||||
throw new Exception('Bird workspaceId is not configured for gate calls');
|
||||
}
|
||||
if ($ch === '') {
|
||||
throw new Exception('Bird channelId is not configured for gate calls');
|
||||
}
|
||||
|
||||
$normalizedRingTimeout = $this->normalizeRingTimeoutValue($timeout);
|
||||
if ($normalizedRingTimeout === null) {
|
||||
$normalizedRingTimeout = 30;
|
||||
}
|
||||
$options = [
|
||||
'to' => '+' . $countryCode . $phone,
|
||||
'maxPollSeconds' => (int)$timeout,
|
||||
'maxPollSeconds' => max(5, (int)$timeout),
|
||||
'ringTimeout' => $normalizedRingTimeout,
|
||||
];
|
||||
|
||||
$result = $this->executeCallAndHangupWhenAccepted($ws, $ch, $options, 'BIRD_GATE_CALL');
|
||||
@@ -640,10 +675,105 @@ class bird
|
||||
$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';
|
||||
} elseif ($result['terminal_failure'] ?? false) {
|
||||
$status = isset($result['final_status']) ? (string)$result['final_status'] : 'unknown';
|
||||
$msg = 'Gate call reached terminal status: ' . $status;
|
||||
}
|
||||
throw new Exception("Failed to call gate (+{$countryCode}{$phone}): " . $msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getConfiguredWorkspaceId(): string
|
||||
{
|
||||
if (!is_object($this->config)) {
|
||||
return '';
|
||||
}
|
||||
$workspaceConfig = null;
|
||||
if (property_exists($this->config, 'workspaceId')) {
|
||||
$workspaceConfig = $this->config->workspaceId;
|
||||
} elseif (property_exists($this->config, 'workplaceId')) {
|
||||
// Backward compatibility with existing Bird module variable naming.
|
||||
$workspaceConfig = $this->config->workplaceId;
|
||||
}
|
||||
if (!is_object($workspaceConfig) || !method_exists($workspaceConfig, 'getVariableValue')) {
|
||||
return '';
|
||||
}
|
||||
return $this->normalizeOptionalString($workspaceConfig->getVariableValue());
|
||||
}
|
||||
|
||||
protected function getConfiguredChannelId(): string
|
||||
{
|
||||
if (!is_object($this->config)) {
|
||||
return '';
|
||||
}
|
||||
$channelConfig = null;
|
||||
if (property_exists($this->config, 'channelId')) {
|
||||
$channelConfig = $this->config->channelId;
|
||||
}
|
||||
if (!is_object($channelConfig) || !method_exists($channelConfig, 'getVariableValue')) {
|
||||
return '';
|
||||
}
|
||||
return $this->normalizeOptionalString($channelConfig->getVariableValue());
|
||||
}
|
||||
|
||||
protected function normalizeOptionalString(mixed $value): string
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return '';
|
||||
}
|
||||
$normalized = trim((string)$value);
|
||||
if ($normalized === '') {
|
||||
return '';
|
||||
}
|
||||
$lower = strtolower($normalized);
|
||||
if ($lower === 'undefined' || $lower === 'null') {
|
||||
return '';
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align create-call payload with Bird voice call schema.
|
||||
* - Map legacy `timeout` to documented `ringTimeout`.
|
||||
* - Clamp `ringTimeout` to documented [3,120] range.
|
||||
*/
|
||||
protected function normalizeCreateVoiceCallPayload(array $payload, int $fallbackRingTimeout): array
|
||||
{
|
||||
if (array_key_exists('timeout', $payload) && !array_key_exists('ringTimeout', $payload)) {
|
||||
$payload['ringTimeout'] = $payload['timeout'];
|
||||
}
|
||||
unset($payload['timeout']);
|
||||
|
||||
$normalizedRingTimeout = null;
|
||||
if (array_key_exists('ringTimeout', $payload)) {
|
||||
$normalizedRingTimeout = $this->normalizeRingTimeoutValue($payload['ringTimeout']);
|
||||
}
|
||||
if ($normalizedRingTimeout === null) {
|
||||
$normalizedRingTimeout = $this->normalizeRingTimeoutValue($fallbackRingTimeout);
|
||||
}
|
||||
if ($normalizedRingTimeout !== null) {
|
||||
$payload['ringTimeout'] = $normalizedRingTimeout;
|
||||
} else {
|
||||
unset($payload['ringTimeout']);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
protected function normalizeRingTimeoutValue(mixed $value): ?int
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
$timeout = (int)$value;
|
||||
if ($timeout < 3) {
|
||||
$timeout = 3;
|
||||
}
|
||||
if ($timeout > 120) {
|
||||
$timeout = 120;
|
||||
}
|
||||
return $timeout;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user