This commit is contained in:
wuweihua
2026-11-11 13:52:35 +08:00
5 changed files with 127 additions and 8 deletions
@@ -1,9 +1,11 @@
package com.bms.report;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
@@ -11,18 +13,93 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.core.domain.model.LoginUser;
import com.bms.receivable.domain.ArBalanceWriteoff;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.mapper.ArBillMapper;
import com.bms.receivable.mapper.ArCommissionMapper;
import com.bms.receivable.profit.ArCommissionRate;
import com.bms.receivable.profit.ArCommissionResult;
import com.bms.receivable.profit.ArCommissionService;
import com.bms.receivable.profit.ArProfitScope;
import com.bms.receivable.profit.ArProfitScopeResolver;
import com.bms.receivable.profit.ArWriteoffRate;
import com.bms.common.exception.ServiceException;
class ArCommissionServiceTest
{
@Test
void rateConfirmationRequiresCompleteProfitDataBeforeAnyWrite()
{
List<String> calls = new ArrayList<>();
AtomicReference<Integer> complete = new AtomicReference<>(0);
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
calls.add(method.getName());
return switch (method.getName()) {
case "selectBillIdByWriteoffId" -> 25L;
case "isBillProfitDataComplete" -> complete.get();
case "confirmMissingRate", "reverseResults" -> 1;
case "countMissingRates" -> 0;
case "selectSettlementInputs" -> List.of();
default -> throw new AssertionError(method.getName());
};
});
ArBill bill = new ArBill();
bill.setProfitSalesUserId(7L);
bill.setGenerationStatus("READY");
ArBillMapper billMapper = (ArBillMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { ArBillMapper.class }, (proxy, method, args) -> {
if ("selectArBillByIdForUpdate".equals(method.getName())) return bill;
throw new AssertionError(method.getName());
});
ArProfitScopeResolver resolver = new ArProfitScopeResolver() {
@Override public ArProfitScope resolve() { return new ArProfitScope(true, false, false, 1L, 1L); }
};
ArCommissionService service = new ArCommissionService(mapper, resolver, null, billMapper);
ArWriteoffRate input = new ArWriteoffRate();
input.setWriteoffId(18L);
input.setRate(new BigDecimal("5"));
input.setRemark("已核对");
for (String status : List.of("CONFIRMED", "PARTIAL_WRITTEN_OFF", "WRITTEN_OFF"))
{
bill.setStatus(status);
calls.clear();
ServiceException error = assertThrows(ServiceException.class, () -> service.confirmMissingRate(input));
assertTrue(error.getMessage().contains("请先处理后再确认比例"));
assertFalse(calls.contains("confirmMissingRate"));
assertFalse(calls.contains("reverseResults"));
}
complete.set(null);
assertThrows(ServiceException.class, () -> service.confirmMissingRate(input));
complete.set(1);
SysUser user = new SysUser();
user.setUserName("finance");
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new LoginUser(user, java.util.Set.of()), null));
try
{
bill.setStatus("PARTIAL_WRITTEN_OFF");
calls.clear();
service.confirmMissingRate(input);
assertTrue(calls.contains("confirmMissingRate"));
assertFalse(calls.contains("reverseResults"));
bill.setStatus("WRITTEN_OFF");
calls.clear();
service.confirmMissingRate(input);
assertTrue(calls.indexOf("isBillProfitDataComplete") < calls.indexOf("confirmMissingRate"));
assertTrue(calls.contains("reverseResults"));
assertTrue(calls.contains("selectSettlementInputs"));
}
finally
{
SecurityContextHolder.clearContext();
}
}
@Test
void commissionReadsUseServerResolvedScopeAndManagementRemainsRestricted()
{
@@ -158,6 +158,17 @@ class ArProfitMapperSqlTest
.getBoundSql(Map.of("billId", 1L)).getSql();
assertTrue(settlement.contains("w.status = 'EFFECTIVE'"));
assertTrue(settlement.contains("b.status = 'WRITTEN_OFF'"));
String validation = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.isBillProfitDataComplete")
.getBoundSql(Map.of("billId", 1L)).getSql();
assertTrue(validation.contains("detail.ar_bill_id = ?"));
assertTrue(validation.contains("d.ar_bill_id = ?"));
assertTrue(validation.contains("a.detail_total = b.total_amount"));
assertTrue(validation.contains("f.sales_amount = d.total_amount"));
assertTrue(validation.contains("f.fee_currency = d.currency"));
assertTrue(validation.contains("f.product_complete = 1"));
assertTrue(validation.contains("f.redline_complete = 1"));
assertFalse(validation.contains("b.status = 'WRITTEN_OFF'"));
assertFalse(validation.contains("profit_calc_status"));
var insert = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.insertResults")
.getBoundSql(Map.of("items", java.util.List.of(new ArCommissionResult(), new ArCommissionResult()), "operator", "test"));
assertTrue(insert.getSql().contains("ar_profit_commission_result"));
@@ -23,6 +23,7 @@ public interface ArCommissionMapper
List<ArWriteoffRate> selectMissingRates();
List<ArWriteoffRate> selectWriteoffRates(@Param("billId") Long billId);
Long selectBillIdByWriteoffId(@Param("writeoffId") Long writeoffId);
Integer isBillProfitDataComplete(@Param("billId") Long billId);
int countMissingRates(@Param("billId") Long billId);
int confirmMissingRate(@Param("writeoffId") Long writeoffId, @Param("rate") java.math.BigDecimal rate,
@Param("operator") String operator, @Param("remark") String remark);
@@ -134,6 +134,8 @@ public class ArCommissionService
|| ArBillStatus.PARTIAL_WRITTEN_OFF.getCode().equals(bill.getStatus())
|| ArBillStatus.WRITTEN_OFF.getCode().equals(bill.getStatus())))
throw new ServiceException("账单未生成完成或状态不允许确认提成比例");
if (!Integer.valueOf(1).equals(mapper.isBillProfitDataComplete(billId)))
throw new ServiceException("账单价格、金额或币种数据不完整或不一致,请先处理后再确认比例");
String operator = SecurityUtils.getUsername();
if (mapper.confirmMissingRate(input.getWriteoffId(), input.getRate(), operator, input.getRemark().trim()) != 1)
throw new ServiceException("比例已确认或核销状态已变化,请刷新");
@@ -79,6 +79,41 @@
<select id="selectBillIdByWriteoffId" resultType="long">
select ar_bill_id from ar_balance_writeoff where writeoff_id = #{writeoffId} and status = 'EFFECTIVE'
</select>
<!-- 供比例确认和正式提成共用费用快照口径。 -->
<sql id="feeSnapshotAggregation">
select fee.detail_id, case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count,
min(fee.currency) as fee_currency,
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount,
case when count(fee.product_amount) = count(*) then 1 else 0 end as product_complete,
case when count(fee.redline_amount) = count(*) then 1 else 0 end as redline_complete
from ar_bill_detail_fee fee join ar_bill_detail detail on detail.detail_id = fee.detail_id
where detail.ar_bill_id = #{billId} group by fee.detail_id
</sql>
<!-- 不依赖核销完成状态或账单汇总快照,确认/部分核销/已核销账单均检查明细。 -->
<select id="isBillProfitDataComplete" resultType="int">
<bind name="shenzhenArea" value="@com.bms.price.enums.ProfitAreaRule@SHENZHEN.getCustomerArea()"/>
<bind name="guangzhouArea" value="@com.bms.price.enums.ProfitAreaRule@GUANGZHOU.getCustomerArea()"/>
select case when b.total_amount is not null and b.total_amount > 0
and a.detail_count > 0 and a.invalid_count = 0 and a.detail_total = b.total_amount
then 1 else 0 end
from ar_bill b
left join (
select d.ar_bill_id, count(*) as detail_count, sum(d.total_amount) as detail_total,
sum(case when d.total_amount is not null and d.currency is not null and b.currency is not null
and d.currency = b.currency and f.currency_count = 1 and f.fee_currency = d.currency
and f.sales_amount is not null and f.sales_amount = d.total_amount
and ((b.profit_customer_area = #{shenzhenArea} and f.product_complete = 1)
or (b.profit_customer_area = #{guangzhouArea} and f.redline_complete = 1))
then 0 else 1 end) as invalid_count
from ar_bill_detail d join ar_bill b on b.ar_bill_id = d.ar_bill_id
left join (<include refid="feeSnapshotAggregation"/>) f on f.detail_id = d.detail_id
where d.ar_bill_id = #{billId}
group by d.ar_bill_id
) a on a.ar_bill_id = b.ar_bill_id
where b.ar_bill_id = #{billId}
</select>
<select id="countMissingRates" resultType="int">
select count(*) from ar_balance_writeoff w
left join ar_profit_writeoff_rate r on r.writeoff_id = w.writeoff_id
@@ -157,14 +192,7 @@
else null end as issue
from ar_bill b inner join ar_bill_detail d on d.ar_bill_id = b.ar_bill_id
left join (
select fee.detail_id, case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count, min(fee.currency) as fee_currency,
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount,
case when count(fee.product_amount) = count(*) then 1 else 0 end as product_complete,
case when count(fee.redline_amount) = count(*) then 1 else 0 end as redline_complete
from ar_bill_detail_fee fee join ar_bill_detail detail on detail.detail_id = fee.detail_id
where detail.ar_bill_id = #{billId} group by fee.detail_id
<include refid="feeSnapshotAggregation"/>
) f on f.detail_id = d.detail_id
left join (
select w.ar_bill_id as bill_id, sum(w.writeoff_amount) as written_amount,