@@ -28,7 +28,11 @@ import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.price.domain.BmsProduct ;
import com.bms.price.domain.BmsProductCountry ;
import com.bms.price.domain.BmsRedlineProductCountry ;
import com.bms.price.domain.PayCostLine ;
import com.bms.price.domain.PayOrderCostResult ;
import com.bms.price.domain.PayProduct ;
import com.bms.price.domain.PrcCustomerPriceSet ;
import com.bms.price.domain.PrcOrderBillExt ;
import com.bms.price.domain.PrcPriceSet ;
import com.bms.price.domain.PrcPriceSetDetail ;
import com.bms.price.domain.PrcPriceSetProduct ;
@@ -37,6 +41,7 @@ import com.bms.price.domain.PrcQuote;
import com.bms.price.domain.PrcQuoteFee ;
import com.bms.price.domain.PrcSurchargeRel ;
import com.bms.price.domain.PrcZone ;
import com.bms.price.pay.PayCostCalculator ;
import com.bms.price.mapper.PrcZoneDetailMapper ;
import com.bms.price.domain.vo.TmsPriceBillRequest ;
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine ;
@@ -48,6 +53,7 @@ import com.bms.price.mapper.BmsProductCountryMapper;
import com.bms.price.mapper.BmsProductMapper ;
import com.bms.price.mapper.BmsRedlineProductCountryMapper ;
import com.bms.price.mapper.PrcCustomerPriceSetMapper ;
import com.bms.price.mapper.PrcOrderBillExtMapper ;
import com.bms.price.mapper.PrcPriceSetDetailMapper ;
import com.bms.price.mapper.PrcPriceSetMapper ;
import com.bms.price.mapper.PrcPriceSetProductMapper ;
@@ -58,12 +64,14 @@ import com.bms.price.mapper.PrcSurchargeRelMapper;
import com.bms.price.search.EsBizLogSupport ;
import com.bms.price.search.EsLogType ;
import com.bms.price.search.IBillSearchStore ;
import com.bms.price.service.IPayProductService ;
import com.bms.price.service.ITmsPriceQuoteService ;
import com.bms.system.service.IBmsProductCodeMappingService ;
/**
* 共用的试算/计费计算:按 bizDate 取客户绑定和套餐版本(版本 effective_time)。
* 绑定的 pr_begindate/pr_enddate 不作为计费时间门槛。
* 先按 prc_price_set.set_category 分流:LABEL 走打单 pay_*; PACKAGE/EXCLUSIVE 走销售快照。
* 销售价只来自套餐快照(FIXED 明细 / 按目录底价 DISCOUNT)。
* 无绑定或无生效版本:试算和计费都直接失败(不用 PRODUCT_DEFAULT)。
* 账单同步时,产品/国家/重量段未命中为软失败(billing_fail_status 1/2/3,金额为 0)。
@@ -78,6 +86,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
private static final String TYPE_DISCOUNT = " DISCOUNT " ;
private static final String SRC_FIXED = " PACKAGE_FIXED " ;
private static final String SRC_DISCOUNT = " PACKAGE_DISCOUNT " ;
private static final String SRC_LABEL = " LABEL_PAY " ;
private static final String FEE_BASIC = " BASIC_FREIGHT " ;
private static final String FEE_REG = " REGISTRATION_FEE " ;
@@ -128,6 +137,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
@Autowired
private BmsCustomerMapper bmsCustomerMapper ;
@Autowired
private IPayProductService payProductService ;
@Autowired
private PrcOrderBillExtMapper orderBillExtMapper ;
@Override
@Transactional ( rollbackFor = Exception . class )
public TmsPriceQuoteResult quote ( TmsPriceQuoteRequest request )
@@ -169,7 +184,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return toResult ( cached , fees , true ) ;
}
TmsPriceQuoteResult refreshed = doQuote ( request , bizDate ) ;
applySurchargesAndFees ( refreshed ) ;
applySalesSurchargesIfNeeded ( refreshed ) ;
replaceQuoteFees ( cached . getQuoteId ( ) , refreshed ) ;
refreshed . setQuoteId ( cached . getQuoteId ( ) ) ;
refreshed . setFromCache ( false ) ;
@@ -179,11 +194,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
}
TmsPriceQuoteResult result = doQuote ( request , bizDate ) ;
applySurchargesAndFees ( result ) ;
applySalesSurchargesIfNeeded ( result ) ;
persistQuote ( quoteKey , request , weight , bizDate , result ) ;
return result ;
}
/** LABEL 费用已由 PayCostCalculator 算完,不再叠加销售附加费。 */
private void applySalesSurchargesIfNeeded ( TmsPriceQuoteResult result )
{
if ( result = = null | | SRC_LABEL . equals ( result . getMatchSource ( ) ) )
{
return ;
}
applySurchargesAndFees ( result ) ;
}
/**
* Cached quotes from pre-package-snapshot logic (PRODUCT_DEFAULT / missing package id)
* must not be reused after billing rules changed.
@@ -211,7 +236,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
TmsPriceQuoteResult result = doQuote ( request , bizDate ) ;
if ( ! BillBillingFailStatus . isFail ( result . getBillingFailStatus ( ) ) )
{
applySurchargesAndFees ( result ) ;
applySalesSurchargesIfNeeded ( result ) ;
result . setBillingFailStatus ( BillBillingFailStatus . SUCCESS . getCode ( ) ) ;
}
result . setCustomerActiveStatus ( customerActiveStatus ) ;
@@ -442,6 +467,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
continue ;
}
anyEnabledSet = true ;
// 打单套餐:独立 pay_* 链路,不走销售版本明细
if ( PrcPriceSet . CATEGORY_LABEL . equals (
PrcPriceSetServiceImpl . normalizeSetCategory ( set . getSetCategory ( ) ) ) )
{
return doLabelQuote ( request , bind , set , productCode , productName , countryCode , countryName ,
weight , bizDate ) ;
}
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
PrcPriceSetVersion version = versionMapper . selectActiveByPriceSetAt ( set . getPriceSetId ( ) , asOf ) ;
if ( version = = null | | version . getPsvId ( ) = = null )
@@ -622,6 +654,171 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
throw new ServiceException ( MessageUtils . message ( missKey ) ) ;
}
/**
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → term, PayCostCalculator 算费。
*/
private TmsPriceQuoteResult doLabelQuote ( TmsPriceQuoteRequest request , PrcCustomerPriceSet bind ,
PrcPriceSet set , String productCode , String productName , String countryCode , String countryName ,
BigDecimal weight , Date bizDate )
{
String channelId = resolveLabelChannelId ( request ) ;
if ( StringUtils . isEmpty ( channelId ) )
{
if ( request instanceof TmsPriceBillRequest )
{
return buildBillingMissResult ( request , productCode , productName , countryCode , countryName ,
weight , bizDate , BillBillingFailStatus . PRICE_NOT_HIT . getCode ( ) ) ;
}
throw new ServiceException ( " 打单计费需要渠道 " ) ;
}
PayProduct product ;
try
{
product = payProductService . selectLabelPriceForDate ( set . getPriceSetId ( ) , channelId , bizDate ) ;
}
catch ( ServiceException ex )
{
if ( request instanceof TmsPriceBillRequest )
{
return buildBillingMissResult ( request , productCode , productName , countryCode , countryName ,
weight , bizDate , BillBillingFailStatus . NO_ACTIVE_PACKAGE_VERSION . getCode ( ) ) ;
}
throw ex ;
}
BigDecimal length = null ;
BigDecimal width = null ;
BigDecimal height = null ;
Integer orderStatus = null ;
if ( request instanceof TmsPriceBillRequest )
{
TmsPriceBillRequest billReq = ( TmsPriceBillRequest ) request ;
length = billReq . getPkgLength ( ) ;
width = billReq . getPkgWidth ( ) ;
height = billReq . getPkgHeight ( ) ;
orderStatus = billReq . getOrderStatus ( ) ;
}
PayOrderCostResult cost = PayCostCalculator . calculate ( product , weight , length , width , height ,
request . getDestPostcode ( ) , bizDate , orderStatus ) ;
if ( cost = = null | | cost . getFees ( ) = = null | | cost . getFees ( ) . isEmpty ( ) )
{
if ( request instanceof TmsPriceBillRequest )
{
return buildBillingMissResult ( request , productCode , productName , countryCode , countryName ,
weight , bizDate , BillBillingFailStatus . PRICE_NOT_HIT . getCode ( ) ) ;
}
throw new ServiceException ( cost ! = null & & StringUtils . isNotEmpty ( cost . getMessage ( ) )
? cost . getMessage ( ) : " 打单价卡未算出费用 " ) ;
}
TmsPriceQuoteResult r = mapLabelCostToQuote ( cost , product , request , bind , set ,
productCode , productName , countryCode , countryName , weight , bizDate ) ;
stampRequestMeta ( request , r ) ;
return r ;
}
/** 试算用 request.channelId;计费优先订单扩展 productChannelId。 */
private String resolveLabelChannelId ( TmsPriceQuoteRequest request )
{
if ( request ! = null & & StringUtils . isNotEmpty ( request . getChannelId ( ) ) )
{
return request . getChannelId ( ) . trim ( ) ;
}
if ( ! ( request instanceof TmsPriceBillRequest ) )
{
return null ;
}
TmsPriceBillRequest billReq = ( TmsPriceBillRequest ) request ;
PrcOrderBillExt ext = null ;
if ( StringUtils . isNotEmpty ( billReq . getOrderNo ( ) ) )
{
ext = orderBillExtMapper . selectByOrderNo ( billReq . getOrderNo ( ) . trim ( ) ) ;
}
if ( ext = = null )
{
return null ;
}
if ( StringUtils . isNotEmpty ( ext . getProductChannelId ( ) ) )
{
return ext . getProductChannelId ( ) . trim ( ) ;
}
if ( StringUtils . isNotEmpty ( ext . getProductFloorCode ( ) ) )
{
return ext . getProductFloorCode ( ) . trim ( ) ;
}
return null ;
}
private TmsPriceQuoteResult mapLabelCostToQuote ( PayOrderCostResult cost , PayProduct product ,
TmsPriceQuoteRequest request , PrcCustomerPriceSet bind , PrcPriceSet set ,
String productCode , String productName , String countryCode , String countryName ,
BigDecimal weight , Date bizDate )
{
RoundingMode rm = RoundingMode . HALF_UP ;
String currency = normalizeCurrency ( cost . getCurrency ( ) ) ;
List < TmsPriceQuoteFeeLine > lines = new ArrayList < TmsPriceQuoteFeeLine > ( ) ;
BigDecimal freightAmount = BigDecimal . ZERO ;
BigDecimal surchargeAmount = BigDecimal . ZERO ;
BigDecimal registrationFee = BigDecimal . ZERO ;
for ( PayCostLine fee : cost . getFees ( ) )
{
if ( fee = = null | | fee . getAmount ( ) = = null )
{
continue ;
}
String feeType = StringUtils . isEmpty ( fee . getFeeType ( ) ) ? FEE_SUR : fee . getFeeType ( ) . trim ( ) ;
String feeName = StringUtils . isEmpty ( fee . getFeeName ( ) ) ? feeNameOf ( feeType ) : fee . getFeeName ( ) ;
BigDecimal amount = fee . getAmount ( ) . setScale ( 2 , rm ) ;
Long scId = fee . getScId ( ) = = null ? 0L : fee . getScId ( ) ;
lines . add ( feeLine ( feeType , feeName , scId , amount , currency , " HALF_UP " , fee . getRemark ( ) ) ) ;
if ( FEE_BASIC . equals ( feeType ) )
{
freightAmount = freightAmount . add ( amount ) ;
}
else if ( FEE_REG . equals ( feeType ) )
{
registrationFee = registrationFee . add ( amount ) ;
}
else
{
surchargeAmount = surchargeAmount . add ( amount ) ;
}
}
BigDecimal incomeTotal = nvl ( cost . getPayTotal ( ) ) ;
if ( incomeTotal . compareTo ( BigDecimal . ZERO ) < = 0 )
{
incomeTotal = freightAmount . add ( registrationFee ) . add ( surchargeAmount ) . setScale ( 2 , rm ) ;
}
String labelProductName = StringUtils . isNotEmpty ( product . getProductName ( ) )
? product . getProductName ( ) : productName ;
TmsPriceQuoteResult r = new TmsPriceQuoteResult ( ) ;
r . setMatchSource ( SRC_LABEL ) ;
r . setMatchMessage ( " hit label package pay card V "
+ ( product . getVersionNo ( ) = = null ? " ? " : product . getVersionNo ( ) ) ) ;
fillPackage ( r , bind , set ) ;
r . setVersionNo ( product . getVersionNo ( ) = = null ? null : ( " V " + product . getVersionNo ( ) ) ) ;
r . setCustomerId ( bind . getCustomerId ( ) ) ;
r . setProductCode ( productCode ) ;
r . setProductName ( labelProductName ) ;
r . setCountryCode ( countryCode ) ;
r . setCountryName ( countryName ) ;
r . setWeight ( weight ) ;
r . setChargeWeight ( weight ) ;
r . setBizDate ( bizDate ) ;
r . setEffectiveDate ( product . getTermStart ( ) ! = null ? product . getTermStart ( ) : product . getEffectiveDate ( ) ) ;
r . setFreightRate ( weight ! = null & & weight . compareTo ( BigDecimal . ZERO ) > 0
? freightAmount . divide ( weight , 2 , rm ) : freightAmount ) ;
r . setFreightAmount ( freightAmount . setScale ( 2 , rm ) ) ;
r . setRegistrationFee ( registrationFee . setScale ( 2 , rm ) ) ;
r . setSurchargeAmount ( surchargeAmount . setScale ( 2 , rm ) ) ;
r . setPayableTotal ( incomeTotal ) ;
r . setIncomeTotal ( incomeTotal ) ;
r . setCurrency ( currency ) ;
r . setRoundMode ( " HALF_UP " ) ;
r . setFromCache ( false ) ;
r . setFeeDetails ( lines ) ;
r . setPayFeeDetails ( Collections . < TmsPriceQuoteFeeLine > emptyList ( ) ) ;
return r ;
}
/**
* Diagnose FIXED miss: product rows absent -> 1; country/zone absent -> 2; else weight -> 3.
*/