增加订单失败状态

This commit is contained in:
wuweihua
2026-10-14 16:50:11 +08:00
parent c1c5c6579c
commit c0c9c798b3
16 changed files with 455 additions and 28 deletions
@@ -116,6 +116,8 @@ public class PrcOrderBill extends BaseEntity
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** 计费失败状态:0成功/1未匹配产品/2未匹配国家/3未匹配重量段;软失败时金额为0且不落费用明细 */
private Integer billingFailStatus;
private String roundMode;
private List<PrcIncomeBillFee> incomeFees;
@@ -4,10 +4,8 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.bms.price.domain.PrcOrderBill;
import lombok.Data;
/** Preview matched bills before async batch add fee. */
@Data
public class BatchAddIncomeBillFeePreview implements Serializable
{
private static final long serialVersionUID = 1L;
@@ -15,4 +13,24 @@ public class BatchAddIncomeBillFeePreview implements Serializable
private int matchCount;
/** First N rows for UI preview. */
private List<PrcOrderBill> samples = new ArrayList<PrcOrderBill>();
public int getMatchCount()
{
return matchCount;
}
public void setMatchCount(int matchCount)
{
this.matchCount = matchCount;
}
public List<PrcOrderBill> getSamples()
{
return samples;
}
public void setSamples(List<PrcOrderBill> samples)
{
this.samples = samples;
}
}
@@ -103,6 +103,8 @@ public class TmsPriceQuoteResult implements Serializable
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** 计费失败状态:0成功/1未匹配产品/2未匹配国家/3未匹配重量段(仅上游同步计费软失败) */
private Integer billingFailStatus;
private Integer pieces;
private BigDecimal actualWeight;
/** Receive weight from upstream TAB_ORDER.WEIGHT */
@@ -0,0 +1,46 @@
package com.bms.price.enums;
/**
* prc_order_bill.billing_fail_status 计费失败状态。
* 0=成功;1=未匹配产品;2=未匹配国家;3=未匹配重量段。
*/
public enum BillBillingFailStatus
{
SUCCESS(0),
PRODUCT(1),
COUNTRY(2),
WEIGHT(3);
private final int code;
BillBillingFailStatus(int code)
{
this.code = code;
}
public int getCode()
{
return code;
}
public static BillBillingFailStatus of(Integer code)
{
if (code == null)
{
return SUCCESS;
}
for (BillBillingFailStatus status : values())
{
if (status.code == code.intValue())
{
return status;
}
}
return null;
}
public static boolean isFail(Integer code)
{
return code != null && code.intValue() > SUCCESS.code;
}
}
@@ -104,4 +104,20 @@ public interface BmsProductCountryMapper
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("bizDate") Date bizDate);
/** Count catalog rows by product (+ bizDate window), ignore country/zone/weight. */
public int countByProduct(
@Param("productCode") String productCode,
@Param("bizDate") Date bizDate);
/**
* Count catalog rows by product + country + zone (+ bizDate), ignore weight.
* Used to diagnose country miss vs weight miss.
*/
public int countByProductCountryZone(
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("bizDate") Date bizDate);
}
@@ -82,6 +82,22 @@ public interface PrcPriceSetDetailMapper
@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
/** Count FIXED details by version + product (ignore country/zone/weight). */
public int countByPsvProduct(
@Param("psvId") Long psvId,
@Param("productCode") String productCode);
/**
* Count FIXED details by version + product + country + zone (ignore weight).
* Used to diagnose country miss vs weight miss.
*/
public int countByPsvProductCountryZone(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode);
/** DISCOUNT by product; optional psvId scopes to version */
public PrcPriceSetDetail selectDiscountMatch(
@Param("priceSetId") Long priceSetId,
@@ -60,6 +60,7 @@ import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.enums.BillCreditArrearsStatus;
import com.bms.price.enums.BillCustomerActiveStatus;
import com.bms.price.enums.BillRefundStatus;
import com.bms.price.enums.BillBillingFailStatus;
import com.bms.price.enums.TmsOrderBillMatchType;
import com.bms.price.mapper.PrcIncomeBillFeeMapper;
import com.bms.price.mapper.PrcIncomeBillMapper;
@@ -1129,15 +1130,51 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
bill.setWeightLabel(result.getWeightLabel());
bill.setEffectiveDate(result.getEffectiveDate());
bill.setChargeWeight(resolvePersistChargeWeight(weight, result.getChargeWeight()));
bill.setFreightRate(result.getFreightRate());
bill.setBaseFreightRate(result.getBaseFreightRate());
bill.setDiscountPercent(result.getDiscountPercent());
bill.setRegistrationFee(result.getRegistrationFee());
bill.setFreightAmount(result.getFreightAmount());
bill.setSurchargeAmount(result.getSurchargeAmount());
bill.setIncomeTotal(result.getPayableTotal());
// 应付总额与产品目录价解耦:试算未产出 pay_total 时留空,不再用 product_amount 兜底
bill.setPayTotal(result.getPayTotal());
Integer billingFailStatus = result.getBillingFailStatus();
if (billingFailStatus == null)
{
billingFailStatus = Integer.valueOf(BillBillingFailStatus.SUCCESS.getCode());
}
bill.setBillingFailStatus(billingFailStatus);
boolean billingFailed = BillBillingFailStatus.isFail(billingFailStatus);
if (billingFailed)
{
// Soft-fail: persist order snapshot with zero amounts; skip fee detail rows.
bill.setFreightRate(BigDecimal.ZERO);
bill.setBaseFreightRate(BigDecimal.ZERO);
bill.setDiscountPercent(null);
bill.setRegistrationFee(BigDecimal.ZERO);
bill.setFreightAmount(BigDecimal.ZERO);
bill.setSurchargeAmount(BigDecimal.ZERO);
bill.setIncomeTotal(BigDecimal.ZERO);
bill.setPayTotal(BigDecimal.ZERO);
result.setFreightRate(BigDecimal.ZERO);
result.setBaseFreightRate(BigDecimal.ZERO);
result.setDiscountPercent(null);
result.setRegistrationFee(BigDecimal.ZERO);
result.setFreightAmount(BigDecimal.ZERO);
result.setSurchargeAmount(BigDecimal.ZERO);
result.setProductAmount(BigDecimal.ZERO);
result.setRedlineAmount(BigDecimal.ZERO);
result.setCostAmount(BigDecimal.ZERO);
result.setIncomeTotal(BigDecimal.ZERO);
result.setPayTotal(BigDecimal.ZERO);
result.setPayableTotal(BigDecimal.ZERO);
result.setFeeDetails(new ArrayList<TmsPriceQuoteFeeLine>());
result.setPayFeeDetails(new ArrayList<TmsPriceQuoteFeeLine>());
}
else
{
bill.setFreightRate(result.getFreightRate());
bill.setBaseFreightRate(result.getBaseFreightRate());
bill.setDiscountPercent(result.getDiscountPercent());
bill.setRegistrationFee(result.getRegistrationFee());
bill.setFreightAmount(result.getFreightAmount());
bill.setSurchargeAmount(result.getSurchargeAmount());
bill.setIncomeTotal(result.getPayableTotal());
// Payable total is decoupled from product catalog; leave null when quote has no pay_total.
bill.setPayTotal(result.getPayTotal());
}
bill.setCurrency(result.getCurrency());
bill.setRoundMode(result.getRoundMode());
// Credit soft-fail before persist so insert/update stores arrears flag in one write.
@@ -1155,6 +1192,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
result.setCustomerActiveStatus(customerActiveStatus);
result.setCreditArrearsStatus(creditArrearsStatus);
result.setRefundStatus(bill.getRefundStatus());
result.setBillingFailStatus(billingFailStatus);
result.setIncomeTotal(bill.getIncomeTotal());
result.setPayTotal(bill.getPayTotal());
result.setPayableTotal(bill.getIncomeTotal());
@@ -1169,23 +1207,34 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
else
{
orderBillMapper.updatePrcOrderBill(bill);
// Keep manually added fees (manual_flag=1) across rebill
incomeBillFeeMapper.deleteAutoFeesByBillId(bill.getBillId());
if (billingFailed)
{
// Soft-fail rebill: clear all fee details (no fee rows for failed billing).
incomeBillFeeMapper.deleteByBillId(bill.getBillId());
}
else
{
// Keep manually added fees (manual_flag=1) across rebill
incomeBillFeeMapper.deleteAutoFeesByBillId(bill.getBillId());
}
// Payable not in scope yet: skip prc_pay_bill_fee cleanup/write.
// payBillFeeMapper.deleteByBillId(bill.getBillId());
}
PrcIncomeBill incomeBill = persistIncomeBill(bill, result);
List<PrcIncomeBillFee> fees = toFeeRows(bill.getBillId(),
incomeBill == null ? null : incomeBill.getIncomeBillId(), result);
if (existed != null)
if (!billingFailed)
{
fees = filterAutoFeesAgainstManual(bill.getBillId(), fees);
List<PrcIncomeBillFee> fees = toFeeRows(bill.getBillId(),
incomeBill == null ? null : incomeBill.getIncomeBillId(), result);
if (existed != null)
{
fees = filterAutoFeesAgainstManual(bill.getBillId(), fees);
}
if (!fees.isEmpty())
{
incomeBillFeeMapper.insertBatch(fees);
}
}
if (!fees.isEmpty())
{
incomeBillFeeMapper.insertBatch(fees);
}
if (existed != null)
if (existed != null && !billingFailed)
{
// Quote totals were written above; recalc so retained manual fees join income/credit
TmsPriceQuoteResult refreshed = refreshBillIncomeTotals(bill);
@@ -1199,7 +1248,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
result.setFeeDetails(refreshed.getFeeDetails());
}
}
else
else if (existed == null)
{
bill.setRefundStatus(BillRefundStatus.NONE.getCode());
result.setRefundStatus(bill.getRefundStatus());
@@ -1619,6 +1668,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
r.setCustomerActiveStatus(bill.getCustomerActiveStatus());
r.setCreditArrearsStatus(bill.getCreditArrearsStatus());
r.setRefundStatus(bill.getRefundStatus());
r.setBillingFailStatus(bill.getBillingFailStatus() == null
? Integer.valueOf(BillBillingFailStatus.SUCCESS.getCode()) : bill.getBillingFailStatus());
r.setFeeDetails(toIncomeFeeLines(incomeFees));
r.setPayFeeDetails(toPayFeeLines(payFees));
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
@@ -42,6 +42,7 @@ import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.enums.BillBillingFailStatus;
import com.bms.price.enums.BillCustomerActiveStatus;
import com.bms.price.mapper.BmsProductCountryMapper;
import com.bms.price.mapper.BmsProductMapper;
@@ -64,7 +65,8 @@ import com.bms.system.service.IBmsProductCodeMappingService;
* Shared quote/bill calc: customer bind + package version by bizDate (version effective_time).
* Bind pr_begindate/pr_enddate is not used for billing time gate.
* Sales price comes from package snapshot only (FIXED detail / DISCOUNT on catalog base).
* No bind / no active version / package miss: quote and bill both fail hard (no PRODUCT_DEFAULT).
* No bind / no active version: quote and bill both fail hard (no PRODUCT_DEFAULT).
* Bill sync soft-fails for product/country/weight miss (billing_fail_status 1/2/3, zero amounts).
* product_country is only for product_amount/cost and DISCOUNT freight base.
*
* @author bms
@@ -207,7 +209,11 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
String customerActiveStatus = softResolveCustomer(request);
Date bizDate = resolveBizDate(request.getBizDate());
TmsPriceQuoteResult result = doQuote(request, bizDate);
applySurchargesAndFees(result);
if (!BillBillingFailStatus.isFail(result.getBillingFailStatus()))
{
applySurchargesAndFees(result);
result.setBillingFailStatus(BillBillingFailStatus.SUCCESS.getCode());
}
result.setCustomerActiveStatus(customerActiveStatus);
return result;
}
@@ -405,6 +411,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
boolean productExcludedBySetProducts = false;
boolean attemptedPriceMatch = false;
boolean anyZoneRequired = false;
// Highest specificity among soft-failable misses: WEIGHT(3) > COUNTRY(2) > PRODUCT(1).
int missCode = 0;
// Match version by exact bill datetime (effective_time / expire_time), not end-of-day.
Date asOf = bizDate != null ? bizDate : new Date();
for (PrcCustomerPriceSet bind : binds)
@@ -450,6 +458,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
if (anyProducts != null && !anyProducts.isEmpty())
{
productExcludedBySetProducts = true;
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
continue;
}
}
@@ -482,11 +491,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
stampRequestMeta(request, hit);
return hit;
}
missCode = Math.max(missCode, diagnoseFixedMiss(version.getPsvId(), productCode, cc, cn, zone.zoneCode));
}
else if (TYPE_DISCOUNT.equals(setType))
{
if (catalog == null)
{
missCode = Math.max(missCode, diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
continue;
}
TmsPriceQuoteResult hit = tryDiscount(bind, set, version, catalog, productCode, productName, weight, bizDate);
@@ -495,6 +506,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
stampRequestMeta(request, hit);
return hit;
}
// Discount detail missing for product (or invalid discount) after catalog hit.
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
}
}
@@ -506,6 +519,15 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
throw new ServiceException(MessageUtils.message(msgKey));
}
if (!attemptedPriceMatch && productExcludedBySetProducts)
{
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
}
if (request instanceof TmsPriceBillRequest && BillBillingFailStatus.isFail(Integer.valueOf(missCode)))
{
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
weight, bizDate, missCode);
}
if (!attemptedPriceMatch && productExcludedBySetProducts)
{
String msgKey = request instanceof TmsPriceBillRequest
? "price.bill.productNotInPackage"
@@ -528,6 +550,118 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
throw new ServiceException(MessageUtils.message(missKey));
}
/**
* Diagnose FIXED miss: product rows absent -> 1; country/zone absent -> 2; else weight -> 3.
*/
private int diagnoseFixedMiss(Long psvId, String productCode, String countryCode,
String countryName, String zoneCode)
{
if (psvId == null || StringUtils.isEmpty(productCode))
{
return BillBillingFailStatus.PRODUCT.getCode();
}
int byProduct = detailMapper.countByPsvProduct(psvId, productCode);
if (byProduct <= 0)
{
return BillBillingFailStatus.PRODUCT.getCode();
}
int byCountry = detailMapper.countByPsvProductCountryZone(
psvId, productCode, countryCode, countryName, zoneCode);
if (byCountry <= 0)
{
return BillBillingFailStatus.COUNTRY.getCode();
}
return BillBillingFailStatus.WEIGHT.getCode();
}
/**
* Diagnose catalog miss for DISCOUNT base price.
*/
private int diagnoseCatalogMiss(String productCode, String countryCode, String countryName,
String zoneCode, Date bizDate)
{
if (StringUtils.isEmpty(productCode))
{
return BillBillingFailStatus.PRODUCT.getCode();
}
int byProduct = productCountryMapper.countByProduct(productCode, bizDate);
if (byProduct <= 0)
{
return BillBillingFailStatus.PRODUCT.getCode();
}
int byCountry = productCountryMapper.countByProductCountryZone(
productCode, countryCode, countryName, zoneCode, bizDate);
if (byCountry <= 0)
{
return BillBillingFailStatus.COUNTRY.getCode();
}
return BillBillingFailStatus.WEIGHT.getCode();
}
/**
* Soft-fail bill result: keep order snapshot fields, all amounts zero, set billingFailStatus.
*/
private TmsPriceQuoteResult buildBillingMissResult(TmsPriceQuoteRequest request, String productCode,
String productName, String countryCode, String countryName, BigDecimal weight, Date bizDate,
int missCode)
{
TmsPriceQuoteResult r = new TmsPriceQuoteResult();
r.setBillingFailStatus(Integer.valueOf(missCode));
r.setMatchSource("BILLING_FAIL");
r.setMatchMessage(billingFailMessage(missCode));
r.setProductCode(productCode);
r.setProductName(productName);
r.setCountryCode(countryCode);
r.setCountryName(countryName);
r.setWeight(weight);
r.setChargeWeight(weight);
r.setBizDate(bizDate);
r.setFreightRate(BigDecimal.ZERO);
r.setBaseFreightRate(BigDecimal.ZERO);
r.setDiscountPercent(null);
r.setRegistrationFee(BigDecimal.ZERO);
r.setFreightAmount(BigDecimal.ZERO);
r.setSurchargeAmount(BigDecimal.ZERO);
r.setProductAmount(BigDecimal.ZERO);
r.setRedlineAmount(BigDecimal.ZERO);
r.setCostAmount(BigDecimal.ZERO);
r.setIncomeTotal(BigDecimal.ZERO);
r.setPayTotal(BigDecimal.ZERO);
r.setPayableTotal(BigDecimal.ZERO);
r.setCurrency("CNY");
r.setRoundMode("HALF_UP");
r.setFromCache(Boolean.FALSE);
r.setFeeDetails(Collections.<TmsPriceQuoteFeeLine>emptyList());
r.setPayFeeDetails(Collections.<TmsPriceQuoteFeeLine>emptyList());
stampRequestMeta(request, r);
if (request instanceof TmsPriceBillRequest)
{
TmsPriceBillRequest billReq = (TmsPriceBillRequest) request;
if (StringUtils.isEmpty(r.getCustomerName()) && StringUtils.isNotEmpty(billReq.getCustomerName()))
{
r.setCustomerName(billReq.getCustomerName());
}
}
return r;
}
private String billingFailMessage(int missCode)
{
if (missCode == BillBillingFailStatus.PRODUCT.getCode())
{
return MessageUtils.message("price.bill.billingFail.product");
}
if (missCode == BillBillingFailStatus.COUNTRY.getCode())
{
return MessageUtils.message("price.bill.billingFail.country");
}
if (missCode == BillBillingFailStatus.WEIGHT.getCode())
{
return MessageUtils.message("price.bill.billingFail.weight");
}
return MessageUtils.message("price.bill.packagePriceNotHit");
}
/**
* Soft lookup of package product row; null when version has no such product.
*/