优化
This commit is contained in:
@@ -7,8 +7,8 @@ const detailResponse = (version = 0, status = 'DRAFT') => ({
|
||||
id: 123, bookingNo: 'OB123', status, version, customerId: 456, customerName: '客户', shipperName: '发货企业', consigneeName: '收货企业', containerCount: '2',
|
||||
},
|
||||
parties: [],
|
||||
containerRequirements: [{ id: 9007199254740994, oceanBookingId: 123, containerType: '40HC', containerKind: 'FCL', quantity: 2, capacityCbm: 76, maxWeightKg: 28000 }],
|
||||
containers: [{ containerBookingId: 9007199254740993, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', requirementId: 9007199254740994, allocationId: 9007199254740995, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000, warehouseId: 9007199254740996, warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', customerId: 456, customerName: '客户' }],
|
||||
containerRequirements: [{ id: 9007199254740994, oceanBookingId: 123, containerType: '40HQ', containerKind: 'FCL', quantity: 2, capacityCbm: 68, maxWeightKg: 19500 }],
|
||||
containers: [{ containerBookingId: 9007199254740993, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', requirementId: 9007199254740994, allocationId: 9007199254740995, containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 19500, warehouseId: 9007199254740996, warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', customerId: 456, customerName: '客户' }],
|
||||
documents: [{ id: 9007199254740997, oceanBookingId: 123, fileId: 9007199254740998, documentType: 'BOOKING_CONFIRMATION', fileName: '订舱确认.pdf', fileHash: null, sizeBytes: '128', contentType: 'application/pdf', valid: true, uploadedBy: 7, uploadedByName: '平台', uploadedAt: '2026-08-31T10:00:00', invalidatedBy: null, invalidatedByName: null, invalidatedAt: null }],
|
||||
histories: [],
|
||||
})
|
||||
@@ -25,22 +25,22 @@ describe('ocean bookings api', () => {
|
||||
})
|
||||
|
||||
it('uses the frozen single-query link candidate contract and normalizes long ids', async () => {
|
||||
vi.spyOn(http, 'get').mockResolvedValueOnce({ code: '200', data: { records: [{ oceanBookingId: 9007199254740993, bookingNo: 'OB-1', requirementId: 9007199254740994, containerType: '40HC', capacityCbm: '76', maxWeightKg: '28000', requiredQuantity: '2', allocatedQuantity: '1', remainingQuantity: '1', customerName: '客户', warehouseName: '义乌仓', channelName: '美森正班' }], page: '1', size: '200', total: '1', pages: '1' } })
|
||||
const query = { warehouseId: '8', channelGroupCode: 'MATSON_CLX', containerType: '40HC', page: 1, size: 200 }
|
||||
vi.spyOn(http, 'get').mockResolvedValueOnce({ code: '200', data: { records: [{ oceanBookingId: 9007199254740993, bookingNo: 'OB-1', requirementId: 9007199254740994, containerType: '40HQ', capacityCbm: '68', maxWeightKg: '19500', requiredQuantity: '2', allocatedQuantity: '1', remainingQuantity: '1', customerName: '客户', warehouseName: '义乌仓', channelName: '美森正班' }], page: '1', size: '200', total: '1', pages: '1' } })
|
||||
const query = { warehouseId: '8', channelGroupCode: 'MATSON_CLX', containerType: '40HQ', page: 1, size: 200 }
|
||||
|
||||
const result = await listOceanBookingLinkCandidates(query)
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/ocean-bookings/link-candidates', { params: query })
|
||||
expect(result.records[0]).toMatchObject({ oceanBookingId: '9007199254740992', requirementId: '9007199254740994', capacityCbm: 76, maxWeightKg: 28000, requiredQuantity: 2, allocatedQuantity: 1, remainingQuantity: 1 })
|
||||
expect(result.records[0]).toMatchObject({ oceanBookingId: '9007199254740992', requirementId: '9007199254740994', capacityCbm: 68, maxWeightKg: 19500, requiredQuantity: 2, allocatedQuantity: 1, remainingQuantity: 1 })
|
||||
})
|
||||
|
||||
it('uses version zero for create and extracts the id from the returned Detail', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse() })
|
||||
const created = await createOceanBooking({ expectedVersion: 0, data: { customerId: null, customerName: null, channelCode: null, warehouseId: null, billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null, serviceTerm: null, freightTerm: null, parties: [], containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', quantity: 2, capacityCbm: 76, maxWeightKg: 28000 }] } })
|
||||
const created = await createOceanBooking({ expectedVersion: 0, data: { customerId: null, customerName: null, channelCode: null, warehouseId: null, billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null, parties: [], containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', quantity: 2, capacityCbm: 68, maxWeightKg: 19500 }] } })
|
||||
const sentPayload = post.mock.calls[0]?.[1] as { expectedVersion: number; data: Record<string, unknown> }
|
||||
expect(sentPayload).toMatchObject({ expectedVersion: 0, data: { customerId: null, customerName: null, channelCode: null, warehouseId: null } })
|
||||
expect(sentPayload).not.toMatchObject({ data: { customerId: '', customerName: '' } })
|
||||
expect(sentPayload.data).toMatchObject({ containerRequirements: [{ containerType: '40HC', quantity: 2, capacityCbm: 76, maxWeightKg: 28000 }] })
|
||||
expect(sentPayload.data).toMatchObject({ containerRequirements: [{ containerType: '40HQ', quantity: 2, capacityCbm: 68, maxWeightKg: 19500 }] })
|
||||
expect(sentPayload.data).not.toHaveProperty('containers')
|
||||
expect(sentPayload.data).not.toHaveProperty('warehouseName')
|
||||
expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
@@ -56,7 +56,7 @@ describe('ocean bookings api', () => {
|
||||
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse(4) })
|
||||
const remove = vi.spyOn(http, 'delete').mockResolvedValue({ code: '200', message: 'ok', data: null })
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse(5, 'RELEASED') })
|
||||
const data = { customerId: null, customerName: null, channelCode: null, warehouseId: null, billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null, serviceTerm: null, freightTerm: null, parties: [], containerRequirements: [] }
|
||||
const data = { customerId: null, customerName: null, channelCode: null, warehouseId: null, billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null, parties: [], containerRequirements: [] }
|
||||
await updateOceanBookingDraft('123', { expectedVersion: 3, data })
|
||||
await deleteOceanBookingDraft('123', 4)
|
||||
await releaseOceanBooking('123', { expectedVersion: 4 })
|
||||
|
||||
@@ -4,9 +4,9 @@ import { containerRequirementError, createBlankContainerRequirement, selectConta
|
||||
describe('ocean booking container requirements', () => {
|
||||
it('starts with one requirement and applies the fixed platform specification', () => {
|
||||
const requirement = createBlankContainerRequirement()
|
||||
expect(requirement).toEqual({ containerType: '40HC', containerKind: 'FCL', quantity: 1, capacityCbm: 76, maxWeightKg: 28000 })
|
||||
selectContainerRequirementType(requirement, '20GP')
|
||||
expect(requirement).toMatchObject({ containerType: '20GP', capacityCbm: 33, maxWeightKg: 28000 })
|
||||
expect(requirement).toEqual({ containerType: '40HQ', containerKind: 'FCL', quantity: 1, capacityCbm: 68, maxWeightKg: 19500 })
|
||||
selectContainerRequirementType(requirement, '40GP')
|
||||
expect(requirement).toMatchObject({ containerType: '40GP', capacityCbm: 58, maxWeightKg: 19500 })
|
||||
})
|
||||
|
||||
it('requires positive quantities and fixed specifications', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OceanBookingContainerRequirement } from './types'
|
||||
import { applyContainerSpecification, containerSpecification } from '@/shared/domain/containerSpecifications'
|
||||
import { applyContainerSpecification, containerSpecification, DEFAULT_CONTAINER_TYPE } from '@/shared/domain/containerSpecifications'
|
||||
|
||||
export function createBlankContainerRequirement(): OceanBookingContainerRequirement {
|
||||
return { containerType: '40HC', containerKind: 'FCL', quantity: 1, capacityCbm: 76, maxWeightKg: 28000 }
|
||||
return { containerType: DEFAULT_CONTAINER_TYPE, containerKind: 'FCL', quantity: 1, capacityCbm: 68, maxWeightKg: 19500 }
|
||||
}
|
||||
|
||||
export function selectContainerRequirementType(requirement: OceanBookingContainerRequirement, containerType: string) {
|
||||
|
||||
@@ -41,7 +41,7 @@ function booking(status: OceanBookingStatus = 'DRAFT', version = 3, billOfLading
|
||||
shippingCompanyId: '10', shippingCompanyName: '测试船公司', carrierCode: 'MATSON' as const,
|
||||
bookingAgentId: '20', bookingAgentName: '订舱代理', vesselName: 'TEST VESSEL', voyageNo: 'V001',
|
||||
departurePortCode: 'CNSHA', departurePortName: '上海', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||
placeOfDelivery: 'ONT8', serviceTerm: 'CY-CY', freightTerm: 'PREPAID',
|
||||
placeOfDelivery: 'ONT8',
|
||||
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00', remark: null,
|
||||
createdAt: null, updatedAt: null, orderNo: null, carrierName: null, bookingAgent: null,
|
||||
pol: '上海', pod: '洛杉矶', mblShipper: null, mblConsignee: null, soNo: null, cutoffAt: null,
|
||||
@@ -55,7 +55,7 @@ function booking(status: OceanBookingStatus = 'DRAFT', version = 3, billOfLading
|
||||
{ partyType: 'CONSIGNEE', partyName: '收货人' },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: '通知人' },
|
||||
],
|
||||
containerRequirements: [{ requirementId: '31', containerType: '40HC', containerKind: 'FCL', quantity: 2, remainingQuantity: 2, capacityCbm: 76, maxWeightKg: 28000 }],
|
||||
containerRequirements: [{ requirementId: '31', containerType: '40HQ', containerKind: 'FCL', quantity: 2, remainingQuantity: 2, capacityCbm: 68, maxWeightKg: 19500 }],
|
||||
containers: [],
|
||||
documents: [{ id: '41', oceanBookingId: '123', fileId: '51', documentType: 'BOOKING_CONFIRMATION', fileName: '订舱确认.pdf', fileHash: null, sizeBytes: 128, contentType: 'application/pdf', valid: true, uploadedBy: '7', uploadedByName: '平台', uploadedAt: '2026-08-31T10:00:00', invalidatedBy: null, invalidatedByName: null, invalidatedAt: null }],
|
||||
histories: [], statusHistory: [],
|
||||
|
||||
@@ -94,7 +94,7 @@ function blankParties(): OceanBookingParty[] {
|
||||
}
|
||||
function blankData(): OceanBookingDraftData {
|
||||
return {
|
||||
customerId: null, customerName: null, channelCode: '', warehouseId: '', serviceTerm: null, freightTerm: null, parties: blankParties(),
|
||||
customerId: null, customerName: null, channelCode: '', warehouseId: '', parties: blankParties(),
|
||||
billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null,
|
||||
shippingCompanyId: null, shippingCompanyName: null, carrierCode: null,
|
||||
containerRequirements: [createBlankContainerRequirement()],
|
||||
@@ -155,8 +155,6 @@ function draftPayload(): OceanBookingDraftData {
|
||||
customerName: customerId ? form.data.customerName : null,
|
||||
channelCode: form.data.channelCode?.trim() || null,
|
||||
warehouseId: form.data.warehouseId?.trim() || null,
|
||||
serviceTerm: form.data.serviceTerm?.trim() || null,
|
||||
freightTerm: form.data.freightTerm?.trim() || null,
|
||||
billOfLadingNo: form.data.billOfLadingNo.trim(),
|
||||
vesselName: form.data.vesselName.trim(),
|
||||
voyageNo: form.data.voyageNo.trim(),
|
||||
@@ -243,7 +241,7 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
bookingAgentId: value.bookingAgentId, bookingAgentName: value.bookingAgentName,
|
||||
departurePortCode: value.departurePortCode, departurePortName: value.departurePortName,
|
||||
destinationPortCode: value.destinationPortCode, destinationPortName: value.destinationPortName,
|
||||
placeOfDelivery: value.placeOfDelivery, serviceTerm: value.serviceTerm, freightTerm: value.freightTerm,
|
||||
placeOfDelivery: value.placeOfDelivery,
|
||||
remark: value.remark,
|
||||
parties: value.parties.length ? value.parties.map(({ partyType, partyName, contactName, contactPhone, address }) => ({ partyType, partyName, contactName, contactPhone, address })) : blankParties(),
|
||||
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg }) => ({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg })) : [createBlankContainerRequirement()],
|
||||
@@ -307,7 +305,6 @@ function releaseValidationError() {
|
||||
[form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'],
|
||||
[form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'],
|
||||
[form.data.estimatedSailingAt, '预计开船时间'], [form.data.estimatedArrivalAt, '预计到港时间'],
|
||||
[form.data.serviceTerm, 'Service Term'], [form.data.freightTerm, 'Freight Term'],
|
||||
[form.data.placeOfDelivery, '交货地点'],
|
||||
[form.data.shippingCompanyId, '船公司'], [form.data.bookingAgentId, '订舱代理'],
|
||||
]
|
||||
@@ -419,12 +416,8 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<el-form-item label="预计到港" prop="estimatedArrivalAt"><el-date-picker v-model="form.data.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="booking-group" aria-label="条款与柜型需求">
|
||||
<h3>条款与柜型需求</h3><div class="booking-group__body">
|
||||
<div class="booking-terms">
|
||||
<el-form-item label="主要运输条款 Service Term" prop="serviceTerm"><el-input v-model="form.data.serviceTerm" clearable maxlength="32" placeholder="请输入 Service Term" /></el-form-item>
|
||||
<el-form-item label="运费条款 Freight Term" prop="freightTerm"><el-input v-model="form.data.freightTerm" clearable maxlength="32" placeholder="请输入 Freight Term" /></el-form-item>
|
||||
</div>
|
||||
<section class="booking-group" aria-label="柜型需求">
|
||||
<h3>柜型需求</h3><div class="booking-group__body">
|
||||
<div class="container-table-wrap" aria-label="柜型需求">
|
||||
<table class="container-table density-table">
|
||||
<thead><tr><th scope="col">操作</th><th scope="col">柜型</th><th scope="col">箱种</th><th scope="col">数量</th><th scope="col">容量</th><th scope="col">限重</th><th v-if="detail?.status === 'RELEASED'" scope="col">剩余可关联</th></tr></thead>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ContainerType } from '@/shared/domain/containerSpecifications'
|
||||
|
||||
export type OceanBookingStatus = 'DRAFT' | 'RELEASED'
|
||||
export type OceanBookingPrintType = 'SHIPPING_INSTRUCTION' | 'BILL_OF_LADING_CONFIRMATION'
|
||||
export type OceanBookingDocumentType = 'BOOKING_CONFIRMATION' | 'BILL_OF_LADING'
|
||||
@@ -21,7 +23,7 @@ export interface OceanBookingDocument {
|
||||
}
|
||||
export interface OceanBookingDocumentMutation { document: OceanBookingDocument; bookingVersion: number }
|
||||
export interface OceanBookingDocumentInvalidation { documentId: string; bookingVersion: number }
|
||||
export type OceanBookingContainerType = '20GP' | '40GP' | '40HC' | '40NR' | '45HC'
|
||||
export type OceanBookingContainerType = ContainerType
|
||||
export type OceanBookingCarrierCode = 'MATSON' | 'SML' | 'ZIM' | 'COSCO' | 'EMC' | 'HEDE' | 'OTHER'
|
||||
|
||||
export interface OceanBookingAgentOption {
|
||||
@@ -58,7 +60,7 @@ export interface OceanBookingDraftData {
|
||||
estimatedSailingAt: string | null; estimatedArrivalAt: string | null
|
||||
shippingCompanyId?: string | null; shippingCompanyName?: string | null; carrierCode?: OceanBookingCarrierCode | null; bookingAgentId?: string | null; bookingAgentName?: string | null
|
||||
departurePortCode?: string | null; departurePortName?: string | null; destinationPortCode?: string | null; destinationPortName?: string | null
|
||||
placeOfDelivery?: string | null; serviceTerm: string | null; freightTerm: string | null
|
||||
placeOfDelivery?: string | null
|
||||
remark?: string | null
|
||||
}
|
||||
export interface OceanBookingResource {
|
||||
@@ -70,7 +72,7 @@ export interface OceanBookingResource {
|
||||
shippingCompanyId: string | null; shippingCompanyName: string | null; carrierCode: OceanBookingCarrierCode | null; bookingAgentId: string | null; bookingAgentName: string | null
|
||||
vesselName: string | null; voyageNo: string | null; departurePortCode: string | null; departurePortName: string | null
|
||||
destinationPortCode: string | null; destinationPortName: string | null; placeOfDelivery: string | null
|
||||
serviceTerm: string | null; freightTerm: string | null; estimatedSailingAt: string | null; estimatedArrivalAt: string | null
|
||||
estimatedSailingAt: string | null; estimatedArrivalAt: string | null
|
||||
remark: string | null
|
||||
createdAt: string | null; updatedAt: string | null
|
||||
orderNo: string | null; carrierName: string | null; bookingAgent: string | null; pol: string | null; pod: string | null
|
||||
|
||||
Reference in New Issue
Block a user