Skip to content

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”.

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.

  • 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.

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 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

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.

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 补货决策要素(零售补货所需的商品、库存与供应参数)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 补货决策结果
*/
@Data
public 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);
}
}
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);
}
<?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>
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("可售天数不足提前期,建议加急补货");
}
}
}
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 at the project root:

Terminal window
mvn test -Dtest=ReplenishmentServiceTest
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

Two verified outputs recorded in USE-CASES.md Scenario 7:

Urgent shortage (current stock 20, target stock 290, shortage 270):

目标库存: 290 缺口: 270 紧急度: CRITICAL 建议加急: true

Promotion peak large order (target 490, shortage 390, constrained by MOQ):

目标库存: 490 缺口: 390 下单量: 500(按 MOQ 上调)
  • 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.
  • roundToPack uses 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.
  • stockDays calculates 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. targetStock with 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.