⚡ 移除应收账单确认时的明细校验
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
package com.bms.receivable;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.nullable;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import com.bms.common.core.domain.entity.SysUser;
|
||||||
|
import com.bms.common.core.domain.model.LoginUser;
|
||||||
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.receivable.domain.ArBill;
|
||||||
|
import com.bms.receivable.domain.ArBillLog;
|
||||||
|
import com.bms.receivable.mapper.ArBillDetailMapper;
|
||||||
|
import com.bms.receivable.mapper.ArBillExportMapper;
|
||||||
|
import com.bms.receivable.mapper.ArBillMapper;
|
||||||
|
import com.bms.receivable.security.ArBillScopeResolver;
|
||||||
|
import com.bms.receivable.service.IArBillLogService;
|
||||||
|
import com.bms.receivable.service.impl.ArBillServiceImpl;
|
||||||
|
|
||||||
|
class ArBillConfirmTest {
|
||||||
|
private final ArBillMapper billMapper = mock(ArBillMapper.class);
|
||||||
|
private final ArBillDetailMapper detailMapper = mock(ArBillDetailMapper.class);
|
||||||
|
private final ArBillExportMapper exportMapper = mock(ArBillExportMapper.class);
|
||||||
|
private final ArBillScopeResolver scope = mock(ArBillScopeResolver.class);
|
||||||
|
private final IArBillLogService logService = mock(IArBillLogService.class);
|
||||||
|
private final ArBillServiceImpl service = new ArBillServiceImpl();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ReflectionTestUtils.setField(service, "billMapper", billMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "detailMapper", detailMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "exportMapper", exportMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "billScope", scope);
|
||||||
|
ReflectionTestUtils.setField(service, "logService", logService);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||||
|
new LoginUser(7L, 1L, new SysUser(), Set.of()), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void clearSecurity() {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readyDraftCanBeConfirmedWithoutReadingDetails() {
|
||||||
|
ArBill draft = bill("DRAFT", "READY");
|
||||||
|
draft.setTotalAmount(new BigDecimal("123.45"));
|
||||||
|
ArBill confirmed = bill("CONFIRMED", "READY");
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(draft);
|
||||||
|
when(billMapper.updateArBillStatus(11L, "CONFIRMED", null, "finance", "DRAFT")).thenReturn(1);
|
||||||
|
when(billMapper.selectArBillByIdScoped(11L, null)).thenReturn(confirmed);
|
||||||
|
when(exportMapper.selectByBillId(11L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
assertSame(confirmed, service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
verify(scope).requireBillAccessible(11L);
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
verify(logService).record(any(ArBillLog.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unfinishedOrNonDraftBillStillCannotBeConfirmed() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(
|
||||||
|
bill("DRAFT", "GENERATING"), bill("CONFIRMED", "READY"));
|
||||||
|
|
||||||
|
ServiceException unfinished = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
assertEquals("账单尚未生成完成,暂不能确认", unfinished.getMessage());
|
||||||
|
ServiceException notDraft = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-2"));
|
||||||
|
assertEquals("只有草稿账单可以确认,当前状态为 CONFIRMED", notDraft.getMessage());
|
||||||
|
verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any());
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inaccessibleBillStillCannotBeConfirmed() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY"));
|
||||||
|
doThrow(new ServiceException("账单不存在或无权访问")).when(scope).requireBillAccessible(11L);
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any());
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentStatusChangeStillPreventsConfirmation() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY"));
|
||||||
|
|
||||||
|
ServiceException error = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
assertEquals("账单状态已发生变化,请刷新后重试", error.getMessage());
|
||||||
|
verify(logService, never()).record(any(ArBillLog.class));
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArBill bill(String status, String generationStatus) {
|
||||||
|
ArBill bill = new ArBill();
|
||||||
|
bill.setArBillId(11L);
|
||||||
|
bill.setStatus(status);
|
||||||
|
bill.setGenerationStatus(generationStatus);
|
||||||
|
return bill;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,6 @@ public interface ArBillDetailMapper {
|
|||||||
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
||||||
@Param("sourceBillIds") List<String> sourceBillIds);
|
@Param("sourceBillIds") List<String> sourceBillIds);
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询账单明细及费用明细,避免详情页逐层查询。
|
|
||||||
*/
|
|
||||||
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
|
||||||
|
|
||||||
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
||||||
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
||||||
|
|
||||||
|
|||||||
+1
-26
@@ -985,7 +985,7 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认已完成生成且明细金额校验通过的草稿账单。
|
* 确认已完成生成的草稿账单。
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -1007,11 +1007,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
bill.getStatus(),
|
bill.getStatus(),
|
||||||
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
|
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
|
||||||
}
|
}
|
||||||
if (!validateDetails(arBillId, bill.getTotalAmount())) {
|
|
||||||
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
|
|
||||||
bill.getStatus(),
|
|
||||||
"账单费用明细汇总与订单或账单总额不一致", operator, requestId);
|
|
||||||
}
|
|
||||||
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
|
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
|
||||||
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
|
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
|
||||||
operator, ArBillStatus.DRAFT.getCode()) != 1) {
|
operator, ArBillStatus.DRAFT.getCode()) != 1) {
|
||||||
@@ -1313,26 +1308,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
return message.length() > 500 ? message.substring(0, 500) : message;
|
return message.length() > 500 ? message.substring(0, 500) : message;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateDetails(Long arBillId, BigDecimal billTotal) {
|
|
||||||
List<ArBillDetail> details = detailMapper.selectByBillIdWithFees(arBillId);
|
|
||||||
BigDecimal detailTotal = BigDecimal.ZERO;
|
|
||||||
for (ArBillDetail detail : details) {
|
|
||||||
BigDecimal feeTotal = BigDecimal.ZERO;
|
|
||||||
List<ArBillDetailFee> fees = detail.getFees();
|
|
||||||
if (fees == null) {
|
|
||||||
fees = new ArrayList<ArBillDetailFee>();
|
|
||||||
}
|
|
||||||
for (ArBillDetailFee fee : fees) {
|
|
||||||
feeTotal = feeTotal.add(nvl(fee.getAmount()));
|
|
||||||
}
|
|
||||||
if (feeTotal.compareTo(nvl(detail.getTotalAmount())) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
detailTotal = detailTotal.add(nvl(detail.getTotalAmount()));
|
|
||||||
}
|
|
||||||
return !details.isEmpty() && detailTotal.compareTo(nvl(billTotal)) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, PrcIncomeBill sourceIncomeBill,
|
private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, PrcIncomeBill sourceIncomeBill,
|
||||||
int detailNo, String operator) {
|
int detailNo, String operator) {
|
||||||
ArBillDetail detail = new ArBillDetail();
|
ArBillDetail detail = new ArBillDetail();
|
||||||
|
|||||||
@@ -69,49 +69,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by detail_no
|
order by detail_no
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<resultMap id="ArBillDetailWithFeesResult" type="ArBillDetail" extends="ArBillDetailResult">
|
|
||||||
<collection property="fees" ofType="ArBillDetailFee">
|
|
||||||
<id property="detailFeeId" column="fee_detail_id"/>
|
|
||||||
<result property="detailId" column="fee_parent_detail_id"/>
|
|
||||||
<result property="prcIncomeBillFeeId" column="fee_prc_income_bill_fee_id"/>
|
|
||||||
<result property="feeNo" column="fee_no"/>
|
|
||||||
<result property="feeType" column="fee_type"/>
|
|
||||||
<result property="feeName" column="fee_name"/>
|
|
||||||
<result property="scId" column="fee_sc_id"/>
|
|
||||||
<result property="quantity" column="fee_quantity"/>
|
|
||||||
<result property="unit" column="fee_unit"/>
|
|
||||||
<result property="unitPrice" column="fee_unit_price"/>
|
|
||||||
<result property="amount" column="fee_amount"/>
|
|
||||||
<result property="productAmount" column="fee_product_amount"/>
|
|
||||||
<result property="redlineAmount" column="fee_redline_amount"/>
|
|
||||||
<result property="currency" column="fee_currency"/>
|
|
||||||
<result property="calculationFormula" column="fee_calculation_formula"/>
|
|
||||||
<result property="roundMode" column="fee_round_mode"/>
|
|
||||||
<result property="remark" column="fee_remark"/>
|
|
||||||
<result property="createBy" column="fee_create_by"/>
|
|
||||||
<result property="createTime" column="fee_create_time"/>
|
|
||||||
</collection>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectByBillIdWithFees" parameterType="long" resultMap="ArBillDetailWithFeesResult">
|
|
||||||
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time,
|
|
||||||
d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode,
|
|
||||||
d.actual_weight, d.receive_weight, d.volume_weight, d.charge_weight, d.freight_rate, d.registration_fee, d.pkg_length, d.pkg_width, d.pkg_height,
|
|
||||||
d.currency, d.total_amount, d.product_amount, d.redline_amount, d.billing_fail_status, d.remark, d.create_by, d.create_time,
|
|
||||||
f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id,
|
|
||||||
f.prc_income_bill_fee_id as fee_prc_income_bill_fee_id,
|
|
||||||
f.fee_no, f.fee_type, f.fee_name, f.sc_id as fee_sc_id, f.quantity as fee_quantity,
|
|
||||||
f.unit as fee_unit, f.unit_price as fee_unit_price, f.amount as fee_amount,
|
|
||||||
f.product_amount as fee_product_amount, f.redline_amount as fee_redline_amount,
|
|
||||||
f.currency as fee_currency, f.calculation_formula as fee_calculation_formula,
|
|
||||||
f.round_mode as fee_round_mode, f.remark as fee_remark, f.create_by as fee_create_by,
|
|
||||||
f.create_time as fee_create_time
|
|
||||||
from ar_bill_detail d
|
|
||||||
left join ar_bill_detail_fee f on f.detail_id = d.detail_id
|
|
||||||
where d.ar_bill_id = #{arBillId}
|
|
||||||
order by d.detail_no, d.detail_id, f.fee_no, f.detail_fee_id
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectExportPage" resultMap="ArBillDetailResult">
|
<select id="selectExportPage" resultMap="ArBillDetailResult">
|
||||||
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
||||||
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||||
|
|||||||
Reference in New Issue
Block a user