Add API test suite for Bird Voice Webhook endpoints, including comprehensive lifecycle tests for inbound call handling scenarios. Extend fixtures with department gate creation support and update OpenAPI spec validations.

This commit is contained in:
Jeppe Bundgaard
2026-04-13 17:08:37 +02:00
parent 2a8e8986e6
commit 8465ee794d
16 changed files with 3036 additions and 2106 deletions
+289 -92
View File
@@ -901,16 +901,22 @@ paths:
summary: Process inbound Bird voice call lifecycle
operationId: birdInboundVoiceCallWebhook
description: >
Stateful inbound-call webhook that answers the call immediately, runs a DTMF-driven IVR for
phone-controlled entrance and exit gates, and enforces a timeout hangup flow. During the
first 300 seconds from call start, DTMF input is extracted from payload fields such as
`dtmf`, `digit`, `digits`, `keys`, `result.keys`, and nested `conditions[].value`. Values
like `1#` are normalized to a single menu digit before the route resolves the department
selection and then the gate type selection (`1=entrance`, `2=exit`). When a valid gate is
resolved, the webhook triggers the corresponding `department_gates` phone-call gate, announces
the result, sends a hangup command, and keeps the completed state cached until Bird reports a
terminal call status. At or after 300 seconds, the webhook says `timeout reached`, waits 10
seconds, sends a hangup command once, and polls Bird call status until terminal.
Stateful inbound-call webhook that owns department and gate selection for phone-controlled
gates. The preferred Bird Flow Builder integration is the native-flow mode:
send top-level `callId`, `channelId`, and `workspaceId` to fetch IVR prompt data,
then submit the selected DTMF digits as `keys` in a follow-up request. In this mode
the webhook returns a plain `200 OK` JSON body with `prompt`, `stage`, and `gather`
settings that Bird native voice steps can consume directly. For backward compatibility,
the webhook also supports the older `{ payload, request, waitConditions }` contract and
returns a raw `202 Accepted` Bird `callCommand` gather envelope that resumes on
`call_command_gather_finished`. The Bird Flow itself must answer the inbound call before
invoking this HTTP step; the backend answer attempt is only a best-effort fallback.
Department options are generated from `department_gates` records with `config.type=PHONE_CALL`,
ordered by `departments.order_priority`, and support multi-digit DTMF selections such as `10#`.
After a department is chosen, the webhook returns compact gate options, for example `Press 1 for exit`
when exit is the only available phone-controlled gate for that department. When a valid gate is confirmed,
the webhook opens the gate through the corresponding `department_gates` phone-call record and returns
a `200 OK` completion result.
parameters:
- in: query
name: workspaceId
@@ -940,11 +946,25 @@ paths:
$ref: '#/components/schemas/BirdInboundCallWebhookRequest'
responses:
'200':
description: Lifecycle phase result for this webhook invocation
description: Native-flow gather data or gate action result for this webhook invocation
content:
application/json:
schema:
$ref: '#/components/schemas/BirdInboundCallWebhookResponse'
oneOf:
- $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse'
- $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse'
'202':
description: Gather command accepted and returned to Bird
content:
application/json:
schema:
$ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse'
'400':
description: Malformed Bird webhook payload
content:
application/json:
schema:
$ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse'
# Bird Numbers
/bird/numbers:
@@ -16460,122 +16480,299 @@ components:
channelId:
type: string
format: uuid
status:
type: string
description: Optional inbound call status from webhook payload
example: ongoing
payload:
type: object
additionalProperties: true
properties:
endKey:
type: string
example: "#"
retries:
type: integer
example: 3
timeout:
type: integer
example: 30
say:
type: object
additionalProperties: true
properties:
locale:
type: string
example: "en-US"
voice:
type: string
example: "female"
request:
type: object
additionalProperties: true
properties:
callId:
type: string
example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
workspaceId:
type: string
format: uuid
channelId:
type: string
format: uuid
waitConditions:
type: object
additionalProperties: true
properties:
timeout:
type: string
example: "PT10M"
events:
type: array
items:
type: object
additionalProperties: true
properties:
action:
type: string
example: "continue"
name:
type: string
example: "call_command_gather_finished"
event:
type: object
additionalProperties: true
result:
type: object
additionalProperties: true
resumeData:
type: object
additionalProperties: true
dtmf:
type: string
description: DTMF value when present, for example `1` or `1#`
example: "5"
description: DTMF value when present, for example `1`, `1#`, or `10#`
example: "10#"
digit:
type: string
description: Alternate DTMF field, also accepts values such as `1#`
example: "5"
description: Alternate DTMF field, also accepts values such as `10#`
example: "10#"
digits:
type: string
description: Alternate DTMF field
example: "5"
example: "10#"
keys:
type: string
description: Alternate DTMF field returned by gather results
example: "1#"
call:
type: object
additionalProperties: true
data:
type: object
additionalProperties: true
event:
example: "10#"
key:
type: string
example: "1"
input:
oneOf:
- type: string
- type: object
additionalProperties: true
conditions:
type: array
items:
type: object
additionalProperties: true
BirdInboundCallWebhookResponse:
type: object
oneOf:
- $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse'
- $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse'
- $ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse'
- $ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse'
BirdInboundCallWebhookFlowGatherResponse:
type: object
properties:
success:
requestId:
type: string
example: "request-123"
callId:
type: string
example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
status:
type: string
enum: [gather]
completed:
type: boolean
example: true
data:
enum: [false]
stage:
type: string
enum: [department_select, gate_type_select]
prompt:
type: string
gather:
type: object
properties:
phase:
input:
type: string
enum: [lock_not_acquired, input_window, timeout_window, completed, terminal_completion]
stage:
enum: [dtmf]
maxNumKeys:
type: integer
endKey:
type: string
nullable: true
enum: [department_select, gate_type_select, completed]
call_id:
type: string
example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
completed:
type: boolean
elapsed_seconds:
example: "#"
timeout:
type: integer
retries:
type: integer
say:
type: object
properties:
locale:
type: string
example: "en-US"
voice:
type: string
example: "female"
text:
type: string
selection:
type: object
properties:
departmentId:
type: integer
nullable: true
timeout_seconds:
type: integer
nullable: true
input_received:
type: boolean
nullable: true
input_changed:
type: boolean
nullable: true
last_input:
departmentName:
type: string
nullable: true
selected_department_id:
type: integer
nullable: true
selected_gate_type:
gateType:
type: string
nullable: true
enum: [entrance, exit]
gate_id:
gateId:
type: integer
nullable: true
gate_opened:
invalidSelectionCount:
type: integer
resumed:
type: boolean
statusCode:
type: integer
enum: [200]
statusText:
type: string
enum: [OK]
BirdInboundCallWebhookActionResultResponse:
type: object
properties:
requestId:
type: string
example: "request-123"
result:
type: object
properties:
callId:
type: string
example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
status:
type: string
enum: [completed, failed, ignored]
action:
type: string
enum: [gate_opened, gate_open_failed, no_action, ignored]
message:
type: string
departmentId:
type: integer
nullable: true
gateType:
type: string
nullable: true
enum: [entrance, exit]
gateId:
type: integer
nullable: true
gateOpened:
type: boolean
nullable: true
answered_at:
type: integer
nullable: true
timeout_announced_at:
type: integer
nullable: true
hangup_sent_at:
type: integer
nullable: true
poll_attempts:
type: integer
nullable: true
poll_error:
resumeData:
type: object
additionalProperties: true
properties:
action:
type: string
nullable: true
terminal_status:
example: "continue"
completed:
type: boolean
example: true
result:
type: string
nullable: true
reason:
example: "gate_opened"
gateOpened:
type: boolean
example: true
completedAt:
type: string
format: date-time
statusCode:
type: integer
enum: [200]
statusText:
type: string
enum: [OK]
BirdInboundCallWebhookGatherAcceptedResponse:
type: object
properties:
event:
type: object
additionalProperties: true
requestId:
type: string
example: "request-123"
result:
type: object
properties:
callId:
type: string
example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
command:
type: string
enum: [gather]
id:
type: string
status:
type: string
enum: [accepted]
resumeData:
type: object
additionalProperties: true
properties:
action:
type: string
example: "continue"
statusCode:
type: integer
enum: [202]
statusText:
type: string
enum: [Accepted]
suspendedAt:
type: string
format: date-time
resumedAt:
type: string
format: date-time
nullable: true
BirdInboundCallWebhookTransportErrorResponse:
type: object
properties:
requestId:
type: string
statusCode:
type: integer
enum: [400, 500]
statusText:
type: string
enum: [Bad Request, Internal Server Error]
error:
type: object
properties:
message:
type: string
nullable: true
meta:
oneOf:
- type: array
items: {}
- type: object
additionalProperties: true
example: []
includes:
oneOf:
- type: array
items: {}
- type: object
additionalProperties: true
example: []
BirdVoiceCallCreateRequest:
type: object