✨ 新增应收账单毛利分析与提成管理
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.mapper.ArCommissionMapper;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
|
||||
class ArCommissionServiceTest
|
||||
{
|
||||
@Test
|
||||
void snapshotsTheRateOnceAtWriteoffAndPreservesMissingRates()
|
||||
{
|
||||
List<String> calls = new ArrayList<>();
|
||||
AtomicReference<Object[]> inserted = new AtomicReference<>();
|
||||
AtomicReference<ArCommissionRate> effective = new AtomicReference<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
calls.add(method.getName());
|
||||
return switch (method.getName()) {
|
||||
case "lockSalesUser" -> 7L;
|
||||
case "selectEffective" -> effective.get();
|
||||
case "insertWriteoffRate" -> { inserted.set(args.clone()); yield 1; }
|
||||
default -> throw new AssertionError(method.getName());
|
||||
};
|
||||
});
|
||||
ArCommissionService service = new ArCommissionService(mapper, null, null, null);
|
||||
ArBill bill = new ArBill();
|
||||
bill.setProfitSalesUserId(7L);
|
||||
ArBalanceWriteoff writeoff = new ArBalanceWriteoff();
|
||||
writeoff.setWriteoffId(18L);
|
||||
ArCommissionRate rate = new ArCommissionRate();
|
||||
rate.setRateId(3L);
|
||||
rate.setRate(new BigDecimal("10"));
|
||||
effective.set(rate);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertEquals(List.of("lockSalesUser", "selectEffective", "insertWriteoffRate"), calls);
|
||||
assertEquals(3L, inserted.get()[2]);
|
||||
assertEquals(new BigDecimal("10"), inserted.get()[3]);
|
||||
|
||||
calls.clear();
|
||||
effective.set(null);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertNull(inserted.get()[2]);
|
||||
assertNull(inserted.get()[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesOnlyCalculatedAmountsForReadyRows()
|
||||
{
|
||||
ArCommissionResult ready = new ArCommissionResult();
|
||||
ready.setResultStatus("READY");
|
||||
ready.setGrossProfit(new BigDecimal("-40"));
|
||||
ready.setWeightedRate(new BigDecimal("1000"));
|
||||
ready.setBillAmount(new BigDecimal("100"));
|
||||
ArCommissionResult missing = new ArCommissionResult();
|
||||
missing.setResultStatus("MISSING_PRICE");
|
||||
List<ArCommissionResult> saved = new ArrayList<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
if ("selectSettlementInputs".equals(method.getName())) return List.of(ready, missing);
|
||||
if ("insertResults".equals(method.getName())) {
|
||||
@SuppressWarnings("unchecked") List<ArCommissionResult> batch = (List<ArCommissionResult>) args[0];
|
||||
saved.addAll(batch);
|
||||
return batch.size();
|
||||
}
|
||||
throw new AssertionError(method.getName());
|
||||
});
|
||||
new ArCommissionService(mapper, null, null, null).createFinalResults(1L, "finance");
|
||||
assertEquals(new BigDecimal("-4.00"), saved.get(0).getCommissionAmount());
|
||||
assertNull(saved.get(1).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitScope;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
|
||||
class ArProfitMapperSqlTest
|
||||
{
|
||||
@Test
|
||||
void parsesReportAndCommissionMappersAndScopesEachReportQuery() throws Exception
|
||||
{
|
||||
Configuration configuration = new Configuration();
|
||||
for (String resource : new String[] { "mapper/ar/ArProfitMapper.xml", "mapper/ar/ArCommissionMapper.xml" })
|
||||
{
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
for (String id : new String[] { "selectBills", "selectSummary", "selectBillDetails", "selectFees" })
|
||||
{
|
||||
String name = "com.bms.ar.mapper.ArProfitMapper." + id;
|
||||
String self = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String manager = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, false, true, 8L, 10L))).getSql();
|
||||
assertTrue(self.contains("b.profit_sales_user_id = ?"));
|
||||
assertFalse(self.contains("find_in_set"));
|
||||
assertTrue(manager.contains("find_in_set"));
|
||||
assertTrue(manager.contains("b.status in"));
|
||||
}
|
||||
ArProfitQuery orderFilter = new ArProfitQuery();
|
||||
orderFilter.setOrderNo("ORDER-1");
|
||||
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
|
||||
.getBoundSql(Map.of("query", orderFilter, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(bills.contains("from ar_bill b"));
|
||||
assertTrue(bills.contains("exists (select 1 from ar_bill_detail matched"));
|
||||
assertFalse(bills.contains("and d.order_no like"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
.getBoundSql(Map.of("billId", 9L)).getSql();
|
||||
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
|
||||
assertTrue(billCommission.contains("then null"));
|
||||
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
|
||||
.getBoundSql(Map.of("keyword", "sales"));
|
||||
assertTrue(salesSql.getSql().contains("from sys_user u"));
|
||||
assertTrue(salesSql.getSql().contains("u.nick_name like"));
|
||||
assertTrue(salesSql.getSql().contains("u.user_name like"));
|
||||
assertFalse(salesSql.getSql().contains("ar_bill"));
|
||||
assertFalse(salesSql.getSql().contains("sys_role"));
|
||||
assertTrue(salesSql.getSql().contains("limit 50"));
|
||||
String settlement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
.getBoundSql(Map.of("billId", 1L)).getSql();
|
||||
assertTrue(settlement.contains("w.status = 'EFFECTIVE'"));
|
||||
assertTrue(settlement.contains("b.status = 'WRITTEN_OFF'"));
|
||||
var insert = configuration.getMappedStatement("com.bms.ar.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"));
|
||||
assertTrue(insert.getParameterMappings().size() == 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitScopeResolver;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
|
||||
class ArProfitScopeResolverTest
|
||||
{
|
||||
private static final String PERMISSION = "report:arProfit:list";
|
||||
|
||||
private LoginUser actor(String... keys)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(99L);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
user.setRoles(java.util.Arrays.stream(keys).map(key -> {
|
||||
SysRole role = new SysRole();
|
||||
role.setRoleKey(key);
|
||||
role.setStatus("0");
|
||||
role.setPermissions(Set.of(PERMISSION));
|
||||
return role;
|
||||
}).toList());
|
||||
return new LoginUser(99L, 10L, user, Set.of(PERMISSION));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowedRolesHaveOnlyTheirApprovedScope()
|
||||
{
|
||||
ArProfitScopeResolver resolver = new ArProfitScopeResolver();
|
||||
var sales = resolver.resolve(actor("ORA_6041"), PERMISSION);
|
||||
assertTrue(sales.self());
|
||||
assertFalse(sales.all());
|
||||
var manager = resolver.resolve(actor("ORA_6040"), PERMISSION);
|
||||
assertTrue(manager.department());
|
||||
assertFalse(manager.all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6042"), PERMISSION).all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6043"), PERMISSION).all());
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6163"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("UNKNOWN"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6042"), "other:permission"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.bms.ar.profit.CommissionCalculator;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.CommissionCalculator.Writeoff;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CommissionCalculatorTest
|
||||
{
|
||||
private static BigDecimal n(String value) { return new BigDecimal(value); }
|
||||
|
||||
@Test
|
||||
void waitsForFullSettlementAndUsesEachWriteoffRate()
|
||||
{
|
||||
var first = new Writeoff(n("50"), n("10"));
|
||||
var partial = CommissionCalculator.calculate(n("100"), n("20"), List.of(first));
|
||||
assertEquals(n("1.00"), partial.estimated());
|
||||
assertNull(partial.formal());
|
||||
var complete = CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(first, new Writeoff(n("50"), n("20"))));
|
||||
assertEquals(n("3.00"), complete.formal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deductsNegativeProfitAndKeepsMissingRatesUnknown()
|
||||
{
|
||||
assertEquals(n("-4.00"), CommissionCalculator.calculate(n("100"), n("-40"),
|
||||
List.of(new Writeoff(n("100"), n("10")))).formal());
|
||||
assertNull(CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("100"), null))).formal());
|
||||
assertThrows(IllegalArgumentException.class, () -> CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("101"), n("10")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocatesRoundingRemainderAcrossOrderResults()
|
||||
{
|
||||
List<ArCommissionResult> orders = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
ArCommissionResult order = new ArCommissionResult();
|
||||
order.setResultStatus("READY");
|
||||
order.setBillAmount(n("100"));
|
||||
order.setGrossProfit(n("0.05"));
|
||||
order.setWeightedRate(n("1000"));
|
||||
orders.add(order);
|
||||
}
|
||||
CommissionCalculator.allocateFinalAmounts(orders);
|
||||
assertEquals(n("0.01"), orders.get(0).getCommissionAmount());
|
||||
assertEquals(n("0.00"), orders.get(1).getCommissionAmount());
|
||||
assertEquals(n("0.01"), orders.get(2).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user