Skip to content

Case 30: Presale final payment eligibility

This case corresponds to scenario 30 “Presale final payment eligibility / fulfillment-condition check” in the JQuick-Java repository USE-CASES, in the e-commerce retail domain. When a presale order enters the final-payment stage, the system makes one comprehensive adjudication: first verifying the merchant’s presale fulfillment eligibility, then, based on the final-payment window state, the buyer’s real-name status, and the available spot stock after deducting locked inventory, deciding whether the order is payable, waiting, on hold, cancelled, deposit forfeited, or closed with a refund.

A presale transaction has two stages: deposit and final payment. The buyer pays a deposit first to lock goods, and the merchant opens the final-payment window before shipping at the agreed time. When the buyer clicks “pay the final payment”, the platform must simultaneously control risk on both the merchant side and the buyer side: when a merchant’s credit deteriorates (credit score below 80, historical breaches, performance bond unpaid), the presale must be suspended and deposits refunded; on the buyer side there are states such as window not yet open, overdue without topping up, not real-name verified, and spot stock occupied by locked inventory.

Merchant eligibility is enclosed in a single three-condition boolean rule: credit score of at least 80, zero historical presale breaches, and performance bond paid — all three must hold simultaneously. If any fails, the order action is set to BLOCKED, the deposit is refunded via the original route (REFUNDABLE), and reasons are listed item by item for each unsatisfied condition. The buyer-side state machine revolves around the final-payment window status code: 0 not open, 1 in progress, 2 ended (overdue). Cancelling proactively before the window opens is CANCELLED with the deposit fully refunded; still not topping up after the window ends is FORFEIT with the deposit confiscated per the agreement; window not open is WAITING with the deposit held by the platform (HOLD); window in progress but the buyer is not real-name verified is HOLD, and payment becomes possible after completing verification.

The stock caliber is computed independently: available spot stock = current spot inventory − inventory already locked for presale. With the window in progress and the buyer verified, if the available spot stock is less than this order’s required quantity, the order queues in final-payment order waiting for transfer and replenishment (WAITING, deposit as guarantee offset DEDUCT); only when spot stock is sufficient is it set to PAYABLE, with the payable final amount taken from the agreed final-payment amount and the deposit serving as a fulfillment guarantee.

The decision output is PresaleFinalPaymentDecision: six actions (PAYABLE/WAITING/CANCELLED/FORFEIT/HOLD/BLOCKED), the payable final amount this time, the deposit treatment (DEDUCT/REFUNDABLE/FORFEIT/HOLD), the available spot stock after deducting locks, the merchant-eligibility conclusion, and itemized review explanations (reasons), shared by the order center, cashier desk and customer-service systems.

  • Presale rules are constrained by platform agreements and regulatory requirements and adjust per promotion (credit thresholds, bond policies, window durations). Hard-coding eligibility thresholds in the order center means every adjustment must wait for a release window.
  • One final payment chains four independent calibers — merchant eligibility, window state, real-name status, and inventory — with many state combinations. Written in one blob of if-else, it is easy to miss priorities such as “an ineligible merchant must close the order and refund the deposit even while the window is in progress”.
  • The deposit carries financial consequences of confiscation (FORFEIT) and refund (REFUNDABLE); the mapping from window status codes 0/1/2 to business dispositions must be auditable and explainable to buyers.
  • Spot inventory must deduct the presale-locked quantity before comparing with the required quantity, including the “just enough” boundary case. Coupling the inventory caliber with the eligibility caliber makes replenishment logic hard to maintain independently.
  • Merchant ineligibility may hit multiple conditions at once — insufficient credit, breaches, unpaid bond. The adjudication needs to list reasons item by item rather than a blanket rejection, to facilitate merchant appeals and platform evidence.
  • Legacy e-commerce order centers and cashier desks mostly run on JDK 8 and domestic-innovation JDKs, so heavy decision middleware and standalone services cannot be introduced for a single presale stage.

This case uses the six-part structure “input model + decision model + rule interface + XML rule script + Service orchestration + JUnit tests”: the four rule methods — merchant eligibility, spot-stock caliber, deposit treatment, final-payment condition — are each independent and externalized in XML, and the Service orchestrates the short-circuit chain by business priority and assembles reason copy.

Layer Responsibility Files
Domain model layer Carries presale-order final-payment and merchant-fulfillment elements; outputs action, deposit treatment, spot stock remaining and explanations PresaleOrder.java, PresaleFinalPaymentDecision.java
Rule interface layer Declares four methods — merchant eligibility, available stock, deposit treatment, final-payment condition — bound by name via @Param PresaleFinalPaymentRules.java
Rule script layer Credit threshold, inventory delta, window-status mapping and other calibers externalized; supports hot update presale-final-payment.xml
Orchestration & test layer Eligibility → cancellation → overdue → not-open → real-name → stock → pass short-circuit chain with reason assembly; regression over 6 profiles PresaleFinalPaymentService.java, PresaleFinalPaymentServiceTest.java
Rule method Caliber Score/output
merchantEligible Credit score ≥ 80, zero breaches, bond paid — all three must hold for presale eligibility true/false
availableStock Current spot inventory minus inventory already locked for presale Available spot quantity
depositTreatment Cancelled before window = REFUNDABLE; window overdue (2) = FORFEIT; in progress (1) verified = DEDUCT, unverified = HOLD; not open (0) = HOLD REFUNDABLE/FORFEIT/DEDUCT/HOLD
canPayBalance Final payment is allowed only when the window is in progress (status 1) and the buyer is real-name verified true/false

Place the following files into the JQuick-Java test project under their original packages: the two models into src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test class into src/test/java/com/github/paohaijiao/demo/, the Service into src/test/java/com/github/paohaijiao/demo/service/, and the XML script into src/test/resources/demo/. Run mvn test -Dtest=PresaleFinalPaymentServiceTest 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 PresaleOrder {
/**
* 订单号
*/
private String orderId;
/**
* 商品编码
*/
private String sku;
/**
* 已付定金(元)
*/
private double depositAmount;
/**
* 应付尾款(元)
*/
private double balanceAmount;
/**
* 尾款窗口状态:0 未开启 / 1 进行中 / 2 已结束(逾期)
*/
private int windowStatus;
/**
* 是否在窗口开启前主动取消
*/
private boolean cancelledBeforeWindow;
/**
* 本单购买件数
*/
private int demandQty;
/**
* 当前现货库存
*/
private int stockOnHand;
/**
* 已为预售锁定的库存
*/
private int reservedStock;
/**
* 买家是否已实名认证
*/
private boolean identityVerified;
/**
* 商家信用分(0-100)
*/
private int merchantCreditScore;
/**
* 商家历史预售违约次数
*/
private int merchantBreaches;
/**
* 商家是否已缴纳履约保证金
*/
private boolean merchantBondPaid;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 预售尾款资格与履约决策
*/
@Data
public class PresaleFinalPaymentDecision {
/**
* 订单号
*/
private String orderId;
/**
* 处置动作:PAYABLE 可付尾款 / WAITING 待窗口或待库存 / CANCELLED 已取消 /
* FORFEIT 定金没收 / HOLD 待实名 / BLOCKED 商家失格关单
*/
private String action;
/**
* 本次应支付尾款
*/
private double payableAmount;
/**
* 定金处理:DEDUCT 担保抵扣 / REFUNDABLE 可退 / FORFEIT 没收 / HOLD 暂挂
*/
private String depositTreatment;
/**
* 扣减锁定库存后的可售现货
*/
private int availableStock;
/**
* 商家是否具备预售履约资格
*/
private boolean merchantEligible;
/**
* 审核说明
*/
private List<String> reasons = new ArrayList<String>();
public void addReason(String reason) {
this.reasons.add(reason);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 30:预售尾款支付资格与商家履约规则
*/
public interface PresaleFinalPaymentRules {
/**
* 商家预售资格:信用分 ≥80、无违约记录、已缴保证金
*/
boolean merchantEligible(@Param("creditScore") int creditScore,
@Param("breaches") int breaches,
@Param("bondPaid") boolean bondPaid);
/**
* 扣减锁定库存后的可售现货
*/
int availableStock(@Param("stockOnHand") int stockOnHand,
@Param("reservedStock") int reservedStock);
/**
* 定金处理方式
* windowStatus:0 未开启 / 1 进行中 / 2 逾期
*/
String depositTreatment(@Param("windowStatus") int windowStatus,
@Param("identity") boolean identity,
@Param("cancelledBefore") boolean cancelledBefore);
/**
* 当前是否满足尾款支付条件(窗口进行中且已实名)
*/
boolean canPayBalance(@Param("windowStatus") int windowStatus,
@Param("identity") boolean identity);
}
<?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.PresaleFinalPaymentRules">
<java name="merchantEligible" returnClass="boolean">
<![CDATA[
boolean def merchantEligible(int:creditScore, int:breaches, boolean:bondPaid) {
if (creditScore >= 80) {
if (breaches == 0) {
if (bondPaid) {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
]]>
</java>
<java name="availableStock" returnClass="int">
<![CDATA[
int def availableStock(int:stockOnHand, int:reservedStock) {
return stockOnHand - reservedStock;
}
]]>
</java>
<java name="depositTreatment" returnClass="java.lang.String">
<![CDATA[
str def depositTreatment(int:windowStatus, boolean:identity, boolean:cancelledBefore) {
if (cancelledBefore) {
return "REFUNDABLE";
} else if (windowStatus == 2) {
return "FORFEIT";
} else if (windowStatus == 1) {
if (identity) {
return "DEDUCT";
} else {
return "HOLD";
}
} else {
return "HOLD";
}
}
]]>
</java>
<java name="canPayBalance" returnClass="boolean">
<![CDATA[
boolean def canPayBalance(int:windowStatus, boolean:identity) {
if (windowStatus == 1) {
if (identity) {
return true;
} else {
return false;
}
} else {
return false;
}
}
]]>
</java>
</javas>
package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.PresaleFinalPaymentRules;
import com.github.paohaijiao.demo.model.PresaleFinalPaymentDecision;
import com.github.paohaijiao.demo.model.PresaleOrder;
/**
* 场景 30:预售尾款资格、定金处置与商家履约校验
*/
public class PresaleFinalPaymentService {
private final PresaleFinalPaymentRules rules;
public PresaleFinalPaymentService(PresaleFinalPaymentRules rules) {
this.rules = rules;
}
public PresaleFinalPaymentDecision process(PresaleOrder order) {
PresaleFinalPaymentDecision d = new PresaleFinalPaymentDecision();
d.setOrderId(order.getOrderId());
int available = rules.availableStock(order.getStockOnHand(), order.getReservedStock());
d.setAvailableStock(available);
boolean eligible = rules.merchantEligible(order.getMerchantCreditScore(),
order.getMerchantBreaches(), order.isMerchantBondPaid());
d.setMerchantEligible(eligible);
if (!eligible) {
d.setAction("BLOCKED");
d.setPayableAmount(0.0);
d.setDepositTreatment("REFUNDABLE");
if (order.getMerchantCreditScore() < 80) {
d.addReason("商家信用分 " + order.getMerchantCreditScore() + " 低于 80 分预售门槛");
}
if (order.getMerchantBreaches() >= 1) {
d.addReason("商家存在 " + order.getMerchantBreaches() + " 次历史预售违约记录");
}
if (!order.isMerchantBondPaid()) {
d.addReason("商家未缴纳履约保证金");
}
d.addReason("商家预售资格中止,订单关闭,定金原路退还买家");
return d;
}
String treatment = rules.depositTreatment(order.getWindowStatus(),
order.isIdentityVerified(), order.isCancelledBeforeWindow());
if (order.isCancelledBeforeWindow()) {
d.setAction("CANCELLED");
d.setPayableAmount(0.0);
d.setDepositTreatment("REFUNDABLE");
d.addReason("尾款窗口开启前取消订单,定金全额退还");
return d;
}
if (order.getWindowStatus() == 2) {
d.setAction("FORFEIT");
d.setPayableAmount(0.0);
d.setDepositTreatment("FORFEIT");
d.addReason("尾款支付窗口已关闭且未补款,定金按预售协议不予退还");
return d;
}
if (order.getWindowStatus() == 0) {
d.setAction("WAITING");
d.setPayableAmount(0.0);
d.setDepositTreatment("HOLD");
d.addReason("尾款支付窗口尚未开启,定金由平台暂存");
return d;
}
if (!rules.canPayBalance(order.getWindowStatus(), order.isIdentityVerified())) {
d.setAction("HOLD");
d.setPayableAmount(0.0);
d.setDepositTreatment("HOLD");
d.addReason("买家尚未完成实名认证,补全实名后即可支付尾款");
return d;
}
if (available < order.getDemandQty()) {
d.setAction("WAITING");
d.setPayableAmount(0.0);
d.setDepositTreatment("DEDUCT");
d.addReason("扣减预售锁定库存后仅剩 " + available
+ " 件现货,不足本单 " + order.getDemandQty()
+ " 件需求,按尾款支付顺序排队等待调拨补货");
return d;
}
d.setAction("PAYABLE");
d.setPayableAmount(order.getBalanceAmount());
d.setDepositTreatment(treatment);
d.addReason("尾款窗口进行中且库存充足,应支付尾款 "
+ order.getBalanceAmount() + " 元,定金作履约担保");
return d;
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.PresaleFinalPaymentDecision;
import com.github.paohaijiao.demo.model.PresaleOrder;
import com.github.paohaijiao.demo.service.PresaleFinalPaymentService;
import com.github.paohaijiao.xml.JQuickJava;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 场景 30:预售尾款资格与履约条件
* <p>
* 6 组形态:正常支付、窗口前取消退定金、逾期定金没收、库存不足排队与临界满足、
* 商家失格三种情形关单、未实名暂挂与补全放行。
*/
public class PresaleFinalPaymentServiceTest {
private PresaleFinalPaymentService service;
@Before
public void setUp() {
PresaleFinalPaymentRules rules = JQuickJava.create()
.createApi(PresaleFinalPaymentRules.class, "demo/presale-final-payment.xml");
this.service = new PresaleFinalPaymentService(rules);
}
/**
* 窗口进行中、实名、现货充足 → 支付 800 元尾款,定金担保
*/
@Test
public void shouldApproveFinalPayment() {
PresaleOrder o = base("PRE001");
o.setDepositAmount(200);
o.setBalanceAmount(800);
o.setWindowStatus(1);
o.setDemandQty(2);
o.setStockOnHand(100);
o.setReservedStock(30);
o.setIdentityVerified(true);
PresaleFinalPaymentDecision d = service.process(o);
Assert.assertEquals("PAYABLE", d.getAction());
Assert.assertEquals(800.0, d.getPayableAmount(), 0.0001);
Assert.assertEquals("DEDUCT", d.getDepositTreatment());
Assert.assertEquals(70, d.getAvailableStock());
Assert.assertTrue(d.isMerchantEligible());
}
/**
* 窗口未开启即取消 → 定金可退
*/
@Test
public void shouldRefundDepositWhenCancelledBeforeWindow() {
PresaleOrder o = base("PRE002");
o.setDepositAmount(200);
o.setWindowStatus(0);
o.setCancelledBeforeWindow(true);
PresaleFinalPaymentDecision d = service.process(o);
Assert.assertEquals("CANCELLED", d.getAction());
Assert.assertEquals(0.0, d.getPayableAmount(), 0.0001);
Assert.assertEquals("REFUNDABLE", d.getDepositTreatment());
Assert.assertTrue(d.getReasons().contains("尾款窗口开启前取消订单,定金全额退还"));
}
/**
* 窗口结束仍未付尾款 → 定金没收
*/
@Test
public void shouldForfeitDepositAfterWindowClosed() {
PresaleOrder o = base("PRE003");
o.setDepositAmount(200);
o.setWindowStatus(2);
o.setIdentityVerified(true);
PresaleFinalPaymentDecision d = service.process(o);
Assert.assertEquals("FORFEIT", d.getAction());
Assert.assertEquals("FORFEIT", d.getDepositTreatment());
Assert.assertEquals(0.0, d.getPayableAmount(), 0.0001);
Assert.assertTrue(d.getReasons().get(0).contains("定金按预售协议不予退还"));
}
/**
* 现货仅 1 件不足 5 件 → 排队;补库后恰好 5 件临界 → 放行
*/
@Test
public void shouldWaitWhenStockShortThenApproveAtExactAvailable() {
PresaleOrder o = base("PRE004");
o.setBalanceAmount(800);
o.setWindowStatus(1);
o.setIdentityVerified(true);
o.setDemandQty(5);
o.setStockOnHand(50);
o.setReservedStock(49);
PresaleFinalPaymentDecision d1 = service.process(o);
Assert.assertEquals("WAITING", d1.getAction());
Assert.assertEquals("DEDUCT", d1.getDepositTreatment());
Assert.assertTrue(d1.getReasons().get(0).contains("仅剩 1 件现货,不足本单 5 件需求"));
o.setReservedStock(45);
PresaleFinalPaymentDecision d2 = service.process(o);
Assert.assertEquals("PAYABLE", d2.getAction());
Assert.assertEquals(5, d2.getAvailableStock());
Assert.assertEquals(800.0, d2.getPayableAmount(), 0.0001);
}
/**
* 信用不足、历史违约、未缴保证金分别导致关单退定金
*/
@Test
public void shouldBlockWhenMerchantNotEligible() {
PresaleOrder lowCredit = base("PRE005");
lowCredit.setMerchantCreditScore(75);
PresaleFinalPaymentDecision d1 = service.process(lowCredit);
Assert.assertEquals("BLOCKED", d1.getAction());
Assert.assertEquals("REFUNDABLE", d1.getDepositTreatment());
Assert.assertTrue(d1.getReasons().contains("商家信用分 75 低于 80 分预售门槛"));
PresaleOrder breached = base("PRE006");
breached.setMerchantBreaches(1);
PresaleFinalPaymentDecision d2 = service.process(breached);
Assert.assertEquals("BLOCKED", d2.getAction());
Assert.assertTrue(d2.getReasons().contains("商家存在 1 次历史预售违约记录"));
PresaleOrder noBond = base("PRE007");
noBond.setMerchantBondPaid(false);
PresaleFinalPaymentDecision d3 = service.process(noBond);
Assert.assertEquals("BLOCKED", d3.getAction());
Assert.assertTrue(d3.getReasons().contains("商家未缴纳履约保证金"));
}
/**
* 窗口内未实名 → 暂挂;完成实名后放行
*/
@Test
public void shouldHoldForIdentityThenApprove() {
PresaleOrder o = base("PRE008");
o.setBalanceAmount(800);
o.setWindowStatus(1);
o.setIdentityVerified(false);
o.setDemandQty(1);
o.setStockOnHand(10);
o.setReservedStock(0);
PresaleFinalPaymentDecision d1 = service.process(o);
Assert.assertEquals("HOLD", d1.getAction());
Assert.assertEquals("HOLD", d1.getDepositTreatment());
Assert.assertTrue(d1.getReasons().get(0).contains("尚未完成实名认证"));
o.setIdentityVerified(true);
PresaleFinalPaymentDecision d2 = service.process(o);
Assert.assertEquals("PAYABLE", d2.getAction());
Assert.assertEquals(800.0, d2.getPayableAmount(), 0.0001);
}
private PresaleOrder base(String id) {
PresaleOrder o = new PresaleOrder();
o.setOrderId(id);
o.setSku("SKU300");
o.setDepositAmount(100);
o.setBalanceAmount(900);
o.setWindowStatus(1);
o.setCancelledBeforeWindow(false);
o.setDemandQty(1);
o.setStockOnHand(100);
o.setReservedStock(0);
o.setIdentityVerified(true);
o.setMerchantCreditScore(90);
o.setMerchantBreaches(0);
o.setMerchantBondPaid(true);
return o;
}
}

Execute at the project root:

Terminal window
mvn test -Dtest=PresaleFinalPaymentServiceTest

Test coverage (6 @Test methods in total):

Test method Business profile Key assertions / expected decision
shouldApproveFinalPayment Window in progress, verified, needs 2 units, 100 spot with 30 locked, final payment 800 action=PAYABLE, payableAmount=800, depositTreatment=DEDUCT, availableStock=70, merchantEligible=true
shouldRefundDepositWhenCancelledBeforeWindow Window not open (0) and the buyer cancels proactively before the window action=CANCELLED, payable 0, deposit REFUNDABLE; reason contains “尾款窗口开启前取消订单,定金全额退还”
shouldForfeitDepositAfterWindowClosed Window ended (2), buyer still has not topped up the final payment action=FORFEIT, depositTreatment=FORFEIT, payable 0; first reason contains “定金按预售协议不予退还”
shouldWaitWhenStockShortThenApproveAtExactAvailable Needs 5 units: 50 spot with 49 locked (only 1 left) queues; lock changed to 45 (exactly 5 left) passes at the boundary d1=WAITING + DEDUCT, reason contains “仅剩 1 件现货,不足本单 5 件需求”; d2=PAYABLE, availableStock=5, payable 800
shouldBlockWhenMerchantNotEligible Three merchant ineligibility cases: credit 75, 1 breach, bond unpaid All three BLOCKED; deposits all REFUNDABLE; reasons respectively contain “商家信用分 75 低于 80 分预售门槛”, “商家存在 1 次历史预售违约记录”, “商家未缴纳履约保证金”
shouldHoldForIdentityThenApprove Window in progress but unverified, stock sufficient; rerun after completing verification First HOLD + deposit HOLD, reason contains “尚未完成实名认证”; after verification PAYABLE, payable 800
  • Eligibility and state calibers externalized with hot update: the credit-80 threshold, the mapping from window status codes to deposit treatments, and the inventory delta all live in presale-final-payment.xml. Presale-agreement adjustments only change the XML; the order center needs no release.
  • The Service short-circuit chain is the business priority: merchant ineligibility BLOCKED → pre-window cancellation CANCELLED → overdue FORFEIT → not-open WAITING → unverified HOLD → insufficient stock WAITING → PAYABLE. Seven chain segments return on first hit, each carrying a readable reason.
  • Hard veto upfront: merchant eligibility precedes all buyer-side state judgments — even with the window in progress, insufficient credit or unpaid bond directly closes the order and refunds the deposit; the risk adjudication is not diluted by subsequent conditions.
  • Inventory caliber independently testable: availableStock is a one-line “spot − locked” script, decoupled from eligibility and real-name status; the boundary assertions “queue with only 1 left, pass at exactly 5” lock down the replenishment boundary.
  • Positive returns avoid negation: boolean scripts such as merchantEligible and canPayBalance uniformly use nested if positive branches returning true with everything else returning false, combined with status-code == equality checks, without !, making the state-machine caliber intuitive and auditable.
  • ASM proxy, Java 8 with no heavy dependencies: the four rule methods execute via dynamic proxy and can be reused, with no standalone decision-service process, embeddable directly into JDK 8 legacy order centers, cashier desks and domestic-innovation environments.