openapi: 3.0.3 info: title: Phase 3 Container-level Domestic Outbound Dispatch API version: 2.0.0 paths: /api/platform/v1/first-leg/outbound-dispatches: get: summary: Query the outbound aggregate for one container booking description: Returns one container-level aggregate containing every real inbound binding. No per-inbound partial outbound aggregate is exposed. x-permission: platform:firstleg:outbound-dispatch:list parameters: - {$ref: '#/components/parameters/ContainerBookingIdQuery'} responses: '200': description: Business envelope containing a single matching aggregate in a list content: application/json: schema: type: object properties: data: {type: array, items: {$ref: '#/components/schemas/Aggregate'}} /api/platform/v1/first-leg/outbound-dispatches/{containerBookingId}: get: summary: Query one container-level aggregate and unified timeline x-permission: platform:firstleg:outbound-dispatch:view parameters: - {$ref: '#/components/parameters/ContainerBookingIdPath'} responses: '200': description: Business envelope containing all inbound bindings and one visible dispatch version content: application/json: schema: type: object properties: data: {$ref: '#/components/schemas/Detail'} /api/platform/v1/first-leg/outbound-dispatches/{containerBookingId}/gate: get: summary: Evaluate every inbound binding in the fail-closed container outbound gate x-permission: platform:firstleg:outbound-dispatch:check parameters: - {$ref: '#/components/parameters/ContainerBookingIdPath'} responses: '200': {description: Business envelope containing the container gate and per-inbound gate results} /api/platform/v1/first-leg/outbound-dispatches/{containerBookingId}/confirm: post: summary: Atomically confirm domestic departure for the entire container description: Locks every real inbound binding, requires every gate to pass, advances all technical rows in one transaction, records one visible history, and creates the CarrierScheduleQuery/DEPARTED_WAREHOUSE fact once. No partial success is possible. x-permission: platform:firstleg:outbound-dispatch:confirm parameters: - {$ref: '#/components/parameters/ContainerBookingIdPath'} - {$ref: '#/components/parameters/IdempotencyKey'} requestBody: required: true content: application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}} responses: '200': {description: Same key and same payload replays the single container-level result; stale version or changed payload returns A0409} /api/platform/v1/first-leg/outbound-dispatches/{containerBookingId}/reverse: post: summary: Atomically reverse the entire container before any actual shipping node exists description: Once an actual DEPARTED_WAREHOUSE node exists, reversal is rejected with business code A0409 so an outbound fact can never coexist with READY_FOR_DISPATCH. x-permission: platform:firstleg:outbound-dispatch:reverse parameters: - {$ref: '#/components/parameters/ContainerBookingIdPath'} - {$ref: '#/components/parameters/IdempotencyKey'} requestBody: required: true content: application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}} responses: '200': {description: Business envelope containing the reverted container aggregate} components: parameters: ContainerBookingIdQuery: name: containerBookingId in: query required: true schema: {type: integer, format: int64, minimum: 1} ContainerBookingIdPath: name: containerBookingId in: path required: true schema: {type: integer, format: int64, minimum: 1} IdempotencyKey: name: Idempotency-Key in: header required: true schema: {type: string, maxLength: 128} schemas: Aggregate: type: object required: [containerBookingId, cabinetNo, loadingPlanId, loadingPlanStatus, dispatchStatus, version, inbounds, gate] properties: containerBookingId: {type: integer, format: int64} cabinetNo: {type: string, description: Stable cabinet number} containerNo: {type: string, nullable: true, description: Real container number} loadingPlanId: {type: integer, format: int64} loadingPlanStatus: {type: string} oceanBookingId: {type: integer, format: int64, nullable: true} oceanBookingStatus: {type: string, nullable: true} truckAppointmentId: {type: integer, format: int64, nullable: true, description: Reference-only truck appointment; absence never blocks manual outbound confirmation} truckAppointmentStatus: {type: string, nullable: true, description: Reference-only status; PENDING or any non-terminal state never affects gate.passed} dispatchStatus: {type: string, description: One visible container-level status} version: {type: integer, minimum: 0, description: One optimistic-lock version shared by all technical inbound rows} inbounds: type: array minItems: 1 items: {$ref: '#/components/schemas/InboundBinding'} gate: {$ref: '#/components/schemas/ContainerGateResult'} InboundBinding: type: object required: [orderId, orderNo, inboundId, inboundCode, tenantId, inboundStatus] properties: orderId: {type: integer, format: int64} orderNo: {type: string} inboundId: {type: integer, format: int64} inboundCode: {type: string} tenantId: {type: integer, format: int64} customerId: {type: integer, format: int64, nullable: true} customerName: {type: string, nullable: true} inboundStatus: {type: string} pickTaskId: {type: integer, format: int64, nullable: true} pickTaskStatus: {type: string, nullable: true} ContainerGateResult: type: object required: [passed, inbounds, checkedAt] properties: passed: {type: boolean} inbounds: type: array items: type: object required: [inboundId, inboundCode, gate] properties: inboundId: {type: integer, format: int64} inboundCode: {type: string} gate: {type: object} checkedAt: {type: string, format: date-time} Detail: type: object required: [aggregate, timeline] properties: aggregate: {$ref: '#/components/schemas/Aggregate'} timeline: type: array description: Unified immutable facts; a container confirm or reverse contributes one visible history item. items: {type: object} ActionRequest: type: object required: [expectedVersion] properties: expectedVersion: {type: integer, minimum: 0} remark: {type: string, maxLength: 500}