fix: enforce stable shadow snapshot boundaries
This commit is contained in:
@@ -379,7 +379,9 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
+ "ORDER BY lane_code, depth, space_id", parameters, rawSpaceMapper());
|
+ "ORDER BY lane_code, depth, space_id", parameters, rawSpaceMapper());
|
||||||
List<InventoryItem> inventory = named.query("SELECT i.stock_id, i.space_id, i.tray_code, i.project_no, "
|
List<InventoryItem> inventory = named.query("SELECT i.stock_id, i.space_id, i.tray_code, i.project_no, "
|
||||||
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at, i.quantity, i.out_quantity "
|
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at, i.quantity, i.out_quantity "
|
||||||
+ "FROM " + dialect.inventoryView() + " i JOIN " + dialect.spaceStateView() + " s ON s.space_id = i.space_id "
|
+ "FROM " + dialect.inventoryView() + " i JOIN " + dialect.spaceStateView()
|
||||||
|
+ " s ON s.warehouse_id = i.warehouse_id AND s.snapshot_generation = i.snapshot_generation "
|
||||||
|
+ "AND s.space_id = i.space_id "
|
||||||
+ "WHERE s.warehouse_id = :warehouse AND s.lane_code IN (:lanes) "
|
+ "WHERE s.warehouse_id = :warehouse AND s.lane_code IN (:lanes) "
|
||||||
+ "ORDER BY i.space_id, i.stock_id", parameters, inventoryMapper());
|
+ "ORDER BY i.space_id, i.stock_id", parameters, inventoryMapper());
|
||||||
Map<String, List<InventoryItem>> inventoryBySpace = inventory.stream()
|
Map<String, List<InventoryItem>> inventoryBySpace = inventory.stream()
|
||||||
@@ -474,7 +476,9 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
.addValue("product", blankToNull(search.productNo()));
|
.addValue("product", blankToNull(search.productNo()));
|
||||||
String sql = "SELECT i.stock_id, i.space_id, s.lane_code, s.floor_no, i.tray_code, i.project_no, "
|
String sql = "SELECT i.stock_id, i.space_id, s.lane_code, s.floor_no, i.tray_code, i.project_no, "
|
||||||
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at FROM " + dialect.inventoryView()
|
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at FROM " + dialect.inventoryView()
|
||||||
+ " i JOIN " + dialect.spaceStateView() + " s ON s.space_id = i.space_id "
|
+ " i JOIN " + dialect.spaceStateView()
|
||||||
|
+ " s ON s.warehouse_id = i.warehouse_id AND s.snapshot_generation = i.snapshot_generation "
|
||||||
|
+ "AND s.space_id = i.space_id "
|
||||||
+ "WHERE s.warehouse_id = :warehouse AND " + predicate
|
+ "WHERE s.warehouse_id = :warehouse AND " + predicate
|
||||||
+ " ORDER BY i.inbound_at, i.inbound_time_raw, i.stock_id";
|
+ " ORDER BY i.inbound_at, i.inbound_time_raw, i.stock_id";
|
||||||
List<OutboundStockMatch> matches = named.query(sql, parameters, (rs, row) -> new OutboundStockMatch(
|
List<OutboundStockMatch> matches = named.query(sql, parameters, (rs, row) -> new OutboundStockMatch(
|
||||||
@@ -503,7 +507,9 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
+ "WHEN s.stock_tray_count = 0 THEN NULLIF(LTRIM(RTRIM(s.space_tray_code)), '') END";
|
+ "WHEN s.stock_tray_count = 0 THEN NULLIF(LTRIM(RTRIM(s.space_tray_code)), '') END";
|
||||||
Integer count = named.queryForObject("SELECT COUNT(DISTINCT " + usableTray + ") FROM "
|
Integer count = named.queryForObject("SELECT COUNT(DISTINCT " + usableTray + ") FROM "
|
||||||
+ dialect.spaceStateView() + " s JOIN " + dialect.inventoryView()
|
+ dialect.spaceStateView() + " s JOIN " + dialect.inventoryView()
|
||||||
+ " i ON i.space_id = s.space_id WHERE s.warehouse_id = :warehouse "
|
+ " i ON i.warehouse_id = s.warehouse_id "
|
||||||
|
+ "AND i.snapshot_generation = s.snapshot_generation AND i.space_id = s.space_id "
|
||||||
|
+ "WHERE s.warehouse_id = :warehouse "
|
||||||
+ "AND NULLIF(LTRIM(RTRIM(i.project_no)), '') = :project AND s.space_status = '2' "
|
+ "AND NULLIF(LTRIM(RTRIM(i.project_no)), '') = :project AND s.space_status = '2' "
|
||||||
+ "AND s.role = 'STORAGE' AND s.depth IS NOT NULL AND s.pending_outbound = "
|
+ "AND s.role = 'STORAGE' AND s.depth IS NOT NULL AND s.pending_outbound = "
|
||||||
+ dialect.falseLiteral() + " AND s.reserved = " + dialect.falseLiteral()
|
+ dialect.falseLiteral() + " AND s.reserved = " + dialect.falseLiteral()
|
||||||
|
|||||||
@@ -966,9 +966,9 @@ BEGIN
|
|||||||
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id)
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '')
|
||||||
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id);
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0));
|
||||||
|
|
||||||
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
||||||
@@ -1028,22 +1028,27 @@ BEGIN
|
|||||||
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
||||||
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
||||||
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
||||||
job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id]
|
job.warehouse_id AS [@warehouse_id], job.task_no AS [@task_no],
|
||||||
|
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
|
||||||
|
detail.in_or_out_id AS [@request_id]
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
ORDER BY inbound.stockin_detail_id
|
ORDER BY inbound.stockin_detail_id
|
||||||
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
|
|
||||||
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
||||||
|
warehouse.project_no AS [@project_no],
|
||||||
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
||||||
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
||||||
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
|
||||||
ORDER BY outbound.stockout_detail_id
|
ORDER BY outbound.stockout_detail_id
|
||||||
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
@@ -1096,22 +1101,22 @@ BEGIN
|
|||||||
AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '');
|
AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '');
|
||||||
|
|
||||||
;WITH inbound_source AS (
|
;WITH inbound_source AS (
|
||||||
SELECT job.warehouse_id, job.task_no, job.tray_code,
|
SELECT @warehouse_id AS warehouse_id, job.task_no, job.tray_code,
|
||||||
detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
|
detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
|
||||||
inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
|
inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY job.warehouse_id, job.task_no, job.tray_code
|
PARTITION BY job.task_no, job.tray_code
|
||||||
ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
|
ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
||||||
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
)
|
)
|
||||||
INSERT INTO alg.shadow_inbound_request_snapshot (
|
INSERT INTO alg.shadow_inbound_request_snapshot (
|
||||||
warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
|
warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
|
||||||
project_no, product_id, lot_no, snapshot_generation, synchronized_at)
|
project_no, product_id, lot_no, snapshot_generation, synchronized_at)
|
||||||
SELECT warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id,
|
SELECT @warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id,
|
||||||
raw_warehouse_id, project_no, product_id, lot_no,
|
raw_warehouse_id, project_no, product_id, lot_no,
|
||||||
@snapshot_generation, @synchronized_at
|
@snapshot_generation, @synchronized_at
|
||||||
FROM inbound_source
|
FROM inbound_source
|
||||||
@@ -1120,11 +1125,14 @@ BEGIN
|
|||||||
INSERT INTO alg.shadow_outbound_request_snapshot (
|
INSERT INTO alg.shadow_outbound_request_snapshot (
|
||||||
warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
|
warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
|
||||||
requested_quantity, snapshot_generation, synchronized_at)
|
requested_quantity, snapshot_generation, synchronized_at)
|
||||||
SELECT outbound.warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
|
SELECT @warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
|
||||||
NULL, outbound.product_id, CONVERT(decimal(38,6), outbound.quantity),
|
NULLIF(LTRIM(RTRIM(warehouse.project_no)), ''), outbound.product_id,
|
||||||
|
CONVERT(decimal(38,6), outbound.quantity),
|
||||||
@snapshot_generation, @synchronized_at
|
@snapshot_generation, @synchronized_at
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id;
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0);
|
||||||
|
|
||||||
EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
|
EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
|
||||||
@snapshot_generation = @snapshot_generation;
|
@snapshot_generation = @snapshot_generation;
|
||||||
@@ -1145,9 +1153,9 @@ BEGIN
|
|||||||
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id)
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '')
|
||||||
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id);
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0));
|
||||||
|
|
||||||
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
||||||
@@ -1203,21 +1211,26 @@ BEGIN
|
|||||||
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
||||||
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
||||||
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
||||||
job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id]
|
job.warehouse_id AS [@warehouse_id], job.task_no AS [@task_no],
|
||||||
|
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
|
||||||
|
detail.in_or_out_id AS [@request_id]
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
ORDER BY inbound.stockin_detail_id
|
ORDER BY inbound.stockin_detail_id
|
||||||
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
||||||
|
warehouse.project_no AS [@project_no],
|
||||||
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
||||||
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
||||||
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
|
||||||
ORDER BY outbound.stockout_detail_id
|
ORDER BY outbound.stockout_detail_id
|
||||||
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
|
|||||||
@@ -304,9 +304,9 @@ BEGIN
|
|||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
||||||
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id)
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '')
|
||||||
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id);
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0));
|
||||||
|
|
||||||
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
||||||
@@ -363,22 +363,27 @@ BEGIN
|
|||||||
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
|
||||||
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
|
||||||
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
||||||
job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id]
|
job.warehouse_id AS [@warehouse_id], job.task_no AS [@task_no],
|
||||||
|
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
|
||||||
|
detail.in_or_out_id AS [@request_id]
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
||||||
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
ORDER BY inbound.stockin_detail_id
|
ORDER BY inbound.stockin_detail_id
|
||||||
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
||||||
|
warehouse.project_no AS [@project_no],
|
||||||
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
|
||||||
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
|
||||||
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
|
||||||
ORDER BY outbound.stockout_detail_id
|
ORDER BY outbound.stockout_detail_id
|
||||||
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
@@ -430,22 +435,22 @@ BEGIN
|
|||||||
AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '');
|
AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '');
|
||||||
|
|
||||||
;WITH inbound_source AS (
|
;WITH inbound_source AS (
|
||||||
SELECT job.warehouse_id, job.task_no, job.tray_code,
|
SELECT @warehouse_id AS warehouse_id, job.task_no, job.tray_code,
|
||||||
detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
|
detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
|
||||||
inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
|
inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY job.warehouse_id, job.task_no, job.tray_code
|
PARTITION BY job.task_no, job.tray_code
|
||||||
ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
|
ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
|
||||||
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
)
|
)
|
||||||
INSERT INTO alg.shadow_inbound_request_snapshot (
|
INSERT INTO alg.shadow_inbound_request_snapshot (
|
||||||
warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
|
warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
|
||||||
project_no, product_id, lot_no, snapshot_generation, synchronized_at)
|
project_no, product_id, lot_no, snapshot_generation, synchronized_at)
|
||||||
SELECT warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id,
|
SELECT @warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id,
|
||||||
raw_warehouse_id, project_no, product_id, lot_no,
|
raw_warehouse_id, project_no, product_id, lot_no,
|
||||||
@snapshot_generation, @synchronized_at
|
@snapshot_generation, @synchronized_at
|
||||||
FROM inbound_source
|
FROM inbound_source
|
||||||
@@ -454,11 +459,14 @@ BEGIN
|
|||||||
INSERT INTO alg.shadow_outbound_request_snapshot (
|
INSERT INTO alg.shadow_outbound_request_snapshot (
|
||||||
warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
|
warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
|
||||||
requested_quantity, snapshot_generation, synchronized_at)
|
requested_quantity, snapshot_generation, synchronized_at)
|
||||||
SELECT outbound.warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
|
SELECT @warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
|
||||||
NULL, outbound.product_id, CONVERT(decimal(38,6), outbound.quantity),
|
NULLIF(LTRIM(RTRIM(warehouse.project_no)), ''), outbound.product_id,
|
||||||
|
CONVERT(decimal(38,6), outbound.quantity),
|
||||||
@snapshot_generation, @synchronized_at
|
@snapshot_generation, @synchronized_at
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id;
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0);
|
||||||
|
|
||||||
EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
|
EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
|
||||||
@snapshot_generation = @snapshot_generation;
|
@snapshot_generation = @snapshot_generation;
|
||||||
@@ -479,8 +487,9 @@ BEGIN
|
|||||||
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id)
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '')
|
||||||
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK) WHERE outbound.warehouse_id = @warehouse_id);
|
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0));
|
||||||
|
|
||||||
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
|
||||||
@@ -535,20 +544,25 @@ BEGIN
|
|||||||
SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
|
SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
|
||||||
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], inbound.product_id AS [@product_id],
|
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], inbound.product_id AS [@product_id],
|
||||||
inbound.quantity AS [@quantity], inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
inbound.quantity AS [@quantity], inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
|
||||||
job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id]
|
job.warehouse_id AS [@warehouse_id], job.task_no AS [@task_no],
|
||||||
|
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
|
||||||
|
detail.in_or_out_id AS [@request_id]
|
||||||
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
|
||||||
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
|
||||||
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
|
||||||
WHERE job.warehouse_id = @warehouse_id
|
WHERE job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''
|
||||||
ORDER BY inbound.stockin_detail_id
|
ORDER BY inbound.stockin_detail_id
|
||||||
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), (
|
||||||
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
|
||||||
|
warehouse.project_no AS [@project_no],
|
||||||
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], outbound.quantity AS [@quantity],
|
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], outbound.quantity AS [@quantity],
|
||||||
outbound.assign_num AS [@assign_num], outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
outbound.assign_num AS [@assign_num], outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time]
|
||||||
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
|
||||||
WHERE outbound.warehouse_id = @warehouse_id
|
LEFT JOIN dbo.bas_warehouse warehouse WITH (NOLOCK)
|
||||||
|
ON warehouse.warehouse_id = outbound.warehouse_id
|
||||||
|
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
|
||||||
ORDER BY outbound.stockout_detail_id
|
ORDER BY outbound.stockout_detail_id
|
||||||
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE
|
||||||
)), '');
|
)), '');
|
||||||
|
|||||||
@@ -86,6 +86,24 @@ class JdbcWarehouseRepositoryTest {
|
|||||||
assertThat(space(lanes, "OUT-INNER").state().zoneCode()).isEqualTo("FLEX");
|
assertThat(space(lanes, "OUT-INNER").state().zoneCode()).isEqualTo("FLEX");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void publishedReadModelJoinsDoNotCrossWarehouseBoundaries() throws Exception {
|
||||||
|
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('SHARED-SPACE','W1','R','A','Shared id',9,90,1,'2','',"
|
||||||
|
+ "'TRAY-W1','SHARED-LANE','1')");
|
||||||
|
execute("INSERT INTO ALG_INVENTORY_ITEM_SNAPSHOT (WAREHOUSE_ID, STOCK_ID, SPACE_ID, TRAY_CODE, "
|
||||||
|
+ "PROJECT_NO, PRODUCT_NO, LOT_NO, INBOUND_TIME_RAW, INBOUND_AT, QUANTITY, OUT_QUANTITY) "
|
||||||
|
+ "VALUES ('W2','CROSS-W2','SHARED-SPACE',NULL,'PROJECT-W2','SKU-W2','LOT-W2',"
|
||||||
|
+ "'2026-09-03 00:00:00',TIMESTAMP '2026-09-03 00:00:00',1,0)");
|
||||||
|
rebuildTopology("W1");
|
||||||
|
|
||||||
|
List<LaneSnapshot> lanes = repository.loadLaneSnapshots("W1", List.of("SHARED-LANE"));
|
||||||
|
|
||||||
|
assertThat(space(lanes, "SHARED-SPACE").inventory()).isEmpty();
|
||||||
|
assertThat(repository.searchOutboundStock("W1", OutboundStockSearch.project("PROJECT-W2", null)))
|
||||||
|
.isEmpty();
|
||||||
|
assertThat(repository.countProjectPallets("W1", "PROJECT-W2")).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void statusFourPhysicalOuterPalletIsPreservedAndMakesDeepOutboundWait() throws Exception {
|
void statusFourPhysicalOuterPalletIsPreservedAndMakesDeepOutboundWait() throws Exception {
|
||||||
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('ALLOC-TARGET','W1','RA','A','Target',7,21,2,'2','',"
|
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('ALLOC-TARGET','W1','RA','A','Target',7,21,2,'2','',"
|
||||||
|
|||||||
@@ -208,6 +208,25 @@ class ShadowSqlServerMigrationScriptTest {
|
|||||||
"snapshot_generation = @snapshot_generation");
|
"snapshot_generation = @snapshot_generation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refreshPreservesRawRequestWarehouseUnderAuthoritativeSnapshotWarehouse() throws IOException {
|
||||||
|
for (String script : new String[]{"db/sqlserver/install-shadow.sql",
|
||||||
|
"db/sqlserver/upgrade-shadow-v2-to-v3.sql"}) {
|
||||||
|
String refresh = normalize(resource(script));
|
||||||
|
refresh = refresh.substring(refresh.indexOf("create procedure alg.usp_refresh_shadow_snapshot"));
|
||||||
|
|
||||||
|
assertThat(refresh).as(script).contains(
|
||||||
|
"select @warehouse_id, isnull(task_no, ''), isnull(tray_code, ''), request_id, raw_warehouse_id",
|
||||||
|
"select @warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id",
|
||||||
|
"left join dbo.bas_warehouse warehouse with (nolock)",
|
||||||
|
"nullif(ltrim(rtrim(warehouse.project_no)), '')",
|
||||||
|
"job.finish_time is null or ltrim(rtrim(job.finish_time)) = ''",
|
||||||
|
"isnull(outbound.quantity, 0) > isnull(outbound.allot_quantity, 0)");
|
||||||
|
assertThat(refresh).as(script).doesNotContain(
|
||||||
|
"select outbound.warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id, null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void plannerViewsContainOnlyGenerationQualifiedAlgSnapshots() throws IOException {
|
void plannerViewsContainOnlyGenerationQualifiedAlgSnapshots() throws IOException {
|
||||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ $$;
|
|||||||
|
|
||||||
CREATE VIEW ALG_V_INVENTORY_ITEM AS
|
CREATE VIEW ALG_V_INVENTORY_ITEM AS
|
||||||
SELECT
|
SELECT
|
||||||
|
WAREHOUSE_ID, CAST(0 AS BIGINT) AS SNAPSHOT_GENERATION,
|
||||||
STOCK_ID, SPACE_ID, TRAY_CODE, PROJECT_NO, PRODUCT_NO, LOT_NO,
|
STOCK_ID, SPACE_ID, TRAY_CODE, PROJECT_NO, PRODUCT_NO, LOT_NO,
|
||||||
INBOUND_TIME_RAW, INBOUND_AT, QUANTITY, OUT_QUANTITY
|
INBOUND_TIME_RAW, INBOUND_AT, QUANTITY, OUT_QUANTITY
|
||||||
FROM ALG_INVENTORY_ITEM_SNAPSHOT;
|
FROM ALG_INVENTORY_ITEM_SNAPSHOT;
|
||||||
@@ -281,6 +282,7 @@ LANE_GAPS AS (
|
|||||||
SELECT
|
SELECT
|
||||||
C.CARGO_SPACE_ID AS SPACE_ID,
|
C.CARGO_SPACE_ID AS SPACE_ID,
|
||||||
C.WAREHOUSE_ID,
|
C.WAREHOUSE_ID,
|
||||||
|
CAST(0 AS BIGINT) AS SNAPSHOT_GENERATION,
|
||||||
C.GROUP_CODING_NO AS LANE_CODE,
|
C.GROUP_CODING_NO AS LANE_CODE,
|
||||||
C.CARGO_AREA_ID AS AREA_ID,
|
C.CARGO_AREA_ID AS AREA_ID,
|
||||||
CASE
|
CASE
|
||||||
|
|||||||
@@ -1,26 +1,43 @@
|
|||||||
package com.ruoyi.algorithm.shadow;
|
package com.ruoyi.algorithm.shadow;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
public record InboundShadowObservation(
|
public record InboundShadowObservation(
|
||||||
boolean observed,
|
boolean observed,
|
||||||
long snapshotGeneration,
|
long snapshotGeneration,
|
||||||
String warehouseId,
|
String warehouseId,
|
||||||
|
Instant snapshotSynchronizedAt,
|
||||||
|
String sourceSignature,
|
||||||
String requestId,
|
String requestId,
|
||||||
String resultCode,
|
String resultCode,
|
||||||
String proposedSpaceId,
|
String proposedSpaceId,
|
||||||
String reason) {
|
String reason) {
|
||||||
|
|
||||||
|
public InboundShadowObservation(boolean observed, long snapshotGeneration, String warehouseId,
|
||||||
|
String requestId, String resultCode, String proposedSpaceId, String reason) {
|
||||||
|
this(observed, snapshotGeneration, warehouseId, null, null,
|
||||||
|
requestId, resultCode, proposedSpaceId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
public InboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
public InboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||||
String resultCode, String proposedSpaceId, String reason) {
|
String resultCode, String proposedSpaceId, String reason) {
|
||||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedSpaceId, reason);
|
this(observed, 0L, warehouseId, null, null, requestId, resultCode, proposedSpaceId, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InboundShadowObservation empty() {
|
public static InboundShadowObservation empty() {
|
||||||
return new InboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null);
|
return new InboundShadowObservation(false, 0L, null, null, null,
|
||||||
|
null, "NOT_OBSERVED", null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
public static InboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||||
String requestId, String resultCode, String reason) {
|
String requestId, String resultCode, String reason) {
|
||||||
return new InboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
return new InboundShadowObservation(true, snapshotGeneration, warehouseId, null, null,
|
||||||
resultCode, null, reason);
|
requestId, resultCode, null, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InboundShadowObservation unavailable(PublishedShadowSnapshot snapshot,
|
||||||
|
String requestId, String resultCode, String reason) {
|
||||||
|
return new InboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||||
|
snapshot.synchronizedAt(), snapshot.sourceSignature(), requestId, resultCode, null, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
package com.ruoyi.algorithm.shadow;
|
package com.ruoyi.algorithm.shadow;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
public record OutboundShadowObservation(
|
public record OutboundShadowObservation(
|
||||||
boolean observed,
|
boolean observed,
|
||||||
long snapshotGeneration,
|
long snapshotGeneration,
|
||||||
String warehouseId,
|
String warehouseId,
|
||||||
|
Instant snapshotSynchronizedAt,
|
||||||
|
String sourceSignature,
|
||||||
String requestId,
|
String requestId,
|
||||||
String resultCode,
|
String resultCode,
|
||||||
String proposedStockId,
|
String proposedStockId,
|
||||||
String proposedSpaceId,
|
String proposedSpaceId,
|
||||||
String reason) {
|
String reason) {
|
||||||
|
|
||||||
|
public OutboundShadowObservation(boolean observed, long snapshotGeneration, String warehouseId,
|
||||||
|
String requestId, String resultCode, String proposedStockId, String proposedSpaceId,
|
||||||
|
String reason) {
|
||||||
|
this(observed, snapshotGeneration, warehouseId, null, null,
|
||||||
|
requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
public OutboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
public OutboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||||
String resultCode, String proposedStockId, String proposedSpaceId, String reason) {
|
String resultCode, String proposedStockId, String proposedSpaceId, String reason) {
|
||||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
this(observed, 0L, warehouseId, null, null,
|
||||||
|
requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OutboundShadowObservation empty() {
|
public static OutboundShadowObservation empty() {
|
||||||
return new OutboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null, null);
|
return new OutboundShadowObservation(false, 0L, null, null, null,
|
||||||
|
null, "NOT_OBSERVED", null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OutboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
public static OutboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||||
String requestId, String resultCode, String reason) {
|
String requestId, String resultCode, String reason) {
|
||||||
return new OutboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
return new OutboundShadowObservation(true, snapshotGeneration, warehouseId, null, null,
|
||||||
resultCode, null, null, reason);
|
requestId, resultCode, null, null, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OutboundShadowObservation unavailable(PublishedShadowSnapshot snapshot,
|
||||||
|
String requestId, String resultCode, String reason) {
|
||||||
|
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||||
|
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||||
|
requestId, resultCode, null, null, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.util.function.Supplier;
|
|||||||
@Component
|
@Component
|
||||||
public class ShadowPlanningEngine {
|
public class ShadowPlanningEngine {
|
||||||
private static final int INSTANCE_STALE_SECONDS = 120;
|
private static final int INSTANCE_STALE_SECONDS = 120;
|
||||||
|
private static final int DECISION_REASON_MAX_LENGTH = 2000;
|
||||||
private static final String PUBLISHED = "PUBLISHED";
|
private static final String PUBLISHED = "PUBLISHED";
|
||||||
|
|
||||||
private final SchedulingFacade schedulingFacade;
|
private final SchedulingFacade schedulingFacade;
|
||||||
@@ -81,8 +82,8 @@ public class ShadowPlanningEngine {
|
|||||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
||||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
|
||||||
stockoutDetailId, "SKIPPED_MISSING_PROJECT",
|
"SKIPPED_MISSING_PROJECT",
|
||||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||||
"No project number was available for shadow planning"));
|
"No project number was available for shadow planning"));
|
||||||
}
|
}
|
||||||
@@ -100,8 +101,8 @@ public class ShadowPlanningEngine {
|
|||||||
ShadowStockReference reference = inputMapper.selectStockReference(
|
ShadowStockReference reference = inputMapper.selectStockReference(
|
||||||
warehouseId, snapshot.generation(), stockId);
|
warehouseId, snapshot.generation(), stockId);
|
||||||
if (reference == null || isBlank(reference.getTrayNo())) {
|
if (reference == null || isBlank(reference.getTrayNo())) {
|
||||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
|
||||||
stockoutDetailId, "SKIPPED_MISSING_STOCK",
|
"SKIPPED_MISSING_STOCK",
|
||||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||||
"Selected stock was unavailable to shadow planning"));
|
"Selected stock was unavailable to shadow planning"));
|
||||||
}
|
}
|
||||||
@@ -116,37 +117,41 @@ public class ShadowPlanningEngine {
|
|||||||
String legacySpaceId) {
|
String legacySpaceId) {
|
||||||
Objects.requireNonNull(observation, "observation");
|
Objects.requireNonNull(observation, "observation");
|
||||||
transactions.executeWithoutResult(status -> {
|
transactions.executeWithoutResult(status -> {
|
||||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
validateComparisonSnapshot(observation.snapshotGeneration(), observation.warehouseId(),
|
||||||
"published snapshot changed before comparison");
|
observation.snapshotSynchronizedAt(), observation.sourceSignature());
|
||||||
inputMapper.insertComparison(
|
int inserted = inputMapper.insertComparison(
|
||||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||||
observation.snapshotGeneration(),
|
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||||
|
observation.sourceSignature(),
|
||||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
|
||||||
+ "; request=" + observation.requestId()
|
+ "; request=" + observation.requestId()
|
||||||
+ "; shadow=" + observation.resultCode()
|
+ "; shadow=" + observation.resultCode()
|
||||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||||
+ "; legacySpace=" + nullSafe(legacySpaceId));
|
+ "; legacySpace=" + nullSafe(legacySpaceId), DECISION_REASON_MAX_LENGTH));
|
||||||
|
requireComparisonInserted(inserted);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
||||||
Objects.requireNonNull(observation, "observation");
|
Objects.requireNonNull(observation, "observation");
|
||||||
transactions.executeWithoutResult(status -> {
|
transactions.executeWithoutResult(status -> {
|
||||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
validateComparisonSnapshot(observation.snapshotGeneration(), observation.warehouseId(),
|
||||||
"published snapshot changed before comparison");
|
observation.snapshotSynchronizedAt(), observation.sourceSignature());
|
||||||
inputMapper.insertComparison(
|
int inserted = inputMapper.insertComparison(
|
||||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||||
observation.snapshotGeneration(), "LEGACY_" + nullSafe(legacyResultCode),
|
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode),
|
||||||
|
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
|
||||||
+ "; request=" + observation.requestId()
|
+ "; request=" + observation.requestId()
|
||||||
+ "; shadow=" + observation.resultCode()
|
+ "; shadow=" + observation.resultCode()
|
||||||
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
||||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||||
+ "; legacy=" + nullSafe(legacyResultCode));
|
+ "; legacy=" + nullSafe(legacyResultCode), DECISION_REASON_MAX_LENGTH));
|
||||||
|
requireComparisonInserted(inserted);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +161,7 @@ public class ShadowPlanningEngine {
|
|||||||
ShadowInboundReference reference = inputMapper.selectInboundInput(
|
ShadowInboundReference reference = inputMapper.selectInboundInput(
|
||||||
warehouseId, snapshot.generation(), taskNo, trayNo);
|
warehouseId, snapshot.generation(), taskNo, trayNo);
|
||||||
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
||||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId, taskNo,
|
return InboundShadowObservation.unavailable(snapshot, taskNo,
|
||||||
"SKIPPED_MISSING_INPUT",
|
"SKIPPED_MISSING_INPUT",
|
||||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
||||||
"Inbound task details were unavailable to shadow planning"));
|
"Inbound task details were unavailable to shadow planning"));
|
||||||
@@ -169,11 +174,12 @@ public class ShadowPlanningEngine {
|
|||||||
if (plan.assignment().isPresent()) {
|
if (plan.assignment().isPresent()) {
|
||||||
var assignment = plan.assignment().orElseThrow();
|
var assignment = plan.assignment().orElseThrow();
|
||||||
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||||
|
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||||
reference.getRequestId(), "READY", assignment.spaceId(),
|
reference.getRequestId(), "READY", assignment.spaceId(),
|
||||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, assignment.reason()));
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference, assignment.reason()));
|
||||||
}
|
}
|
||||||
var rejection = plan.rejection().orElseThrow();
|
var rejection = plan.rejection().orElseThrow();
|
||||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
return InboundShadowObservation.unavailable(snapshot,
|
||||||
reference.getRequestId(), "REJECTED_" + rejection.code().name(),
|
reference.getRequestId(), "REJECTED_" + rejection.code().name(),
|
||||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, rejection.reason()));
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference, rejection.reason()));
|
||||||
}
|
}
|
||||||
@@ -186,17 +192,18 @@ public class ShadowPlanningEngine {
|
|||||||
if (plan.assignment().isPresent()) {
|
if (plan.assignment().isPresent()) {
|
||||||
var assignment = plan.assignment().orElseThrow();
|
var assignment = plan.assignment().orElseThrow();
|
||||||
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||||
|
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||||
requestId, "READY", reference.getStockId(), assignment.sourceSpaceId(),
|
requestId, "READY", reference.getStockId(), assignment.sourceSpaceId(),
|
||||||
outboundDiagnostic(snapshot, requestId, reference, assignment.reason()));
|
outboundDiagnostic(snapshot, requestId, reference, assignment.reason()));
|
||||||
}
|
}
|
||||||
if (plan.waiting().isPresent()) {
|
if (plan.waiting().isPresent()) {
|
||||||
var waiting = plan.waiting().orElseThrow();
|
var waiting = plan.waiting().orElseThrow();
|
||||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
return OutboundShadowObservation.unavailable(snapshot,
|
||||||
requestId, "WAITING_" + waiting.code().name(),
|
requestId, "WAITING_" + waiting.code().name(),
|
||||||
outboundDiagnostic(snapshot, requestId, reference, waiting.reason()));
|
outboundDiagnostic(snapshot, requestId, reference, waiting.reason()));
|
||||||
}
|
}
|
||||||
var error = plan.error().orElseThrow();
|
var error = plan.error().orElseThrow();
|
||||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
return OutboundShadowObservation.unavailable(snapshot,
|
||||||
requestId, "ERROR_" + error.code().name(),
|
requestId, "ERROR_" + error.code().name(),
|
||||||
outboundDiagnostic(snapshot, requestId, reference, error.message()));
|
outboundDiagnostic(snapshot, requestId, reference, error.message()));
|
||||||
}
|
}
|
||||||
@@ -253,16 +260,17 @@ public class ShadowPlanningEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requirePublishedGeneration(long generation, String warehouseId, String message) {
|
private void validateComparisonSnapshot(long generation, String warehouseId,
|
||||||
if (generation <= 0 || isBlank(warehouseId)) {
|
Instant synchronizedAt, String sourceSignature) {
|
||||||
throw new IllegalStateException(message);
|
if (generation <= 0 || isBlank(warehouseId) || synchronizedAt == null) {
|
||||||
|
throw new IllegalStateException("published snapshot changed before comparison");
|
||||||
}
|
}
|
||||||
ShadowSnapshotDescriptor descriptor = inputMapper.selectPublishedSnapshot(warehouseId);
|
requireUppercaseHexSignature(sourceSignature, "comparison source signature");
|
||||||
if (descriptor == null
|
}
|
||||||
|| !PUBLISHED.equals(descriptor.getSnapshotStatus())
|
|
||||||
|| descriptor.getSnapshotGeneration() != generation
|
private void requireComparisonInserted(int inserted) {
|
||||||
|| !WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), warehouseId)) {
|
if (inserted != 1) {
|
||||||
throw new IllegalStateException(message);
|
throw new IllegalStateException("published snapshot changed before comparison");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.ruoyi.algorithm.shadow.ShadowStockReference;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ShadowInputMapper {
|
public interface ShadowInputMapper {
|
||||||
ShadowInboundReference selectInboundInput(@Param("warehouseId") String warehouseId,
|
ShadowInboundReference selectInboundInput(@Param("warehouseId") String warehouseId,
|
||||||
@@ -31,6 +33,8 @@ public interface ShadowInputMapper {
|
|||||||
@Param("warehouseId") String warehouseId,
|
@Param("warehouseId") String warehouseId,
|
||||||
@Param("decisionType") String decisionType,
|
@Param("decisionType") String decisionType,
|
||||||
@Param("inputVersion") long inputVersion,
|
@Param("inputVersion") long inputVersion,
|
||||||
|
@Param("snapshotSynchronizedAt") Instant snapshotSynchronizedAt,
|
||||||
|
@Param("sourceSignature") String sourceSignature,
|
||||||
@Param("resultCode") String resultCode,
|
@Param("resultCode") String resultCode,
|
||||||
@Param("reason") String reason);
|
@Param("reason") String reason);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,15 +96,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
state.snapshot_status as snapshotStatus,
|
state.snapshot_status as snapshotStatus,
|
||||||
state.synchronized_at as synchronizedAt,
|
state.synchronized_at as synchronizedAt,
|
||||||
convert(varchar(64), state.source_signature, 2) as sourceSignature
|
convert(varchar(64), state.source_signature, 2) as sourceSignature
|
||||||
from alg.shadow_snapshot_state state
|
from alg.shadow_snapshot_state state with (readcommittedlock)
|
||||||
where state.warehouse_id = #{warehouseId}
|
where state.warehouse_id = #{warehouseId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertComparison">
|
<insert id="insertComparison">
|
||||||
insert into alg.decision_log (
|
insert into alg.decision_log (
|
||||||
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
|
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
|
||||||
values (
|
select try_convert(uniqueidentifier, #{decisionId}), state.warehouse_id, #{decisionType},
|
||||||
try_convert(uniqueidentifier, #{decisionId}), #{warehouseId}, #{decisionType},
|
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason}
|
||||||
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason})
|
from alg.shadow_snapshot_state state with (updlock, holdlock)
|
||||||
|
where state.warehouse_id = #{warehouseId}
|
||||||
|
and state.snapshot_status = 'PUBLISHED'
|
||||||
|
and state.snapshot_generation = #{inputVersion}
|
||||||
|
and state.synchronized_at = #{snapshotSynchronizedAt}
|
||||||
|
and convert(varchar(64), state.source_signature, 2) = #{sourceSignature}
|
||||||
</insert>
|
</insert>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ class ShadowPlanningEngineTest {
|
|||||||
assertThatThrownBy(() -> engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1"))
|
assertThatThrownBy(() -> engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1"))
|
||||||
.isInstanceOf(IllegalStateException.class)
|
.isInstanceOf(IllegalStateException.class)
|
||||||
.hasMessageContaining("published snapshot changed during planning");
|
.hasMessageContaining("published snapshot changed during planning");
|
||||||
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
|
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class),
|
||||||
|
any(Instant.class), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -160,30 +161,66 @@ class ShadowPlanningEngineTest {
|
|||||||
@Test
|
@Test
|
||||||
void comparisonPersistsLeasedGenerationAsInputVersion() {
|
void comparisonPersistsLeasedGenerationAsInputVersion() {
|
||||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
|
||||||
|
any(Instant.class), any(), any(), any())).thenReturn(1);
|
||||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||||
InboundShadowObservation observation = new InboundShadowObservation(
|
InboundShadowObservation observation = new InboundShadowObservation(
|
||||||
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||||
|
"JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
||||||
|
|
||||||
engine.compareInbound(observation, "READY", "SPACE-9");
|
engine.compareInbound(observation, "READY", "SPACE-9");
|
||||||
|
|
||||||
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
|
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
|
||||||
eq("MATCH"), argThat(reason -> reason.contains("request=JOB-1")));
|
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("MATCH"),
|
||||||
|
argThat(reason -> reason.contains("request=JOB-1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void comparisonDoesNotPersistAfterPublishedGenerationChanges() {
|
void comparisonFailsWhenAtomicDescriptorGuardRejectsTheInsert() {
|
||||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||||
when(mapper.selectPublishedSnapshot("01"))
|
|
||||||
.thenReturn(descriptor(snapshot(43L, "01")));
|
|
||||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||||
InboundShadowObservation observation = new InboundShadowObservation(
|
InboundShadowObservation observation = new InboundShadowObservation(
|
||||||
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||||
|
"JOB-1", "READY", "SPACE-9", "shadow diagnostic");
|
||||||
|
|
||||||
assertThatThrownBy(() -> engine.compareInbound(observation, "READY", "SPACE-9"))
|
assertThatThrownBy(() -> engine.compareInbound(observation, "READY", "SPACE-9"))
|
||||||
.isInstanceOf(IllegalStateException.class)
|
.isInstanceOf(IllegalStateException.class)
|
||||||
.hasMessageContaining("published snapshot changed before comparison");
|
.hasMessageContaining("published snapshot changed before comparison");
|
||||||
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
|
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
|
||||||
|
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void outboundComparisonCarriesTheCompleteSnapshotDescriptor() {
|
||||||
|
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||||
|
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
|
||||||
|
any(Instant.class), any(), any(), any())).thenReturn(1);
|
||||||
|
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||||
|
OutboundShadowObservation observation = new OutboundShadowObservation(
|
||||||
|
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||||
|
"DETAIL-1", "READY", "STOCK-1", "SPACE-9", "shadow diagnostic");
|
||||||
|
|
||||||
|
engine.compareOutbound(observation, "10");
|
||||||
|
|
||||||
|
verify(mapper).insertComparison(any(), eq("01"), eq("OUTBOUND_COMPARE"), eq(42L),
|
||||||
|
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("LEGACY_10"), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void comparisonReasonIsBoundedToDecisionLogCapacity() {
|
||||||
|
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||||
|
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
|
||||||
|
any(Instant.class), any(), any(), any())).thenReturn(1);
|
||||||
|
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||||
|
InboundShadowObservation observation = new InboundShadowObservation(
|
||||||
|
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||||
|
"JOB-1", "READY", "SPACE-9", "X".repeat(2500));
|
||||||
|
|
||||||
|
engine.compareInbound(observation, "READY", "SPACE-9");
|
||||||
|
|
||||||
|
verify(mapper).insertComparison(any(), any(), any(), any(Long.class),
|
||||||
|
any(Instant.class), any(), any(),
|
||||||
|
argThat(reason -> reason.length() <= 2000));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -247,7 +284,12 @@ class ShadowPlanningEngineTest {
|
|||||||
assertThat(statements.get("selectStockReference"))
|
assertThat(statements.get("selectStockReference"))
|
||||||
.contains("alg.inventory_item_snapshot", "stock_id = #{stockid}");
|
.contains("alg.inventory_item_snapshot", "stock_id = #{stockid}");
|
||||||
assertThat(statements.get("insertComparison"))
|
assertThat(statements.get("insertComparison"))
|
||||||
.contains("convert(binary(8), #{inputversion})");
|
.contains("convert(binary(8), #{inputversion})",
|
||||||
|
"from alg.shadow_snapshot_state state with (updlock, holdlock)",
|
||||||
|
"state.snapshot_status = 'published'",
|
||||||
|
"state.snapshot_generation = #{inputversion}",
|
||||||
|
"state.synchronized_at = #{snapshotsynchronizedat}",
|
||||||
|
"convert(varchar(64), state.source_signature, 2) = #{sourcesignature}");
|
||||||
assertThat(statements.get("selectPublishedSnapshot"))
|
assertThat(statements.get("selectPublishedSnapshot"))
|
||||||
.contains("alg.shadow_snapshot_state", "convert(varchar(64),",
|
.contains("alg.shadow_snapshot_state", "convert(varchar(64),",
|
||||||
"source_signature", ", 2)");
|
"source_signature", ", 2)");
|
||||||
|
|||||||
Reference in New Issue
Block a user