Add bird_flash_calls_client implementation with endpoint builder, request schemas, and validator for managing flash call functionality.
This commit is contained in:
+1332
-143
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,25 @@
|
||||
|
||||
namespace classes;
|
||||
|
||||
require_once WD . '/interfaces/bird_i.php';
|
||||
require_once WD . '/modules/bird/bird_c.php';
|
||||
require_once WD . '/modules/bird/classes/bird_api_client.php';
|
||||
require_once WD . '/modules/bird/classes/bird_voice_calls_client.php';
|
||||
require_once WD . '/modules/bird/classes/bird_voice_recordings_client.php';
|
||||
require_once WD . '/modules/bird/classes/bird_voice_insights_client.php';
|
||||
require_once WD . '/modules/bird/classes/bird_flash_calls_client.php';
|
||||
|
||||
|
||||
use bird\bird_c;
|
||||
use bird\classes\bird_flash_calls_client;
|
||||
use bird\classes\bird_voice_calls_client;
|
||||
use bird\classes\bird_voice_insights_client;
|
||||
use bird\classes\bird_voice_recordings_client;
|
||||
use Exception;
|
||||
use interfaces\bird_i;
|
||||
use objects\logs_o;
|
||||
|
||||
class bird
|
||||
class bird implements bird_i
|
||||
{
|
||||
public const TEST_OUTBOUND_NUMBER_RAW = '+45 42 33 11 28';
|
||||
public const TEST_OUTBOUND_NUMBER_E164 = '+4542331128';
|
||||
@@ -28,11 +41,48 @@ class bird
|
||||
*/
|
||||
public bird_c $config;
|
||||
|
||||
private ?bird_voice_calls_client $voice_calls_client = null;
|
||||
private ?bird_voice_recordings_client $voice_recordings_client = null;
|
||||
private ?bird_voice_insights_client $voice_insights_client = null;
|
||||
private ?bird_flash_calls_client $flash_calls_client = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new bird_c();
|
||||
}
|
||||
|
||||
private function voiceCallsClient(): bird_voice_calls_client
|
||||
{
|
||||
if ($this->voice_calls_client === null) {
|
||||
$this->voice_calls_client = new bird_voice_calls_client($this);
|
||||
}
|
||||
return $this->voice_calls_client;
|
||||
}
|
||||
|
||||
private function voiceRecordingsClient(): bird_voice_recordings_client
|
||||
{
|
||||
if ($this->voice_recordings_client === null) {
|
||||
$this->voice_recordings_client = new bird_voice_recordings_client($this);
|
||||
}
|
||||
return $this->voice_recordings_client;
|
||||
}
|
||||
|
||||
private function voiceInsightsClient(): bird_voice_insights_client
|
||||
{
|
||||
if ($this->voice_insights_client === null) {
|
||||
$this->voice_insights_client = new bird_voice_insights_client($this);
|
||||
}
|
||||
return $this->voice_insights_client;
|
||||
}
|
||||
|
||||
private function flashCallsClient(): bird_flash_calls_client
|
||||
{
|
||||
if ($this->flash_calls_client === null) {
|
||||
$this->flash_calls_client = new bird_flash_calls_client($this);
|
||||
}
|
||||
return $this->flash_calls_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure module is enabled
|
||||
* @throws Exception
|
||||
@@ -279,30 +329,50 @@ class bird
|
||||
|
||||
public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null
|
||||
{
|
||||
$base = $this->voiceBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_CREATE', 'workspace=' . $workspaceId . ' channel=' . $channelId);
|
||||
return $this->sendPostRequest($base, $payload);
|
||||
return $this->voiceCallsClient()->createVoiceCall($workspaceId, $channelId, $payload);
|
||||
}
|
||||
|
||||
public function listVoiceCalls(string $workspaceId, string $channelId, array $query = []): array|object|null
|
||||
{
|
||||
$base = $this->voiceBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_LIST', 'workspace=' . $workspaceId . ' channel=' . $channelId);
|
||||
return $this->sendGetRequest($base, $query);
|
||||
return $this->voiceCallsClient()->listVoiceCalls($workspaceId, $channelId, $query);
|
||||
}
|
||||
|
||||
public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
$base = $this->voiceBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_GET', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->sendGetRequest($base . '/' . rawurlencode($callId));
|
||||
return $this->voiceCallsClient()->getVoiceCall($workspaceId, $channelId, $callId);
|
||||
}
|
||||
|
||||
public function updateVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_UPDATE', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->updateVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function answerVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_ANSWER', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->answerVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function ringVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RINGING', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->ringVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
$base = $this->voiceBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_HANGUP', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->sendPostRequest($base . '/' . rawurlencode($callId) . '/hangup', $payload);
|
||||
return $this->voiceCallsClient()->hangupVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function playbackVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_PLAYBACK', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->playbackVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function sayMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
@@ -330,7 +400,7 @@ class bird
|
||||
'timeout' => 1,
|
||||
...$payload,
|
||||
];
|
||||
return $this->sendPostRequest($this->sayBase($workspaceId, $channelId, $callId), $tmp);
|
||||
return $this->voiceCallsClient()->sayVoiceCall($workspaceId, $channelId, $callId, $tmp);
|
||||
}
|
||||
|
||||
public function gatherMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
@@ -383,7 +453,55 @@ class bird
|
||||
'input' => 'dtmf',
|
||||
...$payload,
|
||||
];
|
||||
return $this->sendPostRequest($this->gatherBase($workspaceId, $channelId, $callId), $tmp);
|
||||
return $this->voiceCallsClient()->gatherVoiceCall($workspaceId, $channelId, $callId, $tmp);
|
||||
}
|
||||
|
||||
public function bridgeVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_BRIDGE', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->bridgeVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function recordVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RECORD', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceCallsClient()->recordVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function createVoiceCallRecordingSession(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RECORDING_CREATE', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceRecordingsClient()->createVoiceCallRecordingSession($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function listVoiceCallRecordings(string $workspaceId, string $channelId, string $callId, array $query = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RECORDING_LIST', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceRecordingsClient()->listVoiceCallRecordings($workspaceId, $channelId, $callId, $query);
|
||||
}
|
||||
|
||||
public function getVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RECORDING_GET', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId . ' recording=' . $recordingId);
|
||||
return $this->voiceRecordingsClient()->getVoiceCallRecording($workspaceId, $channelId, $callId, $recordingId);
|
||||
}
|
||||
|
||||
public function updateVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId, array $payload): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_RECORDING_UPDATE', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId . ' recording=' . $recordingId);
|
||||
return $this->voiceRecordingsClient()->updateVoiceCallRecording($workspaceId, $channelId, $callId, $recordingId, $payload);
|
||||
}
|
||||
|
||||
public function getVoiceCallInsights(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_INSIGHTS_GET', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->voiceInsightsClient()->getVoiceCallInsights($workspaceId, $channelId, $callId);
|
||||
}
|
||||
|
||||
public function getVoiceCallsLog(string $workspaceId, array $query = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_VOICE_CALL_LOG_LIST', 'workspace=' . $workspaceId);
|
||||
return $this->voiceInsightsClient()->getVoiceCallsLog($workspaceId, $query);
|
||||
}
|
||||
|
||||
public function listNumbers(string $workspaceId, array $query = []): array|object|null
|
||||
@@ -406,16 +524,32 @@ class bird
|
||||
|
||||
public function createFlashCall(string $workspaceId, string $channelId, array $payload): array|object|null
|
||||
{
|
||||
$base = $this->flashBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_FLASH_CALL_CREATE', 'workspace=' . $workspaceId . ' channel=' . $channelId);
|
||||
return $this->sendPostRequest($base, $payload);
|
||||
return $this->flashCallsClient()->createFlashCall($workspaceId, $channelId, $payload);
|
||||
}
|
||||
|
||||
public function listFlashCalls(string $workspaceId, string $channelId, array $query = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_FLASH_CALL_LIST', 'workspace=' . $workspaceId . ' channel=' . $channelId);
|
||||
return $this->flashCallsClient()->listFlashCalls($workspaceId, $channelId, $query);
|
||||
}
|
||||
|
||||
public function getFlashCall(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
$base = $this->flashBase($workspaceId, $channelId);
|
||||
$this->logBirdAction('BIRD_FLASH_CALL_GET', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->sendGetRequest($base . '/' . rawurlencode($callId));
|
||||
return $this->flashCallsClient()->getFlashCall($workspaceId, $channelId, $callId);
|
||||
}
|
||||
|
||||
public function endFlashCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_FLASH_CALL_END', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
|
||||
return $this->flashCallsClient()->endFlashCall($workspaceId, $channelId, $callId, $payload);
|
||||
}
|
||||
|
||||
public function hangupFlashCall(string $workspaceId, string $channelId, array $payload = []): array|object|null
|
||||
{
|
||||
$this->logBirdAction('BIRD_FLASH_CALL_HANGUP', 'workspace=' . $workspaceId . ' channel=' . $channelId);
|
||||
return $this->flashCallsClient()->hangupFlashCall($workspaceId, $channelId, $payload);
|
||||
}
|
||||
|
||||
public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array
|
||||
|
||||
@@ -63,6 +63,7 @@ class selfserve_schema_bootstrap
|
||||
machine_relay_enabled_at DATETIME NULL,
|
||||
machine_start_triggered TINYINT(1) NOT NULL DEFAULT 0,
|
||||
machine_start_triggered_at DATETIME NULL,
|
||||
wash_started_at DATETIME NULL,
|
||||
order_id INT NULL,
|
||||
completed_at DATETIME NULL,
|
||||
metadata_json JSON NULL,
|
||||
@@ -149,6 +150,11 @@ class selfserve_schema_bootstrap
|
||||
'dynamic_images_vehicle_type',
|
||||
'ALTER TABLE selfserve_wash_session_tasks ADD COLUMN dynamic_images_vehicle_type INT NULL AFTER buttons'
|
||||
);
|
||||
self::ensureColumn(
|
||||
'selfserve_wash_sessions',
|
||||
'wash_started_at',
|
||||
'ALTER TABLE selfserve_wash_sessions ADD COLUMN wash_started_at DATETIME NULL AFTER machine_start_triggered_at'
|
||||
);
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace interfaces;
|
||||
|
||||
require_once WD . '/interfaces/universal_module_i.php';
|
||||
|
||||
interface bird_i extends universal_module_i
|
||||
{
|
||||
public function requireValidApiKey(): void;
|
||||
|
||||
public function requireValidServerURL(): void;
|
||||
|
||||
public function sendRequest(string $endpoint, array $data = [], string $method = 'POST'): object|array|null;
|
||||
|
||||
public function sendPostRequest(string $endpoint, array $data): array|object|null;
|
||||
|
||||
public function sendGetRequest(string $endpoint, array $query = []): array|object|null;
|
||||
|
||||
public function sendPatchRequest(string $endpoint, array $data = []): array|object|null;
|
||||
|
||||
public function sendDeleteRequest(string $endpoint, array $query = []): array|object|null;
|
||||
|
||||
public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null;
|
||||
|
||||
public function listVoiceCalls(string $workspaceId, string $channelId, array $query = []): array|object|null;
|
||||
|
||||
public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function updateVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function answerVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function ringVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function playbackVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function sayMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function gatherMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function bridgeVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function recordVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function createVoiceCallRecordingSession(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function listVoiceCallRecordings(string $workspaceId, string $channelId, string $callId, array $query = []): array|object|null;
|
||||
|
||||
public function getVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId): array|object|null;
|
||||
|
||||
public function updateVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId, array $payload): array|object|null;
|
||||
|
||||
public function getVoiceCallInsights(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function getVoiceCallsLog(string $workspaceId, array $query = []): array|object|null;
|
||||
|
||||
public function listNumbers(string $workspaceId, array $query = []): array|object|null;
|
||||
|
||||
public function getNumber(string $workspaceId, string $numberId): array|object|null;
|
||||
|
||||
public function deleteNumber(string $workspaceId, string $numberId): array|object|null;
|
||||
|
||||
public function createFlashCall(string $workspaceId, string $channelId, array $payload): array|object|null;
|
||||
|
||||
public function listFlashCalls(string $workspaceId, string $channelId, array $query = []): array|object|null;
|
||||
|
||||
public function getFlashCall(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function endFlashCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function hangupFlashCall(string $workspaceId, string $channelId, array $payload = []): array|object|null;
|
||||
|
||||
public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array;
|
||||
|
||||
public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout);
|
||||
|
||||
public function callGateViaFlashCall(int $countryCode, int $phone, int $ringTimeout): void;
|
||||
|
||||
public function callGatePreferringFlashCall(int $countryCode, int $phone, int $timeout): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace bird\classes;
|
||||
|
||||
require_once WD . '/classes/bird.php';
|
||||
require_once WD . '/modules/bird/traits/bird_endpoint_builder_t.php';
|
||||
|
||||
use bird\traits\bird_endpoint_builder_t;
|
||||
use classes\bird;
|
||||
|
||||
abstract class bird_api_client
|
||||
{
|
||||
use bird_endpoint_builder_t;
|
||||
|
||||
protected bird $bird;
|
||||
|
||||
public function __construct(bird $bird)
|
||||
{
|
||||
$this->bird = $bird;
|
||||
}
|
||||
|
||||
protected function get(string $endpoint, array $query = []): array|object|null
|
||||
{
|
||||
return $this->bird->sendGetRequest($endpoint, $query);
|
||||
}
|
||||
|
||||
protected function post(string $endpoint, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->bird->sendPostRequest($endpoint, $payload);
|
||||
}
|
||||
|
||||
protected function patch(string $endpoint, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->bird->sendPatchRequest($endpoint, $payload);
|
||||
}
|
||||
|
||||
protected function delete(string $endpoint, array $query = []): array|object|null
|
||||
{
|
||||
return $this->bird->sendDeleteRequest($endpoint, $query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace bird\classes;
|
||||
|
||||
require_once WD . '/modules/bird/interfaces/bird_flash_calls_client_i.php';
|
||||
require_once WD . '/modules/bird/classes/bird_api_client.php';
|
||||
|
||||
use bird\interfaces\bird_flash_calls_client_i;
|
||||
|
||||
class bird_flash_calls_client extends bird_api_client implements bird_flash_calls_client_i
|
||||
{
|
||||
public function createFlashCall(string $workspaceId, string $channelId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdFlashCallsBasePath($workspaceId, $channelId), $payload);
|
||||
}
|
||||
|
||||
public function listFlashCalls(string $workspaceId, string $channelId, array $query = []): array|object|null
|
||||
{
|
||||
return $this->get($this->birdFlashCallsBasePath($workspaceId, $channelId), $query);
|
||||
}
|
||||
|
||||
public function getFlashCall(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
return $this->get($this->birdFlashCallPath($workspaceId, $channelId, $callId));
|
||||
}
|
||||
|
||||
public function endFlashCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->post($this->birdFlashCallPath($workspaceId, $channelId, $callId), $payload);
|
||||
}
|
||||
|
||||
public function hangupFlashCall(string $workspaceId, string $channelId, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->post($this->birdFlashCallsHangupPath($workspaceId, $channelId), $payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace bird\classes;
|
||||
|
||||
require_once WD . '/modules/bird/interfaces/bird_voice_calls_client_i.php';
|
||||
require_once WD . '/modules/bird/classes/bird_api_client.php';
|
||||
|
||||
use bird\interfaces\bird_voice_calls_client_i;
|
||||
|
||||
class bird_voice_calls_client extends bird_api_client implements bird_voice_calls_client_i
|
||||
{
|
||||
public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallsBasePath($workspaceId, $channelId), $payload);
|
||||
}
|
||||
|
||||
public function listVoiceCalls(string $workspaceId, string $channelId, array $query = []): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallsBasePath($workspaceId, $channelId), $query);
|
||||
}
|
||||
|
||||
public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallPath($workspaceId, $channelId, $callId));
|
||||
}
|
||||
|
||||
public function updateVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->patch($this->birdVoiceCallPath($workspaceId, $channelId, $callId), $payload);
|
||||
}
|
||||
|
||||
public function answerVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'answer'), $payload);
|
||||
}
|
||||
|
||||
public function ringVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'ringing'), $payload);
|
||||
}
|
||||
|
||||
public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'hangup'), $payload);
|
||||
}
|
||||
|
||||
public function playbackVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'playback'), $payload);
|
||||
}
|
||||
|
||||
public function sayVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'say'), $payload);
|
||||
}
|
||||
|
||||
public function gatherVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'gather'), $payload);
|
||||
}
|
||||
|
||||
public function bridgeVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'bridge'), $payload);
|
||||
}
|
||||
|
||||
public function recordVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'record'), $payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace bird\classes;
|
||||
|
||||
require_once WD . '/modules/bird/interfaces/bird_voice_insights_client_i.php';
|
||||
require_once WD . '/modules/bird/classes/bird_api_client.php';
|
||||
|
||||
use bird\interfaces\bird_voice_insights_client_i;
|
||||
|
||||
class bird_voice_insights_client extends bird_api_client implements bird_voice_insights_client_i
|
||||
{
|
||||
public function getVoiceCallInsights(string $workspaceId, string $channelId, string $callId): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallCommandPath($workspaceId, $channelId, $callId, 'insights'));
|
||||
}
|
||||
|
||||
public function getVoiceCallsLog(string $workspaceId, array $query = []): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallsLogPath($workspaceId), $query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace bird\classes;
|
||||
|
||||
require_once WD . '/modules/bird/interfaces/bird_voice_recordings_client_i.php';
|
||||
require_once WD . '/modules/bird/classes/bird_api_client.php';
|
||||
|
||||
use bird\interfaces\bird_voice_recordings_client_i;
|
||||
|
||||
class bird_voice_recordings_client extends bird_api_client implements bird_voice_recordings_client_i
|
||||
{
|
||||
public function createVoiceCallRecordingSession(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
|
||||
{
|
||||
return $this->post($this->birdVoiceCallRecordingsPath($workspaceId, $channelId, $callId), $payload);
|
||||
}
|
||||
|
||||
public function listVoiceCallRecordings(string $workspaceId, string $channelId, string $callId, array $query = []): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallRecordingsPath($workspaceId, $channelId, $callId), $query);
|
||||
}
|
||||
|
||||
public function getVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId): array|object|null
|
||||
{
|
||||
return $this->get($this->birdVoiceCallRecordingPath($workspaceId, $channelId, $callId, $recordingId));
|
||||
}
|
||||
|
||||
public function updateVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId, array $payload): array|object|null
|
||||
{
|
||||
return $this->patch($this->birdVoiceCallRecordingPath($workspaceId, $channelId, $callId, $recordingId), $payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
namespace bird\helpers;
|
||||
|
||||
class bird_request_schemas
|
||||
{
|
||||
public static function noBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceCreateCallBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['to'],
|
||||
'properties' => [
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'ringTimeout' => ['type' => 'integer', 'min' => 3, 'max' => 120],
|
||||
'maxDuration' => ['type' => 'integer', 'min' => 1],
|
||||
'sendKeys' => ['type' => 'string', 'maxLength' => 20, 'pattern' => '^[0-9*#]+$'],
|
||||
'record' => ['type' => 'boolean'],
|
||||
'recordStart' => ['type' => 'string', 'enum' => ['record-from-answer', 'record-from-ringing']],
|
||||
'flowStart' => ['type' => 'string', 'enum' => ['from-answer', 'from-ringing']],
|
||||
'stereo' => ['type' => 'boolean'],
|
||||
'callFlow' => self::callFlowCommands(),
|
||||
'scheduledFor' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'notification' => self::notificationSchema(),
|
||||
'amdSettings' => self::amdSettingsSchema(),
|
||||
'tags' => ['type' => 'array', 'maxItems' => 10, 'items' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 64]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceListCallsQuery(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'limit' => ['type' => 'integer', 'min' => 1, 'max' => 1000],
|
||||
'pageToken' => ['type' => 'string', 'maxLength' => 8000],
|
||||
'startAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'endAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'status' => ['type' => 'string', 'enum' => self::callStatuses()],
|
||||
'type' => ['type' => 'string', 'enum' => ['pstn', 'sip', 'webrtc']],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'duration' => ['type' => 'integer', 'min' => 0],
|
||||
'direction' => ['type' => 'string', 'enum' => ['incoming', 'outgoing']],
|
||||
'id' => ['type' => 'string', 'format' => 'uuid'],
|
||||
'tag' => ['type' => 'array', 'csv' => true, 'items' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 64]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceUpdateCallBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'status' => ['type' => 'string', 'nullable' => true, 'enum' => ['completed']],
|
||||
'callFlow' => self::callFlowCommands(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceHangupBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'cause' => ['type' => 'string', 'enum' => ['rejected', 'busy']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voicePlaybackBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['media'],
|
||||
'properties' => [
|
||||
'media' => ['type' => 'array', 'minItems' => 1, 'maxItems' => 80, 'items' => ['type' => 'string', 'minLength' => 1]],
|
||||
'loop' => ['type' => 'integer', 'min' => 0],
|
||||
'timeout' => ['type' => 'integer', 'min' => 0],
|
||||
'pauseMilliseconds' => ['type' => 'integer', 'min' => 0, 'max' => 30000],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceSayBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['text'],
|
||||
'properties' => [
|
||||
'text' => ['type' => 'string', 'minLength' => 1],
|
||||
'locale' => ['type' => 'string', 'minLength' => 2, 'maxLength' => 20],
|
||||
'voice' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 64],
|
||||
'loop' => ['type' => 'integer', 'min' => 0],
|
||||
'timeout' => ['type' => 'integer', 'min' => 0],
|
||||
'hangup' => ['type' => 'boolean'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceGatherBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'maxNumKeys' => ['type' => 'integer', 'min' => 1],
|
||||
'endKey' => ['type' => 'string', 'enum' => self::digitEnums()],
|
||||
'timeout' => ['type' => 'integer', 'min' => 0],
|
||||
'retries' => ['type' => 'integer', 'min' => 0],
|
||||
'input' => ['type' => 'string', 'enum' => ['dtmf', 'speech', 'dtmf speech']],
|
||||
'speechLocale' => ['type' => 'string', 'minLength' => 2, 'maxLength' => 20],
|
||||
'playback' => self::voicePlaybackBody(),
|
||||
'say' => self::voiceSayBody(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceBridgeBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['to'],
|
||||
'properties' => [
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'ringTimeout' => ['type' => 'integer', 'min' => 3, 'max' => 120],
|
||||
'maxDuration' => ['type' => 'integer', 'min' => 1],
|
||||
'ringTone' => ['type' => 'string', 'enum' => ['be', 'ca', 'cn', 'cy', 'cz', 'de', 'dk', 'dz', 'eg', 'fi', 'fr', 'hk', 'hu', 'il', 'in', 'jp', 'ko', 'pk', 'pl', 'ro', 'rs', 'ru', 'sa', 'tr', 'uk', 'us']],
|
||||
'hangupAfterBridge' => ['type' => 'boolean'],
|
||||
'record' => ['type' => 'boolean'],
|
||||
'recordStart' => ['type' => 'string', 'enum' => ['record-from-answer', 'record-from-ringing']],
|
||||
'recordStereo' => ['type' => 'boolean'],
|
||||
'callFlow' => self::callFlowCommands(),
|
||||
'notification' => self::notificationSchema(),
|
||||
'amdSettings' => self::amdSettingsSchema(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceRecordBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'endKey' => ['type' => 'string', 'enum' => self::digitEnums()],
|
||||
'maxLength' => ['type' => 'integer', 'min' => 1],
|
||||
'timeout' => ['type' => 'integer', 'min' => 0],
|
||||
'beep' => ['type' => 'boolean'],
|
||||
'transcribe' => ['type' => 'boolean'],
|
||||
'transcribeLocale' => ['type' => 'string', 'minLength' => 2, 'maxLength' => 20],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceRecordingsCreateBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'maxLength' => ['type' => 'integer', 'min' => 1],
|
||||
'stereo' => ['type' => 'boolean'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceRecordingsListQuery(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'limit' => ['type' => 'integer', 'min' => 1, 'max' => 1000],
|
||||
'pageToken' => ['type' => 'string', 'maxLength' => 8000],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceRecordingUpdateBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['status'],
|
||||
'properties' => [
|
||||
'status' => ['type' => 'string', 'enum' => ['paused', 'ongoing', 'completed']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceCallsLogQuery(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'limit' => ['type' => 'integer', 'min' => 1, 'max' => 1000],
|
||||
'pageToken' => ['type' => 'string', 'maxLength' => 8000],
|
||||
'startAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'endAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'channelId' => ['type' => 'array', 'csv' => true, 'items' => ['type' => 'string', 'format' => 'uuid']],
|
||||
'status' => ['type' => 'string', 'enum' => self::callStatuses()],
|
||||
'type' => ['type' => 'string', 'enum' => ['pstn', 'sip', 'webrtc']],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'duration' => ['type' => 'integer', 'min' => 0],
|
||||
'direction' => ['type' => 'string', 'enum' => ['incoming', 'outgoing']],
|
||||
'id' => ['type' => 'string', 'format' => 'uuid'],
|
||||
'tag' => ['type' => 'array', 'csv' => true, 'items' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 64]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function voiceTestOutboundBody(): array
|
||||
{
|
||||
$create = self::voiceCreateCallBody();
|
||||
$properties = isset($create['properties']) && is_array($create['properties']) ? $create['properties'] : [];
|
||||
|
||||
$properties['timeout'] = ['type' => 'integer', 'min' => 1];
|
||||
$properties['pollIntervalSeconds'] = ['type' => 'integer', 'min' => 1];
|
||||
$properties['maxPollSeconds'] = ['type' => 'integer', 'min' => 5];
|
||||
$properties['hangupCause'] = ['type' => 'string', 'enum' => ['rejected', 'busy']];
|
||||
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => $properties,
|
||||
];
|
||||
}
|
||||
|
||||
public static function flashCreateBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['to'],
|
||||
'properties' => [
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'ringTimeout' => ['type' => 'integer', 'min' => 3, 'max' => 120],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function flashListQuery(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'limit' => ['type' => 'integer', 'min' => 1, 'max' => 1000],
|
||||
'pageToken' => ['type' => 'string', 'maxLength' => 8000],
|
||||
'startAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'endAt' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'status' => ['type' => 'string', 'enum' => self::callStatuses()],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'duration' => ['type' => 'integer', 'min' => 0],
|
||||
'id' => ['type' => 'string', 'format' => 'uuid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function flashEndBody(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['result'],
|
||||
'properties' => [
|
||||
'receivedCli' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'result' => ['type' => 'string', 'enum' => ['unknown', 'verified', 'canceled', 'timeout', 'wrong_cli']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function flashHangupBody(): array
|
||||
{
|
||||
return [
|
||||
'oneOf' => [
|
||||
[
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['result'],
|
||||
'properties' => [
|
||||
'receivedCli' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'result' => ['type' => 'string', 'enum' => ['unknown', 'verified', 'canceled', 'timeout', 'wrong_cli']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['from', 'to'],
|
||||
'properties' => [
|
||||
'from' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'to' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'receivedCli' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100],
|
||||
'result' => ['type' => 'string', 'enum' => ['unknown', 'verified', 'canceled', 'timeout', 'wrong_cli']],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function callFlowCommands(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'array',
|
||||
'maxItems' => 20,
|
||||
'items' => self::callFlowCommandSchema(),
|
||||
];
|
||||
}
|
||||
|
||||
private static function callFlowCommandSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['command'],
|
||||
'properties' => [
|
||||
'command' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['answer', 'hangup', 'playback', 'say', 'gather', 'record', 'bridge', 'pause', 'ringing'],
|
||||
],
|
||||
'conditions' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['variable', 'operator', 'value'],
|
||||
'properties' => [
|
||||
'variable' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 64],
|
||||
'operator' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 16],
|
||||
'value' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 255],
|
||||
],
|
||||
],
|
||||
],
|
||||
'options' => [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => true,
|
||||
'properties' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function notificationSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'required' => ['url'],
|
||||
'properties' => [
|
||||
'url' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 2048],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function amdSettingsSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'enabled' => ['type' => 'boolean'],
|
||||
'wordCount' => ['type' => 'integer', 'min' => 1],
|
||||
'speechTimeout' => ['type' => 'integer', 'min' => 1],
|
||||
'speechLocale' => ['type' => 'string', 'minLength' => 2, 'maxLength' => 20],
|
||||
'beepTimeout' => ['type' => 'integer', 'min' => 1],
|
||||
'ifMachineNotifyAfter' => ['type' => 'string', 'enum' => ['wordCount', 'beep']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function callStatuses(): array
|
||||
{
|
||||
return ['accepted', 'starting', 'ringing', 'ongoing', 'completed', 'no-answer', 'busy', 'failed', 'cancelled', 'scheduled'];
|
||||
}
|
||||
|
||||
private static function digitEnums(): array
|
||||
{
|
||||
return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace bird\helpers;
|
||||
|
||||
class bird_request_validator
|
||||
{
|
||||
public static function validate(mixed $value, array $schema, string $path = '$'): array
|
||||
{
|
||||
$errors = [];
|
||||
self::validateValue($value, $schema, $path, $errors);
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public static function isUuid(string $value): bool
|
||||
{
|
||||
return preg_match('/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/', $value) === 1;
|
||||
}
|
||||
|
||||
private static function validateValue(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
if (($schema['nullable'] ?? false) && $value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($schema['oneOf']) && is_array($schema['oneOf'])) {
|
||||
self::validateOneOf($value, $schema['oneOf'], $path, $errors);
|
||||
return;
|
||||
}
|
||||
|
||||
$type = isset($schema['type']) && is_string($schema['type']) ? $schema['type'] : null;
|
||||
if ($type !== null) {
|
||||
match ($type) {
|
||||
'object' => self::validateObject($value, $schema, $path, $errors),
|
||||
'array' => self::validateArray($value, $schema, $path, $errors),
|
||||
'string' => self::validateString($value, $schema, $path, $errors),
|
||||
'int', 'integer' => self::validateInteger($value, $schema, $path, $errors),
|
||||
'number' => self::validateNumber($value, $schema, $path, $errors),
|
||||
'bool', 'boolean' => self::validateBoolean($value, $schema, $path, $errors),
|
||||
default => $errors[] = $path . ' has unsupported schema type "' . $type . '"',
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($schema['enum']) && is_array($schema['enum'])) {
|
||||
self::validateEnum($value, $schema['enum'], $path, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateOneOf(mixed $value, array $schemas, string $path, array &$errors): void
|
||||
{
|
||||
$bestErrors = null;
|
||||
foreach ($schemas as $schema) {
|
||||
if (!is_array($schema)) {
|
||||
continue;
|
||||
}
|
||||
$tmp = [];
|
||||
self::validateValue($value, $schema, $path, $tmp);
|
||||
if ($tmp === []) {
|
||||
return;
|
||||
}
|
||||
if ($bestErrors === null || count($tmp) < count($bestErrors)) {
|
||||
$bestErrors = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
$errors[] = $path . ' does not match any of the allowed schemas';
|
||||
if (is_array($bestErrors)) {
|
||||
foreach ($bestErrors as $err) {
|
||||
$errors[] = $err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateObject(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
if (!is_array($value) || !self::isAssoc($value)) {
|
||||
$errors[] = $path . ' must be an object';
|
||||
return;
|
||||
}
|
||||
|
||||
$required = isset($schema['required']) && is_array($schema['required']) ? $schema['required'] : [];
|
||||
foreach ($required as $requiredKey) {
|
||||
if (!array_key_exists((string)$requiredKey, $value)) {
|
||||
$errors[] = $path . '.' . $requiredKey . ' is required';
|
||||
}
|
||||
}
|
||||
|
||||
$properties = isset($schema['properties']) && is_array($schema['properties']) ? $schema['properties'] : [];
|
||||
$allowAdditional = (bool)($schema['additionalProperties'] ?? true);
|
||||
|
||||
if (!$allowAdditional) {
|
||||
foreach ($value as $key => $_unused) {
|
||||
if (!array_key_exists((string)$key, $properties)) {
|
||||
$errors[] = $path . '.' . $key . ' is not allowed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($properties as $key => $propertySchema) {
|
||||
if (!array_key_exists((string)$key, $value)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($propertySchema)) {
|
||||
continue;
|
||||
}
|
||||
self::validateValue($value[(string)$key], $propertySchema, $path . '.' . $key, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateArray(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
$arrayValue = $value;
|
||||
if (is_string($arrayValue) && ($schema['csv'] ?? false)) {
|
||||
$parts = array_map('trim', explode(',', $arrayValue));
|
||||
$arrayValue = array_values(array_filter($parts, static fn($part) => $part !== ''));
|
||||
}
|
||||
|
||||
if (!is_array($arrayValue) || self::isAssoc($arrayValue)) {
|
||||
$errors[] = $path . ' must be an array';
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($arrayValue);
|
||||
if (isset($schema['minItems']) && is_numeric($schema['minItems']) && $count < (int)$schema['minItems']) {
|
||||
$errors[] = $path . ' must have at least ' . (int)$schema['minItems'] . ' items';
|
||||
}
|
||||
if (isset($schema['maxItems']) && is_numeric($schema['maxItems']) && $count > (int)$schema['maxItems']) {
|
||||
$errors[] = $path . ' must have at most ' . (int)$schema['maxItems'] . ' items';
|
||||
}
|
||||
|
||||
$itemSchema = isset($schema['items']) && is_array($schema['items']) ? $schema['items'] : null;
|
||||
if ($itemSchema === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($arrayValue as $index => $item) {
|
||||
self::validateValue($item, $itemSchema, $path . '[' . $index . ']', $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateString(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
$errors[] = $path . ' must be a string';
|
||||
return;
|
||||
}
|
||||
|
||||
$length = strlen($value);
|
||||
if (isset($schema['minLength']) && is_numeric($schema['minLength']) && $length < (int)$schema['minLength']) {
|
||||
$errors[] = $path . ' must be at least ' . (int)$schema['minLength'] . ' characters';
|
||||
}
|
||||
if (isset($schema['maxLength']) && is_numeric($schema['maxLength']) && $length > (int)$schema['maxLength']) {
|
||||
$errors[] = $path . ' must be at most ' . (int)$schema['maxLength'] . ' characters';
|
||||
}
|
||||
|
||||
if (isset($schema['pattern']) && is_string($schema['pattern']) && @preg_match('/' . $schema['pattern'] . '/', $value) !== 1) {
|
||||
$errors[] = $path . ' has invalid format';
|
||||
}
|
||||
|
||||
if (isset($schema['format']) && is_string($schema['format'])) {
|
||||
if ($schema['format'] === 'uuid' && !self::isUuid($value)) {
|
||||
$errors[] = $path . ' must be a UUID';
|
||||
}
|
||||
if ($schema['format'] === 'date-time' && strtotime($value) === false) {
|
||||
$errors[] = $path . ' must be a valid date-time';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateInteger(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
$intValue = self::toInt($value);
|
||||
if ($intValue === null) {
|
||||
$errors[] = $path . ' must be an integer';
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($schema['min']) && is_numeric($schema['min']) && $intValue < (int)$schema['min']) {
|
||||
$errors[] = $path . ' must be at least ' . (int)$schema['min'];
|
||||
}
|
||||
if (isset($schema['max']) && is_numeric($schema['max']) && $intValue > (int)$schema['max']) {
|
||||
$errors[] = $path . ' must be at most ' . (int)$schema['max'];
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateNumber(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
$numberValue = self::toFloat($value);
|
||||
if ($numberValue === null) {
|
||||
$errors[] = $path . ' must be a number';
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($schema['min']) && is_numeric($schema['min']) && $numberValue < (float)$schema['min']) {
|
||||
$errors[] = $path . ' must be at least ' . (float)$schema['min'];
|
||||
}
|
||||
if (isset($schema['max']) && is_numeric($schema['max']) && $numberValue > (float)$schema['max']) {
|
||||
$errors[] = $path . ' must be at most ' . (float)$schema['max'];
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateBoolean(mixed $value, array $schema, string $path, array &$errors): void
|
||||
{
|
||||
if (self::toBool($value) === null) {
|
||||
$errors[] = $path . ' must be a boolean';
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateEnum(mixed $value, array $allowedValues, string $path, array &$errors): void
|
||||
{
|
||||
if (in_array($value, $allowedValues, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
foreach ($allowedValues as $allowed) {
|
||||
if (is_scalar($allowed) && (string)$allowed === (string)$value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errors[] = $path . ' must be one of: ' . implode(', ', array_map(static fn($item) => (string)$item, $allowedValues));
|
||||
}
|
||||
|
||||
private static function toInt(mixed $value): ?int
|
||||
{
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value) && preg_match('/^-?[0-9]+$/', $value) === 1) {
|
||||
return (int)$value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function toFloat(mixed $value): ?float
|
||||
{
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function toBool(mixed $value): ?bool
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_int($value) && ($value === 0 || $value === 1)) {
|
||||
return $value === 1;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$normalized = strtolower(trim($value));
|
||||
if ($normalized === 'true' || $normalized === '1') {
|
||||
return true;
|
||||
}
|
||||
if ($normalized === 'false' || $normalized === '0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isAssoc(array $array): bool
|
||||
{
|
||||
return array_keys($array) !== range(0, count($array) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace bird\interfaces;
|
||||
|
||||
interface bird_flash_calls_client_i
|
||||
{
|
||||
public function createFlashCall(string $workspaceId, string $channelId, array $payload): array|object|null;
|
||||
|
||||
public function listFlashCalls(string $workspaceId, string $channelId, array $query = []): array|object|null;
|
||||
|
||||
public function getFlashCall(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function endFlashCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function hangupFlashCall(string $workspaceId, string $channelId, array $payload = []): array|object|null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace bird\interfaces;
|
||||
|
||||
interface bird_voice_calls_client_i
|
||||
{
|
||||
public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null;
|
||||
|
||||
public function listVoiceCalls(string $workspaceId, string $channelId, array $query = []): array|object|null;
|
||||
|
||||
public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function updateVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function answerVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function ringVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null;
|
||||
|
||||
public function playbackVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function sayVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function gatherVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function bridgeVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function recordVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace bird\interfaces;
|
||||
|
||||
interface bird_voice_insights_client_i
|
||||
{
|
||||
public function getVoiceCallInsights(string $workspaceId, string $channelId, string $callId): array|object|null;
|
||||
|
||||
public function getVoiceCallsLog(string $workspaceId, array $query = []): array|object|null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace bird\interfaces;
|
||||
|
||||
interface bird_voice_recordings_client_i
|
||||
{
|
||||
public function createVoiceCallRecordingSession(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null;
|
||||
|
||||
public function listVoiceCallRecordings(string $workspaceId, string $channelId, string $callId, array $query = []): array|object|null;
|
||||
|
||||
public function getVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId): array|object|null;
|
||||
|
||||
public function updateVoiceCallRecording(string $workspaceId, string $channelId, string $callId, string $recordingId, array $payload): array|object|null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace bird\traits;
|
||||
|
||||
trait bird_endpoint_builder_t
|
||||
{
|
||||
protected function birdEncodePathSegment(string $segment): string
|
||||
{
|
||||
return rawurlencode($segment);
|
||||
}
|
||||
|
||||
protected function birdVoiceCallsBasePath(string $workspaceId, string $channelId): string
|
||||
{
|
||||
return '/workspaces/'
|
||||
. $this->birdEncodePathSegment($workspaceId)
|
||||
. '/channels/'
|
||||
. $this->birdEncodePathSegment($channelId)
|
||||
. '/calls';
|
||||
}
|
||||
|
||||
protected function birdVoiceCallPath(string $workspaceId, string $channelId, string $callId): string
|
||||
{
|
||||
return $this->birdVoiceCallsBasePath($workspaceId, $channelId)
|
||||
. '/'
|
||||
. $this->birdEncodePathSegment($callId);
|
||||
}
|
||||
|
||||
protected function birdVoiceCallCommandPath(string $workspaceId, string $channelId, string $callId, string $command): string
|
||||
{
|
||||
return $this->birdVoiceCallPath($workspaceId, $channelId, $callId)
|
||||
. '/'
|
||||
. trim($command, '/');
|
||||
}
|
||||
|
||||
protected function birdVoiceCallRecordingsPath(string $workspaceId, string $channelId, string $callId): string
|
||||
{
|
||||
return $this->birdVoiceCallPath($workspaceId, $channelId, $callId) . '/recordings';
|
||||
}
|
||||
|
||||
protected function birdVoiceCallRecordingPath(string $workspaceId, string $channelId, string $callId, string $recordingId): string
|
||||
{
|
||||
return $this->birdVoiceCallRecordingsPath($workspaceId, $channelId, $callId)
|
||||
. '/'
|
||||
. $this->birdEncodePathSegment($recordingId);
|
||||
}
|
||||
|
||||
protected function birdVoiceCallsLogPath(string $workspaceId): string
|
||||
{
|
||||
return '/workspaces/' . $this->birdEncodePathSegment($workspaceId) . '/channels/calls';
|
||||
}
|
||||
|
||||
protected function birdFlashCallsBasePath(string $workspaceId, string $channelId): string
|
||||
{
|
||||
return '/workspaces/'
|
||||
. $this->birdEncodePathSegment($workspaceId)
|
||||
. '/channels/'
|
||||
. $this->birdEncodePathSegment($channelId)
|
||||
. '/flashcalls';
|
||||
}
|
||||
|
||||
protected function birdFlashCallPath(string $workspaceId, string $channelId, string $callId): string
|
||||
{
|
||||
return $this->birdFlashCallsBasePath($workspaceId, $channelId)
|
||||
. '/'
|
||||
. $this->birdEncodePathSegment($callId);
|
||||
}
|
||||
|
||||
protected function birdFlashCallsHangupPath(string $workspaceId, string $channelId): string
|
||||
{
|
||||
return $this->birdFlashCallsBasePath($workspaceId, $channelId) . '/hangup';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +147,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
if ((int)$lane->getWashStartTime() <= 0) {
|
||||
$lane->setWashStartTime(time());
|
||||
}
|
||||
$washStartedAt = (int)$lane->getWashStartTime();
|
||||
$this->enableCleanerRelayForStartedWash($lane);
|
||||
|
||||
$session->markMachineStartTriggered();
|
||||
$session->markMachineStartTriggered(
|
||||
$washStartedAt > 0 ? date('Y-m-d H:i:s', $washStartedAt) : null
|
||||
);
|
||||
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::MACHINE_START_TRIGGERED, $payload + [
|
||||
'lane_id' => $laneId,
|
||||
'reg' => $effectiveReg,
|
||||
|
||||
@@ -6,6 +6,7 @@ use classes\db;
|
||||
use classes\object_property;
|
||||
use classes\selfserve;
|
||||
use classes\selfserve_schema_bootstrap;
|
||||
use DateTime;
|
||||
use modules\selfserve\helpers\selfserve_wash_session_status;
|
||||
use traits\db_object_t;
|
||||
|
||||
@@ -26,6 +27,7 @@ class selfserve_wash_sessions_o extends db
|
||||
public object_property $machine_relay_enabled_at;
|
||||
public object_property $machine_start_triggered;
|
||||
public object_property $machine_start_triggered_at;
|
||||
public object_property $wash_started_at;
|
||||
public object_property $order_id;
|
||||
public object_property $completed_at;
|
||||
public object_property $metadata_json;
|
||||
@@ -84,6 +86,7 @@ class selfserve_wash_sessions_o extends db
|
||||
$this->machine_relay_enabled_at = new object_property($this->table, $this->id, 'machine_relay_enabled_at', 'datetime', false);
|
||||
$this->machine_start_triggered = new object_property($this->table, $this->id, 'machine_start_triggered', 'bool', false);
|
||||
$this->machine_start_triggered_at = new object_property($this->table, $this->id, 'machine_start_triggered_at', 'datetime', false);
|
||||
$this->wash_started_at = new object_property($this->table, $this->id, 'wash_started_at', 'datetime', false);
|
||||
$this->order_id = new object_property($this->table, $this->id, 'order_id', 'int', false);
|
||||
$this->completed_at = new object_property($this->table, $this->id, 'completed_at', 'datetime', false);
|
||||
$this->metadata_json = new object_property($this->table, $this->id, 'metadata_json', 'json', false);
|
||||
@@ -116,11 +119,13 @@ class selfserve_wash_sessions_o extends db
|
||||
$this->machine_relay_enabled_at->set(null);
|
||||
}
|
||||
|
||||
public function markMachineStartTriggered(): void
|
||||
public function markMachineStartTriggered(?string $washStartedAt = null): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$resolvedWashStartedAt = $washStartedAt ?? $now;
|
||||
$this->machine_start_triggered->set(true);
|
||||
$this->machine_start_triggered_at->set($now);
|
||||
$this->wash_started_at->set($resolvedWashStartedAt);
|
||||
if ((bool)$this->machine_relay_enabled->value() !== true) {
|
||||
$this->machine_relay_enabled->set(true);
|
||||
$this->machine_relay_enabled_at->set($now);
|
||||
@@ -189,6 +194,7 @@ class selfserve_wash_sessions_o extends db
|
||||
'machine_relay_enabled_at' => $this->machine_relay_enabled_at->value() === null ? null : (string)$this->machine_relay_enabled_at->value(),
|
||||
'machine_start_triggered' => (bool)$this->machine_start_triggered->value(),
|
||||
'machine_start_triggered_at' => $this->machine_start_triggered_at->value() === null ? null : (string)$this->machine_start_triggered_at->value(),
|
||||
'wash_started_at' => $this->wash_started_at->value() === null ? null : (string)$this->wash_started_at->value(),
|
||||
'order_id' => $this->order_id->value() === null ? null : (int)$this->order_id->value(),
|
||||
'completed_at' => $this->completed_at->value() === null ? null : (string)$this->completed_at->value(),
|
||||
'metadata' => (array)($this->metadata_json->value() ?? []),
|
||||
@@ -196,4 +202,26 @@ class selfserve_wash_sessions_o extends db
|
||||
'updated_at' => $this->updated_at->value() === null ? null : (string)$this->updated_at->value(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getElapsedMinutes(): int
|
||||
{
|
||||
$startAt = $this->wash_started_at->value() ?? $this->machine_start_triggered_at->value();
|
||||
if ($startAt === null || trim((string)$startAt) === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$start = new DateTime((string)$startAt);
|
||||
$now = new DateTime();
|
||||
} catch (\Throwable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($start > $now) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$diff = $start->diff($now);
|
||||
return (int)(($diff->days * 24 * 60) + ($diff->h * 60) + $diff->i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,158 +2,199 @@
|
||||
|
||||
namespace routes;
|
||||
|
||||
require_once WD . '/classes/bird.php';
|
||||
require_once WD . '/traits/route_t.php';
|
||||
require_once WD . '/traits/bird_route_helpers_t.php';
|
||||
require_once WD . '/traits/bird_route_validation_t.php';
|
||||
require_once WD . '/modules/bird/helpers/bird_request_schemas.php';
|
||||
|
||||
use bird\helpers\bird_request_schemas;
|
||||
use classes\bird;
|
||||
use traits\bird_route_helpers_t;
|
||||
use traits\bird_route_validation_t;
|
||||
use traits\route_t;
|
||||
|
||||
class birdVoiceCallsRoute
|
||||
{
|
||||
use route_t, bird_route_helpers_t;
|
||||
use route_t, bird_route_helpers_t, bird_route_validation_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// List workspace call log
|
||||
$this->get('/bird/voice/calls/log', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_log_list');
|
||||
|
||||
$client = new bird();
|
||||
$workspaceId = $this->birdResolveWorkspaceId($client);
|
||||
|
||||
$query = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$query = $this->birdNormalizeCsvField($query, 'channelId');
|
||||
$query = $this->birdNormalizeCsvField($query, 'tag');
|
||||
$query = $this->birdValidateSchema($query, bird_request_schemas::voiceCallsLogQuery());
|
||||
|
||||
$res = $client->getVoiceCallsLog($workspaceId, $query);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_log_list' => 'List voice call log entries via Bird',
|
||||
]);
|
||||
|
||||
// Create a voice call
|
||||
$this->post('/bird/voice/calls', function () {
|
||||
global $response;
|
||||
// Permission: create/place a voice call via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_create');
|
||||
|
||||
$client = new bird();
|
||||
// Require workspace/channel per Bird API docs
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
$res = $client->createVoiceCall($ws, $ch, $payload);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceCreateCallBody());
|
||||
|
||||
$res = $client->createVoiceCall($workspaceId, $channelId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_create' => 'Create/place a voice call via Bird',
|
||||
]);
|
||||
|
||||
// List voice calls (passthrough of query filters)
|
||||
// List voice calls
|
||||
$this->get('/bird/voice/calls', function () {
|
||||
global $response;
|
||||
// Permission: list voice calls via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_list');
|
||||
|
||||
$client = new bird();
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$query = $this->getParametersAsArray();
|
||||
unset($query['workspaceId'], $query['channelId']);
|
||||
$res = $client->listVoiceCalls($ws, $ch, $query);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$query = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$query = $this->birdNormalizeCsvField($query, 'tag');
|
||||
$query = $this->birdValidateSchema($query, bird_request_schemas::voiceListCallsQuery());
|
||||
|
||||
$res = $client->listVoiceCalls($workspaceId, $channelId, $query);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_list' => 'List voice calls via Bird',
|
||||
]);
|
||||
|
||||
// Get a specific call by ID
|
||||
// Get a voice call by ID
|
||||
$this->get('/bird/voice/calls/{id}', function () {
|
||||
global $response;
|
||||
// Permission: get a specific voice call via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_get');
|
||||
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$res = $client->getVoiceCall($ws, $ch, $id);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$res = $client->getVoiceCall($workspaceId, $channelId, $callId);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_get' => 'Get a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Update a voice call by ID
|
||||
$this->patch('/bird/voice/calls/{id}', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_update');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceUpdateCallBody());
|
||||
|
||||
$res = $client->updateVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_update' => 'Update a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Answer an incoming call by ID
|
||||
$this->post('/bird/voice/calls/{id}/answer', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_answer');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::noBody());
|
||||
|
||||
$res = $client->answerVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_answer' => 'Answer a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Mark call as ringing by ID
|
||||
$this->post('/bird/voice/calls/{id}/ringing', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_ringing');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::noBody());
|
||||
|
||||
$res = $client->ringVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_ringing' => 'Mark a voice call as ringing via Bird',
|
||||
]);
|
||||
|
||||
// Hang up an active call by ID
|
||||
$this->post('/bird/voice/calls/{id}/hangup', function () {
|
||||
global $response;
|
||||
// Permission: hang up a specific voice call via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_hangup');
|
||||
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$payload = [];
|
||||
$cause = $this->normalizeOptionalString($this->fromRequest('cause') ?? $this->fromQuery('cause'));
|
||||
if ($cause !== '') {
|
||||
$payload['cause'] = $cause;
|
||||
}
|
||||
$res = $client->hangupVoiceCall($ws, $ch, $id, $payload);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceHangupBody());
|
||||
|
||||
$res = $client->hangupVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_hangup' => 'Hang up a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Say a message on an active call and hang up afterwards
|
||||
// Playback media on an active call by ID
|
||||
$this->post('/bird/voice/calls/{id}/playback', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_playback');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voicePlaybackBody());
|
||||
|
||||
$res = $client->playbackVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_playback' => 'Playback media on a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Say a message on an active call
|
||||
$this->post('/bird/voice/calls/{id}/say', function () {
|
||||
global $response;
|
||||
// Permission: say a message on a voice call via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_say');
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
|
||||
// Bird's /say REST API endpoint plays the message.
|
||||
// To fulfill the requirement of hanging up afterwards, we should ensure the call is terminated.
|
||||
// Many Bird actions support a 'hangup' field in the payload to terminate the call after the action is complete.
|
||||
if (!isset($payload['hangup'])) {
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceSayBody());
|
||||
if (!array_key_exists('hangup', $payload)) {
|
||||
$payload['hangup'] = true;
|
||||
}
|
||||
|
||||
$res = $client->sayMessage($ws, $ch, $id, $payload);
|
||||
$res = $client->sayMessage($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_say' => 'Say a message on a voice call by ID via Bird',
|
||||
@@ -162,56 +203,156 @@ class birdVoiceCallsRoute
|
||||
// Gather input on an active call
|
||||
$this->post('/bird/voice/calls/{id}/gather', function () {
|
||||
global $response;
|
||||
// Permission: gather input on a voice call via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_gather');
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
|
||||
$res = $client->gatherMessage($ws, $ch, $id, $payload);
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceGatherBody());
|
||||
|
||||
$res = $client->gatherMessage($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_gather' => 'Gather input on a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Bridge current call to another destination
|
||||
$this->post('/bird/voice/calls/{id}/bridge', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_bridge');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceBridgeBody());
|
||||
|
||||
$res = $client->bridgeVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_bridge' => 'Bridge a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Record call command endpoint
|
||||
$this->post('/bird/voice/calls/{id}/record', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_record');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceRecordBody());
|
||||
|
||||
$res = $client->recordVoiceCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_record' => 'Record a voice call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Start call recording session
|
||||
$this->post('/bird/voice/calls/{id}/recordings', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_recordings_create');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceRecordingsCreateBody());
|
||||
|
||||
$res = $client->createVoiceCallRecordingSession($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_recordings_create' => 'Create a voice call recording session via Bird',
|
||||
]);
|
||||
|
||||
// List call recordings
|
||||
$this->get('/bird/voice/calls/{id}/recordings', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_recordings_list');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$query = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$query = $this->birdValidateSchema($query, bird_request_schemas::voiceRecordingsListQuery());
|
||||
|
||||
$res = $client->listVoiceCallRecordings($workspaceId, $channelId, $callId, $query);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_recordings_list' => 'List voice call recordings via Bird',
|
||||
]);
|
||||
|
||||
// Get single call recording
|
||||
$this->get('/bird/voice/calls/{id}/recordings/{recordingId}', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_recordings_get');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
$recordingId = $this->birdResolveRouteRecordingId();
|
||||
|
||||
$res = $client->getVoiceCallRecording($workspaceId, $channelId, $callId, $recordingId);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_recordings_get' => 'Get a voice call recording by ID via Bird',
|
||||
]);
|
||||
|
||||
// Update single call recording
|
||||
$this->patch('/bird/voice/calls/{id}/recordings/{recordingId}', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_recordings_update');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
$recordingId = $this->birdResolveRouteRecordingId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceRecordingUpdateBody());
|
||||
|
||||
$res = $client->updateVoiceCallRecording($workspaceId, $channelId, $callId, $recordingId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_recordings_update' => 'Update a voice call recording via Bird',
|
||||
]);
|
||||
|
||||
// Get call insights
|
||||
$this->get('/bird/voice/calls/{id}/insights', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_insights_get');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$res = $client->getVoiceCallInsights($workspaceId, $channelId, $callId);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_calls_insights_get' => 'Get voice call insights by call ID via Bird',
|
||||
]);
|
||||
|
||||
// Place test outbound call and hang up when accepted
|
||||
$this->post('/bird/voice/calls/test-outbound', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_calls_test_outbound');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::voiceTestOutboundBody());
|
||||
|
||||
try {
|
||||
$result = $client->createOutboundTestCallAndHangupWhenAccepted($ws, $ch, $payload);
|
||||
$result = $client->createOutboundTestCallAndHangupWhenAccepted($workspaceId, $channelId, $payload);
|
||||
$response->success($result);
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 500);
|
||||
|
||||
@@ -2,38 +2,36 @@
|
||||
|
||||
namespace routes;
|
||||
|
||||
require_once WD . '/classes/bird.php';
|
||||
require_once WD . '/traits/route_t.php';
|
||||
require_once WD . '/traits/bird_route_helpers_t.php';
|
||||
require_once WD . '/traits/bird_route_validation_t.php';
|
||||
require_once WD . '/modules/bird/helpers/bird_request_schemas.php';
|
||||
|
||||
use bird\helpers\bird_request_schemas;
|
||||
use classes\bird;
|
||||
use traits\bird_route_helpers_t;
|
||||
use traits\bird_route_validation_t;
|
||||
use traits\route_t;
|
||||
|
||||
class birdVoiceFlashCallsRoute
|
||||
{
|
||||
use route_t, bird_route_helpers_t;
|
||||
use route_t, bird_route_helpers_t, bird_route_validation_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// Create a flash call
|
||||
$this->post('/bird/voice/flash-calls', function () {
|
||||
global $response;
|
||||
// Permission: create/place a flash call via Bird
|
||||
self::requirePermission('modules_bird_voice_flash_calls_create');
|
||||
|
||||
$client = new bird();
|
||||
// Require workspace/channel per Bird API docs
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
$res = $client->sendPostRequest($base, $payload);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::flashCreateBody());
|
||||
|
||||
$res = $client->createFlashCall($workspaceId, $channelId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_create' => 'Create/place a flash call via Bird',
|
||||
@@ -42,24 +40,15 @@ class birdVoiceFlashCallsRoute
|
||||
// List flash calls
|
||||
$this->get('/bird/voice/flash-calls', function () {
|
||||
global $response;
|
||||
// Permission: list flash calls via Bird
|
||||
self::requirePermission('modules_bird_voice_flash_calls_list');
|
||||
|
||||
$client = new bird();
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||
$query = $this->getParametersAsArray();
|
||||
unset($query['workspaceId'], $query['channelId']);
|
||||
$res = $client->sendGetRequest($base, $query);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$query = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$query = $this->birdValidateSchema($query, bird_request_schemas::flashListQuery());
|
||||
|
||||
$res = $client->listFlashCalls($workspaceId, $channelId, $query);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_list' => 'List flash calls via Bird',
|
||||
@@ -68,87 +57,68 @@ class birdVoiceFlashCallsRoute
|
||||
// Get a specific flash call by ID
|
||||
$this->get('/bird/voice/flash-calls/{id}', function () {
|
||||
global $response;
|
||||
// Permission: get a specific flash call via Bird
|
||||
self::requirePermission('modules_bird_voice_flash_calls_get');
|
||||
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||
$res = $client->sendGetRequest($base . '/' . rawurlencode($id));
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$res = $client->getFlashCall($workspaceId, $channelId, $callId);
|
||||
$response->success($res ?? []);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_get' => 'Get a flash call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Complete/end a flash call by ID (POST to the resource)
|
||||
// Complete/end a flash call by ID
|
||||
$this->post('/bird/voice/flash-calls/{id}', function () {
|
||||
global $response;
|
||||
// Permission: complete/end a flash call via Bird
|
||||
self::requirePermission('modules_bird_voice_flash_calls_end');
|
||||
|
||||
$client = new bird();
|
||||
$id = (string)$this->fromRoute('id');
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
// Forward any body fields (e.g., result/status) transparently
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
$res = $client->sendPostRequest($base . '/' . rawurlencode($id), $payload);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
$callId = $this->birdResolveRouteCallId();
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::flashEndBody());
|
||||
|
||||
$res = $client->endFlashCall($workspaceId, $channelId, $callId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_end' => 'Complete/end a flash call by ID via Bird',
|
||||
]);
|
||||
|
||||
// Complete/end a flash call by using from/to numbers
|
||||
$this->post('/bird/voice/flash-calls/end', function () {
|
||||
// Hang up flash calls by payload
|
||||
$this->post('/bird/voice/flash-calls/hangup', function () {
|
||||
global $response;
|
||||
// Permission: complete/end a flash call by numbers via Bird
|
||||
self::requirePermission('modules_bird_voice_flash_calls_end_by_numbers');
|
||||
self::requirePermission('modules_bird_voice_flash_calls_hangup');
|
||||
|
||||
$client = new bird();
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
// Basic validation hints: expect 'from' and 'to' but let Bird validate strictly
|
||||
$res = $client->sendPostRequest($base . '/end', $payload);
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::flashHangupBody());
|
||||
|
||||
$res = $client->hangupFlashCall($workspaceId, $channelId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_end_by_numbers' => 'Complete/end a flash call using from/to numbers via Bird',
|
||||
'modules_bird_voice_flash_calls_hangup' => 'Hang up flash calls via Bird',
|
||||
]);
|
||||
|
||||
// Compatibility alias for flash hangup endpoint
|
||||
$this->post('/bird/voice/flash-calls/end', function () {
|
||||
global $response;
|
||||
self::requirePermission('modules_bird_voice_flash_calls_end_by_numbers');
|
||||
|
||||
$client = new bird();
|
||||
[$workspaceId, $channelId] = $this->birdResolveWorkspaceAndChannelIds($client);
|
||||
|
||||
$payload = $this->birdPayloadWithout(['workspaceId', 'channelId']);
|
||||
$payload = $this->birdValidateSchema($payload, bird_request_schemas::flashHangupBody());
|
||||
|
||||
$res = $client->hangupFlashCall($workspaceId, $channelId, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_flash_calls_end_by_numbers' => 'Compatibility alias for hanging up flash calls via Bird',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use classes\stripe;
|
||||
use modules\selfserve\helpers\selfserve_lane_command;
|
||||
use modules\selfserve\helpers\selfserve_lane_port;
|
||||
use modules\selfserve\helpers\selfserve_lane_relay;
|
||||
use modules\selfserve\helpers\selfserve_wash_session_status;
|
||||
use objects\departments_o;
|
||||
use objects\logs_o;
|
||||
use objects\orders_o;
|
||||
@@ -220,13 +221,30 @@ class moduleSelfServeRoute
|
||||
$customer = $build_customer($customer_number);
|
||||
$vehicle = $build_vehicle($vehicle_id, $session_reg);
|
||||
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
||||
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
||||
if ($wash_started_at === null) {
|
||||
$wash_started_at = $machine_start_triggered_at;
|
||||
}
|
||||
if ($wash_started_at === null) {
|
||||
$wash_started_at = $format_wash_started_at($selfserve->lane($lane_id)->getWashStartTime());
|
||||
}
|
||||
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
||||
$included_minutes = $machine_relay_enabled ? $included_minutes_when_machine_enabled : null;
|
||||
$status = $session->status->value();
|
||||
$in_progress_statusses = [
|
||||
selfserve_wash_session_status::MACHINE_RELAY_ENABLED->value,
|
||||
selfserve_wash_session_status::READY_FOR_MACHINE_START->value,
|
||||
selfserve_wash_session_status::MACHINE_STARTED->value,
|
||||
selfserve_wash_session_status::PENDING_QUESTIONS->value,
|
||||
selfserve_wash_session_status::MACHINE_NOT_ALLOWED->value
|
||||
];
|
||||
$in_progress = in_array($status, $in_progress_statusses);
|
||||
|
||||
$response->success([
|
||||
'lane_id' => $lane_id,
|
||||
'in_progress' => true,
|
||||
'elapsed_minutes' => 0, // TODO: Start when the session was created.
|
||||
'status' => (string)$session->status->value(),
|
||||
'in_progress' => $in_progress,
|
||||
'elapsed_minutes' => $session->getElapsedMinutes(),
|
||||
'session' => [
|
||||
'id' => (int)$session->id,
|
||||
'status' => (string)$session->status->value(),
|
||||
@@ -234,13 +252,13 @@ class moduleSelfServeRoute
|
||||
'customer_number' => $customer_number,
|
||||
'vehicle_id' => $vehicle_id,
|
||||
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
||||
'included_minutes' => $included_minutes,
|
||||
'included_minutes' => $included_minutes ?? 0,
|
||||
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
||||
'machine_relay_enabled' => $machine_relay_enabled,
|
||||
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
||||
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
||||
'machine_start_triggered_at' => $machine_start_triggered_at,
|
||||
'wash_started_at' => $machine_start_triggered_at,
|
||||
'wash_started_at' => $wash_started_at,
|
||||
'created_at' => (string)$session->created_at->value(),
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
|
||||
@@ -27,10 +27,17 @@ class moduleWeatherAPIRoute
|
||||
private const DEPARTMENT_WEATHER_REFRESH_LOCK_PREFIX = 'departments_weather:refresh_lock:v1:';
|
||||
private const DEPARTMENT_WEATHER_TARGET_DEGRADED_KEY = 'weather_status_degraded_threshold';
|
||||
private const DEPARTMENT_WEATHER_TARGET_HEALTHY_KEY = 'weather_status_healthy_threshold';
|
||||
private const DEFAULT_DEPARTMENT_WEATHER_DEGRADED_THRESHOLD = 1.0;
|
||||
private const DEFAULT_DEPARTMENT_WEATHER_HEALTHY_THRESHOLD = 1.3;
|
||||
private const DEPARTMENT_WEATHER_STATUS_SEVERITY = [
|
||||
'healthy' => 1,
|
||||
'degraded' => 2,
|
||||
'unhealthy' => 3,
|
||||
/**
|
||||
* Below 1.0 cars/workhour = red
|
||||
* 1.0-1.3 cars/workhour = yellow
|
||||
* Above 1.3 cars/workhour = green
|
||||
*/
|
||||
'healthy' => self::DEFAULT_DEPARTMENT_WEATHER_HEALTHY_THRESHOLD,
|
||||
'degraded' => self::DEFAULT_DEPARTMENT_WEATHER_DEGRADED_THRESHOLD,
|
||||
'unhealthy' => 0.0,
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
@@ -905,8 +912,8 @@ class moduleWeatherAPIRoute
|
||||
}
|
||||
|
||||
$target = $this->normalizeDepartmentWeatherTarget([
|
||||
'degraded_threshold' => $department->variables->getVariable(self::DEPARTMENT_WEATHER_TARGET_DEGRADED_KEY),
|
||||
'healthy_threshold' => $department->variables->getVariable(self::DEPARTMENT_WEATHER_TARGET_HEALTHY_KEY),
|
||||
'degraded_threshold' => $department->variables->getVariable(self::DEPARTMENT_WEATHER_TARGET_DEGRADED_KEY) ?? self::DEFAULT_DEPARTMENT_WEATHER_DEGRADED_THRESHOLD,
|
||||
'healthy_threshold' => $department->variables->getVariable(self::DEPARTMENT_WEATHER_TARGET_HEALTHY_KEY) ?? self::DEFAULT_DEPARTMENT_WEATHER_HEALTHY_THRESHOLD,
|
||||
]);
|
||||
if ($target !== null) {
|
||||
$targets_by_department[$department_id] = $target;
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
app_require('classes/bird.php');
|
||||
app_require('modules/bird/bird_c.php');
|
||||
app_require('modules/bird/config/bird_enabled_c.php');
|
||||
app_require('modules/bird/config/bird_api_key_c.php');
|
||||
app_require('modules/bird/config/bird_server_url_c.php');
|
||||
|
||||
use bird\bird_c;
|
||||
use bird\config\bird_api_key_c;
|
||||
use bird\config\bird_enabled_c;
|
||||
use bird\config\bird_server_url_c;
|
||||
use classes\bird;
|
||||
|
||||
if (!class_exists('BirdApiTransportCapture')) {
|
||||
class BirdApiTransportCapture extends bird
|
||||
{
|
||||
/** @var array<int,array{method:string,endpoint:string,payload:array}> */
|
||||
public array $calls = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function sendPostRequest(string $endpoint, array $data): array|object|null
|
||||
{
|
||||
$this->calls[] = ['method' => 'POST', 'endpoint' => $endpoint, 'payload' => $data];
|
||||
return ['method' => 'POST', 'endpoint' => $endpoint, 'payload' => $data];
|
||||
}
|
||||
|
||||
public function sendGetRequest(string $endpoint, array $query = []): array|object|null
|
||||
{
|
||||
$this->calls[] = ['method' => 'GET', 'endpoint' => $endpoint, 'payload' => $query];
|
||||
return ['method' => 'GET', 'endpoint' => $endpoint, 'payload' => $query];
|
||||
}
|
||||
|
||||
public function sendPatchRequest(string $endpoint, array $data = []): array|object|null
|
||||
{
|
||||
$this->calls[] = ['method' => 'PATCH', 'endpoint' => $endpoint, 'payload' => $data];
|
||||
return ['method' => 'PATCH', 'endpoint' => $endpoint, 'payload' => $data];
|
||||
}
|
||||
|
||||
public function sendDeleteRequest(string $endpoint, array $query = []): array|object|null
|
||||
{
|
||||
$this->calls[] = ['method' => 'DELETE', 'endpoint' => $endpoint, 'payload' => $query];
|
||||
return ['method' => 'DELETE', 'endpoint' => $endpoint, 'payload' => $query];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('BirdEnabledConfigStub')) {
|
||||
class BirdEnabledConfigStub extends bird_enabled_c
|
||||
{
|
||||
public function __construct(private bool $enabled = true)
|
||||
{
|
||||
}
|
||||
|
||||
public function isTrue(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function getVariableValue(): mixed
|
||||
{
|
||||
return $this->enabled ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('BirdApiKeyConfigStub')) {
|
||||
class BirdApiKeyConfigStub extends bird_api_key_c
|
||||
{
|
||||
public function __construct(private string $apiKey = 'test-key')
|
||||
{
|
||||
}
|
||||
|
||||
public function getVariableValue(): mixed
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('BirdServerUrlConfigStub')) {
|
||||
class BirdServerUrlConfigStub extends bird_server_url_c
|
||||
{
|
||||
public function __construct(private string $baseUrl = 'https://api.bird.test')
|
||||
{
|
||||
}
|
||||
|
||||
public function getVariableValue(): mixed
|
||||
{
|
||||
return $this->baseUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('BirdModuleConfigStub')) {
|
||||
class BirdModuleConfigStub extends bird_c
|
||||
{
|
||||
public function __construct(string $apiKey = 'test-key', string $baseUrl = 'https://api.bird.test')
|
||||
{
|
||||
$this->enabled = new BirdEnabledConfigStub(true);
|
||||
$this->api_key = new BirdApiKeyConfigStub($apiKey);
|
||||
$this->server_url = new BirdServerUrlConfigStub($baseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('BirdHttpHarness')) {
|
||||
class BirdHttpHarness extends bird
|
||||
{
|
||||
/** @var array<int,array{method:string,url:string,headers:array,body:string}> */
|
||||
public array $httpCalls = [];
|
||||
private int $stubStatus;
|
||||
private string|false|null $stubBody;
|
||||
|
||||
public function __construct(int $status = 200, string|false|null $body = '{"ok":true}', string $apiKey = 'test-key', string $baseUrl = 'https://api.bird.test')
|
||||
{
|
||||
$this->stubStatus = $status;
|
||||
$this->stubBody = $body;
|
||||
$this->config = new BirdModuleConfigStub($apiKey, $baseUrl);
|
||||
}
|
||||
|
||||
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||
{
|
||||
$this->httpCalls[] = [
|
||||
'method' => strtoupper($method),
|
||||
'url' => $url,
|
||||
'headers' => $headers,
|
||||
'body' => $body,
|
||||
];
|
||||
|
||||
return [
|
||||
'status_code' => $this->stubStatus,
|
||||
'body' => $this->stubBody,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('maps voice call facade methods to documented endpoints and methods', function (): void {
|
||||
$client = new BirdApiTransportCapture();
|
||||
|
||||
$client->updateVoiceCall('ws', 'ch', 'call', ['status' => 'completed']);
|
||||
$client->answerVoiceCall('ws', 'ch', 'call');
|
||||
$client->ringVoiceCall('ws', 'ch', 'call');
|
||||
$client->hangupVoiceCall('ws', 'ch', 'call', ['cause' => 'busy']);
|
||||
$client->playbackVoiceCall('ws', 'ch', 'call', ['media' => ['https://example.com/a.mp3']]);
|
||||
$client->sayMessage('ws', 'ch', 'call', ['text' => 'hello']);
|
||||
$client->gatherMessage('ws', 'ch', 'call', ['maxNumKeys' => 1]);
|
||||
$client->bridgeVoiceCall('ws', 'ch', 'call', ['to' => '+4511122233']);
|
||||
$client->recordVoiceCall('ws', 'ch', 'call', ['maxLength' => 60]);
|
||||
|
||||
expect($client->calls[0])->toMatchArray([
|
||||
'method' => 'PATCH',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call',
|
||||
]);
|
||||
expect($client->calls[1])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/answer',
|
||||
]);
|
||||
expect($client->calls[2])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/ringing',
|
||||
]);
|
||||
expect($client->calls[3])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/hangup',
|
||||
]);
|
||||
expect($client->calls[4])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/playback',
|
||||
]);
|
||||
expect($client->calls[5])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/say',
|
||||
]);
|
||||
expect($client->calls[6])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/gather',
|
||||
]);
|
||||
expect($client->calls[7])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/bridge',
|
||||
]);
|
||||
expect($client->calls[8])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/record',
|
||||
]);
|
||||
});
|
||||
|
||||
it('maps recordings insights log and flash methods to documented resources', function (): void {
|
||||
$client = new BirdApiTransportCapture();
|
||||
|
||||
$client->createVoiceCallRecordingSession('ws', 'ch', 'call', ['maxLength' => 30]);
|
||||
$client->listVoiceCallRecordings('ws', 'ch', 'call', ['limit' => 10]);
|
||||
$client->getVoiceCallRecording('ws', 'ch', 'call', 'rec');
|
||||
$client->updateVoiceCallRecording('ws', 'ch', 'call', 'rec', ['status' => 'completed']);
|
||||
$client->getVoiceCallInsights('ws', 'ch', 'call');
|
||||
$client->getVoiceCallsLog('ws', ['limit' => 25]);
|
||||
$client->listFlashCalls('ws', 'ch', ['limit' => 5]);
|
||||
$client->endFlashCall('ws', 'ch', 'flash-call', ['result' => 'verified']);
|
||||
$client->hangupFlashCall('ws', 'ch', ['from' => '+4511122233', 'to' => '+4544455566']);
|
||||
|
||||
expect($client->calls[0])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/recordings',
|
||||
]);
|
||||
expect($client->calls[1])->toMatchArray([
|
||||
'method' => 'GET',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/recordings',
|
||||
]);
|
||||
expect($client->calls[2])->toMatchArray([
|
||||
'method' => 'GET',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/recordings/rec',
|
||||
]);
|
||||
expect($client->calls[3])->toMatchArray([
|
||||
'method' => 'PATCH',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/recordings/rec',
|
||||
]);
|
||||
expect($client->calls[4])->toMatchArray([
|
||||
'method' => 'GET',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/calls/call/insights',
|
||||
]);
|
||||
expect($client->calls[5])->toMatchArray([
|
||||
'method' => 'GET',
|
||||
'endpoint' => '/workspaces/ws/channels/calls',
|
||||
]);
|
||||
expect($client->calls[6])->toMatchArray([
|
||||
'method' => 'GET',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/flashcalls',
|
||||
]);
|
||||
expect($client->calls[7])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/flashcalls/flash-call',
|
||||
]);
|
||||
expect($client->calls[8])->toMatchArray([
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/workspaces/ws/channels/ch/flashcalls/hangup',
|
||||
]);
|
||||
});
|
||||
|
||||
it('encodes path segments before building outbound endpoints', function (): void {
|
||||
$client = new BirdApiTransportCapture();
|
||||
|
||||
$client->getVoiceCall('workspace a', 'channel/a', 'call 1');
|
||||
$client->getVoiceCallRecording('workspace a', 'channel/a', 'call 1', 'recording/1');
|
||||
$client->getFlashCall('workspace a', 'channel/a', 'flash 1');
|
||||
|
||||
expect($client->calls[0]['endpoint'])->toBe('/workspaces/workspace%20a/channels/channel%2Fa/calls/call%201');
|
||||
expect($client->calls[1]['endpoint'])->toBe('/workspaces/workspace%20a/channels/channel%2Fa/calls/call%201/recordings/recording%2F1');
|
||||
expect($client->calls[2]['endpoint'])->toBe('/workspaces/workspace%20a/channels/channel%2Fa/flashcalls/flash%201');
|
||||
});
|
||||
|
||||
it('serializes query parameters for GET transport requests', function (): void {
|
||||
$client = new BirdHttpHarness();
|
||||
|
||||
$response = $client->sendGetRequest('/workspaces/ws/channels/calls', [
|
||||
'limit' => 10,
|
||||
'tag' => ['vip', 'north'],
|
||||
'id' => '123',
|
||||
]);
|
||||
|
||||
expect($response)->toBeArray();
|
||||
expect($client->httpCalls)->toHaveCount(1);
|
||||
expect($client->httpCalls[0]['method'])->toBe('GET');
|
||||
expect($client->httpCalls[0]['url'])->toContain('/workspaces/ws/channels/calls?');
|
||||
expect($client->httpCalls[0]['url'])->toContain('limit=10');
|
||||
expect($client->httpCalls[0]['url'])->toContain('tag%5B0%5D=vip');
|
||||
expect($client->httpCalls[0]['url'])->toContain('tag%5B1%5D=north');
|
||||
expect($client->httpCalls[0]['url'])->toContain('id=123');
|
||||
});
|
||||
|
||||
it('maps 4xx and 5xx transport failures into informative exceptions', function (): void {
|
||||
$client4xx = new BirdHttpHarness(422, '{"message":"invalid payload","errors":[{"message":"to is required"}]}');
|
||||
$client5xx = new BirdHttpHarness(503, '{"error":"upstream unavailable"}');
|
||||
|
||||
expect(fn() => $client4xx->sendPostRequest('/workspaces/ws/channels/ch/calls', ['from' => '+4511122233']))
|
||||
->toThrow(Exception::class, 'Bird API request failed with status 422');
|
||||
expect(fn() => $client5xx->sendGetRequest('/workspaces/ws/channels/ch/calls'))
|
||||
->toThrow(Exception::class, 'Bird API request failed with status 503');
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
function bird_openapi_content_or_skip(): string
|
||||
{
|
||||
$candidates = [
|
||||
dirname(__DIR__, 6) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
dirname(__DIR__, 5) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (!is_file($candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($candidate);
|
||||
if ($content !== false) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
test()->markTestSkipped('openapi.yaml is not available in this runtime environment.');
|
||||
}
|
||||
|
||||
function bird_openapi_path_block_or_fail(string $content, string $path): string
|
||||
{
|
||||
$pathMarker = ' ' . $path . ':';
|
||||
$start = strpos($content, $pathMarker);
|
||||
if ($start === false) {
|
||||
throw new RuntimeException('OpenAPI path block not found: ' . $path);
|
||||
}
|
||||
|
||||
$rest = substr($content, $start + strlen($pathMarker));
|
||||
$nextPath = strpos($rest, "\n /");
|
||||
if ($nextPath === false) {
|
||||
return substr($content, $start);
|
||||
}
|
||||
|
||||
return substr($content, $start, strlen($pathMarker) + $nextPath);
|
||||
}
|
||||
|
||||
it('documents all Bird voice call parity endpoints including gather recordings insights and log', function (): void {
|
||||
$content = bird_openapi_content_or_skip();
|
||||
|
||||
expect($content)->toContain('/bird/voice/calls:');
|
||||
expect($content)->toContain('/bird/voice/calls/log:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/answer:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/ringing:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/hangup:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/playback:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/say:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/gather:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/bridge:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/record:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/recordings:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/recordings/{recordingId}:');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/insights:');
|
||||
expect($content)->toContain('/bird/voice/calls/test-outbound:');
|
||||
});
|
||||
|
||||
it('documents flash hangup endpoint and marks end alias as deprecated', function (): void {
|
||||
$content = bird_openapi_content_or_skip();
|
||||
$aliasPath = bird_openapi_path_block_or_fail($content, '/bird/voice/flash-calls/end');
|
||||
|
||||
expect($content)->toContain('/bird/voice/flash-calls:');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/{id}:');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/hangup:');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/end:');
|
||||
expect($aliasPath)->toContain('deprecated: true');
|
||||
expect($aliasPath)->toContain('birdEndFlashCallByNumbers');
|
||||
});
|
||||
|
||||
it('defines request and response schemas for Bird call command recording insight log and flash payloads', function (): void {
|
||||
$content = bird_openapi_content_or_skip();
|
||||
|
||||
expect($content)->toContain('BirdVoiceCallCommandResponse:');
|
||||
expect($content)->toContain('BirdVoiceCallBridgeResponse:');
|
||||
expect($content)->toContain('BirdVoiceCallRecording:');
|
||||
expect($content)->toContain('BirdVoiceCallRecordingListResponse:');
|
||||
expect($content)->toContain('BirdVoiceCallRecordingSingleResponse:');
|
||||
expect($content)->toContain('BirdVoiceCallInsightsResponse:');
|
||||
expect($content)->toContain('BirdVoiceCallsLogResponse:');
|
||||
expect($content)->toContain('BirdFlashCallHangupRequest:');
|
||||
expect($content)->toContain('BirdFlashCallHangupResponse:');
|
||||
|
||||
expect($content)->toContain('BirdVoiceCallCreateRequest:');
|
||||
expect($content)->toContain('BirdVoiceCallUpdateRequest:');
|
||||
expect($content)->toContain('BirdVoiceCallGatherRequest:');
|
||||
expect($content)->toContain('BirdVoiceCallRecordingUpdateRequest:');
|
||||
expect($content)->toContain('BirdFlashCallCreateRequest:');
|
||||
expect($content)->toContain('BirdFlashCallEndRequest:');
|
||||
expect($content)->toContain('BirdTestOutboundCallRequest:');
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
app_require('modules/bird/helpers/bird_request_validator.php');
|
||||
app_require('modules/bird/helpers/bird_request_schemas.php');
|
||||
app_require('traits/bird_route_validation_t.php');
|
||||
|
||||
use bird\helpers\bird_request_schemas;
|
||||
use bird\helpers\bird_request_validator;
|
||||
use traits\bird_route_validation_t;
|
||||
|
||||
if (!class_exists('BirdRouteValidationHarness')) {
|
||||
class BirdRouteValidationHarness
|
||||
{
|
||||
use bird_route_validation_t {
|
||||
birdValidateSchema as public validateSchema;
|
||||
}
|
||||
|
||||
public int $forwardCalls = 0;
|
||||
|
||||
public function validateThenForward(array $payload, array $schema): void
|
||||
{
|
||||
$this->validateSchema($payload, $schema);
|
||||
$this->forwardCalls++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function (): void {
|
||||
global $response;
|
||||
|
||||
$response = new class {
|
||||
public function error(mixed $data, int $status = null): void
|
||||
{
|
||||
$encoded = is_string($data) ? $data : json_encode($data);
|
||||
throw new RuntimeException('HTTP_' . (string)$status . ':' . $encoded);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('accepts valid payloads for create and nested call flow commands', function (): void {
|
||||
$payload = [
|
||||
'from' => '+4532330288',
|
||||
'to' => '+4542331128',
|
||||
'ringTimeout' => 20,
|
||||
'notification' => ['url' => 'https://example.com/webhook'],
|
||||
'callFlow' => [
|
||||
[
|
||||
'command' => 'say',
|
||||
'conditions' => [
|
||||
['variable' => 'keys', 'operator' => 'eq', 'value' => '1'],
|
||||
],
|
||||
'options' => ['text' => 'hello'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$errors = bird_request_validator::validate($payload, bird_request_schemas::voiceCreateCallBody());
|
||||
|
||||
expect($errors)->toBe([]);
|
||||
});
|
||||
|
||||
it('rejects unknown fields and invalid enum values in strict schemas', function (): void {
|
||||
$unknownErrors = bird_request_validator::validate(
|
||||
['to' => '+4542331128', 'unknownField' => 'x'],
|
||||
bird_request_schemas::voiceCreateCallBody()
|
||||
);
|
||||
$enumErrors = bird_request_validator::validate(
|
||||
['cause' => 'completed'],
|
||||
bird_request_schemas::voiceHangupBody()
|
||||
);
|
||||
|
||||
expect($unknownErrors)->not->toBe([]);
|
||||
expect(implode(' | ', $unknownErrors))->toContain('$.unknownField is not allowed');
|
||||
|
||||
expect($enumErrors)->not->toBe([]);
|
||||
expect(implode(' | ', $enumErrors))->toContain('$.cause must be one of: rejected, busy');
|
||||
});
|
||||
|
||||
it('supports CSV normalization in schema validation for log filters', function (): void {
|
||||
$validQuery = [
|
||||
'channelId' => '2cbde4fd-8899-4f2d-95ea-2ab7cc6b8c97,5a6bd8c1-32b7-4a03-8b5a-b6a4a9d7a8b2',
|
||||
'tag' => 'north,vip',
|
||||
];
|
||||
$invalidQuery = [
|
||||
'channelId' => 'not-a-uuid',
|
||||
];
|
||||
|
||||
$validErrors = bird_request_validator::validate($validQuery, bird_request_schemas::voiceCallsLogQuery());
|
||||
$invalidErrors = bird_request_validator::validate($invalidQuery, bird_request_schemas::voiceCallsLogQuery());
|
||||
|
||||
expect($validErrors)->toBe([]);
|
||||
expect($invalidErrors)->not->toBe([]);
|
||||
expect(implode(' | ', $invalidErrors))->toContain('$.channelId[0] must be a UUID');
|
||||
});
|
||||
|
||||
it('validates flash hangup payloads for both supported request shapes', function (): void {
|
||||
$resultMode = [
|
||||
'result' => 'verified',
|
||||
'receivedCli' => '+4542331128',
|
||||
];
|
||||
$numbersMode = [
|
||||
'from' => '+4532330288',
|
||||
'to' => '+4542331128',
|
||||
'result' => 'verified',
|
||||
];
|
||||
$invalid = [
|
||||
'result' => 'ok',
|
||||
];
|
||||
|
||||
$resultErrors = bird_request_validator::validate($resultMode, bird_request_schemas::flashHangupBody());
|
||||
$numbersErrors = bird_request_validator::validate($numbersMode, bird_request_schemas::flashHangupBody());
|
||||
$invalidErrors = bird_request_validator::validate($invalid, bird_request_schemas::flashHangupBody());
|
||||
|
||||
expect($resultErrors)->toBe([]);
|
||||
expect($numbersErrors)->toBe([]);
|
||||
expect($invalidErrors)->not->toBe([]);
|
||||
expect(implode(' | ', $invalidErrors))->toContain('does not match any of the allowed schemas');
|
||||
});
|
||||
|
||||
it('fails before forward step when strict validation fails and forwards when valid', function (): void {
|
||||
$harness = new BirdRouteValidationHarness();
|
||||
|
||||
try {
|
||||
$harness->validateThenForward(['unexpected' => true], bird_request_schemas::noBody());
|
||||
$caught = null;
|
||||
} catch (RuntimeException $e) {
|
||||
$caught = $e;
|
||||
}
|
||||
|
||||
expect($caught)->toBeInstanceOf(RuntimeException::class);
|
||||
expect($caught?->getMessage())->toContain('HTTP_400');
|
||||
expect($harness->forwardCalls)->toBe(0);
|
||||
|
||||
$harness->validateThenForward([], bird_request_schemas::noBody());
|
||||
expect($harness->forwardCalls)->toBe(1);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
it('registers full Bird voice call route surface with strict validation and permissions', function (): void {
|
||||
$content = file_get_contents(app_path('routes/birdVoiceCallsRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('/bird/voice/calls');
|
||||
expect($content)->toContain('/bird/voice/calls/log');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/answer');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/ringing');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/hangup');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/playback');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/say');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/gather');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/bridge');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/record');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/recordings');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/recordings/{recordingId}');
|
||||
expect($content)->toContain('/bird/voice/calls/{id}/insights');
|
||||
expect($content)->toContain('/bird/voice/calls/test-outbound');
|
||||
|
||||
expect($content)->toContain('modules_bird_voice_calls_create');
|
||||
expect($content)->toContain('modules_bird_voice_calls_list');
|
||||
expect($content)->toContain('modules_bird_voice_calls_get');
|
||||
expect($content)->toContain('modules_bird_voice_calls_update');
|
||||
expect($content)->toContain('modules_bird_voice_calls_answer');
|
||||
expect($content)->toContain('modules_bird_voice_calls_ringing');
|
||||
expect($content)->toContain('modules_bird_voice_calls_hangup');
|
||||
expect($content)->toContain('modules_bird_voice_calls_playback');
|
||||
expect($content)->toContain('modules_bird_voice_calls_say');
|
||||
expect($content)->toContain('modules_bird_voice_calls_gather');
|
||||
expect($content)->toContain('modules_bird_voice_calls_bridge');
|
||||
expect($content)->toContain('modules_bird_voice_calls_record');
|
||||
expect($content)->toContain('modules_bird_voice_calls_recordings_create');
|
||||
expect($content)->toContain('modules_bird_voice_calls_recordings_list');
|
||||
expect($content)->toContain('modules_bird_voice_calls_recordings_get');
|
||||
expect($content)->toContain('modules_bird_voice_calls_recordings_update');
|
||||
expect($content)->toContain('modules_bird_voice_calls_insights_get');
|
||||
expect($content)->toContain('modules_bird_voice_calls_log_list');
|
||||
expect($content)->toContain('modules_bird_voice_calls_test_outbound');
|
||||
|
||||
expect($content)->toContain('birdValidateSchema');
|
||||
expect($content)->toContain('birdResolveWorkspaceAndChannelIds');
|
||||
expect($content)->toContain('birdResolveRouteCallId');
|
||||
expect($content)->toContain('birdResolveRouteRecordingId');
|
||||
});
|
||||
|
||||
it('registers flash hangup endpoint and keeps end alias wired as compatibility path', function (): void {
|
||||
$content = file_get_contents(app_path('routes/birdVoiceFlashCallsRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('/bird/voice/flash-calls');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/{id}');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/hangup');
|
||||
expect($content)->toContain('/bird/voice/flash-calls/end');
|
||||
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_create');
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_list');
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_get');
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_end');
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_hangup');
|
||||
expect($content)->toContain('modules_bird_voice_flash_calls_end_by_numbers');
|
||||
|
||||
expect($content)->toContain('flashHangupBody');
|
||||
expect($content)->toContain('hangupFlashCall($workspaceId, $channelId, $payload)');
|
||||
});
|
||||
|
||||
it('keeps Bird number and webhook routes intact', function (): void {
|
||||
$numbers = file_get_contents(app_path('routes/birdNumbersRoute.php'));
|
||||
$webhooks = file_get_contents(app_path('routes/birdVoiceWebhooksRoute.php'));
|
||||
|
||||
expect($numbers)->not->toBeFalse();
|
||||
expect($numbers)->toContain('/bird/numbers');
|
||||
expect($numbers)->toContain('/bird/numbers/{id}');
|
||||
|
||||
expect($webhooks)->not->toBeFalse();
|
||||
expect($webhooks)->toContain('/bird/voice/calls/webhook/inbound');
|
||||
});
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
function economic_v2_openapi_content_or_skip(): string
|
||||
{
|
||||
$candidates = [
|
||||
dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'openapi.yaml', // monorepo root in local workspace
|
||||
dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'openapi.yaml',
|
||||
];
|
||||
$candidates = [];
|
||||
for ($depth = 1; $depth <= 8; $depth++) {
|
||||
$candidates[] = dirname(__DIR__, $depth) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
$cwd = getcwd();
|
||||
if (is_string($cwd) && $cwd !== '') {
|
||||
$candidates[] = $cwd . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
$candidates[] = dirname($cwd) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
$candidates = array_values(array_unique($candidates));
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace classes {
|
||||
class db {}
|
||||
if (!class_exists('classes\\db')) {
|
||||
class db {}
|
||||
}
|
||||
|
||||
class object_property
|
||||
{
|
||||
public function __construct(...$args) {}
|
||||
public function value() { return null; }
|
||||
public function set($v) {}
|
||||
if (!class_exists('classes\\object_property')) {
|
||||
class object_property
|
||||
{
|
||||
public function __construct(...$args) {}
|
||||
public function value() { return null; }
|
||||
public function set($v) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace traits {
|
||||
trait db_object_t {}
|
||||
if (!trait_exists('traits\\db_object_t')) {
|
||||
trait db_object_t {}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -55,4 +61,3 @@ namespace {
|
||||
expect($names)->toContain('MACHINE');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,10 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_program_picker/set:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_cleaner/status:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_cleaner/set:');
|
||||
expect($allowedPathBlock)->toContain('vehicle_type_id:');
|
||||
expect($allowedPathBlock)->toContain('vehicle_type:');
|
||||
expect($summaryPathBlock)->toContain('vehicle_type_id:');
|
||||
expect($summaryPathBlock)->toContain('vehicle_type:');
|
||||
expect($allowedPathBlock)->toContain('name: vehicle_type_id');
|
||||
expect($allowedPathBlock)->toContain('name: vehicle_type');
|
||||
expect($summaryPathBlock)->toContain('name: vehicle_type_id');
|
||||
expect($summaryPathBlock)->toContain('name: vehicle_type');
|
||||
});
|
||||
|
||||
it('defines reusable self-serve wash and machine type schemas', function (): void {
|
||||
@@ -74,6 +74,7 @@ it('defines reusable self-serve wash and machine type schemas', function (): voi
|
||||
expect($content)->toContain('DepartmentSelfserveVehicleConditionMutationResponse:');
|
||||
expect($content)->toContain('machine_type_id:');
|
||||
expect($content)->toContain('SelfServeLaneMachineRelayStatus:');
|
||||
expect($content)->toContain(' wash_started_at:');
|
||||
});
|
||||
|
||||
it('documents in-progress self-serve wash start and machine relay fields', function (): void {
|
||||
|
||||
@@ -8,3 +8,12 @@ it('adds dynamic_images_vehicle_type column for legacy selfserve wash session ta
|
||||
expect($bootstrapContent)->toContain("'dynamic_images_vehicle_type'");
|
||||
expect($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_session_tasks ADD COLUMN dynamic_images_vehicle_type INT NULL AFTER buttons');
|
||||
});
|
||||
|
||||
it('adds wash_started_at column for legacy selfserve wash session schemas', function (): void {
|
||||
$bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php'));
|
||||
|
||||
expect($bootstrapContent)->not->toBeFalse();
|
||||
expect($bootstrapContent)->toContain("'selfserve_wash_sessions'");
|
||||
expect($bootstrapContent)->toContain("'wash_started_at'");
|
||||
expect($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_sessions ADD COLUMN wash_started_at DATETIME NULL AFTER machine_start_triggered_at');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
it('stores wash_started_at in self-serve wash sessions when machine start is triggered', function (): void {
|
||||
$sessionsObject = file_get_contents(app_path('objects/selfserve_wash_sessions_o.php'));
|
||||
|
||||
expect($sessionsObject)->not->toBeFalse();
|
||||
expect($sessionsObject)->toContain('public object_property $wash_started_at;');
|
||||
expect($sessionsObject)->toContain("\$this->wash_started_at = new object_property(");
|
||||
expect($sessionsObject)->toContain("\$this->wash_started_at->set(\$resolvedWashStartedAt);");
|
||||
expect($sessionsObject)->toContain("'wash_started_at' =>");
|
||||
});
|
||||
|
||||
it('passes lane wash start time into the session machine-start marker', function (): void {
|
||||
$washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php'));
|
||||
|
||||
expect($washFlow)->not->toBeFalse();
|
||||
expect($washFlow)->toContain('$washStartedAt = (int)$lane->getWashStartTime();');
|
||||
expect($washFlow)->toContain('$session->markMachineStartTriggered(');
|
||||
expect($washFlow)->toContain("date('Y-m-d H:i:s', \$washStartedAt)");
|
||||
});
|
||||
|
||||
it('resolves in-progress wash_started_at from session with compatibility fallbacks', function (): void {
|
||||
$moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php'));
|
||||
|
||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||
expect($moduleSelfServeRoute)->toContain('$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();');
|
||||
expect($moduleSelfServeRoute)->toContain('$wash_started_at = $machine_start_triggered_at;');
|
||||
expect($moduleSelfServeRoute)->toContain('$wash_started_at = $format_wash_started_at($selfserve->lane($lane_id)->getWashStartTime());');
|
||||
expect($moduleSelfServeRoute)->toContain('\'wash_started_at\' => $wash_started_at');
|
||||
});
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
function department_weather_targets_openapi_content_or_skip(): string
|
||||
{
|
||||
$candidates = [
|
||||
WD . '/openapi.yaml',
|
||||
dirname(WD) . '/openapi.yaml',
|
||||
dirname(WD, 2) . '/openapi.yaml',
|
||||
];
|
||||
$candidates = [WD . '/openapi.yaml'];
|
||||
for ($depth = 1; $depth <= 8; $depth++) {
|
||||
$candidates[] = dirname(WD, $depth) . '/openapi.yaml';
|
||||
}
|
||||
$cwd = getcwd();
|
||||
if (is_string($cwd) && $cwd !== '') {
|
||||
$candidates[] = $cwd . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
$candidates[] = dirname($cwd) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
$candidates = array_values(array_unique($candidates));
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (!is_file($candidate)) {
|
||||
|
||||
+10
-5
@@ -9,11 +9,16 @@ it('includes a date key in department weather timeline entries', function (): vo
|
||||
});
|
||||
|
||||
it('documents the date key in the openapi department weather timeline schema', function (): void {
|
||||
$candidates = [
|
||||
WD . '/openapi.yaml',
|
||||
dirname(WD) . '/openapi.yaml',
|
||||
dirname(WD, 2) . '/openapi.yaml',
|
||||
];
|
||||
$candidates = [WD . '/openapi.yaml'];
|
||||
for ($depth = 1; $depth <= 8; $depth++) {
|
||||
$candidates[] = dirname(WD, $depth) . '/openapi.yaml';
|
||||
}
|
||||
$cwd = getcwd();
|
||||
if (is_string($cwd) && $cwd !== '') {
|
||||
$candidates[] = $cwd . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
$candidates[] = dirname($cwd) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
$candidates = array_values(array_unique($candidates));
|
||||
|
||||
$openApiFile = null;
|
||||
foreach ($candidates as $candidate) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace traits;
|
||||
|
||||
require_once WD . '/classes/bird.php';
|
||||
require_once WD . '/modules/bird/helpers/bird_request_validator.php';
|
||||
|
||||
use bird\helpers\bird_request_validator;
|
||||
use classes\bird;
|
||||
|
||||
trait bird_route_validation_t
|
||||
{
|
||||
private function birdFailValidation(string|array $errors): void
|
||||
{
|
||||
global $response;
|
||||
|
||||
$normalizedErrors = [];
|
||||
if (is_array($errors)) {
|
||||
foreach ($errors as $error) {
|
||||
if (!is_string($error)) {
|
||||
continue;
|
||||
}
|
||||
$trimmed = trim($error);
|
||||
if ($trimmed !== '') {
|
||||
$normalizedErrors[] = $trimmed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$trimmed = trim($errors);
|
||||
if ($trimmed !== '') {
|
||||
$normalizedErrors[] = $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
if ($normalizedErrors === []) {
|
||||
$normalizedErrors[] = 'Validation failed';
|
||||
}
|
||||
|
||||
$response->error([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => array_values(array_unique($normalizedErrors)),
|
||||
], 400);
|
||||
}
|
||||
|
||||
private function birdAssertUuid(string $value, string $field): string
|
||||
{
|
||||
$normalized = trim($value);
|
||||
if ($normalized === '') {
|
||||
$this->birdFailValidation($field . ' is required');
|
||||
}
|
||||
if (!bird_request_validator::isUuid($normalized)) {
|
||||
$this->birdFailValidation($field . ' must be a UUID');
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function birdResolveWorkspaceId(bird $client): string
|
||||
{
|
||||
$workspaceId = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
if ($workspaceId === '') {
|
||||
$workspaceId = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($workspaceId === '') {
|
||||
$this->birdFailValidation('Missing required parameter: workspaceId');
|
||||
}
|
||||
return $this->birdAssertUuid($workspaceId, 'workspaceId');
|
||||
}
|
||||
|
||||
private function birdResolveWorkspaceAndChannelIds(bird $client): array
|
||||
{
|
||||
$workspaceId = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$channelId = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
|
||||
if ($workspaceId === '') {
|
||||
$workspaceId = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($channelId === '') {
|
||||
$channelId = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($workspaceId === '' || $channelId === '') {
|
||||
$this->birdFailValidation('Missing required parameters: workspaceId, channelId');
|
||||
}
|
||||
|
||||
return [
|
||||
$this->birdAssertUuid($workspaceId, 'workspaceId'),
|
||||
$this->birdAssertUuid($channelId, 'channelId'),
|
||||
];
|
||||
}
|
||||
|
||||
private function birdResolveRouteCallId(string $key = 'id'): string
|
||||
{
|
||||
$callId = $this->normalizeOptionalString((string)$this->fromRoute($key));
|
||||
if ($callId === '') {
|
||||
$this->birdFailValidation('Missing required parameter: callId');
|
||||
}
|
||||
return $this->birdAssertUuid($callId, 'callId');
|
||||
}
|
||||
|
||||
private function birdResolveRouteRecordingId(string $key = 'recordingId'): string
|
||||
{
|
||||
$recordingId = $this->normalizeOptionalString((string)$this->fromRoute($key));
|
||||
if ($recordingId === '') {
|
||||
$this->birdFailValidation('Missing required parameter: recordingId');
|
||||
}
|
||||
return $this->birdAssertUuid($recordingId, 'recordingId');
|
||||
}
|
||||
|
||||
private function birdPayloadWithout(array $keys): array
|
||||
{
|
||||
$payload = $this->getParametersAsArray();
|
||||
foreach ($keys as $key) {
|
||||
unset($payload[$key]);
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function birdNormalizeCsvField(array $payload, string $key): array
|
||||
{
|
||||
if (!array_key_exists($key, $payload)) {
|
||||
return $payload;
|
||||
}
|
||||
$value = $payload[$key];
|
||||
if (is_string($value)) {
|
||||
$parts = array_map('trim', explode(',', $value));
|
||||
$payload[$key] = array_values(array_filter($parts, static fn($part) => $part !== ''));
|
||||
return $payload;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$payload[$key] = array_values(array_filter(array_map(static fn($part) => is_scalar($part) ? trim((string)$part) : '', $value), static fn($part) => $part !== ''));
|
||||
return $payload;
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function birdValidateSchema(array $payload, array $schema): array
|
||||
{
|
||||
$errors = bird_request_validator::validate($payload, $schema);
|
||||
if ($errors !== []) {
|
||||
$this->birdFailValidation($errors);
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user