openapi: 3.0.3 info: title: Truck Appointment API version: 2.0.0 description: A truck appointment belongs to one RELEASED ocean booking, independently of when or how that booking's internal containers are linked. Each booking may have at most one non-terminal appointment. The bill-of-lading number and warehouse are frozen from the booking, which must own at least one active BOOKING_CONFIRMATION or BILL_OF_LADING file. Container identities and real container numbers remain in the container/loading modules and are not part of this API. PDA loading completion does not change a booking-level truck appointment. paths: /api/platform/v1/truck-appointments: get: summary: Query truck appointments responses: '200': description: Paged booking-level truck appointments with stable booking snapshots and attachment count content: application/json: schema: type: object properties: data: type: object properties: records: {type: array, items: {$ref: '#/components/schemas/DetailStableReferences'}} post: summary: Create a truck appointment for a released booking description: If the booking already has a non-terminal appointment, the server returns A0409 with the existing appointment number and instructs the operator to continue editing. The database booking-level unique key provides the same guarantee for concurrent creates. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/SaveRequest'} responses: '200': {description: 'Created appointment', content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}}} '409': {description: 'A0409 - this booking already has a non-terminal appointment; message includes its appointment number and asks the operator to continue editing'} /api/platform/v1/truck-appointments/candidates: get: summary: Query authoritative create-time ocean-booking candidates description: Requires platform:firstleg:truck-appointment:list. Returns one row per RELEASED booking with a bill of lading and an active booking-owned confirmation or bill file, excluding bookings that already have a non-terminal truck appointment. Container allocation and loading status do not affect eligibility. Edit pages display their current booking from appointment detail rather than injecting it into this create-only result. parameters: - {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}, description: Matches booking number or bill of lading} - {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}} - {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}} responses: '200': {description: Paged booking candidates, content: {application/json: {schema: {$ref: '#/components/schemas/CandidatePageEnvelope'}}}} /api/platform/v1/truck-appointments/{id}: parameters: [{$ref: '#/components/parameters/AppointmentId'}] get: summary: Query a truck appointment responses: '200': {description: 'Appointment detail', content: {application/json: {schema: {$ref: '#/components/schemas/DetailStableReferences'}}}} put: summary: Update an editable truck appointment with optimistic locking requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/SaveRequest'} responses: '200': {description: Updated appointment} /api/platform/v1/truck-appointments/{id}/{action}: parameters: - {$ref: '#/components/parameters/AppointmentId'} - {$ref: '#/components/parameters/Action'} - {$ref: '#/components/parameters/IdempotencyKey'} - {$ref: '#/components/parameters/RequestHash'} post: summary: Apply an idempotent appointment lifecycle action description: All actions transition the booking-level appointment as one aggregate. PICKUP does not accept or write a container number. ARRIVE, LOAD and DEPART persist their actual event timestamps atomically with the status transition. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ActionRequest'} responses: '200': {description: Updated or replayed appointment} /api/platform/v1/truck-appointments/{id}/reschedule: parameters: - {$ref: '#/components/parameters/AppointmentId'} - {$ref: '#/components/parameters/IdempotencyKey'} - {$ref: '#/components/parameters/RequestHash'} - name: plannedLoadingAt in: query required: true schema: {type: string, format: date-time} post: summary: Idempotently reschedule an appointment requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ActionRequest'} responses: '200': {description: Rescheduled or replayed appointment} /api/platform/v1/truck-appointments/{id}/history: parameters: [{$ref: '#/components/parameters/AppointmentId'}] get: summary: Query immutable appointment status history responses: '200': {description: 'History entries including action, from/to status, operator, reason and time'} /api/platform/v1/truck-appointments/{id}/attachments: parameters: [{$ref: '#/components/parameters/AppointmentId'}] get: summary: Query non-booking operational attachment references owned by the appointment description: 'Requires platform:firstleg:truck-appointment:list.' responses: '200': {description: 'Attachment references', content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/Attachment'}}}}} post: summary: Reference a platform-owned file already scoped to this appointment description: 'Requires platform:firstleg:truck-appointment:edit. The file must belong to platform scope + TRUCK_APPOINTMENT_ATTACHMENT + appointmentId.' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/AttachmentRequest'}}}} responses: '200': {description: 'Created attachment reference', content: {application/json: {schema: {$ref: '#/components/schemas/Attachment'}}}} /api/platform/v1/truck-appointments/{id}/attachments/upload: parameters: [{$ref: '#/components/parameters/AppointmentId'}] post: summary: Upload and reference a truck appointment attachment description: 'Requires platform:firstleg:truck-appointment:edit.' requestBody: required: true content: multipart/form-data: schema: type: object required: [attachmentType, file] properties: attachmentType: {type: string, enum: [DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER], description: Booking confirmations and bills of lading belong to the ocean-booking aggregate} file: {type: string, format: binary} responses: '200': {description: Uploaded attachment reference} /api/platform/v1/truck-appointments/{id}/attachments/{attachmentId}: parameters: - {$ref: '#/components/parameters/AppointmentId'} - {$ref: '#/components/parameters/AttachmentId'} delete: summary: Delete an attachment reference owned by the appointment description: 'Requires platform:firstleg:truck-appointment:edit. Deletes the appointment reference only; the File object is retained.' responses: '200': {description: Reference deleted} components: parameters: AppointmentId: {name: id, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} AttachmentId: {name: attachmentId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} Action: name: action in: path required: true schema: type: string enum: [SUBMIT, DISPATCH, PICKUP, ARRIVE, LOAD, DEPART, COMPLETE, EXCEPTION, CANCEL] IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}} RequestHash: {name: X-Request-Hash, in: header, required: false, schema: {type: string, pattern: '^[0-9a-fA-F]{64}$'}} schemas: CandidatePageEnvelope: type: object required: [code, message, data, timestamp] properties: code: {type: string, example: '200'} message: {type: string} data: type: object required: [records, page, size, total, pages] properties: records: {type: array, items: {$ref: '#/components/schemas/Candidate'}} page: {type: integer, format: int64} size: {type: integer, format: int64} total: {type: integer, format: int64} pages: {type: integer, format: int64} requestId: {type: string, nullable: true} traceId: {type: string, nullable: true} timestamp: {type: string, format: date-time} Candidate: type: object required: [oceanBookingId, oceanBookingNo, warehouseId, bookingDocumentReady] properties: oceanBookingId: {type: integer, format: int64} oceanBookingNo: {type: string} billOfLadingNo: {type: string, nullable: true} warehouseId: {type: integer, format: int64} warehouseName: {type: string, nullable: true} channelCode: {type: string, nullable: true} channelName: {type: string, nullable: true} bookingDocumentReady: {type: boolean, enum: [true]} DetailEnvelope: type: object required: [code, message, data, timestamp] properties: code: {type: string, example: '200'} message: {type: string} data: allOf: [{$ref: '#/components/schemas/DetailStableReferences'}] nullable: true requestId: {type: string, nullable: true} traceId: {type: string, nullable: true} timestamp: {type: string, format: date-time} AttachmentRequest: type: object required: [fileId, attachmentType] properties: fileId: {type: integer, format: int64, minimum: 1} attachmentType: {type: string, enum: [DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER]} fileName: {type: string, maxLength: 255, description: Ignored; authoritative metadata comes from File service} fileHash: {type: string, maxLength: 128, description: Ignored; authoritative metadata comes from File service} Attachment: type: object properties: id: {type: integer, format: int64} appointmentId: {type: integer, format: int64} fileId: {type: integer, format: int64} attachmentType: {type: string, enum: [DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER]} fileName: {type: string} fileHash: {type: string} visibility: {type: string, enum: [PLATFORM]} uploadedBy: {type: integer, format: int64} uploadedAt: {type: string, format: date-time} ActionRequest: type: object required: [expectedVersion] properties: expectedVersion: {type: integer, minimum: 0} reason: {type: string, maxLength: 500} SaveRequest: type: object required: [oceanBookingId, carrierId, plannedLoadingAt, expectedVersion] properties: outboundOrderId: {type: integer, format: int64, minimum: 1, nullable: true} oceanBookingId: {type: integer, format: int64, minimum: 1, description: Required RELEASED booking; must have a bill of lading and own an active BOOKING_CONFIRMATION or BILL_OF_LADING file.} warehouseId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional consistency hint. The persisted warehouse is frozen from the selected booking.} carrierId: {type: integer, format: int64, minimum: 1, description: Required ACTIVE Warehouse supplier ID validated as TRUCKING.} carrierName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot comes from Warehouse supplier master data.} billOfLadingNo: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint; the persisted snapshot is always frozen from the selected RELEASED booking.} plannedLoadingAt: {type: string, format: date-time} payableAmount: {type: number, format: double, minimum: 0, nullable: true, description: Amount payable to the trucking supplier.} currency: {type: string, enum: [RMB, USD], default: RMB, description: Payable amount currency.} expectedVersion: {type: integer, minimum: 0, description: Must be 0 on create; current version on edit.} DetailStableReferences: type: object required: [appointmentNo, carrierId, carrierName, attachmentCount] description: Stable booking and supplier references returned by appointment detail. Container identities are intentionally absent. properties: appointmentNo: {type: string, pattern: '^TA-[0-9]{6}-[0-9]{4}$', example: TA-260818-0001, description: 派车单号;按北京时间创建日逐日编号} oceanBookingId: {type: integer, format: int64, nullable: true} oceanBookingNo: {type: string, nullable: true, pattern: '^OB-[0-9]{6}-[0-9]{4}$', example: OB-260818-0001} warehouseId: {type: integer, format: int64, nullable: true} warehouseName: {type: string, nullable: true} shippingCompanyId: {type: integer, format: int64, nullable: true} shippingCompanyName: {type: string, nullable: true} bookingAgentId: {type: integer, format: int64, nullable: true} bookingAgentName: {type: string, nullable: true} carrierId: {type: integer, format: int64} carrierName: {type: string} billOfLadingNo: {type: string, maxLength: 128, nullable: true, description: Frozen bill of lading snapshot used by dispatch readiness.} payableAmount: {type: number, format: double, minimum: 0, nullable: true} currency: {type: string, enum: [RMB, USD]} attachmentCount: {type: integer, format: int64, minimum: 0, description: Number of attachment references owned by this appointment}