Skip to content

Commit cf272b1

Browse files
stampercaseyclaude
andcommitted
VAPI-3161 Add ReferCompleteCallback model and scenario tests
Unblocked by api-specs#2142 merging. Hand-written to match the OpenAPI generator output pattern (mirrors TransferCompleteCallback). Fields: standard call fields + referCallStatus, referSipResponseCode, notifySipResponseCode. Excludes cause/errorMessage/errorId (not in spec) and enqueuedTime (inbound SIP URI calls only). Adds scenario tests for all four referComplete outcomes: - Success (202 REFER, 200 NOTIFY) - Failure: REFER rejected (e.g. 405, no NOTIFY) - Failure: destination unreachable (202 REFER, 4xx NOTIFY) - Failure: NOTIFY timeout (202 REFER, no NOTIFY) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 9a134a9 commit cf272b1

5 files changed

Lines changed: 314 additions & 0 deletions

File tree

.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ bandwidth/models/thumbnail_alignment_enum.py
197197
bandwidth/models/transcribe_recording.py
198198
bandwidth/models/transcription.py
199199
bandwidth/models/transcription_available_callback.py
200+
bandwidth/models/refer_complete_callback.py
200201
bandwidth/models/transfer_answer_callback.py
201202
bandwidth/models/transfer_complete_callback.py
202203
bandwidth/models/transfer_disconnect_callback.py

bandwidth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@
217217
"TranscribeRecording",
218218
"Transcription",
219219
"TranscriptionAvailableCallback",
220+
"ReferCompleteCallback",
220221
"TransferAnswerCallback",
221222
"TransferCompleteCallback",
222223
"TransferDisconnectCallback",
@@ -443,6 +444,7 @@
443444
from bandwidth.models.transcribe_recording import TranscribeRecording as TranscribeRecording
444445
from bandwidth.models.transcription import Transcription as Transcription
445446
from bandwidth.models.transcription_available_callback import TranscriptionAvailableCallback as TranscriptionAvailableCallback
447+
from bandwidth.models.refer_complete_callback import ReferCompleteCallback as ReferCompleteCallback
446448
from bandwidth.models.transfer_answer_callback import TransferAnswerCallback as TransferAnswerCallback
447449
from bandwidth.models.transfer_complete_callback import TransferCompleteCallback as TransferCompleteCallback
448450
from bandwidth.models.transfer_disconnect_callback import TransferDisconnectCallback as TransferDisconnectCallback

bandwidth/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
from bandwidth.models.transcribe_recording import TranscribeRecording
192192
from bandwidth.models.transcription import Transcription
193193
from bandwidth.models.transcription_available_callback import TranscriptionAvailableCallback
194+
from bandwidth.models.refer_complete_callback import ReferCompleteCallback
194195
from bandwidth.models.transfer_answer_callback import TransferAnswerCallback
195196
from bandwidth.models.transfer_complete_callback import TransferCompleteCallback
196197
from bandwidth.models.transfer_disconnect_callback import TransferDisconnectCallback
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# coding: utf-8
2+
3+
"""
4+
Bandwidth
5+
6+
Bandwidth's Communication APIs
7+
8+
The version of the OpenAPI document: 1.0.0
9+
Contact: letstalk@bandwidth.com
10+
Generated by OpenAPI Generator (https://openapi-generator.tech)
11+
12+
Do not edit the class manually.
13+
""" # noqa: E501
14+
15+
16+
from __future__ import annotations
17+
import pprint
18+
import re # noqa: F401
19+
import json
20+
21+
from datetime import datetime
22+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
23+
from typing import Any, ClassVar, Dict, List, Optional
24+
from bandwidth.models.call_direction_enum import CallDirectionEnum
25+
from typing import Optional, Set
26+
from typing_extensions import Self
27+
28+
class ReferCompleteCallback(BaseModel):
29+
"""
30+
The Refer Complete event is fired when the <Refer> verb finishes executing. This is sent to the referCompleteUrl specified on the <Refer> verb, and the BXML returned in it is executed on the call only on failure — the call is torn down on success.
31+
""" # noqa: E501
32+
event_type: Optional[StrictStr] = Field(default=None, description="The event type, value is referComplete.", alias="eventType")
33+
event_time: Optional[datetime] = Field(default=None, description="The approximate UTC date and time when the event was generated by the Bandwidth server, in ISO 8601 format. This may not be exactly the time of event execution.", alias="eventTime")
34+
account_id: Optional[StrictStr] = Field(default=None, description="The user account associated with the call.", alias="accountId")
35+
application_id: Optional[StrictStr] = Field(default=None, description="The id of the application associated with the call.", alias="applicationId")
36+
var_from: Optional[StrictStr] = Field(default=None, description="The provided identifier of the caller. Must be a phone number in E.164 format (e.g. +15555555555).", alias="from")
37+
to: Optional[StrictStr] = Field(default=None, description="The phone number that received the call, in E.164 format (e.g. +15555555555).")
38+
direction: Optional[CallDirectionEnum] = None
39+
call_id: Optional[StrictStr] = Field(default=None, description="The call id associated with the event.", alias="callId")
40+
call_url: Optional[StrictStr] = Field(default=None, description="The URL of the call associated with the event.", alias="callUrl")
41+
start_time: Optional[datetime] = Field(default=None, description="Time the call was started, in ISO 8601 format.", alias="startTime")
42+
answer_time: Optional[datetime] = Field(default=None, description="Time the call was answered, in ISO 8601 format.", alias="answerTime")
43+
tag: Optional[StrictStr] = Field(default=None, description="(optional) The tag specified on call creation. If no tag was specified or it was previously cleared, this field will not be present.")
44+
refer_call_status: Optional[StrictStr] = Field(default=None, description="The outcome of the REFER operation. Either 'success' or 'failure'.", alias="referCallStatus")
45+
refer_sip_response_code: Optional[StrictInt] = Field(default=None, description="The SIP response code returned for the REFER request itself (e.g. 202, 405, 603). Present when a SIP response was received for the REFER.", alias="referSipResponseCode")
46+
notify_sip_response_code: Optional[StrictInt] = Field(default=None, description="The final SIP response code reported via NOTIFY. Present only when the caller's endpoint sent a final NOTIFY (e.g. 200, 404, 486). Not present on NOTIFY timeout or when REFER was rejected before a subscription was established.", alias="notifySipResponseCode")
47+
additional_properties: Dict[str, Any] = {}
48+
__properties: ClassVar[List[str]] = ["eventType", "eventTime", "accountId", "applicationId", "from", "to", "direction", "callId", "callUrl", "startTime", "answerTime", "tag", "referCallStatus", "referSipResponseCode", "notifySipResponseCode"]
49+
50+
model_config = ConfigDict(
51+
populate_by_name=True,
52+
validate_assignment=True,
53+
protected_namespaces=(),
54+
)
55+
56+
57+
def to_str(self) -> str:
58+
"""Returns the string representation of the model using alias"""
59+
return pprint.pformat(self.model_dump(by_alias=True))
60+
61+
def to_json(self) -> str:
62+
"""Returns the JSON representation of the model using alias"""
63+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
64+
return json.dumps(self.to_dict())
65+
66+
@classmethod
67+
def from_json(cls, json_str: str) -> Optional[Self]:
68+
"""Create an instance of ReferCompleteCallback from a JSON string"""
69+
return cls.from_dict(json.loads(json_str))
70+
71+
def to_dict(self) -> Dict[str, Any]:
72+
"""Return the dictionary representation of the model using alias.
73+
74+
This has the following differences from calling pydantic's
75+
`self.model_dump(by_alias=True)`:
76+
77+
* `None` is only added to the output dict for nullable fields that
78+
were set at model initialization. Other fields with value `None`
79+
are ignored.
80+
* Fields in `self.additional_properties` are added to the output dict.
81+
"""
82+
excluded_fields: Set[str] = set([
83+
"additional_properties",
84+
])
85+
86+
_dict = self.model_dump(
87+
by_alias=True,
88+
exclude=excluded_fields,
89+
exclude_none=True,
90+
)
91+
# puts key-value pairs in additional_properties in the top level
92+
if self.additional_properties is not None:
93+
for _key, _value in self.additional_properties.items():
94+
_dict[_key] = _value
95+
96+
# set to None if answer_time (nullable) is None
97+
# and model_fields_set contains the field
98+
if self.answer_time is None and "answer_time" in self.model_fields_set:
99+
_dict['answerTime'] = None
100+
101+
# set to None if tag (nullable) is None
102+
# and model_fields_set contains the field
103+
if self.tag is None and "tag" in self.model_fields_set:
104+
_dict['tag'] = None
105+
106+
# set to None if refer_sip_response_code (nullable) is None
107+
# and model_fields_set contains the field
108+
if self.refer_sip_response_code is None and "refer_sip_response_code" in self.model_fields_set:
109+
_dict['referSipResponseCode'] = None
110+
111+
# set to None if notify_sip_response_code (nullable) is None
112+
# and model_fields_set contains the field
113+
if self.notify_sip_response_code is None and "notify_sip_response_code" in self.model_fields_set:
114+
_dict['notifySipResponseCode'] = None
115+
116+
return _dict
117+
118+
@classmethod
119+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
120+
"""Create an instance of ReferCompleteCallback from a dict"""
121+
if obj is None:
122+
return None
123+
124+
if not isinstance(obj, dict):
125+
return cls.model_validate(obj)
126+
127+
_obj = cls.model_validate({
128+
"eventType": obj.get("eventType"),
129+
"eventTime": obj.get("eventTime"),
130+
"accountId": obj.get("accountId"),
131+
"applicationId": obj.get("applicationId"),
132+
"from": obj.get("from"),
133+
"to": obj.get("to"),
134+
"direction": obj.get("direction"),
135+
"callId": obj.get("callId"),
136+
"callUrl": obj.get("callUrl"),
137+
"startTime": obj.get("startTime"),
138+
"answerTime": obj.get("answerTime"),
139+
"tag": obj.get("tag"),
140+
"referCallStatus": obj.get("referCallStatus"),
141+
"referSipResponseCode": obj.get("referSipResponseCode"),
142+
"notifySipResponseCode": obj.get("notifySipResponseCode")
143+
})
144+
# store additional fields in additional_properties
145+
for _key in obj.keys():
146+
if _key not in cls.__properties:
147+
_obj.additional_properties[_key] = obj.get(_key)
148+
149+
return _obj
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# coding: utf-8
2+
3+
"""
4+
Bandwidth
5+
6+
Bandwidth's Communication APIs
7+
8+
The version of the OpenAPI document: 1.0.0
9+
Contact: letstalk@bandwidth.com
10+
Generated by OpenAPI Generator (https://openapi-generator.tech)
11+
12+
Do not edit the class manually.
13+
""" # noqa: E501
14+
15+
16+
import unittest
17+
from datetime import datetime
18+
19+
from bandwidth.models.refer_complete_callback import ReferCompleteCallback
20+
21+
22+
class TestReferCompleteCallback(unittest.TestCase):
23+
"""ReferCompleteCallback unit test stubs"""
24+
25+
def setUp(self):
26+
pass
27+
28+
def tearDown(self):
29+
pass
30+
31+
def make_instance(self, include_optional) -> ReferCompleteCallback:
32+
"""Test ReferCompleteCallback
33+
include_optional is a boolean, when False only required
34+
params are included, when True both required and
35+
optional params are included """
36+
if include_optional:
37+
return ReferCompleteCallback(
38+
event_type = 'referComplete',
39+
event_time = '2026-04-30T13:13:34.859Z',
40+
account_id = '9900000',
41+
application_id = '04e88489-df02-4e34-a0ee-27a91849555f',
42+
var_from = '+15555555555',
43+
to = '+15555555555',
44+
direction = 'inbound',
45+
call_id = 'c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85',
46+
call_url = 'https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85',
47+
start_time = '2026-04-30T13:13:34.859Z',
48+
answer_time = '2026-04-30T13:13:40.644Z',
49+
tag = 'exampleTag',
50+
refer_call_status = 'success',
51+
refer_sip_response_code = 202,
52+
notify_sip_response_code = 200
53+
)
54+
else:
55+
return ReferCompleteCallback(
56+
)
57+
58+
def testReferCompleteCallback(self):
59+
"""Test ReferCompleteCallback"""
60+
instance = self.make_instance(True)
61+
assert instance is not None
62+
assert isinstance(instance, ReferCompleteCallback)
63+
assert instance.event_type == 'referComplete'
64+
assert isinstance(instance.event_time, datetime)
65+
assert instance.account_id == '9900000'
66+
assert instance.application_id == '04e88489-df02-4e34-a0ee-27a91849555f'
67+
assert instance.var_from == '+15555555555'
68+
assert instance.to == '+15555555555'
69+
assert instance.direction == 'inbound'
70+
assert instance.call_id == 'c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85'
71+
assert instance.call_url == 'https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85'
72+
assert isinstance(instance.start_time, datetime)
73+
assert isinstance(instance.answer_time, datetime)
74+
assert instance.tag == 'exampleTag'
75+
assert instance.refer_call_status == 'success'
76+
assert instance.refer_sip_response_code == 202
77+
assert instance.notify_sip_response_code == 200
78+
79+
80+
class TestReferCompleteCallbackScenarios(unittest.TestCase):
81+
"""Hand-written scenario tests for the four referComplete callback cases."""
82+
83+
BASE_PAYLOAD = {
84+
"eventType": "referComplete",
85+
"eventTime": "2026-04-30T13:13:34.859Z",
86+
"accountId": "9900000",
87+
"applicationId": "04e88489-df02-4e34-a0ee-27a91849555f",
88+
"from": "+15555555555",
89+
"to": "+15555555555",
90+
"direction": "inbound",
91+
"callId": "c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85",
92+
"callUrl": "https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85",
93+
"startTime": "2026-04-30T13:13:34.859Z",
94+
"answerTime": "2026-04-30T13:13:40.644Z",
95+
}
96+
97+
def _parse(self, extra: dict) -> ReferCompleteCallback:
98+
return ReferCompleteCallback.from_dict({**self.BASE_PAYLOAD, **extra})
99+
100+
def test_success(self):
101+
"""Successful REFER: remote answered, call torn down.
102+
referSipResponseCode=202 (accepted), notifySipResponseCode=200 (OK via NOTIFY)."""
103+
cb = self._parse({
104+
"referCallStatus": "success",
105+
"referSipResponseCode": 202,
106+
"notifySipResponseCode": 200,
107+
})
108+
assert cb.refer_call_status == "success"
109+
assert cb.refer_sip_response_code == 202
110+
assert cb.notify_sip_response_code == 200
111+
112+
def test_failure_refer_rejected(self):
113+
"""REFER rejected by caller's endpoint (e.g. 405 Method Not Allowed).
114+
No NOTIFY subscription was established, so notifySipResponseCode is absent."""
115+
cb = self._parse({
116+
"referCallStatus": "failure",
117+
"referSipResponseCode": 405,
118+
})
119+
assert cb.refer_call_status == "failure"
120+
assert cb.refer_sip_response_code == 405
121+
assert cb.notify_sip_response_code is None
122+
123+
def test_failure_destination_unreachable(self):
124+
"""REFER accepted (202) but destination was unreachable.
125+
NOTIFY reports a 4xx error code (e.g. 404 Not Found)."""
126+
cb = self._parse({
127+
"referCallStatus": "failure",
128+
"referSipResponseCode": 202,
129+
"notifySipResponseCode": 404,
130+
})
131+
assert cb.refer_call_status == "failure"
132+
assert cb.refer_sip_response_code == 202
133+
assert cb.notify_sip_response_code == 404
134+
135+
def test_failure_notify_timeout(self):
136+
"""REFER accepted (202) but no final NOTIFY arrived within 30 seconds.
137+
notifySipResponseCode is absent because no NOTIFY was received."""
138+
cb = self._parse({
139+
"referCallStatus": "failure",
140+
"referSipResponseCode": 202,
141+
})
142+
assert cb.refer_call_status == "failure"
143+
assert cb.refer_sip_response_code == 202
144+
assert cb.notify_sip_response_code is None
145+
146+
def test_from_json_round_trip(self):
147+
"""JSON round-trip: from_json -> to_json preserves all fields."""
148+
payload = {**self.BASE_PAYLOAD, "referCallStatus": "success", "referSipResponseCode": 202, "notifySipResponseCode": 200}
149+
cb = ReferCompleteCallback.from_json(str(payload).replace("'", '"'))
150+
assert cb.refer_call_status == "success"
151+
152+
def test_no_cause_error_fields(self):
153+
"""referComplete callback does not carry cause/errorMessage/errorId fields."""
154+
cb = self._parse({"referCallStatus": "failure", "referSipResponseCode": 405})
155+
assert not hasattr(cb, 'cause')
156+
assert not hasattr(cb, 'error_message')
157+
assert not hasattr(cb, 'error_id')
158+
159+
160+
if __name__ == '__main__':
161+
unittest.main()

0 commit comments

Comments
 (0)