Case 7: Retail inventory replenishment
This case corresponds to Scenario 7: Retail inventory replenishment in the JQuick-Java repository’s USE-CASES.md. It belongs to the supply chain / chain retail domain and solves the full replenishment decision chain for multi-warehouse SKUs: “demand estimation → safety stock → target stock → shortage → MOQ / case-size / cap constraints → urgency & expedite recommendation”.
Industry scenario
Section titled “Industry scenario”A chain retailer’s replenishment system needs to provide daily order suggestions per SKU and per warehouse. Each product maintains a set of replenishment parameters: current stock, base safety stock, average daily sales, replenishment lead time (days), minimum order quantity (MOQ), case pack size, supplier fulfillment rate (0-1), whether it is in a promotion period, peak/off-season flag (PEAK/NORMAL/OFF), and the maximum single order quantity.
The rule chain first calculates the lead-time demand (average daily sales × lead time days). The dynamic safety stock adds an extra 5 days of average daily sales on top of the base value during promotion periods, and adds another 3 days when the supplier fulfillment rate is below 0.9. The target stock is the sum of safety stock and lead-time demand, with an additional 7-day uplift during peak season and a 3-day reduction during off-season, ensuring the result is non-negative.
Next, the shortage is computed (target stock minus current stock, non-negative), and the suggested order quantity passes through three supply constraints in sequence: raised to the MOQ when below it, rounded up to the case pack size, and capped at the supplier single-order limit (with a split-order prompt when exceeded). Meanwhile, the system outputs a CRITICAL/HIGH/MEDIUM/LOW urgency tier based on the current stock’s days-on-hand relative to lead time, and recommends expedited logistics when the stock days are insufficient to cover lead time (or near peak season).
The decision result includes lead-time demand, safety stock, target stock, shortage, final order quantity, stock days, urgency, whether to expedite, and a set of notes (MOQ uplift, case-size rounding, cap split-order, promotion and fulfillment explanations, etc.), directly driving procurement orders and inter-warehouse transfers. Scenario 7 covers 6 profiles: urgent stock-out, sufficient stock short-circuit, promotion peak with MOQ uplift to 500, off-season case-size rounding to 60, medium-urgency HIGH, and hot-item large order capped at 1000 with split-order suggestion.
Business pain points
Section titled “Business pain points”- Adjustment factors such as promotion +5 days, low fulfillment +3 days, peak +7 days, and off-season -3 days are scattered across cron jobs, SQL, and hard-coded logic; seasonal or supplier changes require coordinating multiple redeployments.
- The three supply constraints—MOQ, case-size rounding, and single-order cap—are order-sensitive; hard-coded implementations often miss one, causing purchase orders to be rejected by suppliers.
- Urgency thresholds float with lead time (no more than lead time, +3 days, +7 days), making it hard to keep a unified policy across warehouses and products with different lead times.
- Parameters before big promotions need frequent adjustment, but release windows are limited; even changing safety stock days requires scheduling a deployment, easily missing the stock-up window.
- Replenishment results usually provide only a single order quantity; procurement teams do not understand “why order 500 when the shortage is 390” or “why only 1000 when the shortage is 2085”, lacking explainable constraint notes.
- Many WMS/ERP systems still run on JDK 8 or IT-innovation environments, so heavy supply-chain planning platforms cannot be deployed.
Solution architecture
Section titled “Solution architecture”This case uses the standard “six-piece set”: the input model carries the SKU and supply parameters, the decision result model carries the full chain of intermediate values and recommendations, the rule interface declares 10 rule methods, the XML script externalizes all estimation policies and thresholds, the Service assembles the replenishment pipeline and notes, and JUnit regresses with 6 replenishment profiles.
| Layer | Responsibility | Files |
|---|---|---|
| Domain model | SKU replenishment parameters, replenishment decision result | ReplenishmentItem.java, ReplenishDecision.java |
| Rule set & XML | 10 methods covering demand/safety stock/target stock, MOQ/case-size/cap, stock days/urgency | InventoryRules.java, inventory.xml |
| Orchestration service | Replenishment pipeline assembly, cap split-order prompt, sufficient-stock short-circuit, note assembly | ReplenishmentService.java |
| Tests | 6 replenishment profiles (urgent / sufficient / promotion peak / off-season rounding / medium urgent / cap) | ReplenishmentServiceTest.java |
Rule method breakdown
Section titled “Rule method breakdown”| Rule method | Policy | Output |
|---|---|---|
demandDuringLeadTime |
Average daily sales × lead time days | Lead-time demand |
safetyStockLevel |
Base safety stock; +avgDaily×5 during promotion; +avgDaily×3 when fulfillment <0.9 |
Dynamic safety stock |
targetStock |
Safety stock + lead-time demand; PEAK +avgDaily×7; OFF -avgDaily×3; non-negative | Target stock |
shortage |
Target stock - current stock, non-negative | Shortage |
adjustToMoq |
Order quantity above 0 and below MOQ raised to MOQ | Post-MOQ quantity |
roundToPack |
Round up to case pack size (implemented with a while-loop inside the script) | Case-size quantity |
stockDays |
Current stock / average daily sales; returns 999 when avgDailySales ≤0 to prevent division by zero | Stock days |
urgency |
≤lead time CRITICAL / ≤lead time+3 HIGH / ≤lead time+7 MEDIUM / otherwise LOW | CRITICAL/HIGH/MEDIUM/LOW |
suggestExpress |
Stock days ≤ lead time, or peak season and ≤lead time+3 → expedite | true / false |
cappedQty |
Order quantity exceeding the single-order cap is capped | Final order quantity |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the six files into a Maven project following the repository’s original package structure: the two models go into src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test go into the com.github.paohaijiao.demo package, the orchestration service goes into com.github.paohaijiao.demo.service, and the XML script goes into src/test/resources/demo/inventory.xml; keep all package names exactly as in the source. Run mvn test -Dtest=ReplenishmentServiceTest at the project root to execute.
ReplenishmentItem.java
Section titled “ReplenishmentItem.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 补货决策要素(零售补货所需的商品、库存与供应参数) */@Data@NoArgsConstructor@AllArgsConstructorpublic class ReplenishmentItem {
/** * 商品编码 */ private String sku;
/** * 商品名称 */ private String skuName;
/** * 仓库 */ private String warehouse;
/** * 当前库存 */ private int currentStock;
/** * 基础安全库存 */ private int safetyStock;
/** * 日均销量 */ private int avgDailySales;
/** * 补货提前期(天) */ private int leadTimeDays;
/** * 最小起订量 */ private int moq;
/** * 包装规格(整箱数量) */ private int packSize;
/** * 供应商履约率(0-1) */ private double supplierReliability;
/** * 是否促销期 */ private boolean promotion;
/** * 季节系数:PEAK-旺季 / NORMAL-平季 / OFF-淡季 */ private String seasonFactor;
/** * 单次最大下单量 */ private int maxOrder;}ReplenishDecision.java
Section titled “ReplenishDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 补货决策结果 */@Datapublic class ReplenishDecision {
/** * 商品编码 */ private String sku;
/** * 补货提前期需求 */ private int demandDuringLeadTime;
/** * 动态安全库存 */ private int safetyStockLevel;
/** * 目标库存 */ private int targetStock;
/** * 库存缺口 */ private int shortage;
/** * 最终建议下单量(已考虑 MOQ、包装与上限) */ private int orderQty;
/** * 可售天数 */ private int stockDays;
/** * 紧急度:CRITICAL / HIGH / MEDIUM / LOW */ private String urgency;
/** * 是否建议加急补货 */ private boolean suggestExpress;
/** * 说明与建议 */ private List<String> notes = new ArrayList<String>();
public void addNote(String note) { this.notes.add(note); }}InventoryRules.java
Section titled “InventoryRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 7(生产级样板):零售库存补货规则集 * <p> * 规则按「需求测算 → 缺口计算 → 下单约束 → 紧急度」拆分: * <ul> * <li>测算类:提前期需求、动态安全库存、目标库存</li> * <li>约束类:MOQ、包装规格取整(规则内演示 while 循环)、单次上限</li> * <li>决策类:可售天数、紧急度、是否加急</li> * </ul> * 规则脚本见 {@code src/test/resources/demo/inventory.xml}。 */public interface InventoryRules {
/** * 补货提前期需求 = 日均销量 × 提前期 */ int demandDuringLeadTime(@Param("avgDailySales") int avgDailySales, @Param("leadTimeDays") int leadTimeDays);
/** * 动态安全库存:促销期与低履约率供应商需要更高安全库存 */ int safetyStockLevel(@Param("safetyStock") int safetyStock, @Param("avgDailySales") int avgDailySales, @Param("supplierReliability") double supplierReliability, @Param("promotion") boolean promotion);
/** * 目标库存:安全库存 + 提前期需求,并按季节系数修正 */ int targetStock(@Param("safetyStock") int safetyStock, @Param("demandDuringLeadTime") int demandDuringLeadTime, @Param("seasonFactor") String seasonFactor, @Param("avgDailySales") int avgDailySales);
/** * 库存缺口 */ int shortage(@Param("targetStock") int targetStock, @Param("currentStock") int currentStock);
/** * 最小起订量约束 */ int adjustToMoq(@Param("qty") int qty, @Param("moq") int moq);
/** * 包装规格向上取整(while 循环示例) */ int roundToPack(@Param("qty") int qty, @Param("packSize") int packSize);
/** * 可售天数 */ int stockDays(@Param("currentStock") int currentStock, @Param("avgDailySales") int avgDailySales);
/** * 紧急度:CRITICAL / HIGH / MEDIUM / LOW */ String urgency(@Param("stockDays") int stockDays, @Param("leadTimeDays") int leadTimeDays);
/** * 是否建议加急补货 */ boolean suggestExpress(@Param("stockDays") int stockDays, @Param("leadTimeDays") int leadTimeDays, @Param("seasonFactor") String seasonFactor);
/** * 单次下单量上限 */ int cappedQty(@Param("qty") int qty, @Param("maxOrder") int maxOrder);}inventory.xml
Section titled “inventory.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.InventoryRules">
<!-- 提前期需求 --> <java name="demandDuringLeadTime" returnClass="int"> <![CDATA[ int def demandDuringLeadTime(int:avgDailySales, int:leadTimeDays) { return avgDailySales * leadTimeDays; } ]]> </java>
<!-- 动态安全库存 --> <java name="safetyStockLevel" returnClass="int"> <![CDATA[ int def safetyStockLevel(int:safetyStock, int:avgDailySales, double:supplierReliability, boolean:promotion) { int level = safetyStock; if (promotion) { level = level + avgDailySales * 5; } if (supplierReliability < 0.9) { level = level + avgDailySales * 3; } return level; } ]]> </java>
<!-- 目标库存 --> <java name="targetStock" returnClass="int"> <![CDATA[ int def targetStock(int:safetyStock, int:demandDuringLeadTime, str:seasonFactor, int:avgDailySales) { int target = safetyStock + demandDuringLeadTime; if (seasonFactor == "PEAK") { target = target + avgDailySales * 7; } else if (seasonFactor == "OFF") { target = target - avgDailySales * 3; } if (target < 0) { target = 0; } return target; } ]]> </java>
<!-- 库存缺口 --> <java name="shortage" returnClass="int"> <![CDATA[ int def shortage(int:targetStock, int:currentStock) { int gap = targetStock - currentStock; if (gap < 0) { gap = 0; } return gap; } ]]> </java>
<!-- 最小起订量约束 --> <java name="adjustToMoq" returnClass="int"> <![CDATA[ int def adjustToMoq(int:qty, int:moq) { int result = qty; if (qty > 0 && qty < moq) { result = moq; } return result; } ]]> </java>
<!-- 包装规格向上取整:while 循环示例 --> <java name="roundToPack" returnClass="int"> <![CDATA[ int def roundToPack(int:qty, int:packSize) { if (qty <= 0) { return 0; } if (packSize <= 1) { return qty; } int rounded = 0; while (rounded < qty) { rounded = rounded + packSize; } return rounded; } ]]> </java>
<!-- 可售天数 --> <java name="stockDays" returnClass="int"> <![CDATA[ int def stockDays(int:currentStock, int:avgDailySales) { if (avgDailySales <= 0) { return 999; } return currentStock / avgDailySales; } ]]> </java>
<!-- 紧急度 --> <java name="urgency" returnClass="java.lang.String"> <![CDATA[ str def urgency(int:stockDays, int:leadTimeDays) { if (stockDays <= leadTimeDays) { return "CRITICAL"; } else if (stockDays <= leadTimeDays + 3) { return "HIGH"; } else if (stockDays <= leadTimeDays + 7) { return "MEDIUM"; } else { return "LOW"; } } ]]> </java>
<!-- 是否加急补货 --> <java name="suggestExpress" returnClass="boolean"> <![CDATA[ boolean def suggestExpress(int:stockDays, int:leadTimeDays, str:seasonFactor) { if (stockDays <= leadTimeDays) { return true; } else if (seasonFactor == "PEAK" && stockDays <= leadTimeDays + 3) { return true; } else { return false; } } ]]> </java>
<!-- 单次下单量上限 --> <java name="cappedQty" returnClass="int"> <![CDATA[ int def cappedQty(int:qty, int:maxOrder) { int result = qty; if (result > maxOrder) { result = maxOrder; } return result; } ]]> </java>
</javas>ReplenishmentService.java
Section titled “ReplenishmentService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.InventoryRules;import com.github.paohaijiao.demo.model.ReplenishDecision;import com.github.paohaijiao.demo.model.ReplenishmentItem;
/** * 场景 7:零售库存补货建议服务 * <p> * 职责划分: * <ul> * <li><b>Java 侧</b>:链路串联(需求 → 安全库存 → 目标 → 缺口 → 约束 → 上限)、建议组装</li> * <li><b>规则侧</b>:销量测算口径、安全库存策略、季节系数、取整规则、紧急度阈值,全部外置在 XML 中</li> * </ul> */public class ReplenishmentService {
private final InventoryRules rules;
public ReplenishmentService(InventoryRules rules) { this.rules = rules; }
/** * 计算补货建议 * * @param item 商品与库存快照 * @return 补货决策 */ public ReplenishDecision replenish(ReplenishmentItem item) { ReplenishDecision decision = new ReplenishDecision(); decision.setSku(item.getSku());
int ddlt = rules.demandDuringLeadTime(item.getAvgDailySales(), item.getLeadTimeDays()); int safety = rules.safetyStockLevel(item.getSafetyStock(), item.getAvgDailySales(), item.getSupplierReliability(), item.isPromotion()); int target = rules.targetStock(safety, ddlt, item.getSeasonFactor(), item.getAvgDailySales()); int shortage = rules.shortage(target, item.getCurrentStock()); int roundedQty = rules.roundToPack(rules.adjustToMoq(shortage, item.getMoq()), item.getPackSize()); int orderQty = rules.cappedQty(roundedQty, item.getMaxOrder()); int stockDays = rules.stockDays(item.getCurrentStock(), item.getAvgDailySales()); String urgency = rules.urgency(stockDays, item.getLeadTimeDays());
decision.setDemandDuringLeadTime(ddlt); decision.setSafetyStockLevel(safety); decision.setTargetStock(target); decision.setShortage(shortage); decision.setOrderQty(orderQty); decision.setStockDays(stockDays); decision.setUrgency(urgency); decision.setSuggestExpress(rules.suggestExpress(stockDays, item.getLeadTimeDays(), item.getSeasonFactor()));
buildNotes(item, decision, roundedQty, orderQty); return decision; }
private void buildNotes(ReplenishmentItem item, ReplenishDecision decision, int roundedQty, int orderQty) { if (decision.getShortage() == 0) { decision.addNote("库存充足,本次无需补货"); return; } if (roundedQty > decision.getShortage()) { decision.addNote("按 MOQ/整箱包装取整后下单量高于库存缺口,属正常约束"); } if (orderQty < roundedQty) { decision.addNote("单次下单量受上限约束封顶为 " + orderQty + ",超出部分建议拆单"); } if (decision.getShortage() < item.getMoq()) { decision.addNote("缺口低于最小起订量,按 MOQ 下单"); } if (item.isPromotion()) { decision.addNote("促销期需求上浮,安全库存已自动抬升"); } if (item.getSupplierReliability() < 0.9) { decision.addNote("供应商履约率 " + item.getSupplierReliability() + " 偏低,安全库存已上调"); } if ("PEAK".equals(item.getSeasonFactor())) { decision.addNote("旺季备货,目标库存上调 7 天销量"); } if (decision.isSuggestExpress()) { decision.addNote("可售天数不足提前期,建议加急补货"); } }}ReplenishmentServiceTest.java
Section titled “ReplenishmentServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.ReplenishDecision;import com.github.paohaijiao.demo.model.ReplenishmentItem;import com.github.paohaijiao.demo.service.ReplenishmentService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 7 生产级样板:零售库存补货建议 * <p> * 覆盖 6 组补货形态:紧急缺货、库存充足、促销旺季、淡季整箱取整、 * 中等紧急、单次上限封顶。 */public class ReplenishmentServiceTest {
private ReplenishmentService service;
@Before public void setUp() { InventoryRules rules = JQuickJava.create() .createApi(InventoryRules.class, "demo/inventory.xml"); this.service = new ReplenishmentService(rules); }
/** * 紧急缺货:库存 20、日均 30、供应商履约率 0.8 → 目标 290,紧急 CRITICAL */ @Test public void shouldUrgentlyReplenishShortage() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7001"); item.setSkuName("矿泉水 550ml"); item.setWarehouse("华东仓"); item.setCurrentStock(20); item.setSafetyStock(50); item.setAvgDailySales(30); item.setLeadTimeDays(5); item.setMoq(50); item.setPackSize(10); item.setSupplierReliability(0.8); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(150, decision.getDemandDuringLeadTime()); Assert.assertEquals(140, decision.getSafetyStockLevel()); Assert.assertEquals(290, decision.getTargetStock()); Assert.assertEquals(270, decision.getShortage()); Assert.assertEquals(270, decision.getOrderQty()); Assert.assertEquals(0, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.isSuggestExpress()); }
/** * 库存充足:库存 500、目标 200 → 无需补货,紧急度 LOW */ @Test public void shouldSkipWhenStockSufficient() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7002"); item.setSkuName("卷纸"); item.setWarehouse("华北仓"); item.setCurrentStock(500); item.setSafetyStock(50); item.setAvgDailySales(30); item.setLeadTimeDays(5); item.setMoq(50); item.setPackSize(10); item.setSupplierReliability(0.95); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(150, decision.getDemandDuringLeadTime()); Assert.assertEquals(50, decision.getSafetyStockLevel()); Assert.assertEquals(200, decision.getTargetStock()); Assert.assertEquals(0, decision.getShortage()); Assert.assertEquals(0, decision.getOrderQty()); Assert.assertEquals(16, decision.getStockDays()); Assert.assertEquals("LOW", decision.getUrgency()); Assert.assertFalse(decision.isSuggestExpress()); Assert.assertTrue(decision.getNotes().contains("库存充足,本次无需补货")); }
/** * 促销旺季:低履约 + 促销 + 旺季,缺口 390 但 MOQ 500 → 按 MOQ 下单 */ @Test public void shouldBoostSafetyStockInPromotionPeak() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7003"); item.setSkuName("空调扇"); item.setWarehouse("华南仓"); item.setCurrentStock(100); item.setSafetyStock(50); item.setAvgDailySales(20); item.setLeadTimeDays(7); item.setMoq(500); item.setPackSize(100); item.setSupplierReliability(0.85); item.setPromotion(true); item.setSeasonFactor("PEAK"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(140, decision.getDemandDuringLeadTime()); Assert.assertEquals(210, decision.getSafetyStockLevel()); Assert.assertEquals(490, decision.getTargetStock()); Assert.assertEquals(390, decision.getShortage()); Assert.assertEquals(500, decision.getOrderQty()); Assert.assertEquals(5, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.getNotes().contains("促销期需求上浮,安全库存已自动抬升")); }
/** * 淡季整箱取整:目标 110、缺口 50、包装 20 → 取整为 60 */ @Test public void shouldRoundUpToPackSize() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7004"); item.setSkuName("暖宝宝"); item.setWarehouse("华北仓"); item.setCurrentStock(60); item.setSafetyStock(40); item.setAvgDailySales(10); item.setLeadTimeDays(10); item.setMoq(20); item.setPackSize(20); item.setSupplierReliability(0.98); item.setPromotion(false); item.setSeasonFactor("OFF"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(100, decision.getDemandDuringLeadTime()); Assert.assertEquals(110, decision.getTargetStock()); Assert.assertEquals(50, decision.getShortage()); Assert.assertEquals(60, decision.getOrderQty()); Assert.assertEquals(6, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); }
/** * 中等紧急:可售 6 天、提前期 3 天 → HIGH(提前期 +3 天内) */ @Test public void shouldMarkHighUrgency() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7005"); item.setSkuName("咖啡豆"); item.setWarehouse("华东仓"); item.setCurrentStock(30); item.setSafetyStock(40); item.setAvgDailySales(5); item.setLeadTimeDays(3); item.setMoq(10); item.setPackSize(5); item.setSupplierReliability(0.95); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(1000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(15, decision.getDemandDuringLeadTime()); Assert.assertEquals(55, decision.getTargetStock()); Assert.assertEquals(25, decision.getShortage()); Assert.assertEquals(25, decision.getOrderQty()); Assert.assertEquals(6, decision.getStockDays()); Assert.assertEquals("HIGH", decision.getUrgency()); Assert.assertFalse(decision.isSuggestExpress()); }
/** * 单次上限封顶:缺口 2085、整箱取整 2100,但单次上限 1000 → 下单 1000 */ @Test public void shouldCapOrderQuantity() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7006"); item.setSkuName("爆款零食"); item.setWarehouse("华中仓"); item.setCurrentStock(15); item.setSafetyStock(100); item.setAvgDailySales(400); item.setLeadTimeDays(5); item.setMoq(100); item.setPackSize(100); item.setSupplierReliability(0.9); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(1000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(2000, decision.getDemandDuringLeadTime()); Assert.assertEquals(100, decision.getSafetyStockLevel()); Assert.assertEquals(2100, decision.getTargetStock()); Assert.assertEquals(2085, decision.getShortage()); Assert.assertEquals(1000, decision.getOrderQty()); Assert.assertEquals(0, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.getNotes().contains("按 MOQ/整箱包装取整后下单量高于库存缺口,属正常约束")); }}Run & verified results
Section titled “Run & verified results”Run at the project root:
mvn test -Dtest=ReplenishmentServiceTestTest coverage
Section titled “Test coverage”| Test method | Business profile constructed | Key assertions / expected decision |
|---|---|---|
shouldUrgentlyReplenishShortage |
Bottled water: stock 20, avgDaily 30, lead time 5 days, fulfillment 0.8 | Demand 150, safety stock 140, target 290, shortage 270, order 270, stock days 0, CRITICAL, expedite true |
shouldSkipWhenStockSufficient |
Toilet paper: stock 500, fulfillment 0.95 | Target 200, shortage 0, order 0, stock days 16, LOW, no expedite, includes “库存充足,本次无需补货” note |
shouldBoostSafetyStockInPromotionPeak |
Air cooler fan: promotion + peak + fulfillment 0.85, MOQ 500 | Demand 140, safety stock 210, target 490, shortage 390, order 500 (raised to MOQ), stock days 5, CRITICAL |
shouldRoundUpToPackSize |
Heat patch: off-season OFF, shortage 50, pack size 20 | Demand 100, target 110, shortage 50, order 60 after case rounding, stock days 6, CRITICAL |
shouldMarkHighUrgency |
Coffee beans: stock 30, avgDaily 5, lead time 3 days | Demand 15, target 55, shortage 25, order 25, stock days 6, HIGH, no expedite |
shouldCapOrderQuantity |
Hot snack: avgDaily 400, shortage 2085, case-round 2100, single cap 1000 | Demand 2000, target 2100, order 1000 (capped), stock days 0, CRITICAL, includes rounding-constraint note |
Verified output
Section titled “Verified output”Two verified outputs recorded in USE-CASES.md Scenario 7:
Urgent shortage (current stock 20, target stock 290, shortage 270):
目标库存: 290 缺口: 270 紧急度: CRITICAL 建议加急: truePromotion peak large order (target 490, shortage 390, constrained by MOQ):
目标库存: 490 缺口: 390 下单量: 500(按 MOQ 上调)Technical highlights
Section titled “Technical highlights”- Safety stock adjustment days (5/3), peak/off-season adjustments (+7/-3), MOQ, case size, single cap, and urgency thresholds are all externalized in XML; seasonal and supplier changes only require editing the script, with no redeployment.
roundToPackuses a while-loop inside the rule script to implement case-size rounding up, with boundary guards for quantity ≤0 and pack size ≤1, demonstrating the script’s loop expressiveness.- The Service assembles the pipeline in a chain of “demand → safety stock → target → shortage → MOQ → case-size → cap”, short-circuits with a note when stock is sufficient, and outputs a split-order prompt when capped, making the constraint order clear at a glance.
stockDayscalculates stock days using integer division; for items with avgDailySales ≤0 it returns 999 days to avoid division by zero; shortage and target stock are uniformly guarded against negatives in the script.- The rule interface binds parameters by name via
@Param(e.g.targetStockwith 4 inputs), and the proxy instance can be reused in batch replenishment tasks after creation, backed by an ASM call chain. - Runs on Java 8 with no heavy rule platform dependency, and can be embedded directly into WMS/ERP systems on JDK 8 and IT-innovation environments, with no extra process overhead for high-frequency SKU decisions.