⚡ 移除应收账单确认时的明细校验
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,
|
||||
@Param("sourceBillIds") List<String> sourceBillIds);
|
||||
|
||||
/**
|
||||
* 查询账单明细及费用明细,避免详情页逐层查询。
|
||||
*/
|
||||
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
||||
|
||||
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
||||
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
||||
|
||||
|
||||
+1
-26
@@ -985,7 +985,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认已完成生成且明细金额校验通过的草稿账单。
|
||||
* 确认已完成生成的草稿账单。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -1007,11 +1007,6 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
bill.getStatus(),
|
||||
"只有草稿账单可以确认,当前状态为 " + 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(),
|
||||
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
|
||||
operator, ArBillStatus.DRAFT.getCode()) != 1) {
|
||||
@@ -1313,26 +1308,6 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
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,
|
||||
int detailNo, String operator) {
|
||||
ArBillDetail detail = new ArBillDetail();
|
||||
|
||||
@@ -69,49 +69,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by detail_no
|
||||
</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 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,
|
||||
|
||||
Reference in New Issue
Block a user