feat(fleet): enforce reviewed trips and original fee coverage for dispatch
This commit is contained in:
+3
-2
@@ -10,6 +10,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DispatchCandidateMapperContractTest {
|
||||
@@ -131,7 +132,7 @@ class DispatchCandidateMapperContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateGrossWeightIsAllocatedByPalletAndFallsBackToVolumeWeight() throws Exception {
|
||||
void candidateGrossWeightUsesPhysicalSourcesAndNeverBillingVolumeWeight() throws Exception {
|
||||
for (Method method : List.of(
|
||||
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
|
||||
String.class, String.class, String.class),
|
||||
@@ -142,7 +143,7 @@ class DispatchCandidateMapperContractTest {
|
||||
assertTrue(sql.contains("ll_inbound_order inbound"));
|
||||
assertTrue(sql.contains("a.allocated_volume_cbm"));
|
||||
assertTrue(sql.contains("a2.line_id=a.line_id"));
|
||||
assertTrue(sql.contains("MIN(o.volume_weight_kg)) AS grossWeightKg"));
|
||||
assertFalse(sql.contains("MIN(o.volume_weight_kg)) AS grossWeightKg"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.lastleg.dispatch;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DispatchPhysicalWeightTest {
|
||||
@Test void billingVolumeWeightCannotFillMissingPhysicalWeight() {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""));
|
||||
jdbc.execute("CREATE TABLE ll_order_detail(customer_id VARCHAR(64),order_id BIGINT,gross_weight_kg DECIMAL(12,3))");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_order(id BIGINT,customer_id VARCHAR(64),gross_weight_kg DECIMAL(12,3))");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(line_id BIGINT)");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1)");
|
||||
int tested = 0;
|
||||
for (var method : DispatchCandidateMapper.class.getDeclaredMethods()) {
|
||||
var annotation = method.getAnnotation(Select.class);
|
||||
if (annotation == null) continue;
|
||||
String sql = String.join(" ", annotation.value()).replace(">", ">").replace("<", "<");
|
||||
int from = sql.indexOf("AS volumeWeightKg,");
|
||||
int to = sql.indexOf(" AS grossWeightKg,");
|
||||
if (from < 0 || to < 0) continue;
|
||||
String projection = sql.substring(from + "AS volumeWeightKg,".length(), to);
|
||||
String query = "SELECT " + projection + " FROM (SELECT '10' AS customer_id) ip "
|
||||
+ "CROSS JOIN (SELECT 1 AS order_id,10 AS volume_cbm) l "
|
||||
+ "CROSS JOIN (SELECT 1 AS line_id,5 AS allocated_volume_cbm) a "
|
||||
+ "CROSS JOIN (SELECT 999 AS volume_weight_kg) o";
|
||||
assertThat(jdbc.queryForObject(query, BigDecimal.class)).as(method.getName() + " missing gross weight").isNull();
|
||||
if (projection.trim().equals("NULL")) continue; // Relabel candidates have no measured pallet weight source yet.
|
||||
jdbc.update("INSERT INTO ll_order_detail VALUES('10',1,40)");
|
||||
assertThat(jdbc.queryForObject(query, BigDecimal.class)).as(method.getName() + " physical weight allocation").isEqualByComparingTo("20");
|
||||
String mixed = query.replace("SELECT 1 AS order_id,10 AS volume_cbm", "SELECT 1 AS order_id,10 AS volume_cbm UNION ALL SELECT 2,10");
|
||||
assertThat(jdbc.queryForObject(mixed, BigDecimal.class)).as(method.getName() + " incomplete aggregate").isNull();
|
||||
jdbc.update("INSERT INTO ll_order_detail VALUES('10',2,40)");
|
||||
assertThat(jdbc.queryForObject(mixed, BigDecimal.class)).as(method.getName() + " complete aggregate").isEqualByComparingTo("40");
|
||||
jdbc.update("DELETE FROM ll_order_detail");
|
||||
tested++;
|
||||
}
|
||||
assertThat(tested).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -32,7 +32,8 @@ class SmartDispatchOpenApiContractTest {
|
||||
assertTrue(yaml.contains("SmartDispatchPreArrange"));
|
||||
assertTrue(yaml.contains("SmartDispatchCandidate"));
|
||||
assertTrue(yaml.contains("overseasWarehouseCode: { type: string, nullable: true, description: 实际海外接柜仓编码"));
|
||||
assertTrue(yaml.contains("grossWeightKg: { type: number, nullable: true, description: 候选托盘实际毛重"));
|
||||
assertTrue(yaml.contains("grossWeightKg: { type: number, nullable: true, description: 候选托盘分摊毛重"));
|
||||
assertTrue(yaml.contains("禁止使用体积重替代"));
|
||||
String preArrangeSchema = yaml.substring(yaml.indexOf(" SmartDispatchPreArrange:"),
|
||||
yaml.indexOf(" SmartDispatchDraftPallet:"));
|
||||
String updateSchema = yaml.substring(yaml.indexOf(" DispatchUpdate:"),
|
||||
|
||||
+32
-3
@@ -40,6 +40,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
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.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -57,12 +59,14 @@ class DispatchPdaWorkflowServiceTest {
|
||||
@Mock InventoryPositionMapper positions;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock DispatchPickOrderAttributionService attribution;
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
||||
private DispatchPdaWorkflowService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
|
||||
@@ -167,6 +171,7 @@ class DispatchPdaWorkflowServiceTest {
|
||||
DispatchPdaResponses.Picked result = service.pick("WH-1", "DSP-1", 21L, "7", "Picker", request);
|
||||
|
||||
assertEquals(2, result.packages());
|
||||
verify(fleetBindings, never()).requirePickingApproved(anyLong());
|
||||
verify(inventory, never()).allocate(any(), any(), any());
|
||||
verify(picks, never()).insert((DispatchPdaPick) any());
|
||||
verify(items, never()).bindReceivedPallets(any(), any());
|
||||
@@ -231,12 +236,36 @@ class DispatchPdaWorkflowServiceTest {
|
||||
when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(outbound);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.depart("WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null)));
|
||||
() -> service.depart("WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now()));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verify(outboundOrders, never()).updateById((OutboundOrder) any());
|
||||
}
|
||||
|
||||
@Test void pendingDispatchReviewBlocksPickingAndCompletionBeforeInventoryChanges() {
|
||||
DispatchOrder order = order("100", "DSP-1", "WH-1", "PENDING_DEPARTURE"); order.setVehiclePlate("CA-100");
|
||||
when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order);
|
||||
when(orders.lockById("100", 1L)).thenReturn(order);
|
||||
when(tasks.lockByDispatch("100", 1L)).thenReturn(task(11L, "100", "WH-1", "PDA_PENDING"));
|
||||
org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "请先通过调度审核再拣货"))
|
||||
.when(fleetBindings).requirePickingApproved(1L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.pick("WH-1", "DSP-1", 21L, "7", "作业员", new DispatchPdaRequests.Pick(31L, 2, 0, "pending-pick", null)));
|
||||
assertThrows(ResponseStatusException.class, () -> service.completePicking("WH-1", "DSP-1", new DispatchPdaRequests.CompletePicking("pending-complete")));
|
||||
org.mockito.Mockito.verifyNoInteractions(inventory, picks, attribution);
|
||||
verify(orders, never()).updateById((DispatchOrder) any());
|
||||
}
|
||||
|
||||
@Test void boundDispatchCannotDepartBeforeTripCoordination() {
|
||||
DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); order.setVehiclePlate("CA-100");
|
||||
when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order);
|
||||
when(orders.lockById("100", 1L)).thenReturn(order);
|
||||
org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "按车次统一发车"))
|
||||
.when(fleetBindings).requireCoordinatedDeparture(1L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.depart("WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now()));
|
||||
org.mockito.Mockito.verifyNoInteractions(attribution, inventory, outboundOrders);
|
||||
verify(orders, never()).updateById((DispatchOrder) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulDeparturePersistsBusinessDepartureTime() {
|
||||
DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART");
|
||||
@@ -262,7 +291,7 @@ class DispatchPdaWorkflowServiceTest {
|
||||
when(palletLocks.selectList(any())).thenReturn(List.of());
|
||||
|
||||
DispatchPdaResponses.Departed result = service.depart(
|
||||
"WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null));
|
||||
"WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now());
|
||||
|
||||
assertEquals("DEPARTED", result.status());
|
||||
assertNotNull(order.getDepartAt());
|
||||
@@ -284,7 +313,7 @@ class DispatchPdaWorkflowServiceTest {
|
||||
when(orders.lockById("100", 1L)).thenReturn(order);
|
||||
when(operations.findKey("100", "depart-1")).thenReturn(operation("DEPART", request, persisted));
|
||||
|
||||
DispatchPdaResponses.Departed result = service.depart("WH-1", "DSP-1", "7", request);
|
||||
DispatchPdaResponses.Departed result = service.depart("WH-1", "DSP-1", "7", request, java.time.LocalDateTime.now());
|
||||
|
||||
assertEquals("DEPARTED", result.status());
|
||||
assertTrue(result.replayed());
|
||||
|
||||
+16
@@ -43,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -59,11 +60,13 @@ class DispatchServiceTest {
|
||||
@Mock DispatchPdaTaskMapper pdaTasks;
|
||||
@Mock LastlegOrderMapper tailOrders;
|
||||
@Mock OutboundOrderMapper outboundOrders;
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
||||
private DispatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchService();
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "bolLines", bolLines);
|
||||
@@ -383,9 +386,22 @@ class DispatchServiceTest {
|
||||
assertEquals("ONT8,ABE4", result.route());
|
||||
assertEquals("ONT8,ABE4", dispatch.getRoute());
|
||||
verify(orders).updateById(dispatch);
|
||||
verify(fleetBindings).requireUnbound(1L);
|
||||
verify(items).findByDispatchOrder("100", 1L);
|
||||
}
|
||||
|
||||
@Test void boundDispatchCannotEditRemoveOrCancelIndependently() {
|
||||
DispatchOrder dispatch = new DispatchOrder(); dispatch.setId(1L); dispatch.setStatus("PENDING_DEPARTURE");
|
||||
when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch);
|
||||
org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "请先解除车次关联"))
|
||||
.when(fleetBindings).requireUnbound(1L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.update("PC1", null));
|
||||
assertThrows(ResponseStatusException.class, () -> service.removeItem("PC1", 2L));
|
||||
assertThrows(ResponseStatusException.class, () -> service.cancel("PC1"));
|
||||
verify(orders, never()).updateById((DispatchOrder) any());
|
||||
org.mockito.Mockito.verifyNoInteractions(items, locks, outboundOrders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformEditRejectsAnExecutedDispatchWithoutWriting() {
|
||||
DispatchOrder dispatch = new DispatchOrder();
|
||||
|
||||
Reference in New Issue
Block a user