暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -0,0 +1,40 @@
package com.baiyue.lastleg.relabel;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import org.junit.jupiter.api.Test;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RelabelContractTest {
@Test
void createHeaderDoesNotExposeWaybillLabelFileAndItemCarriesRequestedFileFields() {
assertFalse(Arrays.stream(RelabelRequests.Create.class.getRecordComponents())
.map(RecordComponent::getName)
.anyMatch(name -> name.toLowerCase().contains("waybill") || name.toLowerCase().contains("labelFile")));
assertTrue(Arrays.stream(RelabelRequests.Item.class.getRecordComponents())
.map(RecordComponent::getName).toList().containsAll(Arrays.asList("skuCode", "productImageFileId",
"productNameZh", "totalPackageCount", "notifiedPackageCount", "shipmentNo", "trackingCode",
"newSkuCode", "labelRequirement", "relabelFileId")));
}
@Test
void schemasDefineRelabelApplicationItemsAndHistoryContracts() throws Exception {
String baseline = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8).toLowerCase();
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8).toLowerCase();
for (String schema : new String[] { baseline, migration }) {
assertTrue(schema.contains("ll_relabel_application"));
assertTrue(schema.contains("ll_relabel_item"));
assertTrue(schema.contains("ll_relabel_status_history"));
assertTrue(schema.contains("customer_id"));
assertTrue(schema.contains("idempotency_key"));
assertTrue(schema.contains("request_hash"));
}
}
}
@@ -0,0 +1,259 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RelabelApplicationServiceTest {
@Mock RelabelApplicationMapper applications;
@Mock RelabelItemMapper items;
@Mock RelabelStatusHistoryMapper histories;
@Mock LastlegOrderMapper orders;
@Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock LastlegOrderNoGenerator orderNoGenerator;
private RelabelApplicationService service;
@BeforeEach
void setUp() {
service = new RelabelApplicationService();
ReflectionTestUtils.setField(service, "applications", applications);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(orderNoGenerator.next()).thenReturn("LMO-RELABEL-1");
}
@Test
void createBuildsPendingRelabelTailOrderItemsAndBothHistories() {
RelabelRequests.Create request = request("key-1", 10, 6);
RelabelApplication persisted = application(11L, "FBR-1", "TENANT-A", "key-1", null,
RelabelApplicationService.PENDING_PROCESSING);
persisted.setLastlegOrderId(21L);
persisted.setItemCount(1); persisted.setTotalPackageCount(10); persisted.setNotifiedPackageCount(6);
when(applications.findNo(eq("TENANT-A"), any())).thenReturn(persisted);
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 10, 6)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "TENANT-A", "LMO-RELABEL-1",
RelabelApplicationService.PENDING_PROCESSING));
RelabelResponses.Detail result = service.create("TENANT-A", "operator-1", request);
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
ArgumentCaptor<RelabelApplication> application = ArgumentCaptor.forClass(RelabelApplication.class);
ArgumentCaptor<RelabelItem> relabelItem = ArgumentCaptor.forClass(RelabelItem.class);
ArgumentCaptor<RelabelStatusHistory> relabelHistory = ArgumentCaptor.forClass(RelabelStatusHistory.class);
ArgumentCaptor<LastlegOrderStatusHistory> tailHistory = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class);
verify(orders).insert(tailOrder.capture());
verify(applications).insert(application.capture());
verify(items).insert(relabelItem.capture());
verify(histories).insert(relabelHistory.capture());
verify(orderHistories).insert(tailHistory.capture());
assertEquals("RELABEL_APPLICATION", tailOrder.getValue().getOrderType());
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
assertEquals("LMO-RELABEL-1", tailOrder.getValue().getOrderNo());
assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId());
assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo());
assertTrue(tailOrder.getValue().getInboundCode().startsWith("FBR"));
assertEquals(10, tailOrder.getValue().getPackageCount());
assertEquals(6, application.getValue().getNotifiedPackageCount());
assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
assertEquals("PENDING_PROCESSING", relabelHistory.getValue().getToStatus());
assertEquals("PENDING_ASSIGNMENT", tailHistory.getValue().getToStatus());
assertEquals("FBR-1", result.application().relabelNo());
}
@Test
void createRejectsNotifiedBoxesAboveTotalBeforeAnyWrite() {
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.create("TENANT-A", "operator-1", request("key-1", 5, 6)));
assertEquals(400, exception.getStatusCode().value());
verify(orders, never()).insert(any(LastlegOrder.class));
verify(applications, never()).insert(any(RelabelApplication.class));
verify(items, never()).insert(any(RelabelItem.class));
}
@Test
void sameIdempotencyKeyAndPayloadReturnsExistingApplicationWithoutWrites() {
RelabelRequests.Create request = request("key-1", 10, 6);
RelabelApplication existing = application(11L, "FBR-1", "TENANT-A", "key-1", hash(request),
RelabelApplicationService.PENDING_PROCESSING);
existing.setLastlegOrderId(21L); existing.setItemCount(1); existing.setTotalPackageCount(10);
existing.setNotifiedPackageCount(6);
when(applications.findKey("TENANT-A", "key-1")).thenReturn(existing);
stubDetail(existing);
RelabelResponses.Detail result = service.create("TENANT-A", "operator-1", request);
assertEquals("FBR-1", result.application().relabelNo());
verify(orders, never()).insert(any(LastlegOrder.class));
verify(applications, never()).insert(any(RelabelApplication.class));
verify(items, never()).insert(any(RelabelItem.class));
}
@Test
void idempotencyKeyCannotBeReusedWithDifferentRelabelPayload() {
RelabelRequests.Create request = request("key-1", 10, 6);
RelabelApplication existing = application(11L, "FBR-1", "TENANT-A", "key-1", "different-hash",
RelabelApplicationService.PENDING_PROCESSING);
when(applications.findKey("TENANT-A", "key-1")).thenReturn(existing);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.create("TENANT-A", "operator-1", request));
assertEquals(409, exception.getStatusCode().value());
verify(orders, never()).insert(any(LastlegOrder.class));
}
@Test
void statusChangeIsTenantScopedAndWritesApplicationAndTailOrderHistory() {
RelabelApplication application = application(11L, "FBR-1", "TENANT-A", "key-1", "hash",
RelabelApplicationService.PENDING_PROCESSING);
application.setLastlegOrderId(21L);
when(applications.lockNo("TENANT-A", "FBR-1")).thenReturn(application);
stubDetail(application);
RelabelResponses.Detail result = service.changeStatus("TENANT-A", "FBR-1", "operator-2",
new RelabelRequests.ChangeStatus("processing", "started"));
assertEquals("PROCESSING", application.getStatus());
assertEquals("PROCESSING", result.application().status());
verify(applications).updateById(application);
ArgumentCaptor<LastlegOrder> updatedOrder = ArgumentCaptor.forClass(LastlegOrder.class);
verify(orders).updateById(updatedOrder.capture());
assertEquals("PENDING_ASSIGNMENT", updatedOrder.getValue().getCurrentStatus());
assertEquals("RELABEL:PROCESSING", updatedOrder.getValue().getSubOrderStatus());
verify(histories).insert(any(RelabelStatusHistory.class));
verify(orderHistories).insert(any(LastlegOrderStatusHistory.class));
}
@Test
void completedApplicationCannotReturnToProcessingAndOtherTenantCannotReadIt() {
RelabelApplication completed = application(11L, "FBR-1", "TENANT-A", "key-1", "hash", "COMPLETED");
completed.setLastlegOrderId(21L);
when(applications.lockNo("TENANT-A", "FBR-1")).thenReturn(completed);
ResponseStatusException transition = assertThrows(ResponseStatusException.class, () ->
service.changeStatus("TENANT-A", "FBR-1", "operator-2", new RelabelRequests.ChangeStatus("PROCESSING", null)));
assertEquals(409, transition.getStatusCode().value());
when(applications.findNo("TENANT-B", "FBR-1")).thenReturn(null);
ResponseStatusException boundary = assertThrows(ResponseStatusException.class, () ->
service.detail("TENANT-B", "FBR-1"));
assertEquals(404, boundary.getStatusCode().value());
verify(applications).findNo("TENANT-B", "FBR-1");
}
@Test
void completedReturnRelabelOrderEntersPendingAssignment() {
RelabelApplication application = application(11L, "FBR-1", "TENANT-A", "key-1", "hash", "PROCESSING");
application.setLastlegOrderId(21L);
LastlegOrder order = order(21L, "TENANT-A", "LMO-RELABEL-1", "PROCESSING");
order.setOrderType("RETURN_RELABEL");
when(applications.lockNo("TENANT-A", "FBR-1")).thenReturn(application);
stubDetail(application);
when(orders.selectById(21L)).thenReturn(order);
service.changeStatus("TENANT-A", "FBR-1", "operator-2",
new RelabelRequests.ChangeStatus("COMPLETED", "finished"));
assertEquals("COMPLETED", application.getStatus());
assertEquals("PENDING_ASSIGNMENT", order.getCurrentStatus());
assertEquals("COMPLETED", order.getRelabelStatus());
}
private void stubDetail(RelabelApplication application) {
when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application);
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(application.getLastlegOrderId())).thenReturn(order(application.getLastlegOrderId(),
application.getCustomerId(), "LMO-RELABEL-1", application.getStatus()));
}
private static RelabelRequests.Create request(String key, int total, int notified) {
return new RelabelRequests.Create("CUSTOMER-1", "WH-1", LocalDateTime.of(2026, 8, 20, 10, 0),
"Li", "13800000000", "remark", key, List.of(new RelabelRequests.Item("SKU-A", "img-1",
"image.png", "https://files/image.png", "Chinese name", total, notified, "SHP-1", "TRACK-1",
"SKU-NEW", "Label on carton", "file-1", "relabel.pdf", "https://files/relabel.pdf")));
}
private static RelabelApplication application(long id, String no, String customerId, String key, String requestHash,
String status) {
RelabelApplication value = new RelabelApplication();
value.setId(id); value.setCustomerId(customerId); value.setRelabelNo(no); value.setIdempotencyKey(key);
value.setRequestHash(requestHash); value.setStatus(status); value.setCreatedAt(LocalDateTime.now());
value.setContactName("Li"); value.setContactPhone("13800000000"); value.setCustomerOrderNo("CUSTOMER-1");
value.setOverseasWarehouseId("WH-1"); value.setLabelDeadline(LocalDateTime.now().plusDays(1));
value.setItemCount(1); value.setTotalPackageCount(10); value.setNotifiedPackageCount(6);
return value;
}
private static RelabelItem item(long id, long applicationId, String sku, int total, int notified) {
RelabelItem value = new RelabelItem();
value.setId(id); value.setApplicationId(applicationId); value.setCustomerId("TENANT-A"); value.setSkuCode(sku);
value.setProductNameZh("Chinese name"); value.setTotalPackageCount(total); value.setNotifiedPackageCount(notified);
value.setShipmentNo("SHP-1"); value.setNewSkuCode("SKU-NEW");
return value;
}
private static LastlegOrder order(long id, String customerId, String orderNo, String status) {
LastlegOrder value = new LastlegOrder();
value.setId(id); value.setCustomerId(customerId); value.setOrderNo(orderNo); value.setCurrentStatus(status);
return value;
}
private static String hash(RelabelRequests.Create request) {
try {
byte[] digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(mapper().writeValueAsString(request).getBytes(java.nio.charset.StandardCharsets.UTF_8));
return java.util.HexFormat.of().formatHex(digest);
} catch (Exception exception) {
throw new AssertionError(exception);
}
}
private static ObjectMapper mapper() {
return new ObjectMapper().findAndRegisterModules();
}
}