diff --git a/README.md b/README.md index c3dbb35..909010b 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\ReferSipUri("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..daf8411 --- /dev/null +++ b/src/Voice/Bxml/Refer.php @@ -0,0 +1,99 @@ +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. + * Prefer ReferSipUri to avoid accidentally serializing Transfer-specific attributes. + * + * @param ReferSipUri|SipUri $sipUri The SipUri destination for the 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)) { + $element->setAttribute("referCompleteUrl", $this->referCompleteUrl); + } + + if(isset($this->referCompleteMethod)) { + $element->setAttribute("referCompleteMethod", $this->referCompleteMethod); + } + + if(isset($this->tag)) { + $element->setAttribute("tag", $this->tag); + } + + $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 new file mode 100644 index 0000000..d077fd8 --- /dev/null +++ b/src/Voice/Models/ReferCompleteCallback.php @@ -0,0 +1,156 @@ +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); + } +} 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); diff --git a/tests/BxmlTest.php b/tests/BxmlTest.php index 44bdb2f..0b11aa1 100644 --- a/tests/BxmlTest.php +++ b/tests/BxmlTest.php @@ -540,4 +540,97 @@ public function testStopTranscription() { $responseXml = $response->toBxml(); $this->assertEquals($expectedXml, $responseXml); } + + public function testRefer() { + $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"); + $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\ReferSipUri("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); + } + + 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); + } }