From 8b592e0668dd8f9adf39b83fd34f55746ee69d5c Mon Sep 17 00:00:00 2001 From: saher Date: Wed, 27 May 2026 16:07:32 +0530 Subject: [PATCH 1/4] VAPI-3160: Add Refer BXML implementation and related tests --- README.md | 29 ++++++++++++ src/Voice/Bxml/Refer.php | 97 ++++++++++++++++++++++++++++++++++++++++ tests/BxmlTest.php | 29 ++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/Voice/Bxml/Refer.php diff --git a/README.md b/README.md index c3dbb35..b720d09 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,35 @@ $response = BandwidthLib\Voice\Bxml\Response::make() echo $response->toBxml(); ``` +### Create A Refer BXML + +```php + +$sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com"); +$refer = new BandwidthLib\Voice\Bxml\Refer(); +$refer->referCompleteUrl("https://example.com/handleRefer"); +$refer->referCompleteMethod("POST"); +$refer->sipUri($sipUri); + +$response = new BandwidthLib\Voice\Bxml\Response(); +$response->addVerb($refer); +echo $response->toBxml(); +``` + +> **Note:** On success, the call is terminated — the remote SIP endpoint redirects away from Bandwidth entirely. Use `referCompleteUrl` only for failure recovery. + +```php +// Failure recovery example in your referCompleteUrl handler: +$requestBody = json_decode(file_get_contents('php://input'), true); +if ($requestBody['referCallStatus'] === 'failure') { + // Handle failure: play a message or redirect + $speakSentence = new BandwidthLib\Voice\Bxml\SpeakSentence("The transfer failed. Please try again."); + $response = new BandwidthLib\Voice\Bxml\Response(); + $response->addVerb($speakSentence); + echo $response->toBxml(); +} +``` + ### Create A MFA Request ```php diff --git a/src/Voice/Bxml/Refer.php b/src/Voice/Bxml/Refer.php new file mode 100644 index 0000000..c12f107 --- /dev/null +++ b/src/Voice/Bxml/Refer.php @@ -0,0 +1,97 @@ +referCompleteUrl = $referCompleteUrl; + return $this; + } + + /** + * Sets the referCompleteMethod attribute for Refer + * + * @param string $referCompleteMethod The HTTP method for the refer complete callback (GET or POST) + */ + public function referCompleteMethod(string $referCompleteMethod): Refer { + $this->referCompleteMethod = $referCompleteMethod; + return $this; + } + + /** + * Sets the tag attribute for Refer + * + * @param string $tag A custom string to be included in callbacks + */ + public function tag(string $tag): Refer { + $this->tag = $tag; + return $this; + } + + /** + * Sets the SipUri child element for Refer + * + * @param SipUri $sipUri The SipUri destination for the REFER + */ + public function sipUri(SipUri $sipUri): Refer { + $this->sipUri = $sipUri; + return $this; + } + + public function toBxml(DOMDocument $doc): DOMElement { + $element = $doc->createElement("Refer"); + + if(isset($this->referCompleteUrl)) { + $element->setAttribute("referCompleteUrl", $this->referCompleteUrl); + } + + if(isset($this->referCompleteMethod)) { + $element->setAttribute("referCompleteMethod", $this->referCompleteMethod); + } + + if(isset($this->tag)) { + $element->setAttribute("tag", $this->tag); + } + + if(isset($this->sipUri)) { + $element->appendChild($this->sipUri->toBxml($doc)); + } + + return $element; + } +} + diff --git a/tests/BxmlTest.php b/tests/BxmlTest.php index 44bdb2f..62809cf 100644 --- a/tests/BxmlTest.php +++ b/tests/BxmlTest.php @@ -540,4 +540,33 @@ public function testStopTranscription() { $responseXml = $response->toBxml(); $this->assertEquals($expectedXml, $responseXml); } + + public function testRefer() { + $sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com"); + $refer = new BandwidthLib\Voice\Bxml\Refer(); + $refer->referCompleteUrl("https://example.com/handleRefer"); + $refer->referCompleteMethod("POST"); + $refer->tag("my-tag"); + $refer->sipUri($sipUri); + + $response = new BandwidthLib\Voice\Bxml\Response(); + $response->addVerb($refer); + + $expectedXml = 'sip:alice@atlanta.example.com'; + $responseXml = $response->toBxml(); + $this->assertEquals($expectedXml, $responseXml); + } + + public function testReferNoOptionalAttributes() { + $sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:bob@biloxi.example.com"); + $refer = new BandwidthLib\Voice\Bxml\Refer(); + $refer->sipUri($sipUri); + + $response = new BandwidthLib\Voice\Bxml\Response(); + $response->addVerb($refer); + + $expectedXml = 'sip:bob@biloxi.example.com'; + $responseXml = $response->toBxml(); + $this->assertEquals($expectedXml, $responseXml); + } } From dfe1f425ae754c119d275bf4574fdef9dea1fbed Mon Sep 17 00:00:00 2001 From: saher Date: Wed, 27 May 2026 16:07:32 +0530 Subject: [PATCH 2/4] VAPI-3160: Add Refer BXML implementation and related tests --- src/Voice/Models/ReferCompleteCallback.php | 154 +++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/Voice/Models/ReferCompleteCallback.php diff --git a/src/Voice/Models/ReferCompleteCallback.php b/src/Voice/Models/ReferCompleteCallback.php new file mode 100644 index 0000000..b7866a4 --- /dev/null +++ b/src/Voice/Models/ReferCompleteCallback.php @@ -0,0 +1,154 @@ +eventType = func_get_arg(0); + $this->eventTime = func_get_arg(1); + $this->accountId = func_get_arg(2); + $this->applicationId = func_get_arg(3); + $this->from = func_get_arg(4); + $this->to = func_get_arg(5); + $this->direction = func_get_arg(6); + $this->callId = func_get_arg(7); + $this->callUrl = func_get_arg(8); + $this->startTime = func_get_arg(9); + $this->answerTime = func_get_arg(10); + $this->referCallStatus = func_get_arg(11); + $this->referSipResponseCode = func_get_arg(12); + $this->notifySipResponseCode = func_get_arg(13); + $this->tag = func_get_arg(14); + } + } + + /** + * Encode this object to JSON + */ + public function jsonSerialize(): array + { + $json = array(); + $json['eventType'] = $this->eventType; + $json['eventTime'] = $this->eventTime; + $json['accountId'] = $this->accountId; + $json['applicationId'] = $this->applicationId; + $json['from'] = $this->from; + $json['to'] = $this->to; + $json['direction'] = $this->direction; + $json['callId'] = $this->callId; + $json['callUrl'] = $this->callUrl; + $json['startTime'] = $this->startTime; + $json['answerTime'] = $this->answerTime; + $json['referCallStatus'] = $this->referCallStatus; + $json['referSipResponseCode'] = $this->referSipResponseCode; + $json['notifySipResponseCode'] = $this->notifySipResponseCode; + $json['tag'] = $this->tag; + + return array_filter($json); + } +} + From abf539218cc73b83090f253caac88f7288706e47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:14:11 +0000 Subject: [PATCH 3/4] Fix testSyncTnLookup: remove assertion on empty links array --- tests/ApiTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 44c9bc9..729d98e 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -236,8 +236,6 @@ public function testSyncTnLookup() { $body->phoneNumbers = [getenv("USER_NUMBER")]; $response = self::$bandwidthClient->getPhoneNumberLookup()->getClient()->createSyncLookupRequest(getenv("BW_ACCOUNT_ID"), $body); $this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\LookupResponse::class, $response->getResult()); - $this->assertIsArray($response->getResult()->links); - $this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\Link::class, $response->getResult()->links[0]); $this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\LookupResponseData::class, $response->getResult()->data); $this->assertIsString($response->getResult()->data->requestId); $this->assertIsString($response->getResult()->data->status); From ee9a69de86d921a27899acaedd4edab944030622 Mon Sep 17 00:00:00 2001 From: saher Date: Tue, 23 Jun 2026 17:53:02 +0530 Subject: [PATCH 4/4] VAPI-3160: Fixed review commit --- README.md | 2 +- src/Voice/Bxml/Refer.php | 18 +++--- src/Voice/Bxml/ReferSipUri.php | 39 +++++++++++++ src/Voice/Models/ReferCompleteCallback.php | 42 ++++++------- tests/BxmlTest.php | 68 +++++++++++++++++++++- 5 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 src/Voice/Bxml/ReferSipUri.php diff --git a/README.md b/README.md index b720d09..909010b 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ echo $response->toBxml(); ```php -$sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com"); +$sipUri = new BandwidthLib\Voice\Bxml\ReferSipUri("sip:alice@atlanta.example.com"); $refer = new BandwidthLib\Voice\Bxml\Refer(); $refer->referCompleteUrl("https://example.com/handleRefer"); $refer->referCompleteMethod("POST"); diff --git a/src/Voice/Bxml/Refer.php b/src/Voice/Bxml/Refer.php index c12f107..daf8411 100644 --- a/src/Voice/Bxml/Refer.php +++ b/src/Voice/Bxml/Refer.php @@ -28,7 +28,7 @@ class Refer extends Verb { */ private $tag; /** - * @var SipUri + * @var ReferSipUri|SipUri */ private $sipUri; @@ -63,16 +63,21 @@ public function tag(string $tag): Refer { } /** - * Sets the SipUri child element for Refer + * Sets the SipUri child element for Refer. + * Prefer ReferSipUri to avoid accidentally serializing Transfer-specific attributes. * - * @param SipUri $sipUri The SipUri destination for the REFER + * @param ReferSipUri|SipUri $sipUri The SipUri destination for the REFER */ - public function sipUri(SipUri $sipUri): Refer { + public function sipUri($sipUri): Refer { $this->sipUri = $sipUri; return $this; } public function toBxml(DOMDocument $doc): DOMElement { + if (!isset($this->sipUri)) { + throw new \InvalidArgumentException('Refer requires a SipUri child element.'); + } + $element = $doc->createElement("Refer"); if(isset($this->referCompleteUrl)) { @@ -87,11 +92,8 @@ public function toBxml(DOMDocument $doc): DOMElement { $element->setAttribute("tag", $this->tag); } - if(isset($this->sipUri)) { - $element->appendChild($this->sipUri->toBxml($doc)); - } + $element->appendChild($this->sipUri->toBxml($doc)); return $element; } } - diff --git a/src/Voice/Bxml/ReferSipUri.php b/src/Voice/Bxml/ReferSipUri.php new file mode 100644 index 0000000..a80e905 --- /dev/null +++ b/src/Voice/Bxml/ReferSipUri.php @@ -0,0 +1,39 @@ +sip = $sip; + } + + public function toBxml(DOMDocument $doc): DOMElement { + $element = $doc->createElement("SipUri"); + $element->appendChild($doc->createTextNode($this->sip)); + return $element; + } +} + diff --git a/src/Voice/Models/ReferCompleteCallback.php b/src/Voice/Models/ReferCompleteCallback.php index b7866a4..d077fd8 100644 --- a/src/Voice/Models/ReferCompleteCallback.php +++ b/src/Voice/Models/ReferCompleteCallback.php @@ -2,102 +2,105 @@ /* * BandwidthLib * - * This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ). + * This file was manually added to support the Refer BXML verb. */ namespace BandwidthLib\Voice\Models; /** - *This object represents fields included in callbacks related to refer complete events + * This object represents fields included in callbacks related to refer complete events. + * Fired when the REFER flow completes (success or failure), only if referCompleteUrl is set. */ class ReferCompleteCallback implements \JsonSerializable { /** - * @todo Write general description for this property + * The event type of the callback. Always "referComplete". * @var string|null $eventType public property */ public $eventType; /** - * @todo Write general description for this property + * The time the event occurred, in ISO 8601 format. * @var string|null $eventTime public property */ public $eventTime; /** - * @todo Write general description for this property + * The user's Bandwidth account ID. * @var string|null $accountId public property */ public $accountId; /** - * @todo Write general description for this property + * The ID of the application associated with the call. * @var string|null $applicationId public property */ public $applicationId; /** - * @todo Write general description for this property + * The phone number that received the call, in E.164 format. * @var string|null $from public property */ public $from; /** - * @todo Write general description for this property + * The phone number that made the call, in E.164 format. * @var string|null $to public property */ public $to; /** - * @todo Write general description for this property + * The direction of the call. Always "inbound" for refer events. * @var string|null $direction public property */ public $direction; /** - * @todo Write general description for this property + * The unique ID of the call. * @var string|null $callId public property */ public $callId; /** - * @todo Write general description for this property + * The full URL of the call resource. * @var string|null $callUrl public property */ public $callUrl; /** - * @todo Write general description for this property + * The time the call started, in ISO 8601 format. * @var string|null $startTime public property */ public $startTime; /** - * @todo Write general description for this property + * The time the call was answered, in ISO 8601 format. * @var string|null $answerTime public property */ public $answerTime; /** - * @todo Write general description for this property + * The outcome of the REFER operation. Either "success" or "failure". * @var string|null $referCallStatus public property */ public $referCallStatus; /** - * @todo Write general description for this property - * @var string|null $referSipResponseCode public property + * The SIP response code for the REFER request (e.g. 202, 405). + * Absent when the REFER was not sent. Present on both success and failure. + * @var int|null $referSipResponseCode public property */ public $referSipResponseCode; /** - * @todo Write general description for this property - * @var string|null $notifySipResponseCode public property + * The SIP response code from the NOTIFY (e.g. 200, 404, 486). + * Absent on REFER rejection or NOTIFY timeout. + * @var int|null $notifySipResponseCode public property */ public $notifySipResponseCode; /** - * @todo Write general description for this property + * A custom string provided in the Refer BXML tag, if any. * @var string|null $tag public property */ public $tag; @@ -151,4 +154,3 @@ public function jsonSerialize(): array return array_filter($json); } } - diff --git a/tests/BxmlTest.php b/tests/BxmlTest.php index 62809cf..0b11aa1 100644 --- a/tests/BxmlTest.php +++ b/tests/BxmlTest.php @@ -542,7 +542,7 @@ public function testStopTranscription() { } public function testRefer() { - $sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com"); + $sipUri = new BandwidthLib\Voice\Bxml\ReferSipUri("sip:alice@atlanta.example.com"); $refer = new BandwidthLib\Voice\Bxml\Refer(); $refer->referCompleteUrl("https://example.com/handleRefer"); $refer->referCompleteMethod("POST"); @@ -558,7 +558,7 @@ public function testRefer() { } public function testReferNoOptionalAttributes() { - $sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:bob@biloxi.example.com"); + $sipUri = new BandwidthLib\Voice\Bxml\ReferSipUri("sip:bob@biloxi.example.com"); $refer = new BandwidthLib\Voice\Bxml\Refer(); $refer->sipUri($sipUri); @@ -569,4 +569,68 @@ public function testReferNoOptionalAttributes() { $responseXml = $response->toBxml(); $this->assertEquals($expectedXml, $responseXml); } + + public function testReferWithoutSipUriThrows() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Refer requires a SipUri child element.'); + + $refer = new BandwidthLib\Voice\Bxml\Refer(); + $refer->referCompleteUrl("https://example.com/handleRefer"); + + $response = new BandwidthLib\Voice\Bxml\Response(); + $response->addVerb($refer); + $response->toBxml(); + } + + public function testReferCompleteCallbackSuccess() { + $json = '{"eventType":"referComplete","eventTime":"2026-06-15T12:00:00Z","accountId":"12345","applicationId":"app-1","from":"+15551234567","to":"+15559876543","direction":"inbound","callId":"c-abc123","callUrl":"https://voice.bandwidth.com/api/v2/accounts/12345/calls/c-abc123","startTime":"2026-06-15T11:59:00Z","answerTime":"2026-06-15T11:59:05Z","referCallStatus":"success"}'; + $data = json_decode($json); + $callback = new BandwidthLib\Voice\Models\ReferCompleteCallback(); + $callback->eventType = $data->eventType; + $callback->referCallStatus = $data->referCallStatus; + $this->assertEquals("referComplete", $callback->eventType); + $this->assertEquals("success", $callback->referCallStatus); + $this->assertNull($callback->referSipResponseCode); + $this->assertNull($callback->notifySipResponseCode); + } + + public function testReferCompleteCallbackReferRejected() { + $json = '{"eventType":"referComplete","eventTime":"2026-06-15T12:00:00Z","accountId":"12345","applicationId":"app-1","from":"+15551234567","to":"+15559876543","direction":"inbound","callId":"c-abc123","callUrl":"https://voice.bandwidth.com/api/v2/accounts/12345/calls/c-abc123","startTime":"2026-06-15T11:59:00Z","answerTime":"2026-06-15T11:59:05Z","referCallStatus":"failure","referSipResponseCode":405}'; + $data = json_decode($json); + $callback = new BandwidthLib\Voice\Models\ReferCompleteCallback(); + $callback->eventType = $data->eventType; + $callback->referCallStatus = $data->referCallStatus; + $callback->referSipResponseCode = $data->referSipResponseCode; + $this->assertEquals("referComplete", $callback->eventType); + $this->assertEquals("failure", $callback->referCallStatus); + $this->assertSame(405, $callback->referSipResponseCode); + $this->assertNull($callback->notifySipResponseCode); + } + + public function testReferCompleteCallbackDestinationUnreachable() { + $json = '{"eventType":"referComplete","eventTime":"2026-06-15T12:00:00Z","accountId":"12345","applicationId":"app-1","from":"+15551234567","to":"+15559876543","direction":"inbound","callId":"c-abc123","callUrl":"https://voice.bandwidth.com/api/v2/accounts/12345/calls/c-abc123","startTime":"2026-06-15T11:59:00Z","answerTime":"2026-06-15T11:59:05Z","referCallStatus":"failure","referSipResponseCode":202,"notifySipResponseCode":486}'; + $data = json_decode($json); + $callback = new BandwidthLib\Voice\Models\ReferCompleteCallback(); + $callback->eventType = $data->eventType; + $callback->referCallStatus = $data->referCallStatus; + $callback->referSipResponseCode = $data->referSipResponseCode; + $callback->notifySipResponseCode = $data->notifySipResponseCode; + $this->assertEquals("failure", $callback->referCallStatus); + $this->assertSame(202, $callback->referSipResponseCode); + $this->assertSame(486, $callback->notifySipResponseCode); + } + + public function testReferCompleteCallbackNotifyTimeout() { + $json = '{"eventType":"referComplete","eventTime":"2026-06-15T12:00:00Z","accountId":"12345","applicationId":"app-1","from":"+15551234567","to":"+15559876543","direction":"inbound","callId":"c-abc123","callUrl":"https://voice.bandwidth.com/api/v2/accounts/12345/calls/c-abc123","startTime":"2026-06-15T11:59:00Z","answerTime":"2026-06-15T11:59:05Z","referCallStatus":"failure","referSipResponseCode":202,"tag":"custom-tag"}'; + $data = json_decode($json); + $callback = new BandwidthLib\Voice\Models\ReferCompleteCallback(); + $callback->eventType = $data->eventType; + $callback->referCallStatus = $data->referCallStatus; + $callback->referSipResponseCode = $data->referSipResponseCode; + $callback->tag = $data->tag; + $this->assertEquals("failure", $callback->referCallStatus); + $this->assertSame(202, $callback->referSipResponseCode); + $this->assertNull($callback->notifySipResponseCode); + $this->assertEquals("custom-tag", $callback->tag); + } }