|
|
|
|
@@ -12,12 +12,17 @@ import org.springframework.transaction.PlatformTransactionManager;
|
|
|
|
|
import org.springframework.transaction.TransactionDefinition;
|
|
|
|
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
|
|
|
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
import java.util.Set;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
|
|
|
|
@Component
|
|
|
|
|
public class ShadowPlanningEngine {
|
|
|
|
|
private static final int INSTANCE_STALE_SECONDS = 120;
|
|
|
|
|
private static final String PUBLISHED = "PUBLISHED";
|
|
|
|
|
|
|
|
|
|
private final SchedulingFacade schedulingFacade;
|
|
|
|
|
private final ShadowInputMapper inputMapper;
|
|
|
|
|
private final ShadowObservationProperties properties;
|
|
|
|
|
@@ -35,159 +40,313 @@ public class ShadowPlanningEngine {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public InboundShadowObservation planInbound(String taskNo, String trayNo) {
|
|
|
|
|
return transactions.execute(status -> planInboundInsideTransaction(taskNo, trayNo));
|
|
|
|
|
throw leasedSnapshotRequired();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) {
|
|
|
|
|
return transactions.execute(status -> {
|
|
|
|
|
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(stockoutDetailId);
|
|
|
|
|
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
|
|
|
|
if (reference == null || isBlank(reference.getProjectNo())) {
|
|
|
|
|
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
|
|
|
|
"SKIPPED_MISSING_PROJECT", "No project number was available for shadow planning");
|
|
|
|
|
}
|
|
|
|
|
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
|
|
|
|
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
|
|
|
|
return toOutboundObservation(warehouseId, reference.getRequestId(), reference,
|
|
|
|
|
schedulingFacade.planOutbound(warehouseId, request));
|
|
|
|
|
});
|
|
|
|
|
throw leasedSnapshotRequired();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) {
|
|
|
|
|
throw leasedSnapshotRequired();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PublishedShadowSnapshot refreshSnapshot(long generation, String instanceId) {
|
|
|
|
|
if (generation <= 0) {
|
|
|
|
|
throw new IllegalArgumentException("shadow snapshot generation must be positive");
|
|
|
|
|
}
|
|
|
|
|
if (isBlank(instanceId)) {
|
|
|
|
|
throw new IllegalArgumentException("shadow snapshot instanceId is required");
|
|
|
|
|
}
|
|
|
|
|
String warehouseId = configuredWarehouseId();
|
|
|
|
|
return transactions.execute(status -> {
|
|
|
|
|
ShadowStockReference reference = inputMapper.selectStockReference(stockId);
|
|
|
|
|
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
|
|
|
|
if (!inputMapper.claimInstance(warehouseId, instanceId, INSTANCE_STALE_SECONDS)) {
|
|
|
|
|
throw new IllegalStateException("could not claim shadow snapshot refresh for warehouse " + warehouseId);
|
|
|
|
|
}
|
|
|
|
|
inputMapper.refreshSnapshot(warehouseId, generation, instanceId);
|
|
|
|
|
return toPublishedSnapshot(inputMapper.selectPublishedSnapshot(warehouseId), generation, warehouseId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public InboundShadowObservation planInbound(PublishedShadowSnapshot snapshot,
|
|
|
|
|
String taskNo, String trayNo) {
|
|
|
|
|
return planWithStableSnapshot(snapshot,
|
|
|
|
|
() -> planInboundInsideTransaction(snapshot, taskNo, trayNo));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public OutboundShadowObservation planProjectOutbound(PublishedShadowSnapshot snapshot,
|
|
|
|
|
String stockoutDetailId) {
|
|
|
|
|
return planWithStableSnapshot(snapshot, () -> {
|
|
|
|
|
String warehouseId = snapshot.warehouseId();
|
|
|
|
|
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
|
|
|
|
warehouseId, snapshot.generation(), stockoutDetailId);
|
|
|
|
|
if (reference == null || isBlank(reference.getProjectNo())) {
|
|
|
|
|
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
|
|
|
|
stockoutDetailId, "SKIPPED_MISSING_PROJECT",
|
|
|
|
|
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
|
|
|
|
"No project number was available for shadow planning"));
|
|
|
|
|
}
|
|
|
|
|
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
|
|
|
|
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
|
|
|
|
return toOutboundObservation(snapshot, reference.getRequestId(), reference,
|
|
|
|
|
schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public OutboundShadowObservation planExactStockOutbound(PublishedShadowSnapshot snapshot,
|
|
|
|
|
String stockoutDetailId, String stockId) {
|
|
|
|
|
return planWithStableSnapshot(snapshot, () -> {
|
|
|
|
|
String warehouseId = snapshot.warehouseId();
|
|
|
|
|
ShadowStockReference reference = inputMapper.selectStockReference(
|
|
|
|
|
warehouseId, snapshot.generation(), stockId);
|
|
|
|
|
if (reference == null || isBlank(reference.getTrayNo())) {
|
|
|
|
|
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
|
|
|
|
"SKIPPED_MISSING_STOCK", "Selected stock was unavailable to shadow planning");
|
|
|
|
|
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
|
|
|
|
stockoutDetailId, "SKIPPED_MISSING_STOCK",
|
|
|
|
|
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
|
|
|
|
"Selected stock was unavailable to shadow planning"));
|
|
|
|
|
}
|
|
|
|
|
OutboundRequest request = new OutboundRequest(stockoutDetailId, reference.getTrayNo(),
|
|
|
|
|
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
|
|
|
|
return toOutboundObservation(warehouseId, stockoutDetailId, reference,
|
|
|
|
|
schedulingFacade.planOutbound(warehouseId, request));
|
|
|
|
|
return toOutboundObservation(snapshot, stockoutDetailId, reference,
|
|
|
|
|
schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void compareInbound(InboundShadowObservation observation, String legacyResultCode,
|
|
|
|
|
String legacySpaceId) {
|
|
|
|
|
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
|
|
|
|
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
|
|
|
|
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
|
|
|
|
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
|
|
|
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
|
|
|
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
|
|
|
|
+ "; legacy=" + nullSafe(legacyResultCode)
|
|
|
|
|
+ "; legacySpace=" + nullSafe(legacySpaceId)));
|
|
|
|
|
Objects.requireNonNull(observation, "observation");
|
|
|
|
|
transactions.executeWithoutResult(status -> {
|
|
|
|
|
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
|
|
|
|
"published snapshot changed before comparison");
|
|
|
|
|
inputMapper.insertComparison(
|
|
|
|
|
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
|
|
|
|
observation.snapshotGeneration(),
|
|
|
|
|
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
|
|
|
|
"snapshotGeneration=" + observation.snapshotGeneration()
|
|
|
|
|
+ "; request=" + observation.requestId()
|
|
|
|
|
+ "; shadow=" + observation.resultCode()
|
|
|
|
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
|
|
|
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
|
|
|
|
+ "; legacy=" + nullSafe(legacyResultCode)
|
|
|
|
|
+ "; legacySpace=" + nullSafe(legacySpaceId));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
|
|
|
|
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
|
|
|
|
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
|
|
|
|
"LEGACY_" + nullSafe(legacyResultCode),
|
|
|
|
|
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
|
|
|
|
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
|
|
|
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
|
|
|
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
|
|
|
|
+ "; legacy=" + nullSafe(legacyResultCode)));
|
|
|
|
|
Objects.requireNonNull(observation, "observation");
|
|
|
|
|
transactions.executeWithoutResult(status -> {
|
|
|
|
|
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
|
|
|
|
"published snapshot changed before comparison");
|
|
|
|
|
inputMapper.insertComparison(
|
|
|
|
|
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
|
|
|
|
observation.snapshotGeneration(), "LEGACY_" + nullSafe(legacyResultCode),
|
|
|
|
|
"snapshotGeneration=" + observation.snapshotGeneration()
|
|
|
|
|
+ "; request=" + observation.requestId()
|
|
|
|
|
+ "; shadow=" + observation.resultCode()
|
|
|
|
|
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
|
|
|
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
|
|
|
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
|
|
|
|
+ "; legacy=" + nullSafe(legacyResultCode));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private InboundShadowObservation planInboundInsideTransaction(String taskNo, String trayNo) {
|
|
|
|
|
ShadowInboundReference reference = inputMapper.selectInboundInput(taskNo, trayNo);
|
|
|
|
|
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
|
|
|
|
private InboundShadowObservation planInboundInsideTransaction(PublishedShadowSnapshot snapshot,
|
|
|
|
|
String taskNo, String trayNo) {
|
|
|
|
|
String warehouseId = snapshot.warehouseId();
|
|
|
|
|
ShadowInboundReference reference = inputMapper.selectInboundInput(
|
|
|
|
|
warehouseId, snapshot.generation(), taskNo, trayNo);
|
|
|
|
|
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
|
|
|
|
return InboundShadowObservation.unavailable(warehouseId, taskNo,
|
|
|
|
|
"SKIPPED_MISSING_INPUT", "Inbound task details were unavailable to shadow planning");
|
|
|
|
|
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId, taskNo,
|
|
|
|
|
"SKIPPED_MISSING_INPUT",
|
|
|
|
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
|
|
|
|
"Inbound task details were unavailable to shadow planning"));
|
|
|
|
|
}
|
|
|
|
|
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
|
|
|
|
|
reference.getProjectNo(), reference.getProductId(), reference.getLotNo(),
|
|
|
|
|
null, null, null, Set.of(), null);
|
|
|
|
|
InboundPlanningResult result = schedulingFacade.planInbound(warehouseId, request);
|
|
|
|
|
InboundPlanningResult result = schedulingFacade.planInboundFromPublishedSnapshot(warehouseId, request);
|
|
|
|
|
InboundPlan plan = result.plan();
|
|
|
|
|
if (plan.assignment().isPresent()) {
|
|
|
|
|
var assignment = plan.assignment().orElseThrow();
|
|
|
|
|
return new InboundShadowObservation(true, warehouseId, reference.getRequestId(),
|
|
|
|
|
"READY", assignment.spaceId(), inboundDiagnostic(taskNo, trayNo, reference, assignment.reason()));
|
|
|
|
|
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
|
|
|
|
reference.getRequestId(), "READY", assignment.spaceId(),
|
|
|
|
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference, assignment.reason()));
|
|
|
|
|
}
|
|
|
|
|
var rejection = plan.rejection().orElseThrow();
|
|
|
|
|
return InboundShadowObservation.unavailable(warehouseId, reference.getRequestId(),
|
|
|
|
|
"REJECTED_" + rejection.code().name(),
|
|
|
|
|
inboundDiagnostic(taskNo, trayNo, reference, rejection.reason()));
|
|
|
|
|
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
|
|
|
|
reference.getRequestId(), "REJECTED_" + rejection.code().name(),
|
|
|
|
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference, rejection.reason()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private OutboundShadowObservation toOutboundObservation(String warehouseId, String requestId,
|
|
|
|
|
ShadowStockReference reference, OutboundPlan plan) {
|
|
|
|
|
private OutboundShadowObservation toOutboundObservation(PublishedShadowSnapshot snapshot,
|
|
|
|
|
String requestId, ShadowStockReference reference, OutboundPlan plan) {
|
|
|
|
|
if (isBlank(requestId)) {
|
|
|
|
|
requestId = plan.assignment().map(assignment -> assignment.requestId()).orElse(null);
|
|
|
|
|
}
|
|
|
|
|
if (plan.assignment().isPresent()) {
|
|
|
|
|
var assignment = plan.assignment().orElseThrow();
|
|
|
|
|
return new OutboundShadowObservation(true, warehouseId, requestId, "READY",
|
|
|
|
|
reference.getStockId(), assignment.sourceSpaceId(),
|
|
|
|
|
outboundDiagnostic(requestId, reference, assignment.reason()));
|
|
|
|
|
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
|
|
|
|
requestId, "READY", reference.getStockId(), assignment.sourceSpaceId(),
|
|
|
|
|
outboundDiagnostic(snapshot, requestId, reference, assignment.reason()));
|
|
|
|
|
}
|
|
|
|
|
if (plan.waiting().isPresent()) {
|
|
|
|
|
var waiting = plan.waiting().orElseThrow();
|
|
|
|
|
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
|
|
|
|
"WAITING_" + waiting.code().name(), outboundDiagnostic(requestId, reference, waiting.reason()));
|
|
|
|
|
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
|
|
|
|
requestId, "WAITING_" + waiting.code().name(),
|
|
|
|
|
outboundDiagnostic(snapshot, requestId, reference, waiting.reason()));
|
|
|
|
|
}
|
|
|
|
|
var error = plan.error().orElseThrow();
|
|
|
|
|
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
|
|
|
|
"ERROR_" + error.code().name(), outboundDiagnostic(requestId, reference, error.message()));
|
|
|
|
|
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
|
|
|
|
requestId, "ERROR_" + error.code().name(),
|
|
|
|
|
outboundDiagnostic(snapshot, requestId, reference, error.message()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String warehouseId(String preferredWarehouseId) {
|
|
|
|
|
String configuredWarehouseId = isBlank(properties.getWarehouseId())
|
|
|
|
|
private <T> T planWithStableSnapshot(PublishedShadowSnapshot snapshot, Supplier<T> planner) {
|
|
|
|
|
validateLeasedSnapshot(snapshot);
|
|
|
|
|
return transactions.execute(status -> {
|
|
|
|
|
requirePublishedSnapshot(snapshot, "published snapshot changed during planning");
|
|
|
|
|
T result = planner.get();
|
|
|
|
|
requirePublishedSnapshot(snapshot, "published snapshot changed during planning");
|
|
|
|
|
return result;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void validateLeasedSnapshot(PublishedShadowSnapshot snapshot) {
|
|
|
|
|
Objects.requireNonNull(snapshot, "snapshot");
|
|
|
|
|
if (snapshot.generation() <= 0) {
|
|
|
|
|
throw new IllegalArgumentException("leased shadow snapshot generation must be positive");
|
|
|
|
|
}
|
|
|
|
|
if (isBlank(snapshot.warehouseId())) {
|
|
|
|
|
throw new IllegalArgumentException("leased shadow snapshot warehouse is required");
|
|
|
|
|
}
|
|
|
|
|
if (!WarehouseIdNormalizer.equivalent(snapshot.warehouseId(), configuredWarehouseId())) {
|
|
|
|
|
throw new IllegalArgumentException("leased shadow snapshot warehouse does not match configured warehouse");
|
|
|
|
|
}
|
|
|
|
|
requireUppercaseHexSignature(snapshot.sourceSignature(), "leased shadow snapshot source signature");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private PublishedShadowSnapshot toPublishedSnapshot(ShadowSnapshotDescriptor descriptor,
|
|
|
|
|
long expectedGeneration, String expectedWarehouseId) {
|
|
|
|
|
if (descriptor == null) {
|
|
|
|
|
throw new IllegalStateException("published snapshot descriptor is missing");
|
|
|
|
|
}
|
|
|
|
|
if (!PUBLISHED.equals(descriptor.getSnapshotStatus())) {
|
|
|
|
|
throw new IllegalStateException("shadow snapshot descriptor must be PUBLISHED");
|
|
|
|
|
}
|
|
|
|
|
if (descriptor.getSnapshotGeneration() != expectedGeneration) {
|
|
|
|
|
throw new IllegalStateException("published snapshot generation does not match refreshed generation");
|
|
|
|
|
}
|
|
|
|
|
if (!WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), expectedWarehouseId)) {
|
|
|
|
|
throw new IllegalStateException("published snapshot warehouse does not match refreshed warehouse");
|
|
|
|
|
}
|
|
|
|
|
if (descriptor.getSynchronizedAt() == null) {
|
|
|
|
|
throw new IllegalStateException("published snapshot synchronization time is missing");
|
|
|
|
|
}
|
|
|
|
|
requireUppercaseHexSignature(descriptor.getSourceSignature(), "published snapshot source signature");
|
|
|
|
|
return new PublishedShadowSnapshot(descriptor.getSnapshotGeneration(), descriptor.getWarehouseId(),
|
|
|
|
|
descriptor.getSynchronizedAt(), descriptor.getSourceSignature());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void requirePublishedSnapshot(PublishedShadowSnapshot snapshot, String message) {
|
|
|
|
|
if (!matches(snapshot, inputMapper.selectPublishedSnapshot(snapshot.warehouseId()))) {
|
|
|
|
|
throw new IllegalStateException(message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void requirePublishedGeneration(long generation, String warehouseId, String message) {
|
|
|
|
|
if (generation <= 0 || isBlank(warehouseId)) {
|
|
|
|
|
throw new IllegalStateException(message);
|
|
|
|
|
}
|
|
|
|
|
ShadowSnapshotDescriptor descriptor = inputMapper.selectPublishedSnapshot(warehouseId);
|
|
|
|
|
if (descriptor == null
|
|
|
|
|
|| !PUBLISHED.equals(descriptor.getSnapshotStatus())
|
|
|
|
|
|| descriptor.getSnapshotGeneration() != generation
|
|
|
|
|
|| !WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), warehouseId)) {
|
|
|
|
|
throw new IllegalStateException(message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean matches(PublishedShadowSnapshot snapshot, ShadowSnapshotDescriptor descriptor) {
|
|
|
|
|
return descriptor != null
|
|
|
|
|
&& PUBLISHED.equals(descriptor.getSnapshotStatus())
|
|
|
|
|
&& descriptor.getSnapshotGeneration() == snapshot.generation()
|
|
|
|
|
&& WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), snapshot.warehouseId())
|
|
|
|
|
&& Objects.equals(descriptor.getSynchronizedAt(), snapshot.synchronizedAt())
|
|
|
|
|
&& Objects.equals(descriptor.getSourceSignature(), snapshot.sourceSignature());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String configuredWarehouseId() {
|
|
|
|
|
String warehouseId = isBlank(properties.getWarehouseId())
|
|
|
|
|
? schedulingFacade.defaultWarehouseId()
|
|
|
|
|
: properties.getWarehouseId();
|
|
|
|
|
if (!isBlank(preferredWarehouseId)) {
|
|
|
|
|
String normalizedPreferred = preferredWarehouseId.trim();
|
|
|
|
|
if (sameNumericWarehouse(normalizedPreferred, configuredWarehouseId)) {
|
|
|
|
|
return configuredWarehouseId;
|
|
|
|
|
}
|
|
|
|
|
return normalizedPreferred;
|
|
|
|
|
if (isBlank(warehouseId)) {
|
|
|
|
|
throw new IllegalStateException("shadow warehouse is not configured");
|
|
|
|
|
}
|
|
|
|
|
return configuredWarehouseId;
|
|
|
|
|
return warehouseId.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean sameNumericWarehouse(String first, String second) {
|
|
|
|
|
if (isBlank(first) || isBlank(second)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return Integer.parseInt(first) == Integer.parseInt(second);
|
|
|
|
|
} catch (NumberFormatException ignored) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
private IllegalStateException leasedSnapshotRequired() {
|
|
|
|
|
return new IllegalStateException("published shadow snapshot lease is required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String outboundDiagnostic(String requestId, ShadowStockReference reference, String resultReason) {
|
|
|
|
|
return "detail=" + nullSafe(requestId)
|
|
|
|
|
+ "; project=" + nullSafe(reference.getProjectNo())
|
|
|
|
|
+ "; product=" + nullSafe(reference.getProductId())
|
|
|
|
|
+ "; tray=" + nullSafe(reference.getTrayNo())
|
|
|
|
|
+ "; stock=" + nullSafe(reference.getStockId())
|
|
|
|
|
+ "; snapshotSync=" + nullSafe(reference.getSnapshotSynchronizedAt())
|
|
|
|
|
+ "; legacyMatches=" + numberOrUnknown(reference.getLiveMatchCount())
|
|
|
|
|
+ "; shadowMatches=" + numberOrUnknown(reference.getShadowMatchCount())
|
|
|
|
|
private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId,
|
|
|
|
|
ShadowStockReference reference, String resultReason) {
|
|
|
|
|
return snapshotDiagnostic(snapshot)
|
|
|
|
|
+ "; detail=" + nullSafe(requestId)
|
|
|
|
|
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
|
|
|
|
|
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
|
|
|
|
|
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
|
|
|
|
|
+ "; tray=" + nullSafe(reference == null ? null : reference.getTrayNo())
|
|
|
|
|
+ "; stock=" + nullSafe(reference == null ? null : reference.getStockId())
|
|
|
|
|
+ "; space=" + nullSafe(reference == null ? null : reference.getSpaceId())
|
|
|
|
|
+ "; requestedQuantity=" + objectOrEmpty(
|
|
|
|
|
reference == null ? null : reference.getRequestedQuantity())
|
|
|
|
|
+ "; inputSnapshotSync=" + nullSafe(
|
|
|
|
|
reference == null ? null : reference.getSnapshotSynchronizedAt())
|
|
|
|
|
+ "; shadowMatches=" + numberOrUnknown(
|
|
|
|
|
reference == null ? null : reference.getShadowMatchCount())
|
|
|
|
|
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String inboundDiagnostic(String taskNo, String trayNo, ShadowInboundReference reference,
|
|
|
|
|
String resultReason) {
|
|
|
|
|
return "task=" + nullSafe(taskNo)
|
|
|
|
|
private String inboundDiagnostic(PublishedShadowSnapshot snapshot, String taskNo, String trayNo,
|
|
|
|
|
ShadowInboundReference reference, String resultReason) {
|
|
|
|
|
return snapshotDiagnostic(snapshot)
|
|
|
|
|
+ "; task=" + nullSafe(taskNo)
|
|
|
|
|
+ "; tray=" + nullSafe(trayNo)
|
|
|
|
|
+ "; request=" + nullSafe(reference.getRequestId())
|
|
|
|
|
+ "; warehouse=" + nullSafe(reference.getWarehouseId())
|
|
|
|
|
+ "; project=" + nullSafe(reference.getProjectNo())
|
|
|
|
|
+ "; product=" + nullSafe(reference.getProductId())
|
|
|
|
|
+ "; lot=" + nullSafe(reference.getLotNo())
|
|
|
|
|
+ "; request=" + nullSafe(reference == null ? null : reference.getRequestId())
|
|
|
|
|
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
|
|
|
|
|
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
|
|
|
|
|
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
|
|
|
|
|
+ "; lot=" + nullSafe(reference == null ? null : reference.getLotNo())
|
|
|
|
|
+ "; shadowCandidates=" + numberOrUnknown(
|
|
|
|
|
reference == null ? null : reference.getShadowCandidateCount())
|
|
|
|
|
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String snapshotDiagnostic(PublishedShadowSnapshot snapshot) {
|
|
|
|
|
return "snapshotGeneration=" + snapshot.generation()
|
|
|
|
|
+ "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt())
|
|
|
|
|
+ "; sourceSignature=" + nullSafe(snapshot.sourceSignature());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void requireUppercaseHexSignature(String value, String description) {
|
|
|
|
|
if (isBlank(value) || !value.matches("[0-9A-F]+")) {
|
|
|
|
|
throw new IllegalStateException(description + " must be uppercase hexadecimal");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String numberOrUnknown(Integer value) {
|
|
|
|
|
return value == null ? "" : value.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String objectOrEmpty(Object value) {
|
|
|
|
|
return value == null ? "" : value.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String instantOrEmpty(Instant value) {
|
|
|
|
|
return value == null ? "" : value.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String abbreviate(String value, int maxLength) {
|
|
|
|
|
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
|
|
|
|
}
|
|
|
|
|
|