fix(firstleg): allow optional ocean booking customer
This commit is contained in:
+5
-10
@@ -34,6 +34,7 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
|
||||
@Service
|
||||
public class OceanBookingDocumentService {
|
||||
public static final String BUSINESS_TYPE = "OCEAN_BOOKING_DOCUMENT";
|
||||
private static final long FILE_TENANT_ID = 0L;
|
||||
private static final Set<String> DOCUMENT_TYPES = Set.of("BOOKING_CONFIRMATION", "BILL_OF_LADING");
|
||||
|
||||
@Resource private OceanBookingRepository repository;
|
||||
@@ -54,7 +55,7 @@ public class OceanBookingDocumentService {
|
||||
OceanBookingEntity booking = lockDraft(oceanBookingId, expectedVersion);
|
||||
OceanBookingFileClient.Metadata metadata;
|
||||
try {
|
||||
var response = files.upload(internalToken, operatorId, booking.getCustomerId(), BUSINESS_TYPE,
|
||||
var response = files.upload(internalToken, operatorId, FILE_TENANT_ID, BUSINESS_TYPE,
|
||||
Long.toString(oceanBookingId), file);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw bad("订舱文件上传失败");
|
||||
@@ -115,7 +116,7 @@ public class OceanBookingDocumentService {
|
||||
}
|
||||
metadata(booking, document.getFileId());
|
||||
try {
|
||||
ResponseEntity<byte[]> response = files.download(internalToken, booking.getCustomerId(), BUSINESS_TYPE,
|
||||
ResponseEntity<byte[]> response = files.download(internalToken, FILE_TENANT_ID, BUSINESS_TYPE,
|
||||
Long.toString(oceanBookingId), document.getFileId());
|
||||
byte[] content = response.getBody();
|
||||
if (!response.getStatusCode().is2xxSuccessful() || content == null || content.length == 0) {
|
||||
@@ -132,9 +133,6 @@ public class OceanBookingDocumentService {
|
||||
}
|
||||
|
||||
public void requireActiveValidDocument(OceanBookingEntity booking) {
|
||||
if (booking.getCustomerId() == null || booking.getCustomerId() <= 0) {
|
||||
throw conflict("订舱缺少客户归属,无法校验文件");
|
||||
}
|
||||
boolean valid = false;
|
||||
for (OceanBookingDocumentEntity document : repository.activeDocuments(booking.getId())) {
|
||||
if (!DOCUMENT_TYPES.contains(document.getDocumentType())) continue;
|
||||
@@ -178,9 +176,6 @@ public class OceanBookingDocumentService {
|
||||
if (booking == null) throw new BizException(ResultCode.NOT_FOUND, "订舱单不存在");
|
||||
if (!Integer.valueOf(expectedVersion).equals(booking.getVersion())) throw concurrent();
|
||||
if (!"DRAFT".equals(booking.getStatus())) throw conflict("已放舱订舱文件永久只读");
|
||||
if (booking.getCustomerId() == null || booking.getCustomerId() <= 0) {
|
||||
throw bad("请先保存订舱客户后再上传文件");
|
||||
}
|
||||
return booking;
|
||||
}
|
||||
|
||||
@@ -192,7 +187,7 @@ public class OceanBookingDocumentService {
|
||||
|
||||
private OceanBookingFileClient.Metadata metadata(OceanBookingEntity booking, long fileId) {
|
||||
try {
|
||||
var response = files.metadata(internalToken, booking.getCustomerId(), BUSINESS_TYPE,
|
||||
var response = files.metadata(internalToken, FILE_TENANT_ID, BUSINESS_TYPE,
|
||||
Long.toString(booking.getId()), fileId);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "订舱文件不存在或归属不匹配");
|
||||
@@ -207,7 +202,7 @@ public class OceanBookingDocumentService {
|
||||
}
|
||||
|
||||
private static void validateMetadata(OceanBookingEntity booking, OceanBookingFileClient.Metadata metadata) {
|
||||
if (metadata.fileId() == null || !Objects.equals(metadata.tenantId(), booking.getCustomerId())
|
||||
if (metadata.fileId() == null || !Objects.equals(metadata.tenantId(), FILE_TENANT_ID)
|
||||
|| !BUSINESS_TYPE.equals(metadata.businessType())
|
||||
|| !Long.toString(booking.getId()).equals(metadata.businessId())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "订舱文件归属不匹配");
|
||||
|
||||
+17
-6
@@ -50,6 +50,7 @@ public class OceanBookingService {
|
||||
@Resource private WarehousePartnerClient partners;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource private OceanBookingDocumentService documents;
|
||||
@Resource private com.baiyue.firstleg.service.IamCustomerSnapshotService customers;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
|
||||
@@ -200,7 +201,7 @@ public class OceanBookingService {
|
||||
private void validateDraftStructure(DraftData data) {
|
||||
boolean hasCustomerId = data.customerId() != null;
|
||||
boolean hasCustomerName = StringUtils.hasText(data.customerName());
|
||||
if (hasCustomerId != hasCustomerName) throw badRequest("客户ID和客户名称必须同时填写或同时为空");
|
||||
if (!hasCustomerId && hasCustomerName) throw badRequest("客户名称不能脱离客户ID提交");
|
||||
if (data.parties() != null) {
|
||||
Set<String> types = new HashSet<>();
|
||||
for (PartyRequest party : data.parties()) {
|
||||
@@ -221,8 +222,7 @@ public class OceanBookingService {
|
||||
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
|
||||
private static void validateRelease(OceanBookingEntity booking, List<OceanBookingPartyEntity> parties,
|
||||
List<OceanBookingContainerRequirementEntity> requirements) {
|
||||
if (booking.getCustomerId() == null || !StringUtils.hasText(booking.getCustomerName())
|
||||
|| !StringUtils.hasText(booking.getChannelCode()) || booking.getWarehouseId() == null
|
||||
if (!StringUtils.hasText(booking.getChannelCode()) || booking.getWarehouseId() == null
|
||||
|| !StringUtils.hasText(booking.getBillOfLadingNo()) || !StringUtils.hasText(booking.getVesselName())
|
||||
|| !StringUtils.hasText(booking.getVoyageNo()) || !StringUtils.hasText(booking.getServiceTerm())
|
||||
|| !StringUtils.hasText(booking.getFreightTerm()) || booking.getEstimatedSailingAt() == null
|
||||
@@ -258,8 +258,9 @@ public class OceanBookingService {
|
||||
}
|
||||
|
||||
private record ResolvedDraft(String warehouseName, String channelName, String shippingCompanyName,
|
||||
String carrierCode, String bookingAgentName) {}
|
||||
String carrierCode, String bookingAgentName, String customerName) {}
|
||||
private ResolvedDraft resolveDraft(DraftData d) {
|
||||
String customerName = resolveCustomer(d.customerId(), d.customerName());
|
||||
String warehouseName = null; String channelName = null;
|
||||
if (d.warehouseId() != null && StringUtils.hasText(d.channelCode())) {
|
||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(d.warehouseId());
|
||||
@@ -269,10 +270,10 @@ public class OceanBookingService {
|
||||
var carrier = resolveCarrier(d.shippingCompanyId(), d.shippingCompanyName(), d.carrierCode());
|
||||
var agent = resolveBookingAgent(d.bookingAgentId(), d.bookingAgentName(), carrier == null ? null : carrier.carrierCode());
|
||||
return new ResolvedDraft(warehouseName, channelName, carrier == null ? null : carrier.partnerName(),
|
||||
carrier == null ? null : carrier.carrierCode(), agent == null ? null : agent.name());
|
||||
carrier == null ? null : carrier.carrierCode(), agent == null ? null : agent.name(), customerName);
|
||||
}
|
||||
private static void copy(OceanBookingEntity v, DraftData d, ResolvedDraft resolved) {
|
||||
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
|
||||
v.setCustomerId(d.customerId()); v.setCustomerName(resolved.customerName());
|
||||
v.setChannelCode(trim(d.channelCode())); v.setChannelName(resolved.channelName());
|
||||
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(resolved.warehouseName());
|
||||
v.setBillOfLadingNo(trim(d.billOfLadingNo()));
|
||||
@@ -288,10 +289,20 @@ public class OceanBookingService {
|
||||
}
|
||||
|
||||
private void validateMasterData(OceanBookingEntity booking) {
|
||||
resolveCustomer(booking.getCustomerId(), booking.getCustomerName());
|
||||
resolveCarrier(booking.getShippingCompanyId(), booking.getShippingCompanyName(), booking.getCarrierCode());
|
||||
resolveBookingAgent(booking.getBookingAgentId(), booking.getBookingAgentName(), booking.getCarrierCode());
|
||||
}
|
||||
|
||||
private String resolveCustomer(Long id, String name) {
|
||||
if (id == null) {
|
||||
if (StringUtils.hasText(name)) throw badRequest("客户名称不能脱离客户ID提交");
|
||||
return null;
|
||||
}
|
||||
if (id <= 0) throw badRequest("请选择有效客户");
|
||||
return customers.active(id, name).tenantName();
|
||||
}
|
||||
|
||||
private WarehousePartnerClient.Snapshot resolveCarrier(Long id, String suppliedName, String suppliedCode) {
|
||||
if (id == null) {
|
||||
if (StringUtils.hasText(suppliedName) || StringUtils.hasText(suppliedCode)) throw badRequest("船公司名称和编码不能脱离shippingCompanyId提交");
|
||||
|
||||
@@ -32,7 +32,7 @@ paths:
|
||||
/api/platform/v1/ocean-bookings/{oceanBookingId}/release:
|
||||
post:
|
||||
summary: Validate and permanently release a DRAFT
|
||||
description: Release validates the persisted aggregate plus at least one active booking-owned BOOKING_CONFIRMATION or BILL_OF_LADING file. It never requires or creates a real container number.
|
||||
description: Release validates the persisted aggregate plus at least one active booking-owned BOOKING_CONFIRMATION or BILL_OF_LADING file. The customer is optional; a supplied customer must remain ACTIVE. Null denotes no single customer snapshot, never public access. It never requires or creates a real container number.
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ReleaseRequest'}}}}
|
||||
responses: {'200': {description: RELEASED detail}}
|
||||
@@ -45,7 +45,7 @@ paths:
|
||||
'200': {description: Booking document history, content: {application/json: {schema: {$ref: '#/components/schemas/DocumentListResponse'}}}}
|
||||
post:
|
||||
summary: Reference an existing File object in a DRAFT booking
|
||||
description: File scope must be customerId + OCEAN_BOOKING_DOCUMENT + oceanBookingId. The booking version is incremented atomically.
|
||||
description: File scope must be platform tenantId=0 + OCEAN_BOOKING_DOCUMENT + oceanBookingId, independent of the optional customer snapshot. The booking version is incremented atomically.
|
||||
x-permission: platform:firstleg:ocean-booking:edit
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/DocumentRequest'}}}}
|
||||
@@ -162,8 +162,8 @@ components:
|
||||
type: object
|
||||
description: Fields may be incomplete in DRAFT. RELEASE validates the complete persisted aggregate.
|
||||
properties:
|
||||
customerId: {type: integer, format: int64, nullable: true}
|
||||
customerName: {type: string, nullable: true}
|
||||
customerId: {type: integer, format: int64, minimum: 1, nullable: true, description: 可选启用客户ID;多客户订舱留空,空值不是公开访问授权}
|
||||
customerName: {type: string, nullable: true, description: 可选名称提示;有客户ID时由服务端校验并保存客户主数据名称,清空客户时同时置null}
|
||||
channelCode: {type: string, nullable: true}
|
||||
warehouseId: {type: integer, format: int64, nullable: true}
|
||||
billOfLadingNo: {type: string, nullable: true}
|
||||
@@ -279,7 +279,7 @@ components:
|
||||
requiredQuantity: {type: integer, minimum: 1}
|
||||
allocatedQuantity: {type: integer, format: int64, minimum: 0}
|
||||
remainingQuantity: {type: integer, format: int64, minimum: 1}
|
||||
customerName: {type: string}
|
||||
customerName: {type: string, nullable: true}
|
||||
warehouseName: {type: string}
|
||||
channelName: {type: string}
|
||||
LinkCandidatePage:
|
||||
|
||||
+2
@@ -39,6 +39,8 @@ class OceanBookingMapperContractTest {
|
||||
.flatMap(annotation -> Arrays.stream(annotation.value()))
|
||||
.reduce("", (left, right) -> left + '\n' + right);
|
||||
assertThat(sql)
|
||||
.contains("customer_id=#{value.customerId},customer_name=#{value.customerName}")
|
||||
.doesNotContain("customer_id=COALESCE", "customer_name=COALESCE")
|
||||
.contains("status=#{fromStatus} AND version=#{expectedVersion}")
|
||||
.contains("status='DRAFT' AND version=#{version}")
|
||||
.doesNotContain("CONFIRMED", "CHANGE_PENDING", "CANCELLED", "SUBMITTED", "FAILED");
|
||||
|
||||
+45
-9
@@ -40,11 +40,12 @@ class OceanBookingDocumentServiceTest {
|
||||
|
||||
@Test void uploadIsIdempotentAndDoesNotUploadASecondFileOnReplay() throws Exception {
|
||||
OceanBookingEntity booking = draft(2);
|
||||
booking.setCustomerId(null);
|
||||
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))
|
||||
when(files.upload("token", 7L, 0L, "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()))
|
||||
@@ -75,9 +76,9 @@ class OceanBookingDocumentServiceTest {
|
||||
when(repository.claimOperation(anyLong(), eq(7L), eq("UPLOAD_DOCUMENT"), anyString(), anyString()))
|
||||
.thenReturn(true);
|
||||
when(repository.findForUpdate(10L)).thenReturn(draft(0), draft(1));
|
||||
when(files.upload("token", 7L, 9L, "OCEAN_BOOKING_DOCUMENT", "10", confirmation))
|
||||
when(files.upload("token", 7L, 0L, "OCEAN_BOOKING_DOCUMENT", "10", confirmation))
|
||||
.thenReturn(ApiResponse.success(metadata(91L)));
|
||||
when(files.upload("token", 7L, 9L, "OCEAN_BOOKING_DOCUMENT", "10", bill))
|
||||
when(files.upload("token", 7L, 0L, "OCEAN_BOOKING_DOCUMENT", "10", bill))
|
||||
.thenReturn(ApiResponse.success(metadata(92L)));
|
||||
when(repository.touchDraft(eq(10L), anyInt(), eq(7L), eq("admin"))).thenReturn(1);
|
||||
when(repository.completeOperation(eq(10L), eq(7L), eq("UPLOAD_DOCUMENT"), anyString(), anyString()))
|
||||
@@ -101,7 +102,7 @@ class OceanBookingDocumentServiceTest {
|
||||
@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))
|
||||
when(files.metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 92L))
|
||||
.thenReturn(ApiResponse.success(new OceanBookingFileClient.Metadata(92L, 19L,
|
||||
"OCEAN_BOOKING_DOCUMENT", "10", "x.pdf", "hash", "application/pdf", 3L)));
|
||||
|
||||
@@ -144,12 +145,12 @@ class OceanBookingDocumentServiceTest {
|
||||
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))
|
||||
when(files.metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L))
|
||||
.thenReturn(ApiResponse.success(metadata(91L)));
|
||||
|
||||
service.requireActiveValidDocument(booking);
|
||||
|
||||
verify(files).metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
verify(files).metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
}
|
||||
|
||||
@Test void releaseGateRejectsWhenBothActiveFileReferencesNoLongerExist() {
|
||||
@@ -161,8 +162,8 @@ class OceanBookingDocumentServiceTest {
|
||||
assertThatThrownBy(() -> service.requireActiveValidDocument(booking))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("有效订舱确认或提单文件");
|
||||
|
||||
verify(files).metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
verify(files).metadata("token", 9L, "OCEAN_BOOKING_DOCUMENT", "10", 92L);
|
||||
verify(files).metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
verify(files).metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 92L);
|
||||
verify(repository, never()).touchDraft(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@@ -173,13 +174,48 @@ class OceanBookingDocumentServiceTest {
|
||||
return document;
|
||||
}
|
||||
|
||||
@Test void changingOrClearingCustomerKeepsPlatformFileDownloadAndReleaseScope() {
|
||||
OceanBookingEntity booking = draft(3);
|
||||
OceanBookingDocumentEntity document = activeDocument(20L, 91L, "BOOKING_CONFIRMATION");
|
||||
document.setFileName("booking.pdf");
|
||||
when(repository.find(10L)).thenReturn(booking);
|
||||
when(repository.document(10L, 20L)).thenReturn(document);
|
||||
when(repository.activeDocuments(10L)).thenReturn(List.of(document));
|
||||
when(files.metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L))
|
||||
.thenReturn(ApiResponse.success(metadata(91L)));
|
||||
when(files.download("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L))
|
||||
.thenReturn(org.springframework.http.ResponseEntity.ok(new byte[]{1, 2, 3}));
|
||||
for (Long customer : java.util.Arrays.asList(9L, 19L, null)) {
|
||||
booking.setCustomerId(customer);
|
||||
service.requireActiveValidDocument(booking);
|
||||
assertThat(service.download(10L, 20L).content()).containsExactly(1, 2, 3);
|
||||
}
|
||||
verify(files, times(6)).metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
verify(files, times(3)).download("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 91L);
|
||||
}
|
||||
|
||||
@Test void platformFileFromAnotherBookingIsRejected() {
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
OceanBookingEntity booking = draft(1);
|
||||
booking.setCustomerId(null);
|
||||
when(repository.findForUpdate(10L)).thenReturn(booking);
|
||||
when(files.metadata("token", 0L, "OCEAN_BOOKING_DOCUMENT", "10", 92L))
|
||||
.thenReturn(ApiResponse.success(new OceanBookingFileClient.Metadata(92L, 0L,
|
||||
"OCEAN_BOOKING_DOCUMENT", "99", "x.pdf", "hash", "application/pdf", 3L)));
|
||||
assertThatThrownBy(() -> service.attach(10L,
|
||||
new com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.DocumentRequest(1, 92L, "BILL_OF_LADING"),
|
||||
"other-booking", 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||
verify(repository, never()).insert(any(OceanBookingDocumentEntity.class));
|
||||
}
|
||||
|
||||
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",
|
||||
return new OceanBookingFileClient.Metadata(fileId, 0L, "OCEAN_BOOKING_DOCUMENT", "10",
|
||||
"booking.pdf", "hash", "application/pdf", 3L);
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -40,6 +40,7 @@ class OceanBookingServiceTest {
|
||||
@Mock private com.baiyue.firstleg.client.WarehousePartnerClient partners;
|
||||
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Mock private OceanBookingDocumentService documents;
|
||||
@Mock private com.baiyue.firstleg.service.IamCustomerSnapshotService customers;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -47,6 +48,8 @@ class OceanBookingServiceTest {
|
||||
void setUp() throws Exception {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
lenient().when(customers.active(eq(100L), any())).thenReturn(
|
||||
new com.baiyue.firstleg.client.IamCustomerSnapshotClient.Customer(100L, "C100", "客户", "ACTIVE", null, null));
|
||||
lenient().when(documentNumbers.next("OB")).thenReturn("OB-260818-0001");
|
||||
var field = OceanBookingService.class.getDeclaredField("objectMapper");
|
||||
field.setAccessible(true);
|
||||
@@ -76,6 +79,70 @@ class OceanBookingServiceTest {
|
||||
verifyNoInteractions(priceBooks, suppliers, warehouses);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerlessCreateSaveAndReleaseRetainsOtherRequirements() throws Exception {
|
||||
saveAndReleaseWithoutCustomer(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearingExistingCustomerPersistsNullAndAllowsRelease() throws Exception {
|
||||
saveAndReleaseWithoutCustomer(true);
|
||||
}
|
||||
|
||||
private void saveAndReleaseWithoutCustomer(boolean initialCustomer) throws Exception {
|
||||
stubMasterData();
|
||||
when(suppliers.booking(81L, "订舱代理")).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE", List.of("MATSON")));
|
||||
var tree = objectMapper.valueToTree(completeDraft());
|
||||
((com.fasterxml.jackson.databind.node.ObjectNode) tree).putNull("customerId").putNull("customerName");
|
||||
DraftData emptyCustomer = objectMapper.treeToValue(tree, DraftData.class);
|
||||
AtomicReference<OceanBookingEntity> stored = stubCreatedDraft();
|
||||
var created = service.create(9L, "admin", "optional-create",
|
||||
new CreateRequest(0, initialCustomer ? completeDraft() : emptyCustomer));
|
||||
long id = created.oceanBooking().getId();
|
||||
when(repository.updateDraft(any(), eq(0), eq(9L), eq("admin"))).thenAnswer(invocation -> {
|
||||
OceanBookingEntity value = invocation.getArgument(0);
|
||||
assertThat(value.getCustomerId()).isNull();
|
||||
assertThat(value.getCustomerName()).isNull();
|
||||
value.setVersion(1);
|
||||
return 1;
|
||||
});
|
||||
service.updateDraft(id, 9L, "admin", "optional-save", new UpdateRequest(0, emptyCustomer));
|
||||
when(repository.parties(id)).thenReturn(parties());
|
||||
when(repository.requirements(id)).thenReturn(List.of(requirement()));
|
||||
when(repository.transition(id, "DRAFT", "RELEASED", 1, 9L, "admin")).thenAnswer(invocation -> {
|
||||
stored.get().setStatus("RELEASED"); stored.get().setVersion(2); return 1;
|
||||
});
|
||||
|
||||
var result = service.release(id, 9L, "admin", "optional-release", new ReleaseRequest(1, "确认"));
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("RELEASED");
|
||||
assertThat(result.oceanBooking().getCustomerId()).isNull();
|
||||
assertThat(result.oceanBooking().getCustomerName()).isNull();
|
||||
verify(documents).requireActiveValidDocument(stored.get());
|
||||
verify(customers, times(initialCustomer ? 1 : 0)).active(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidSpecifiedCustomerIsRejectedBeforeCreatingDraft() {
|
||||
when(customers.active(100L, "客户")).thenThrow(new BizException(
|
||||
com.baiyue.common.core.api.ResultCode.CONFLICT, "所选客户不存在或未启用"));
|
||||
assertThatThrownBy(() -> service.create(9L, "admin", "bad-customer", new CreateRequest(0, completeDraft())))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("客户不存在或未启用");
|
||||
verify(repository, never()).insert(any(OceanBookingEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseRevalidatesSpecifiedCustomer() {
|
||||
when(repository.find(1L)).thenReturn(completeBooking("DRAFT", 3));
|
||||
when(repository.parties(1L)).thenReturn(parties());
|
||||
when(repository.requirements(1L)).thenReturn(List.of(requirement()));
|
||||
when(customers.active(100L, "客户")).thenThrow(new BizException(
|
||||
com.baiyue.common.core.api.ResultCode.CONFLICT, "所选客户不存在或未启用"));
|
||||
assertThatThrownBy(() -> service.release(1L, 9L, "admin", "inactive-customer", new ReleaseRequest(3, null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("客户不存在或未启用");
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezesServerSnapshotsAndPersistsQuantityRequirementsOnly() {
|
||||
stubMasterData();
|
||||
@@ -86,6 +153,9 @@ class OceanBookingServiceTest {
|
||||
assertThat(inserted.get().getChannelName()).isEqualTo("美西海运");
|
||||
assertThat(inserted.get().getWarehouseName()).isEqualTo("深圳中转仓");
|
||||
assertThat(inserted.get().getBookingAgentName()).isEqualTo("订舱代理");
|
||||
assertThat(inserted.get().getCustomerId()).isEqualTo(100L);
|
||||
assertThat(inserted.get().getCustomerName()).isEqualTo("客户");
|
||||
verify(customers).active(100L, "客户");
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<OceanBookingContainerRequirementEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceRequirements(eq(inserted.get().getId()), captor.capture());
|
||||
|
||||
Reference in New Issue
Block a user