feat: plan shadow decisions from leased generation
This commit is contained in:
@@ -5,9 +5,10 @@ import com.ruoyi.algorithm.inbound.InboundBorrowType;
|
||||
import com.ruoyi.algorithm.inbound.InboundLadderRung;
|
||||
import com.ruoyi.algorithm.inbound.InboundPlan;
|
||||
import com.ruoyi.algorithm.integration.SchedulingFacade;
|
||||
import com.ruoyi.algorithm.outbound.FifoDecision;
|
||||
import com.ruoyi.algorithm.outbound.OutboundAssignment;
|
||||
import com.ruoyi.algorithm.outbound.OutboundPlan;
|
||||
import com.ruoyi.algorithm.outbound.FifoDecision;
|
||||
import com.ruoyi.algorithm.outbound.TaskCountEstimate;
|
||||
import com.ruoyi.algorithm.repository.VersionToken;
|
||||
import com.ruoyi.algorithm.service.InboundPlanningResult;
|
||||
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
|
||||
@@ -16,42 +17,47 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ShadowPlanningEngineTest {
|
||||
private static final PublishedShadowSnapshot SNAPSHOT = new PublishedShadowSnapshot(
|
||||
42L, "01", Instant.parse("2026-09-03T00:00:00Z"), "ABC123");
|
||||
|
||||
@Test
|
||||
void inboundPlanningUsesRequiresNewAndConfiguredTimeout() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("FALLBACK");
|
||||
void inboundPlanningUsesLeasedGenerationPublishedFacadeAndConfiguredTransaction() {
|
||||
ShadowObservationProperties properties = properties();
|
||||
properties.setTimeoutSeconds(7);
|
||||
CapturingTransactionManager transactions = new CapturingTransactionManager();
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowInboundReference input = new ShadowInboundReference();
|
||||
input.setRequestId("JOB-1");
|
||||
input.setWarehouseId("W01");
|
||||
input.setTrayNo("TRAY-1");
|
||||
input.setProjectNo("PROJECT-1");
|
||||
input.setProductId("PRODUCT-1");
|
||||
input.setLotNo("LOT-1");
|
||||
when(mapper.selectInboundInput("TASK-1", "TRAY-1")).thenReturn(input);
|
||||
InboundAssignment assignment = new InboundAssignment(
|
||||
"JOB-1", "SPACE-9", "LANE-2", 4, 2,
|
||||
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
||||
1.0, 2.0, 3.0, 50.0, false, false, "shadow proposal");
|
||||
when(facade.planInbound(eq("W01"), any()))
|
||||
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(assignment), VersionToken.numeric(8)));
|
||||
ShadowInboundReference input = inboundReference("W02");
|
||||
input.setShadowCandidateCount(3);
|
||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(input);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(inboundAssignment()),
|
||||
VersionToken.numeric(8)));
|
||||
ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions);
|
||||
|
||||
InboundShadowObservation observation = engine.planInbound("TASK-1", "TRAY-1");
|
||||
InboundShadowObservation observation = engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1");
|
||||
|
||||
assertThat(transactions.definition.getPropagationBehavior())
|
||||
.isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
@@ -59,70 +65,297 @@ class ShadowPlanningEngineTest {
|
||||
.isEqualTo(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
|
||||
assertThat(transactions.definition.getTimeout()).isEqualTo(7);
|
||||
assertThat(observation.observed()).isTrue();
|
||||
assertThat(observation.snapshotGeneration()).isEqualTo(42L);
|
||||
assertThat(observation.warehouseId()).isEqualTo("01");
|
||||
assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9");
|
||||
assertThat(observation.reason())
|
||||
.contains("snapshotGeneration=42", "snapshotSync=2026-09-03T00:00:00Z",
|
||||
"sourceSignature=ABC123", "task=TASK-1", "tray=TRAY-1",
|
||||
"request=JOB-1", "rawWarehouse=W02", "project=PROJECT-1",
|
||||
"product=PRODUCT-1", "lot=LOT-1", "shadowCandidates=3");
|
||||
verify(mapper).selectInboundInput("01", 42L, "TASK-1", "TRAY-1");
|
||||
verify(facade).planInboundFromPublishedSnapshot(eq("01"), any());
|
||||
verify(facade, never()).planInbound(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundPlanningUsesReferenceWarehouseAndKeepsMatchDiagnostics() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("FALLBACK");
|
||||
CapturingTransactionManager transactions = new CapturingTransactionManager();
|
||||
void projectOutboundKeepsRawWarehouseDiagnosticButPlansForPublishedWarehouse() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowStockReference reference = projectReference("W02");
|
||||
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
OutboundShadowObservation observation = engine.planProjectOutbound(SNAPSHOT, "DETAIL-1");
|
||||
|
||||
assertThat(observation.snapshotGeneration()).isEqualTo(42L);
|
||||
assertThat(observation.warehouseId()).isEqualTo("01");
|
||||
assertThat(observation.requestId()).isEqualTo("DETAIL-1");
|
||||
assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9");
|
||||
assertThat(observation.reason())
|
||||
.contains("snapshotGeneration=42", "snapshotSync=2026-09-03T00:00:00Z",
|
||||
"sourceSignature=ABC123", "detail=DETAIL-1", "rawWarehouse=W02",
|
||||
"project=PROJECT-1", "product=PRODUCT-1", "shadowMatches=1");
|
||||
verify(mapper).selectProjectOutboundReference("01", 42L, "DETAIL-1");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), any());
|
||||
verify(facade, never()).planOutbound(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactStockLookupUsesPublishedWarehouseAndGenerationWhenRawWarehouseIsUnpadded() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowStockReference reference = stockReference("1");
|
||||
when(mapper.selectStockReference("01", 42L, "STOCK-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
OutboundShadowObservation observation =
|
||||
engine.planExactStockOutbound(SNAPSHOT, "DETAIL-1", "STOCK-1");
|
||||
|
||||
assertThat(observation.snapshotGeneration()).isEqualTo(42L);
|
||||
assertThat(observation.warehouseId()).isEqualTo("01");
|
||||
assertThat(observation.reason()).contains("rawWarehouse=1", "stock=STOCK-1");
|
||||
verify(mapper).selectStockReference("01", 42L, "STOCK-1");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void planningRejectsObservationWhenPublishedGenerationChangesDuringWork() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1"))
|
||||
.thenReturn(inboundReference("01"));
|
||||
when(mapper.selectPublishedSnapshot("01"))
|
||||
.thenReturn(descriptor(SNAPSHOT), descriptor(snapshot(43L, "01")));
|
||||
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(inboundAssignment()),
|
||||
VersionToken.numeric(8)));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
assertThatThrownBy(() -> engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("published snapshot changed during planning");
|
||||
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPlanningInputStillVerifiesDescriptorAfterLookup() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
when(mapper.selectPublishedSnapshot("01"))
|
||||
.thenReturn(descriptor(SNAPSHOT), descriptor(snapshot(43L, "01")));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
assertThatThrownBy(() -> engine.planProjectOutbound(SNAPSHOT, "MISSING"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("published snapshot changed during planning");
|
||||
}
|
||||
|
||||
@Test
|
||||
void comparisonPersistsLeasedGenerationAsInputVersion() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
InboundShadowObservation observation = new InboundShadowObservation(
|
||||
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
||||
|
||||
engine.compareInbound(observation, "READY", "SPACE-9");
|
||||
|
||||
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
|
||||
eq("MATCH"), argThat(reason -> reason.contains("request=JOB-1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void comparisonDoesNotPersistAfterPublishedGenerationChanges() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.selectPublishedSnapshot("01"))
|
||||
.thenReturn(descriptor(snapshot(43L, "01")));
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
InboundShadowObservation observation = new InboundShadowObservation(
|
||||
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
||||
|
||||
assertThatThrownBy(() -> engine.compareInbound(observation, "READY", "SPACE-9"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("published snapshot changed before comparison");
|
||||
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshClaimsInstanceAndReturnsValidatedPublishedSnapshot() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.claimInstance("01", "INSTANCE-1", 120)).thenReturn(true);
|
||||
PublishedShadowSnapshot refreshedSnapshot = snapshot(43L, "01");
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(refreshedSnapshot));
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
|
||||
PublishedShadowSnapshot refreshed = engine.refreshSnapshot(43L, "INSTANCE-1");
|
||||
|
||||
verify(mapper).claimInstance("01", "INSTANCE-1", 120);
|
||||
verify(mapper).refreshSnapshot("01", 43L, "INSTANCE-1");
|
||||
assertThat(refreshed).isEqualTo(refreshedSnapshot);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshRejectsUnclaimedInstanceBeforeCallingRefreshProcedure() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.claimInstance("01", "INSTANCE-2", 120)).thenReturn(false);
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
|
||||
assertThatThrownBy(() -> engine.refreshSnapshot(43L, "INSTANCE-2"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("could not claim shadow snapshot refresh");
|
||||
verify(mapper, never()).refreshSnapshot(any(), any(Long.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshRejectsInvalidPublishedDescriptors() {
|
||||
assertInvalidRefreshDescriptor(null, "missing");
|
||||
assertInvalidRefreshDescriptor(descriptor(snapshot(43L, "01"), "REFRESHING"), "PUBLISHED");
|
||||
assertInvalidRefreshDescriptor(descriptor(snapshot(44L, "01")), "generation");
|
||||
assertInvalidRefreshDescriptor(descriptor(snapshot(43L, "W02")), "warehouse");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planningMapperSqlUsesOnlyGenerationQualifiedShadowReadModels() throws Exception {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
Document document;
|
||||
try (InputStream stream = requireResource("mapper/algorithm/ShadowInputMapper.xml")) {
|
||||
document = factory.newDocumentBuilder().parse(stream);
|
||||
}
|
||||
Map<String, String> statements = statementsById(document);
|
||||
|
||||
for (String id : new String[]{"selectInboundInput", "selectProjectOutboundReference",
|
||||
"selectStockReference"}) {
|
||||
assertThat(statements.get(id)).as(id)
|
||||
.contains("#{warehouseid}", "#{generation}")
|
||||
.doesNotContain("dbo.", "from bas_stock", "from mydo_job",
|
||||
"from out_stockout_detail");
|
||||
}
|
||||
assertThat(statements.get("selectInboundInput"))
|
||||
.contains("alg.shadow_inbound_request_snapshot", "alg.lane_state");
|
||||
assertThat(statements.get("selectProjectOutboundReference"))
|
||||
.contains("alg.shadow_outbound_request_snapshot", "alg.inventory_item_snapshot");
|
||||
assertThat(statements.get("selectStockReference"))
|
||||
.contains("alg.inventory_item_snapshot", "stock_id = #{stockid}");
|
||||
assertThat(statements.get("insertComparison"))
|
||||
.contains("convert(binary(8), #{inputversion})");
|
||||
assertThat(statements.get("selectPublishedSnapshot"))
|
||||
.contains("alg.shadow_snapshot_state", "convert(varchar(64),",
|
||||
"source_signature", ", 2)");
|
||||
}
|
||||
|
||||
private void assertInvalidRefreshDescriptor(ShadowSnapshotDescriptor descriptor, String message) {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.claimInstance("01", "INSTANCE-1", 120)).thenReturn(true);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor);
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
|
||||
assertThatThrownBy(() -> engine.refreshSnapshot(43L, "INSTANCE-1"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
|
||||
private static ShadowPlanningEngine engine(SchedulingFacade facade, ShadowInputMapper mapper) {
|
||||
return new ShadowPlanningEngine(facade, mapper, properties(), new CapturingTransactionManager());
|
||||
}
|
||||
|
||||
private static ShadowObservationProperties properties() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("01");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static ShadowInboundReference inboundReference(String rawWarehouse) {
|
||||
ShadowInboundReference input = new ShadowInboundReference();
|
||||
input.setRequestId("JOB-1");
|
||||
input.setWarehouseId(rawWarehouse);
|
||||
input.setTrayNo("TRAY-1");
|
||||
input.setProjectNo("PROJECT-1");
|
||||
input.setProductId("PRODUCT-1");
|
||||
input.setLotNo("LOT-1");
|
||||
return input;
|
||||
}
|
||||
|
||||
private static ShadowStockReference projectReference(String rawWarehouse) {
|
||||
ShadowStockReference reference = new ShadowStockReference();
|
||||
reference.setRequestId("DETAIL-1");
|
||||
reference.setWarehouseId("W02");
|
||||
reference.setWarehouseId(rawWarehouse);
|
||||
reference.setProjectNo("PROJECT-1");
|
||||
reference.setProductId("PRODUCT-1");
|
||||
reference.setSnapshotSynchronizedAt("2026-08-28T02:53:45.133Z");
|
||||
reference.setLiveMatchCount(2);
|
||||
reference.setSnapshotSynchronizedAt("2026-09-03T00:00:00Z");
|
||||
reference.setShadowMatchCount(1);
|
||||
when(mapper.selectProjectOutboundReference("DETAIL-1")).thenReturn(reference);
|
||||
OutboundAssignment assignment = new OutboundAssignment(
|
||||
return reference;
|
||||
}
|
||||
|
||||
private static ShadowStockReference stockReference(String rawWarehouse) {
|
||||
ShadowStockReference reference = projectReference(rawWarehouse);
|
||||
reference.setStockId("STOCK-1");
|
||||
reference.setTrayNo("TRAY-1");
|
||||
reference.setSpaceId("SPACE-9");
|
||||
return reference;
|
||||
}
|
||||
|
||||
private static InboundAssignment inboundAssignment() {
|
||||
return new InboundAssignment(
|
||||
"JOB-1", "SPACE-9", "LANE-2", 4, 2,
|
||||
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
||||
1.0, 2.0, 3.0, 50.0, false, false, "shadow proposal");
|
||||
}
|
||||
|
||||
private static OutboundAssignment outboundAssignment() {
|
||||
return new OutboundAssignment(
|
||||
"DETAIL-1", "TRAY-1", "PROJECT-1", "PRODUCT-1", "SPACE-9", 1,
|
||||
true, java.util.List.of(), java.util.List.of(), false,
|
||||
1.0, 2.0, 3.0, FifoDecision.off(), java.util.Optional.empty(),
|
||||
"shadow proposal");
|
||||
when(facade.planOutbound(eq("W02"), any()))
|
||||
.thenReturn(OutboundPlan.ready(assignment, com.ruoyi.algorithm.outbound.TaskCountEstimate.zero()));
|
||||
ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions);
|
||||
|
||||
OutboundShadowObservation observation = engine.planProjectOutbound("DETAIL-1");
|
||||
engine.compareOutbound(observation, "10");
|
||||
|
||||
assertThat(observation.warehouseId()).isEqualTo("W02");
|
||||
assertThat(observation.requestId()).isEqualTo("DETAIL-1");
|
||||
assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9");
|
||||
verify(mapper).insertComparison(any(), eq("W02"), eq("OUTBOUND_COMPARE"), eq("LEGACY_10"),
|
||||
argThat(reason -> reason.contains("detail=DETAIL-1")
|
||||
&& reason.contains("project=PROJECT-1")
|
||||
&& reason.contains("product=PRODUCT-1")
|
||||
&& reason.contains("snapshotSync=2026-08-28T02:53:45.133Z")
|
||||
&& reason.contains("legacyMatches=2")
|
||||
&& reason.contains("shadowMatches=1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundPlanningTreatsPaddedAndUnpaddedWarehouseIdsAsEquivalent() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("01");
|
||||
CapturingTransactionManager transactions = new CapturingTransactionManager();
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowStockReference reference = new ShadowStockReference();
|
||||
reference.setRequestId("DETAIL-2");
|
||||
reference.setWarehouseId("1");
|
||||
reference.setProjectNo("PROJECT-2");
|
||||
when(mapper.selectProjectOutboundReference("DETAIL-2")).thenReturn(reference);
|
||||
when(facade.planOutbound(eq("01"), any())).thenReturn(
|
||||
OutboundPlan.error(new com.ruoyi.algorithm.outbound.OutboundError(
|
||||
com.ruoyi.algorithm.outbound.OutboundErrorCode.STOCK_NOT_FOUND,
|
||||
"no stock", java.util.Map.of())));
|
||||
ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions);
|
||||
private static PublishedShadowSnapshot snapshot(long generation, String warehouseId) {
|
||||
return new PublishedShadowSnapshot(generation, warehouseId,
|
||||
Instant.parse("2026-09-03T00:00:00Z"), "ABC123");
|
||||
}
|
||||
|
||||
OutboundShadowObservation observation = engine.planProjectOutbound("DETAIL-2");
|
||||
private static ShadowSnapshotDescriptor descriptor(PublishedShadowSnapshot snapshot) {
|
||||
return descriptor(snapshot, "PUBLISHED");
|
||||
}
|
||||
|
||||
assertThat(observation.warehouseId()).isEqualTo("01");
|
||||
private static ShadowSnapshotDescriptor descriptor(PublishedShadowSnapshot snapshot, String status) {
|
||||
ShadowSnapshotDescriptor descriptor = new ShadowSnapshotDescriptor();
|
||||
descriptor.setSnapshotGeneration(snapshot.generation());
|
||||
descriptor.setWarehouseId(snapshot.warehouseId());
|
||||
descriptor.setSnapshotStatus(status);
|
||||
descriptor.setSynchronizedAt(snapshot.synchronizedAt());
|
||||
descriptor.setSourceSignature(snapshot.sourceSignature());
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private static InputStream requireResource(String name) {
|
||||
InputStream stream = ShadowPlanningEngineTest.class.getClassLoader().getResourceAsStream(name);
|
||||
if (stream == null) {
|
||||
throw new IllegalStateException("Missing test resource " + name);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static Map<String, String> statementsById(Document document) {
|
||||
Map<String, String> statements = new HashMap<>();
|
||||
NodeList children = document.getDocumentElement().getChildNodes();
|
||||
for (int index = 0; index < children.getLength(); index++) {
|
||||
if (children.item(index) instanceof Element element && element.hasAttribute("id")) {
|
||||
statements.put(element.getAttribute("id"),
|
||||
element.getTextContent().replaceAll("\\s+", " ").trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
|
||||
private static final class CapturingTransactionManager implements PlatformTransactionManager {
|
||||
|
||||
Reference in New Issue
Block a user