refactor(firstleg): remove customer scope from trucking
This commit is contained in:
+2
-2
@@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TruckAppointmentControllerContractTest {
|
||||
@Test void exposesDedicatedEndpointAndPermissions() {String p=PlatformTruckAppointmentController.class.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0];assertThat(p).isEqualTo("/api/platform/v1/truck-appointments"); for(String n:new String[]{"page","detail","history","attachments","attach","upload","deleteAttachment","create","edit","action","reschedule"}) assertThat(java.util.Arrays.stream(PlatformTruckAppointmentController.class.getDeclaredMethods()).anyMatch(m->m.getName().equals(n))).isTrue();}
|
||||
@Test void schemaHasAggregateTablesStatesAndActualEventTimes() throws Exception {String s=new ClassPathResource("db/phase3-truck-appointment-schema.sql").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("fl_truck_appointment","fl_truck_appointment_history","fl_truck_appointment_attachment","fl_truck_appointment_operation","bill_of_lading_no VARCHAR(128)","chk_truck_attachment_type","DISPATCH_ORDER","PENDING_DISPATCH","PICKUP_IN_PROGRESS","ARRIVED_WAREHOUSE","RESCHEDULED","actual_arrived_warehouse_at","actual_loading_at","actual_departed_at","CALL phase3_truck_add_column");}
|
||||
@Test void schemaHasPlatformAggregateWithoutCustomerColumns() throws Exception {String s=new ClassPathResource("db/phase3-truck-appointment-schema.sql").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("fl_truck_appointment","fl_truck_appointment_history","fl_truck_appointment_attachment","fl_truck_appointment_operation","bill_of_lading_no VARCHAR(128)","chk_truck_attachment_type","DISPATCH_ORDER","PENDING_DISPATCH","PICKUP_IN_PROGRESS","ARRIVED_WAREHOUSE","RESCHEDULED","actual_arrived_warehouse_at","actual_loading_at","actual_departed_at","CALL phase3_truck_add_column","visibility VARCHAR(24) NOT NULL DEFAULT 'PLATFORM'","active_container_booking_id BIGINT UNSIGNED GENERATED ALWAYS AS","UNIQUE KEY uk_truck_active_container(active_container_booking_id)").doesNotContain("customer_id", "customer_name", "idx_truck_appointment_customer");}
|
||||
@Test void existingAppointmentTableReceivesAllLateColumns() throws Exception {
|
||||
String moduleSql=new ClassPathResource("db/phase3-truck-appointment-schema.sql").getContentAsString(StandardCharsets.UTF_8);
|
||||
Path direct=Path.of("deploy","sql","头程数据库统一增量-MySQL8.sql");
|
||||
@@ -30,7 +30,7 @@ class TruckAppointmentControllerContractTest {
|
||||
}
|
||||
}
|
||||
@Test void openApiDocumentsLifecycleIdempotencyAndActualTimes() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("/api/platform/v1/truck-appointments/{id}/{action}:","/api/platform/v1/truck-appointments/{id}/reschedule:","name: Idempotency-Key","required: true","actual event timestamps");}
|
||||
@Test void openApiRequiresReleasedBookingAndAllocatedRealContainer() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("required: [oceanBookingId, containerBookingId, customerId, carrierId, plannedLoadingAt, expectedVersion]","RELEASED ocean booking","ACTIVE real-numbered container","OCEAN_BOOKING_DOCUMENT","valid unique ISO 6346 container number","schema: {$ref: '#/components/schemas/DetailEnvelope'}","required: [code, message, data, timestamp]","allOf: [{$ref: '#/components/schemas/DetailStableReferences'}]","nullable: true");}
|
||||
@Test void openApiRequiresReleasedBookingAndAllocatedRealContainerWithoutCustomerScope() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("required: [oceanBookingId, containerBookingId, carrierId, plannedLoadingAt, expectedVersion]","RELEASED ocean booking","ACTIVE real-numbered container","OCEAN_BOOKING_DOCUMENT","valid unique ISO 6346 container number","schema: {$ref: '#/components/schemas/DetailEnvelope'}","required: [code, message, data, timestamp]","allOf: [{$ref: '#/components/schemas/DetailStableReferences'}]","nullable: true").doesNotContain("customerId", "customerName", "Filter by customer");}
|
||||
@Test void openApiKeepsOnlyNonBookingTruckAttachments() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("AttachmentRequest:","required: [fileId, attachmentType]","required: [attachmentType, file]","TRUCK_APPOINTMENT_ATTACHMENT","Deletes the appointment reference only","DetailStableReferences:","oceanBookingNo:","shippingCompanyName:","bookingAgentName:","cabinetNo:","billOfLadingNo:").doesNotContain("enum: [BILL_OF_LADING, DISPATCH_ORDER");}
|
||||
@Test void methodsHavePlatformPermissionsAndAuthenticatedScope() {
|
||||
Map<String,String> expected=Map.ofEntries(
|
||||
|
||||
+4
-2
@@ -13,8 +13,9 @@ class TruckAppointmentMapperContractTest {
|
||||
@Test void mapsToPhase3TableAndUsesOptimisticGuards() throws Exception {
|
||||
assertThat(TruckAppointmentEntity.class.getAnnotation(TableName.class).value()).isEqualTo("fl_truck_appointment");
|
||||
String page = TruckAppointmentMapper.class.getMethod("page", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class, com.baiyue.firstleg.dto.TruckAppointmentDtos.Query.class).getAnnotation(Select.class).value()[0];
|
||||
assertThat(page).contains("fl_truck_appointment", "appointment_no", "vehicle_no", "status=#{q.status}", "customer_id=#{q.customerId}", "container_booking_id=#{q.containerBookingId}",
|
||||
assertThat(page).contains("fl_truck_appointment", "appointment_no", "vehicle_no", "status=#{q.status}", "container_booking_id=#{q.containerBookingId}",
|
||||
"fl_ocean_booking", "fl_container_booking", "record_status='ACTIVE'", "cabinet_no", "container_no", "fl_truck_appointment_attachment", "attachment_count");
|
||||
assertThat(page).doesNotContain("customer_id", "customerId", "customer_name");
|
||||
String transition = TruckAppointmentMapper.class.getMethod("transition", long.class, String.class, String.class, int.class).getAnnotation(Update.class).value()[0];
|
||||
assertThat(transition).contains("status=#{toStatus}", "status=#{fromStatus}", "version=#{version}", "revision=revision+1", "version=version+1");
|
||||
String dispatch = TruckAppointmentMapper.class.getMethod("dispatch", long.class, String.class, String.class, int.class).getAnnotation(Update.class).value()[0];
|
||||
@@ -29,8 +30,9 @@ class TruckAppointmentMapperContractTest {
|
||||
assertThat(reschedule).contains("status='RESCHEDULED'", "version=#{version}", "revision=revision+1");
|
||||
String edit = TruckAppointmentMapper.class.getMethod("updateEdit", TruckAppointmentEntity.class, int.class).getAnnotation(Update.class).value()[0];
|
||||
assertThat(edit).contains("ocean_booking_id=#{e.oceanBookingId}", "container_booking_id=#{e.containerBookingId}",
|
||||
"loading_plan_id=#{e.loadingPlanId}", "customer_id=#{e.customerId}", "carrier_id=#{e.carrierId}",
|
||||
"loading_plan_id=#{e.loadingPlanId}", "carrier_id=#{e.carrierId}",
|
||||
"planned_loading_at=#{e.plannedLoadingAt}", "bill_of_lading_no=#{e.billOfLadingNo}", "WHERE id=#{e.id} AND version=#{version}",
|
||||
"version=version+1", "revision=revision+1");
|
||||
assertThat(edit).doesNotContain("customer_id", "customer_name", "customerId", "customerName");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,57 @@ class ContainerServiceTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同一订舱箱型数量允许绑定多个不同真实柜号")
|
||||
void linksMultipleRealContainersToTheSameReleasedBookingRequirement() {
|
||||
ContainerBookingEntity first = booking("WAIT_LOADING", 2);
|
||||
first.setId(11L); first.setContainerNo(null); first.setWarehouseId(101L);
|
||||
first.setChannelCode("SEA-US"); first.setContainerType("40HC");
|
||||
ContainerBookingEntity second = booking("WAIT_LOADING", 4);
|
||||
second.setId(12L); second.setContainerNo(null); second.setWarehouseId(101L);
|
||||
second.setChannelCode("SEA-US"); second.setContainerType("40HC");
|
||||
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
||||
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
|
||||
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("SEA-US");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HC");
|
||||
when(repository.find(11L)).thenReturn(first);
|
||||
when(repository.find(12L)).thenReturn(second);
|
||||
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
|
||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L, 1L);
|
||||
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
||||
same(oceanBooking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
||||
when(repository.linkReleasedBooking(eq(12L), eq(21L), eq(31L), eq("TSTU1503306"),
|
||||
same(oceanBooking), eq(4), eq(9L), eq("admin"))).thenReturn(1);
|
||||
|
||||
service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2));
|
||||
service.linkOceanBooking(12L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "TSTU1503306", 4));
|
||||
|
||||
ArgumentCaptor<OceanBookingContainerAllocationEntity> allocations =
|
||||
ArgumentCaptor.forClass(OceanBookingContainerAllocationEntity.class);
|
||||
verify(oceanBookings, org.mockito.Mockito.times(2)).insert(allocations.capture());
|
||||
assertThat(allocations.getAllValues()).extracting(OceanBookingContainerAllocationEntity::getContainerBookingId)
|
||||
.containsExactly(11L, 12L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已经归属其他订舱的真实柜不能改绑")
|
||||
void rejectsContainerAlreadyOwnedByAnotherBooking() {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setContainerNo(null);
|
||||
container.setOceanBookingId(22L);
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已关联真实订舱柜");
|
||||
|
||||
verifyNoInteractions(oceanBookings);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝装柜仓库不一致")
|
||||
void rejectsLinkWhenWarehouseDiffers() {
|
||||
|
||||
+104
-32
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.TruckAppointmentStatus;
|
||||
import com.baiyue.firstleg.dto.TruckAppointmentDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.dto.TruckAppointmentDtos.AttachmentRequest;
|
||||
import com.baiyue.firstleg.dto.TruckAppointmentDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentEntity;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity;
|
||||
@@ -17,7 +18,6 @@ import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.service.OceanBookingDocumentService;
|
||||
import com.baiyue.firstleg.client.IamCustomerSnapshotClient;
|
||||
import com.baiyue.firstleg.client.TruckAppointmentFileClient;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
@@ -28,6 +28,7 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.Spy;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
@@ -46,7 +47,6 @@ class TruckAppointmentServiceTest {
|
||||
@Mock SmartLoadingRepository loadingPlans;
|
||||
@Mock ContainerRepository containers;
|
||||
@Mock WarehouseSupplierSnapshotService suppliers;
|
||||
@Mock IamCustomerSnapshotService customers;
|
||||
@Mock TruckAppointmentFileClient files;
|
||||
@Mock OceanBookingDocumentService oceanBookingDocuments;
|
||||
@Mock FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@@ -56,9 +56,6 @@ class TruckAppointmentServiceTest {
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this);
|
||||
service.internalToken = "test-internal-token";
|
||||
lenient().when(documentNumbers.next("TA")).thenReturn("TA-260818-0001");
|
||||
when(customers.active(anyLong(), any())).thenAnswer(invocation ->
|
||||
new IamCustomerSnapshotClient.Customer(invocation.getArgument(0), "C-7",
|
||||
"Canonical Customer", "ACTIVE", null, null));
|
||||
when(suppliers.trucking(any(), any())).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(8L, "Canonical Carrier", "TRUCKING", "ACTIVE", java.util.List.of()));
|
||||
lenient().when(repository.attachments(anyLong())).thenReturn(List.of());
|
||||
@@ -102,7 +99,6 @@ class TruckAppointmentServiceTest {
|
||||
@Test void createRejectsMissingBookingAndContainer() {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.oceanBookingId()).thenReturn(null);
|
||||
@@ -127,11 +123,70 @@ class TruckAppointmentServiceTest {
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@Test void createRejectsAnotherActiveAppointmentForTheSameRealContainerWithoutWriting() {
|
||||
SaveRequest request = validRequest();
|
||||
when(repository.activeForUpdate(88L)).thenReturn(List.of(entity(TruckAppointmentStatus.DRAFT, 0)));
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已有有效拖车预约");
|
||||
|
||||
verify(repository, never()).insert(any());
|
||||
verify(repository, never()).history(any());
|
||||
}
|
||||
|
||||
@Test void concurrentCreateUniqueCollisionBecomesBusinessConflictWithoutHistory() {
|
||||
SaveRequest request = validRequest();
|
||||
doThrow(new DuplicateKeyException("Duplicate entry for key 'uk_truck_active_container'"))
|
||||
.when(repository).insert(any());
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已有有效拖车预约");
|
||||
|
||||
verify(repository, never()).history(any());
|
||||
}
|
||||
|
||||
@Test void createPreservesMaximumChineseRemarkAndBoundaryValues() {
|
||||
SaveRequest request = validRequest();
|
||||
String remark = "报".repeat(1000);
|
||||
when(request.remark()).thenReturn(remark);
|
||||
when(request.packageCount()).thenReturn(0);
|
||||
when(request.palletCount()).thenReturn(0);
|
||||
when(request.grossWeightKg()).thenReturn(java.math.BigDecimal.ZERO);
|
||||
when(request.volumeCbm()).thenReturn(java.math.BigDecimal.ZERO);
|
||||
when(request.transitMinutes()).thenReturn(0);
|
||||
|
||||
service.create(7L, "admin", request);
|
||||
|
||||
ArgumentCaptor<TruckAppointmentEntity> inserted = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(inserted.capture());
|
||||
assertThat(inserted.getValue().getRemark()).hasSize(1000).isEqualTo(remark);
|
||||
assertThat(inserted.getValue().getPackageCount()).isZero();
|
||||
assertThat(inserted.getValue().getPalletCount()).isZero();
|
||||
assertThat(inserted.getValue().getGrossWeightKg()).isZero();
|
||||
assertThat(inserted.getValue().getVolumeCbm()).isZero();
|
||||
}
|
||||
|
||||
@Test void platformOwnedAttachmentUsesZeroTenantWithoutCustomerSnapshot() {
|
||||
TruckAppointmentEntity appointment = entity(TruckAppointmentStatus.DRAFT, 0);
|
||||
when(repository.find(1L)).thenReturn(appointment);
|
||||
when(files.metadata("test-internal-token", 0L, "TRUCK_APPOINTMENT_ATTACHMENT", "1", 77L))
|
||||
.thenReturn(ApiResponse.success(new TruckAppointmentFileClient.Metadata(77L, 0L,
|
||||
"TRUCK_APPOINTMENT_ATTACHMENT", "1", "派车资料.pdf", "abc", "application/pdf", 12L)));
|
||||
|
||||
TruckAppointmentAttachmentEntity result = service.attach(1L,
|
||||
new AttachmentRequest(77L, "DISPATCH_ORDER", "ignored", "ignored"), 9L);
|
||||
|
||||
assertThat(result.getFileId()).isEqualTo(77L);
|
||||
assertThat(result.getVisibility()).isEqualTo("PLATFORM");
|
||||
verify(files).metadata("test-internal-token", 0L, "TRUCK_APPOINTMENT_ATTACHMENT", "1", 77L);
|
||||
verify(repository).attachment(result);
|
||||
}
|
||||
|
||||
@Test void createFreezesReleasedBookingBillAndRejectsClientMismatch() {
|
||||
SaveRequest request = validRequest();
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L);
|
||||
booking.setId(99L); booking.setStatus("RELEASED");
|
||||
booking.setBillOfLadingNo(" BL-BOOKING-1 ");
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
|
||||
@@ -152,39 +207,26 @@ class TruckAppointmentServiceTest {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
when(request.loadingPlanId()).thenReturn(null);
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("CONFIRMED"); booking.setCustomerId(7L); booking.setWarehouseId(18L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("CONFIRMED"); booking.setWarehouseId(18L);
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已放舱");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void createRejectsBookingFromAnotherCustomer() {
|
||||
SaveRequest request = validRequest();
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(17L); booking.setBillOfLadingNo("BL-1");
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("customerId").hasMessageContaining("订舱");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void createWithBookingAndContainerRequiresTheirAllocation() {
|
||||
SaveRequest request = validRequest();
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L); booking.setWarehouseId(18L); booking.setBillOfLadingNo("BL-1");
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setWarehouseId(18L); booking.setBillOfLadingNo("BL-1");
|
||||
ContainerBookingEntity container = new ContainerBookingEntity();
|
||||
container.setId(88L); container.setRecordStatus("ACTIVE"); container.setCustomerId(7L); container.setWarehouseId(18L); container.setOceanBookingId(99L); container.setContainerNo("MSCU6639870");
|
||||
container.setId(88L); container.setRecordStatus("ACTIVE"); container.setWarehouseId(18L); container.setOceanBookingId(99L); container.setContainerNo("MSCU6639870");
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
when(containers.find(88L)).thenReturn(container);
|
||||
when(oceanBookings.allocations(99L)).thenReturn(List.of());
|
||||
@@ -213,7 +255,7 @@ class TruckAppointmentServiceTest {
|
||||
|
||||
@Test void editCanSwitchOnlyToAnotherMatchedReleasedBookingAndContainer() {
|
||||
TruckAppointmentEntity existing = entity(TruckAppointmentStatus.DRAFT, 3);
|
||||
existing.setCustomerId(7L); existing.setOceanBookingId(99L); existing.setContainerBookingId(88L);
|
||||
existing.setOceanBookingId(99L); existing.setContainerBookingId(88L);
|
||||
when(repository.find(1L)).thenReturn(existing, existing);
|
||||
SaveRequest request = validRequest();
|
||||
when(request.expectedVersion()).thenReturn(3);
|
||||
@@ -258,7 +300,6 @@ class TruckAppointmentServiceTest {
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
when(request.loadingPlanId()).thenReturn(77L);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
@@ -283,7 +324,6 @@ class TruckAppointmentServiceTest {
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
when(request.loadingPlanId()).thenReturn(77L);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
@@ -335,6 +375,40 @@ class TruckAppointmentServiceTest {
|
||||
verify(repository, never()).history(any());
|
||||
}
|
||||
|
||||
@Test void concurrentSameKeyBeforeFirstCompletionReturnsRetryableConflictWithoutDrift() throws Exception {
|
||||
var request = new ActionRequest(0, "并发提交");
|
||||
String hash = requestHash("SUBMIT", 1L, request);
|
||||
TruckAppointmentOperationEntity processing = new TruckAppointmentOperationEntity();
|
||||
processing.setAppointmentId(1L); processing.setAction("SUBMIT");
|
||||
processing.setIdempotencyKey("concurrent-submit"); processing.setRequestHash(hash);
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("SUBMIT"), eq("concurrent-submit"), eq(hash)))
|
||||
.thenReturn(false);
|
||||
when(repository.operation("concurrent-submit")).thenReturn(processing);
|
||||
|
||||
assertThatThrownBy(() -> service.action(1L, "SUBMIT", request, "concurrent-submit", hash, 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("正在处理中");
|
||||
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt());
|
||||
verify(repository, never()).history(any());
|
||||
verify(repository, never()).completeOperation(anyLong(), anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test void sameIdempotencyKeyWithDifferentPayloadConflictsWithoutDrift() {
|
||||
TruckAppointmentOperationEntity existing = new TruckAppointmentOperationEntity();
|
||||
existing.setAppointmentId(1L); existing.setAction("SUBMIT");
|
||||
existing.setIdempotencyKey("reused-key"); existing.setRequestHash("different-hash");
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("SUBMIT"), eq("reused-key"), anyString()))
|
||||
.thenReturn(false);
|
||||
when(repository.operation("reused-key")).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.action(1L, "SUBMIT", new ActionRequest(0, "载荷A"),
|
||||
"reused-key", null, 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt());
|
||||
verify(repository, never()).history(any());
|
||||
}
|
||||
|
||||
@Test void dispatchRequiresBookingBillAndActiveBookingOwnedDocument() {
|
||||
TruckAppointmentEntity missingBill = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
missingBill.setOceanBookingId(99L); missingBill.setContainerBookingId(88L);
|
||||
@@ -349,7 +423,6 @@ class TruckAppointmentServiceTest {
|
||||
|
||||
TruckAppointmentEntity missingAttachment = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
missingAttachment.setOceanBookingId(99L); missingAttachment.setContainerBookingId(88L); missingAttachment.setBillOfLadingNo("BL-BOOKING-1");
|
||||
missingAttachment.setCustomerId(7L);
|
||||
when(oceanBookings.find(99L)).thenReturn(releasedBooking());
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("DISPATCH"), eq("dispatch-no-file"), anyString())).thenReturn(true);
|
||||
when(repository.find(1L)).thenReturn(missingAttachment);
|
||||
@@ -364,9 +437,9 @@ class TruckAppointmentServiceTest {
|
||||
|
||||
@Test void dispatchFreezesReleasedBookingBillAndTransitionsOnceWithActiveAttachment() {
|
||||
TruckAppointmentEntity before = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
before.setCustomerId(7L); before.setOceanBookingId(99L); before.setContainerBookingId(88L); before.setBillOfLadingNo(null);
|
||||
before.setOceanBookingId(99L); before.setContainerBookingId(88L); before.setBillOfLadingNo(null);
|
||||
TruckAppointmentEntity after = entity(TruckAppointmentStatus.DISPATCHED, 2);
|
||||
after.setCustomerId(7L); after.setOceanBookingId(99L); after.setContainerBookingId(88L); after.setBillOfLadingNo("BL-BOOKING-1");
|
||||
after.setOceanBookingId(99L); after.setContainerBookingId(88L); after.setBillOfLadingNo("BL-BOOKING-1");
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setBillOfLadingNo("BL-BOOKING-1");
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("DISPATCH"), eq("dispatch-ok"), anyString())).thenReturn(true);
|
||||
@@ -495,7 +568,6 @@ class TruckAppointmentServiceTest {
|
||||
private static SaveRequest validRequest() {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
@@ -507,14 +579,14 @@ class TruckAppointmentServiceTest {
|
||||
|
||||
private static OceanBookingEntity releasedBooking() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L);
|
||||
booking.setId(99L); booking.setStatus("RELEASED");
|
||||
booking.setWarehouseId(18L); booking.setBillOfLadingNo("BL-BOOKING-1");
|
||||
return booking;
|
||||
}
|
||||
|
||||
private static ContainerBookingEntity realContainer() {
|
||||
ContainerBookingEntity container = new ContainerBookingEntity();
|
||||
container.setId(88L); container.setRecordStatus("ACTIVE"); container.setCustomerId(7L);
|
||||
container.setId(88L); container.setRecordStatus("ACTIVE");
|
||||
container.setWarehouseId(18L); container.setOceanBookingId(99L); container.setContainerNo("MSCU6639870");
|
||||
return container;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user