feat(firstleg): enforce booking-owned truck workflow
This commit is contained in:
+3
-3
@@ -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","BILL_OF_LADING","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 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 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,8 +30,8 @@ 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 openApiFreezesIndependentCreateAndOptionalAssociations() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("required: [customerId, carrierId, plannedLoadingAt, expectedVersion]","independent operational records","no RELEASED-state prerequisite","It may be supplied only with containerBookingId","ACTIVE IAM customer","ACTIVE Warehouse supplier ID validated as TRUCKING","Fulfillment associations remain null","schema: {$ref: '#/components/schemas/DetailEnvelope'}","required: [code, message, data, timestamp]","allOf: [{$ref: '#/components/schemas/DetailStableReferences'}]","nullable: true");}
|
||||
@Test void openApiFreezesAttachmentRequestAndOwnershipContract() 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]","BILL_OF_LADING","TRUCK_APPOINTMENT_ATTACHMENT","Deletes the appointment reference only","DetailStableReferences:","oceanBookingNo:","shippingCompanyName:","bookingAgentName:","cabinetNo:","billOfLadingNo:");}
|
||||
@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 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(
|
||||
Map.entry("page","platform:firstleg:truck-appointment:list"), Map.entry("detail","platform:firstleg:truck-appointment:list"),
|
||||
|
||||
+13
-2
@@ -24,6 +24,13 @@ class PlatformOceanBookingControllerContractTest {
|
||||
Map.entry("bookingAgents", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("channels", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("detail", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("documents", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("downloadDocument", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("printShippingInstruction", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("printBillConfirmation", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("attachDocument", "platform:firstleg:ocean-booking:edit"),
|
||||
Map.entry("uploadDocument", "platform:firstleg:ocean-booking:edit"),
|
||||
Map.entry("invalidateDocument", "platform:firstleg:ocean-booking:edit"),
|
||||
Map.entry("create", "platform:firstleg:ocean-booking:create"),
|
||||
Map.entry("updateDraft", "platform:firstleg:ocean-booking:edit"),
|
||||
Map.entry("deleteDraft", "platform:firstleg:ocean-booking:delete"),
|
||||
@@ -33,7 +40,8 @@ class PlatformOceanBookingControllerContractTest {
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (!name.equals("page") && !name.equals("detail") && !name.equals("channels") && !name.equals("bookingAgents")
|
||||
&& !name.equals("linkCandidates")) {
|
||||
&& !name.equals("linkCandidates") && !name.equals("documents") && !name.equals("downloadDocument")
|
||||
&& !name.startsWith("print")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
});
|
||||
@@ -45,7 +53,8 @@ class PlatformOceanBookingControllerContractTest {
|
||||
.filter(method -> !method.getName().equals("page") && !method.getName().equals("create"))
|
||||
.flatMap(method -> Arrays.stream(method.getParameters()))
|
||||
.filter(parameter -> parameter.isAnnotationPresent(org.springframework.web.bind.annotation.PathVariable.class))
|
||||
.allMatch(parameter -> parameter.getName().equals("oceanBookingId"))).isTrue();
|
||||
.allMatch(parameter -> parameter.getName().equals("oceanBookingId")
|
||||
|| parameter.getName().equals("documentId"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -55,6 +64,8 @@ class PlatformOceanBookingControllerContractTest {
|
||||
"originWarehouseId", "platform:firstleg:ocean-booking:list", "ChannelOption")
|
||||
.contains("Create an editable DRAFT", "Replace a DRAFT", "Delete a DRAFT")
|
||||
.contains("Validate and permanently release a DRAFT")
|
||||
.contains("OCEAN_BOOKING_DOCUMENT", "BOOKING_CONFIRMATION", "BILL_OF_LADING")
|
||||
.contains("/prints/shipping-instruction:", "/prints/bill-of-lading-confirmation:")
|
||||
.contains("containerRequirements:", "ContainerRequirementRequest:")
|
||||
.contains("never requires or creates a real container number")
|
||||
.doesNotContain("change-request", "change-confirm", "/cancel:", "containers[].containerNo");
|
||||
|
||||
+11
@@ -2,9 +2,11 @@ package com.baiyue.firstleg.oceanbooking.repository;
|
||||
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingContainerAllocationMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingDocumentMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.LinkCandidateQuery;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -76,4 +78,13 @@ class OceanBookingMapperContractTest {
|
||||
.contains("allocation.ocean_booking_id=#{oceanBookingId}")
|
||||
.contains("allocation.requirement_id=#{requirementId}");
|
||||
}
|
||||
|
||||
@Test void documentMapperIsRegisteredAndOnlyLogicallyInvalidatesActiveReference() throws Exception {
|
||||
assertThat(OceanBookingDocumentMapper.class.getAnnotation(Mapper.class)).isNotNull();
|
||||
String sql = String.join("\n", OceanBookingDocumentMapper.class
|
||||
.getDeclaredMethod("invalidate", long.class, long.class, long.class, String.class)
|
||||
.getAnnotation(Update.class).value());
|
||||
assertThat(sql).contains("SET valid=0", "ocean_booking_id=#{oceanBookingId}",
|
||||
"id=#{documentId}", "valid=1", "invalidated_at=CURRENT_TIMESTAMP(3)");
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -16,6 +16,7 @@ class OceanBookingSqlContractTest {
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_party")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_document")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation")
|
||||
.contains("customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照'")
|
||||
@@ -42,6 +43,9 @@ class OceanBookingSqlContractTest {
|
||||
.contains("<> 'RELEASED' THEN")
|
||||
.contains("订舱状态历史不可修改")
|
||||
.contains("UNIQUE KEY uk_ocean_allocation_container (container_booking_id)")
|
||||
.contains("document_type IN ('BOOKING_CONFIRMATION','BILL_OF_LADING')")
|
||||
.contains("订舱文件关联不可物理删除")
|
||||
.contains("已放舱订舱文件永久只读")
|
||||
.doesNotContain("CONFIRMED", "CHANGE_PENDING", "CANCELLED");
|
||||
}
|
||||
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package com.baiyue.firstleg.oceanbooking.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.OceanBookingFileClient;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingDocumentEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OceanBookingDocumentServiceTest {
|
||||
@Mock OceanBookingRepository repository;
|
||||
@Mock OceanBookingFileClient files;
|
||||
@Spy ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
|
||||
@InjectMocks OceanBookingDocumentService service;
|
||||
|
||||
@BeforeEach void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||
lenient().when(repository.find(10L)).thenReturn(draft(0));
|
||||
}
|
||||
|
||||
@Test void uploadIsIdempotentAndDoesNotUploadASecondFileOnReplay() throws Exception {
|
||||
OceanBookingEntity booking = draft(2);
|
||||
MockMultipartFile file = new MockMultipartFile("file", "booking.pdf", "application/pdf", "pdf".getBytes());
|
||||
when(repository.claimOperation(eq(10L), eq(7L), eq("UPLOAD_DOCUMENT"), eq("upload-1"), anyString()))
|
||||
.thenReturn(true, false);
|
||||
when(repository.findForUpdate(10L)).thenReturn(booking);
|
||||
when(files.upload("token", 7L, 9L, "OCEAN_BOOKING_DOCUMENT", "10", file))
|
||||
.thenReturn(ApiResponse.success(metadata(91L)));
|
||||
when(repository.touchDraft(10L, 2, 7L, "admin")).thenReturn(1);
|
||||
when(repository.completeOperation(eq(10L), eq(7L), eq("UPLOAD_DOCUMENT"), eq("upload-1"), anyString()))
|
||||
.thenReturn(1);
|
||||
|
||||
var first = service.upload(10L, 2, "BOOKING_CONFIRMATION", "upload-1", file, 7L, "admin");
|
||||
|
||||
ArgumentCaptor<String> hash = ArgumentCaptor.forClass(String.class);
|
||||
verify(repository).claimOperation(eq(10L), eq(7L), eq("UPLOAD_DOCUMENT"), eq("upload-1"), hash.capture());
|
||||
OceanBookingOperationEntity operation = new OceanBookingOperationEntity();
|
||||
operation.setRequestHash(hash.getValue()); operation.setStatus("SUCCESS");
|
||||
operation.setResultJson(objectMapper.writeValueAsString(first));
|
||||
when(repository.operation(7L, "UPLOAD_DOCUMENT", "upload-1")).thenReturn(operation);
|
||||
|
||||
var replay = service.upload(10L, 2, "BOOKING_CONFIRMATION", "upload-1", file, 7L, "admin");
|
||||
|
||||
assertThat(replay.bookingVersion()).isEqualTo(3);
|
||||
assertThat(replay.document().getFileId()).isEqualTo(91L);
|
||||
verify(files, times(1)).upload(anyString(), anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(repository, times(1)).insert(any(OceanBookingDocumentEntity.class));
|
||||
}
|
||||
|
||||
@Test void attachRejectsCrossScopeFileBeforePersisting() {
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findForUpdate(10L)).thenReturn(draft(1));
|
||||
when(files.metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 92L))
|
||||
.thenReturn(ApiResponse.success(new OceanBookingFileClient.Metadata(92L, 19L,
|
||||
"OCEAN_BOOKING_DOCUMENT", "10", "x.pdf", "hash", "application/pdf", 3L)));
|
||||
|
||||
assertThatThrownBy(() -> service.attach(10L,
|
||||
new com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.DocumentRequest(1, 92L, "BILL_OF_LADING"),
|
||||
"attach-1", 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||
verify(repository, never()).insert(any(OceanBookingDocumentEntity.class));
|
||||
verify(repository, never()).touchDraft(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test void sameIdempotencyKeyWithDifferentPayloadIsRejectedWithoutFileLookup() {
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(false);
|
||||
OceanBookingOperationEntity operation = new OceanBookingOperationEntity();
|
||||
operation.setRequestHash("different"); operation.setStatus("SUCCESS"); operation.setResultJson("{}");
|
||||
when(repository.operation(7L, "ATTACH_DOCUMENT", "same-key")).thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.attach(10L,
|
||||
new com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.DocumentRequest(1, 92L, "BILL_OF_LADING"),
|
||||
"same-key", 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("其他请求");
|
||||
verifyNoInteractions(files);
|
||||
verify(repository, never()).insert(any(OceanBookingDocumentEntity.class));
|
||||
}
|
||||
|
||||
@Test void invalidationIsRejectedAfterReleaseWithoutDrift() {
|
||||
OceanBookingEntity released = draft(4); released.setStatus("RELEASED");
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findForUpdate(10L)).thenReturn(released);
|
||||
|
||||
assertThatThrownBy(() -> service.invalidate(10L, 20L, 4, "delete-1", 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("永久只读");
|
||||
verify(repository, never()).invalidateDocument(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(repository, never()).touchDraft(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test void activeDocumentMustStillExistInFileService() {
|
||||
OceanBookingEntity booking = draft(3);
|
||||
OceanBookingDocumentEntity document = new OceanBookingDocumentEntity();
|
||||
document.setId(20L); document.setOceanBookingId(10L); document.setFileId(91L);
|
||||
document.setDocumentType("BILL_OF_LADING"); document.setValid(true);
|
||||
when(repository.activeDocuments(10L)).thenReturn(List.of(document));
|
||||
when(files.metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 91L))
|
||||
.thenReturn(ApiResponse.success(metadata(91L)));
|
||||
|
||||
service.requireActiveValidDocument(booking);
|
||||
|
||||
verify(files).metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
}
|
||||
|
||||
private static OceanBookingEntity draft(int version) {
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(10L); booking.setCustomerId(9L); booking.setStatus("DRAFT"); booking.setVersion(version);
|
||||
return booking;
|
||||
}
|
||||
private static OceanBookingFileClient.Metadata metadata(long fileId) {
|
||||
return new OceanBookingFileClient.Metadata(fileId, 9L, "OCEAN_BOOKING_DOCUMENT", "10",
|
||||
"booking.pdf", "hash", "application/pdf", 3L);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.firstleg.oceanbooking.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OceanBookingPrintServiceTest {
|
||||
@Mock OceanBookingService bookings;
|
||||
@Mock PrintPdfClient print;
|
||||
@InjectMocks OceanBookingPrintService service;
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); ReflectionTestUtils.setField(service, "internalToken", "token"); }
|
||||
|
||||
@Test void shippingInstructionUsesOnlyAuthoritativeReleasedAggregate() {
|
||||
when(bookings.detail(10L)).thenReturn(detail("RELEASED"));
|
||||
HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.set("X-Print-Page-Count", "1");
|
||||
when(print.generateFreightA4Documents(eq("token"), eq("req-1"), any()))
|
||||
.thenReturn(new ResponseEntity<>(new byte[]{1, 2, 3}, headers, 200));
|
||||
|
||||
var pdf = service.shippingInstruction(10L, "req-1");
|
||||
|
||||
assertThat(pdf.fileName()).isEqualTo("OB-1-出口货物托运单.pdf");
|
||||
ArgumentCaptor<PrintPdfClient.FreightA4PdfRequest> request =
|
||||
ArgumentCaptor.forClass(PrintPdfClient.FreightA4PdfRequest.class);
|
||||
verify(print).generateFreightA4Documents(eq("token"), eq("req-1"), request.capture());
|
||||
assertThat(request.getValue().templateCode()).isEqualTo("SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1");
|
||||
var document = request.getValue().documents().get(0);
|
||||
assertThat(document.fields()).containsEntry("jobNumber", "OB-1")
|
||||
.containsEntry("shipper", "Shipper\nAddress")
|
||||
.containsEntry("oceanVesselVoyage", "Vessel / V001")
|
||||
.containsKeys("toCompany", "consignee", "notifyParty", "cargoQuantity",
|
||||
"portOfLoading", "portOfDischarge", "placeOfDelivery", "totalPackagesInWords");
|
||||
assertThat(document.cargoRows()).singleElement().satisfies(row -> {
|
||||
assertThat(row.quantity()).isEqualTo("2");
|
||||
assertThat(row.descriptionOfGoods()).isEqualTo("40HQ DRY x 2");
|
||||
assertThat(row.grossWeight()).isEqualTo("56000.000 KGS");
|
||||
});
|
||||
}
|
||||
|
||||
@Test void printRejectsDraftWithoutCallingPrintService() {
|
||||
when(bookings.detail(10L)).thenReturn(detail("DRAFT"));
|
||||
assertThatThrownBy(() -> service.billConfirmation(10L, "req-2"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("放舱后");
|
||||
verifyNoInteractions(print);
|
||||
}
|
||||
|
||||
private static Detail detail(String status) {
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(10L); booking.setBookingNo("OB-1"); booking.setStatus(status);
|
||||
booking.setBillOfLadingNo("BL-1"); booking.setVesselName("Vessel"); booking.setVoyageNo("V001");
|
||||
booking.setDeparturePortName("Ningbo"); booking.setDestinationPortName("LA");
|
||||
booking.setPlaceOfDelivery("LA DC"); booking.setServiceTerm("CY-CY");
|
||||
booking.setFreightTerm("PREPAID"); booking.setBookingAgentName("Agent");
|
||||
booking.setEstimatedSailingAt(LocalDateTime.of(2026, 9, 1, 0, 0));
|
||||
OceanBookingPartyEntity shipper = new OceanBookingPartyEntity();
|
||||
shipper.setPartyType("SHIPPER"); shipper.setPartyName("Shipper"); shipper.setAddress("Address");
|
||||
OceanBookingPartyEntity consignee = new OceanBookingPartyEntity();
|
||||
consignee.setPartyType("CONSIGNEE"); consignee.setPartyName("Consignee");
|
||||
OceanBookingPartyEntity notify = new OceanBookingPartyEntity();
|
||||
notify.setPartyType("NOTIFY_PARTY"); notify.setPartyName("Notify");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
requirement.setContainerType("40HQ"); requirement.setContainerKind("DRY"); requirement.setQuantity(2);
|
||||
requirement.setCapacityCbm(new BigDecimal("68")); requirement.setMaxWeightKg(new BigDecimal("28000"));
|
||||
return new Detail(booking, List.of(shipper, consignee, notify), List.of(requirement), List.of(), List.of(), List.of());
|
||||
}
|
||||
}
|
||||
+23
@@ -39,6 +39,7 @@ class OceanBookingServiceTest {
|
||||
@Mock private WarehouseMasterDataClient warehouses;
|
||||
@Mock private com.baiyue.firstleg.client.WarehousePartnerClient partners;
|
||||
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Mock private OceanBookingDocumentService documents;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -53,7 +54,9 @@ class OceanBookingServiceTest {
|
||||
when(repository.parties(anyLong())).thenReturn(List.of());
|
||||
when(repository.requirements(anyLong())).thenReturn(List.of());
|
||||
when(repository.containers(anyLong())).thenReturn(List.of());
|
||||
when(repository.documents(anyLong())).thenReturn(List.of());
|
||||
when(repository.histories(anyLong())).thenReturn(List.of());
|
||||
lenient().when(repository.findForUpdate(anyLong())).thenAnswer(invocation -> repository.find(invocation.getArgument(0)));
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.completeOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(1);
|
||||
}
|
||||
@@ -115,6 +118,23 @@ class OceanBookingServiceTest {
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReleaseWithoutActiveBookingOwnedFileWithoutStateDrift() {
|
||||
stubMasterData();
|
||||
OceanBookingEntity draft = completeBooking("DRAFT", 3);
|
||||
when(repository.find(1L)).thenReturn(draft);
|
||||
when(repository.parties(1L)).thenReturn(parties());
|
||||
when(repository.requirements(1L)).thenReturn(List.of(requirement()));
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"订舱缺少有效订舱确认或提单文件"))
|
||||
.when(documents).requireActiveValidDocument(draft);
|
||||
|
||||
assertThatThrownBy(() -> service.release(1L, 9L, "admin", "release-no-file",
|
||||
new ReleaseRequest(3, "确认")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("有效订舱确认或提单文件");
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesCompleteAggregateAndReplaysFrozenResult() throws Exception {
|
||||
stubMasterData();
|
||||
@@ -331,6 +351,9 @@ class OceanBookingServiceTest {
|
||||
value.setWarehouseId(701L); value.setBillOfLadingNo("BL-001"); value.setShippingCompanyId(71L);
|
||||
value.setCarrierCode("MATSON");
|
||||
value.setBookingAgentId(81L); value.setVesselName("VESSEL"); value.setVoyageNo("V001");
|
||||
value.setDeparturePortCode("CNSZX"); value.setDeparturePortName("深圳");
|
||||
value.setDestinationPortCode("USLAX"); value.setDestinationPortName("洛杉矶");
|
||||
value.setPlaceOfDelivery("LAX");
|
||||
value.setServiceTerm("CY-CY"); value.setFreightTerm("PREPAID");
|
||||
value.setEstimatedSailingAt(LocalDateTime.of(2026, 9, 1, 0, 0));
|
||||
value.setEstimatedArrivalAt(LocalDateTime.of(2026, 9, 20, 0, 0));
|
||||
|
||||
+106
-51
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
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;
|
||||
@@ -47,6 +48,7 @@ class TruckAppointmentServiceTest {
|
||||
@Mock WarehouseSupplierSnapshotService suppliers;
|
||||
@Mock IamCustomerSnapshotService customers;
|
||||
@Mock TruckAppointmentFileClient files;
|
||||
@Mock OceanBookingDocumentService oceanBookingDocuments;
|
||||
@Mock FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Spy ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
|
||||
@InjectMocks TruckAppointmentService service;
|
||||
@@ -60,6 +62,13 @@ class TruckAppointmentServiceTest {
|
||||
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());
|
||||
OceanBookingEntity booking = releasedBooking();
|
||||
ContainerBookingEntity container = realContainer();
|
||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(88L);
|
||||
lenient().when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
lenient().when(containers.find(88L)).thenReturn(container);
|
||||
lenient().when(oceanBookings.allocations(99L)).thenReturn(List.of(allocation));
|
||||
}
|
||||
|
||||
@Test void detailResolvesStableNamesAndIdentifiersFromLinkedResources() {
|
||||
@@ -90,7 +99,7 @@ class TruckAppointmentServiceTest {
|
||||
verifyNoInteractions(repository);
|
||||
}
|
||||
|
||||
@Test void createWithoutBookingContainerOrPlanWritesIndependentDraft() {
|
||||
@Test void createRejectsMissingBookingAndContainer() {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
@@ -101,28 +110,20 @@ class TruckAppointmentServiceTest {
|
||||
when(request.loadingPlanId()).thenReturn(null);
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
|
||||
TruckAppointmentEntity created = service.create(7L, "admin", request);
|
||||
|
||||
ArgumentCaptor<TruckAppointmentEntity> entity = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
assertThat(entity.getValue().getOceanBookingId()).isNull();
|
||||
assertThat(entity.getValue().getContainerBookingId()).isNull();
|
||||
assertThat(entity.getValue().getLoadingPlanId()).isNull();
|
||||
assertThat(entity.getValue().getCustomerName()).isEqualTo("Canonical Customer");
|
||||
assertThat(entity.getValue().getCarrierName()).isEqualTo("Canonical Carrier");
|
||||
assertThat(entity.getValue().getAppointmentNo()).isEqualTo("TA-260818-0001");
|
||||
assertThat(created.getAttachmentCount()).isZero();
|
||||
verifyNoInteractions(oceanBookings, containers, loadingPlans);
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("oceanBookingId");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void createAllowsDraftWithoutBillOfLadingOrAttachment() {
|
||||
@Test void createFreezesBookingBillAndRequiresBookingOwnedDocument() {
|
||||
SaveRequest request = validRequest();
|
||||
|
||||
service.create(7L, "admin", request);
|
||||
|
||||
ArgumentCaptor<TruckAppointmentEntity> entity = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
assertThat(entity.getValue().getBillOfLadingNo()).isNull();
|
||||
assertThat(entity.getValue().getBillOfLadingNo()).isEqualTo("BL-BOOKING-1");
|
||||
verify(oceanBookingDocuments).requireActiveValidDocument(any(OceanBookingEntity.class));
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@@ -147,35 +148,28 @@ class TruckAppointmentServiceTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("billOfLadingNo").hasMessageContaining("不一致");
|
||||
}
|
||||
|
||||
@Test void createAcceptsConfirmedBookingWithoutContainerOrPlan() {
|
||||
@Test void createRejectsBookingThatIsNotReleased() {
|
||||
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(null);
|
||||
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);
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
service.create(7L, "admin", request);
|
||||
ArgumentCaptor<TruckAppointmentEntity> entity = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
assertThat(entity.getValue().getCustomerId()).isEqualTo(7L);
|
||||
assertThat(entity.getValue().getCustomerName()).isEqualTo("Canonical Customer");
|
||||
assertThat(entity.getValue().getOceanBookingId()).isEqualTo(99L);
|
||||
assertThat(entity.getValue().getContainerBookingId()).isNull();
|
||||
assertThat(entity.getValue().getLoadingPlanId()).isNull();
|
||||
assertThat(entity.getValue().getWarehouseId()).isEqualTo(18L);
|
||||
verify(repository).history(any(TruckAppointmentHistoryEntity.class));
|
||||
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.setCustomerId(17L);
|
||||
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))
|
||||
@@ -188,9 +182,9 @@ class TruckAppointmentServiceTest {
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setCustomerId(7L); booking.setWarehouseId(18L);
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L); booking.setWarehouseId(18L); booking.setBillOfLadingNo("BL-1");
|
||||
ContainerBookingEntity container = new ContainerBookingEntity();
|
||||
container.setId(88L); container.setCustomerId(7L); container.setWarehouseId(18L);
|
||||
container.setId(88L); container.setRecordStatus("ACTIVE"); container.setCustomerId(7L); 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());
|
||||
@@ -200,13 +194,61 @@ class TruckAppointmentServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void createRejectsLoadingPlanWithoutContainerAssociation() {
|
||||
@Test void createRejectsContainerWithoutRealIsoNumberAndMissingBookingDocument() {
|
||||
SaveRequest request = validRequest();
|
||||
ContainerBookingEntity missingNo = realContainer(); missingNo.setContainerNo(null);
|
||||
when(containers.find(88L)).thenReturn(missingNo);
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("集装箱号");
|
||||
verify(repository, never()).insert(any());
|
||||
|
||||
when(containers.find(88L)).thenReturn(realContainer());
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"订舱缺少有效订舱确认或提单文件"))
|
||||
.when(oceanBookingDocuments).requireActiveValidDocument(any());
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("有效订舱确认或提单文件");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void editCanSwitchOnlyToAnotherMatchedReleasedBookingAndContainer() {
|
||||
TruckAppointmentEntity existing = entity(TruckAppointmentStatus.DRAFT, 3);
|
||||
existing.setCustomerId(7L); existing.setOceanBookingId(99L); existing.setContainerBookingId(88L);
|
||||
when(repository.find(1L)).thenReturn(existing, existing);
|
||||
SaveRequest request = validRequest();
|
||||
when(request.expectedVersion()).thenReturn(3);
|
||||
when(request.oceanBookingId()).thenReturn(199L);
|
||||
when(request.containerBookingId()).thenReturn(188L);
|
||||
OceanBookingEntity booking = releasedBooking(); booking.setId(199L); booking.setBillOfLadingNo("BL-NEW");
|
||||
ContainerBookingEntity container = realContainer(); container.setId(188L); container.setOceanBookingId(199L);
|
||||
container.setContainerNo("MSCU6639870");
|
||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(188L);
|
||||
when(oceanBookings.find(199L)).thenReturn(booking);
|
||||
when(containers.find(188L)).thenReturn(container);
|
||||
when(oceanBookings.allocations(199L)).thenReturn(List.of(allocation));
|
||||
when(repository.updateEdit(any(), eq(3))).thenReturn(1);
|
||||
|
||||
service.edit(1L, 7L, "admin", request);
|
||||
|
||||
ArgumentCaptor<TruckAppointmentEntity> updated = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).updateEdit(updated.capture(), eq(3));
|
||||
assertThat(updated.getValue().getOceanBookingId()).isEqualTo(199L);
|
||||
assertThat(updated.getValue().getContainerBookingId()).isEqualTo(188L);
|
||||
assertThat(updated.getValue().getBillOfLadingNo()).isEqualTo("BL-NEW");
|
||||
|
||||
container.setOceanBookingId(99L);
|
||||
assertThatThrownBy(() -> service.edit(1L, 7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("未关联至所选订舱");
|
||||
}
|
||||
|
||||
@Test void createRejectsMissingContainerBeforeLoadingPlanValidation() {
|
||||
SaveRequest request = validRequest();
|
||||
when(request.loadingPlanId()).thenReturn(77L);
|
||||
when(request.containerBookingId()).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("loadingPlanId")
|
||||
.hasMessageContaining("containerBookingId");
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("containerBookingId");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@@ -220,9 +262,9 @@ class TruckAppointmentServiceTest {
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("CONFIRMED"); booking.setCustomerId(7L);
|
||||
OceanBookingEntity booking = releasedBooking();
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
ContainerBookingEntity container = new ContainerBookingEntity(); container.setId(88L);
|
||||
ContainerBookingEntity container = realContainer();
|
||||
when(containers.find(88L)).thenReturn(container);
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(88L);
|
||||
@@ -245,9 +287,9 @@ class TruckAppointmentServiceTest {
|
||||
when(request.carrierId()).thenReturn(8L);
|
||||
when(request.plannedLoadingAt()).thenReturn(LocalDateTime.of(2026, 8, 17, 10, 0));
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("CONFIRMED"); booking.setCustomerId(7L);
|
||||
OceanBookingEntity booking = releasedBooking();
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
ContainerBookingEntity container = new ContainerBookingEntity(); container.setId(88L);
|
||||
ContainerBookingEntity container = realContainer();
|
||||
when(containers.find(88L)).thenReturn(container);
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(88L);
|
||||
@@ -293,8 +335,11 @@ class TruckAppointmentServiceTest {
|
||||
verify(repository, never()).history(any());
|
||||
}
|
||||
|
||||
@Test void dispatchRequiresBillOfLadingNumberAndActiveOwnedAttachment() {
|
||||
@Test void dispatchRequiresBookingBillAndActiveBookingOwnedDocument() {
|
||||
TruckAppointmentEntity missingBill = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
missingBill.setOceanBookingId(99L); missingBill.setContainerBookingId(88L);
|
||||
OceanBookingEntity bookingWithoutBill = releasedBooking(); bookingWithoutBill.setBillOfLadingNo(null);
|
||||
when(oceanBookings.find(99L)).thenReturn(bookingWithoutBill);
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("DISPATCH"), eq("dispatch-no-bill"), anyString())).thenReturn(true);
|
||||
when(repository.find(1L)).thenReturn(missingBill);
|
||||
|
||||
@@ -303,34 +348,30 @@ class TruckAppointmentServiceTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("提单号");
|
||||
|
||||
TruckAppointmentEntity missingAttachment = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
missingAttachment.setBillOfLadingNo("BL-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);
|
||||
when(repository.attachments(1L)).thenReturn(List.of());
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,"订舱缺少有效订舱确认或提单文件"))
|
||||
.when(oceanBookingDocuments).requireActiveValidDocument(any());
|
||||
|
||||
assertThatThrownBy(() -> service.action(1L, "DISPATCH", new ActionRequest(1, null),
|
||||
"dispatch-no-file", null, 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("有效提单附件");
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("有效订舱确认或提单文件");
|
||||
verify(repository, never()).dispatch(anyLong(), anyString(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test void dispatchFreezesReleasedBookingBillAndTransitionsOnceWithActiveAttachment() {
|
||||
TruckAppointmentEntity before = entity(TruckAppointmentStatus.PENDING_DISPATCH, 1);
|
||||
before.setCustomerId(7L); before.setOceanBookingId(99L); before.setBillOfLadingNo(null);
|
||||
before.setCustomerId(7L); before.setOceanBookingId(99L); before.setContainerBookingId(88L); before.setBillOfLadingNo(null);
|
||||
TruckAppointmentEntity after = entity(TruckAppointmentStatus.DISPATCHED, 2);
|
||||
after.setCustomerId(7L); after.setOceanBookingId(99L); after.setBillOfLadingNo("BL-BOOKING-1");
|
||||
after.setCustomerId(7L); 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");
|
||||
TruckAppointmentAttachmentEntity attachment = new TruckAppointmentAttachmentEntity();
|
||||
attachment.setFileId(88L); attachment.setAttachmentType("BILL_OF_LADING");
|
||||
when(repository.claimOperation(anyLong(), eq(1L), eq("DISPATCH"), eq("dispatch-ok"), anyString())).thenReturn(true);
|
||||
when(repository.find(1L)).thenReturn(before, after);
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
when(repository.attachments(1L)).thenReturn(List.of(attachment));
|
||||
when(files.metadata("test-internal-token", 7L, "TRUCK_APPOINTMENT_ATTACHMENT", "1", 88L))
|
||||
.thenReturn(ApiResponse.success(new TruckAppointmentFileClient.Metadata(88L, 7L,
|
||||
"TRUCK_APPOINTMENT_ATTACHMENT", "1", "bill.pdf", "hash", "application/pdf", 10L)));
|
||||
when(repository.dispatch(1L, "PENDING_DISPATCH", "BL-BOOKING-1", 1)).thenReturn(1);
|
||||
when(repository.completeOperation(eq(1L), eq("DISPATCH"), eq("dispatch-ok"), anyString())).thenReturn(1);
|
||||
|
||||
@@ -457,10 +498,24 @@ class TruckAppointmentServiceTest {
|
||||
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);
|
||||
when(request.containerBookingId()).thenReturn(null);
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
when(request.loadingPlanId()).thenReturn(null);
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static OceanBookingEntity releasedBooking() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L);
|
||||
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.setWarehouseId(18L); container.setOceanBookingId(99L); container.setContainerNo("MSCU6639870");
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user