555 lines
25 KiB
Java
555 lines
25 KiB
Java
package com.baiyue.firstleg.service;
|
|
|
|
import com.baiyue.common.core.api.PageResult;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.firstleg.dto.FeeItemDtos;
|
|
import com.baiyue.firstleg.dto.InboundFeeDetailDtos;
|
|
import com.baiyue.firstleg.entity.FeeItemEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|
import com.baiyue.firstleg.repository.FeeItemRepository;
|
|
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
|
import jakarta.annotation.Resource;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyLong;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.doAnswer;
|
|
import static org.mockito.Mockito.doThrow;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.mockito.Mockito.lenient;
|
|
|
|
@DisplayName("费用项目和入库费用明细")
|
|
@SpringJUnitConfig({FeeItemService.class, InboundFeeDetailService.class})
|
|
class FeeManagementServiceTest {
|
|
@MockBean
|
|
private FeeItemRepository repository;
|
|
@MockBean
|
|
private FbaPricingRepository pricingRepository;
|
|
@MockBean
|
|
private FinanceFeeSourceOutboxService financeOutbox;
|
|
@MockBean
|
|
private WarehouseSupplierSnapshotService suppliers;
|
|
@MockBean
|
|
private FeePaymentSnapshotValidator paymentSnapshots;
|
|
@Resource
|
|
private FeeItemService feeItems;
|
|
@Resource
|
|
private InboundFeeDetailService details;
|
|
|
|
@BeforeEach
|
|
void paymentSnapshot() {
|
|
lenient().when(paymentSnapshots.validate(anyLong(), any(), any(), any(), any(), any(), any()))
|
|
.thenAnswer(invocation -> new FeePaymentSnapshotValidator.Snapshot(
|
|
invocation.getArgument(2), invocation.getArgument(3), invocation.getArgument(4),
|
|
invocation.getArgument(5), invocation.getArgument(6) == null ? List.of() : invocation.getArgument(6)));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("客户B不能删除客户A的费用明细")
|
|
void rejectsCrossCustomerFeeDetailDelete() {
|
|
assertThatThrownBy(() -> details.delete(10002L, 21L, 31L))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("费用明细不存在");
|
|
|
|
verify(repository).findDetail(10002L, 21L, 31L);
|
|
verify(repository, never()).deleteDetail(10002L, 21L, 31L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("待审核费用明细按客户和入库单作用域删除")
|
|
void deletesPendingFeeDetailInInboundScope() {
|
|
InboundFeeDetailEntity pending = new InboundFeeDetailEntity();
|
|
pending.setReviewStatus("PENDING");
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(pending);
|
|
when(repository.deleteDetail(10001L, 21L, 31L)).thenReturn(1);
|
|
|
|
details.delete(10001L, 21L, 31L);
|
|
|
|
verify(repository).deleteDetail(10001L, 21L, 31L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("费用项目列表返回分页结果")
|
|
void pagesItems() {
|
|
PageResult<FeeItemEntity> expected = new PageResult<>(List.of(), 2, 20, 21, 2);
|
|
when(repository.listItems("仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
|
|
.thenReturn(expected);
|
|
|
|
assertThat(feeItems.list("仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
|
|
.isSameAs(expected);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("新增费用项目保存原型字段")
|
|
void createsItem() {
|
|
doAnswer(invocation -> {
|
|
((FeeItemEntity) invocation.getArgument(0)).setId(11L);
|
|
return null;
|
|
}).when(repository).insertItem(any());
|
|
|
|
FeeItemEntity created = feeItems.create(9L, "admin", itemRequest());
|
|
|
|
assertThat(created.getId()).isEqualTo(11L);
|
|
assertThat(created.getAllocationFee()).isTrue();
|
|
assertThat(created.getDefaultPrice()).isEqualByComparingTo("60.00");
|
|
assertThat(created.getWarehouseReceivable()).isTrue();
|
|
assertThat(created.getWarehousePayable()).isTrue();
|
|
assertThat(created.getSupplierCategory()).isNull();
|
|
assertThat(created.getFeeCode()).startsWith("MANUAL_");
|
|
assertThat(created.getTenantId()).isZero();
|
|
assertThat(created.getCreatedByName()).isEqualTo("admin");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("自定义费用编码统一转为大写并检查客户内唯一性")
|
|
void normalizesCustomFeeCode() {
|
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
|
"storage_fee", null, "仓库租赁费", false, false, true, true, true, "应付仓库租赁费", null);
|
|
|
|
FeeItemEntity created = feeItems.create(9L, "admin", request);
|
|
|
|
verify(repository).itemCodeExists("STORAGE_FEE", null);
|
|
assertThat(created.getFeeCode()).isEqualTo("STORAGE_FEE");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("并发创建相同费用编码返回业务冲突")
|
|
void rejectsConcurrentDuplicateFeeCode() {
|
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
|
"STORAGE_FEE", null, "仓库租赁费", false, false, true, true, true, null, null);
|
|
doThrow(new DuplicateKeyException("duplicate fee code")).when(repository).insertItem(any());
|
|
|
|
assertThatThrownBy(() -> feeItems.create(9L, "admin", request))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("费用项目编码已存在");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("编辑时省略费用编码保留原稳定编码")
|
|
void preservesFeeCodeWhenUpdateOmitsIt() {
|
|
FeeItemEntity existing = item(true, true);
|
|
existing.setFeeCode("STORAGE_FEE");
|
|
when(repository.findItem(11L)).thenReturn(existing);
|
|
when(repository.updateItem(any())).thenReturn(1);
|
|
|
|
FeeItemEntity updated = feeItems.update(11L, 9L, "admin", itemRequest());
|
|
|
|
assertThat(updated.getFeeCode()).isEqualTo("STORAGE_FEE");
|
|
verify(repository, never()).itemCodeExists("STORAGE_FEE", 11L);
|
|
}
|
|
|
|
/** 清空默认价只修改公共项目,不触碰已生成的费用。 */
|
|
@Test
|
|
void clearsDefaultPrice() {
|
|
FeeItemEntity existing = item(true, true);
|
|
existing.setDefaultPrice(new BigDecimal("60.00"));
|
|
when(repository.findItem(11L)).thenReturn(existing);
|
|
when(repository.updateItem(any())).thenReturn(1);
|
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
|
null, null, "仓库租赁费", false, false, true, true, true, null, null);
|
|
|
|
assertThat(feeItems.update(11L, 9L, "admin", request).getDefaultPrice()).isNull();
|
|
verify(repository).updateItem(existing);
|
|
}
|
|
|
|
/** 请求价格允许空值和零元,拒绝负数、过多小数和超出字段范围的金额。 */
|
|
@Test
|
|
void validatesDefaultPrice() {
|
|
try (var factory = jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
|
var validator = factory.getValidator();
|
|
for (String price : List.of("0", "60.25", "9999999999.99")) {
|
|
var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null,
|
|
"操作费", false, true, false, false, false, null, new BigDecimal(price));
|
|
assertThat(validator.validate(request)).isEmpty();
|
|
}
|
|
for (String price : List.of("-1", "60.255", "10000000000")) {
|
|
var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null,
|
|
"操作费", false, true, false, false, false, null, new BigDecimal(price));
|
|
assertThat(validator.validate(request)).isNotEmpty();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("任一客户引用公共费用项目后都禁止删除")
|
|
void rejectsDeletingItemBoundByAnyCustomer() {
|
|
when(repository.findItem(11L)).thenReturn(item(true, true));
|
|
when(repository.countBindings(11L)).thenReturn(1L);
|
|
|
|
assertThatThrownBy(() -> feeItems.delete(11L))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("已被入库费用明细使用");
|
|
|
|
verify(repository, never()).deleteItem(11L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("入库费用绑定项目并生成订单快照")
|
|
void bindsInboundFee() {
|
|
InboundOrderEntity inbound = inbound();
|
|
FeeItemEntity item = item(true, true);
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
|
when(repository.findItem(11L)).thenReturn(item);
|
|
doAnswer(invocation -> {
|
|
((InboundFeeDetailEntity) invocation.getArgument(0)).setId(31L);
|
|
return null;
|
|
}).when(repository).insertDetail(any());
|
|
|
|
InboundFeeDetailEntity created = details.create(10001L, 21L, 9L,
|
|
detailRequest("RECEIVABLE"));
|
|
|
|
assertThat(created.getId()).isEqualTo(31L);
|
|
assertThat(created.getInboundCode()).isEqualTo("RK-20260805-001");
|
|
assertThat(created.getFeeTypeNameSnapshot()).isEqualTo("仓库租赁费");
|
|
assertThat(created.getActualGrossWeightKg()).isEqualByComparingTo("10.5000");
|
|
assertThat(created.getReviewStatus()).isEqualTo("PENDING");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("发布计费快照生成客户与后台共用的入库运费明细")
|
|
void publishesSharedFreightDetail() {
|
|
InboundOrderEntity inbound = inbound();
|
|
inbound.setCustomerId(101L);
|
|
inbound.setCustomerName("客户A");
|
|
inbound.setCompanyName("客户A有限公司");
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
|
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
|
FeeItemEntity freightItem = item(true, false);
|
|
freightItem.setId(11L);
|
|
freightItem.setFeeCode("FBA_HEAD_FREIGHT");
|
|
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(freightItem);
|
|
|
|
InboundFeeEntity fee = new InboundFeeEntity();
|
|
fee.setId(501L);
|
|
fee.setInboundId(21L);
|
|
fee.setFeeNo("FBAF-21-1");
|
|
fee.setFeeVersion(1);
|
|
fee.setFeeStatus("SENT");
|
|
fee.setActualWeightKg(new BigDecimal("100.0000"));
|
|
fee.setActualVolumeCbm(new BigDecimal("1.000000"));
|
|
fee.setChargeableQuantity(new BigDecimal("167.0000"));
|
|
fee.setChargeUnit("KG");
|
|
fee.setCurrency("USD");
|
|
fee.setTotalAmount(new BigDecimal("354.00"));
|
|
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
|
InboundFeeItemEntity base = new InboundFeeItemEntity();
|
|
base.setUnitPrice(new BigDecimal("2.0000"));
|
|
when(pricingRepository.findBaseItem(10001L, 501L)).thenReturn(base);
|
|
|
|
LocalDateTime publishedAt = LocalDateTime.of(2026, 8, 5, 12, 0);
|
|
InboundFeeDetailEntity result = details.publishFreight(
|
|
10001L, 21L, 501L, 9L, "admin", publishedAt);
|
|
|
|
ArgumentCaptor<InboundFeeDetailEntity> captor = ArgumentCaptor.forClass(InboundFeeDetailEntity.class);
|
|
verify(repository).insertDetail(captor.capture());
|
|
InboundFeeDetailEntity saved = captor.getValue();
|
|
assertThat(result).isSameAs(saved);
|
|
assertThat(saved.getFeeItemId()).isEqualTo(11L);
|
|
assertThat(saved.getSourceFeeId()).isEqualTo(501L);
|
|
assertThat(saved.getDirection()).isEqualTo("RECEIVABLE");
|
|
assertThat(saved.getAmount()).isEqualByComparingTo("354.00");
|
|
assertThat(saved.getUnitPrice()).isEqualByComparingTo("2.1198");
|
|
assertThat(saved.getReviewStatus()).isEqualTo("APPROVED");
|
|
assertThat(saved.getPublicationStatus()).isEqualTo("PUBLISHED");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("查询费用明细会幂等补齐历史初算缺失的派生行")
|
|
void materializesMissingLatestCalculatedFreightOnList() {
|
|
InboundOrderEntity inbound = inbound();
|
|
inbound.setCustomerId(101L);
|
|
inbound.setCustomerName("客户A");
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
|
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
|
when(repository.listDetails(10001L, 21L, false)).thenReturn(List.of());
|
|
FeeItemEntity freightItem = item(true, false);
|
|
freightItem.setFeeCode("FBA_HEAD_FREIGHT");
|
|
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(freightItem);
|
|
|
|
InboundFeeEntity fee = new InboundFeeEntity();
|
|
fee.setId(501L); fee.setInboundId(21L); fee.setFeeNo("FBAF-21-2");
|
|
fee.setFeeVersion(2); fee.setFeeStatus("CALCULATED");
|
|
fee.setActualWeightKg(new BigDecimal("100.0000"));
|
|
fee.setActualVolumeCbm(new BigDecimal("1.000000"));
|
|
fee.setChargeableQuantity(new BigDecimal("167.0000"));
|
|
fee.setChargeUnit("KG"); fee.setCurrency("USD");
|
|
fee.setTotalAmount(new BigDecimal("354.00"));
|
|
fee.setCalculatedAt(LocalDateTime.of(2026, 8, 17, 12, 0));
|
|
when(pricingRepository.findLatestFee(10001L, 21L)).thenReturn(fee);
|
|
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
|
|
|
details.list(10001L, 21L, false);
|
|
|
|
verify(repository).insertDetail(any(InboundFeeDetailEntity.class));
|
|
verify(repository).listDetails(10001L, 21L, false);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("系统头程运费项目缺失时拒绝运行期补建")
|
|
void rejectsMissingSystemFreightItem() {
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
|
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
|
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(null);
|
|
|
|
InboundFeeEntity fee = new InboundFeeEntity();
|
|
fee.setId(501L);
|
|
fee.setInboundId(21L);
|
|
fee.setFeeStatus("SENT");
|
|
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
|
|
|
assertThatThrownBy(() -> details.publishFreight(
|
|
10001L, 21L, 501L, 9L, "admin", LocalDateTime.of(2026, 8, 5, 12, 0)))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("FBA头程运费未初始化");
|
|
verify(repository, never()).insertItem(any());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("并发发布同一计费快照时返回已生成的运费明细")
|
|
void reusesConcurrentFreightDetail() {
|
|
InboundFeeDetailEntity concurrent = new InboundFeeDetailEntity();
|
|
concurrent.setId(99L);
|
|
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L))
|
|
.thenReturn(null, concurrent);
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
|
|
|
FeeItemEntity freightItem = item(true, false);
|
|
freightItem.setFeeCode("FBA_HEAD_FREIGHT");
|
|
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(freightItem);
|
|
|
|
InboundFeeEntity fee = new InboundFeeEntity();
|
|
fee.setId(501L);
|
|
fee.setInboundId(21L);
|
|
fee.setFeeNo("FBAF-21-1");
|
|
fee.setFeeVersion(1);
|
|
fee.setFeeStatus("SENT");
|
|
fee.setActualWeightKg(new BigDecimal("100.0000"));
|
|
fee.setActualVolumeCbm(new BigDecimal("1.000000"));
|
|
fee.setChargeableQuantity(new BigDecimal("1.0000"));
|
|
fee.setChargeUnit("CBM");
|
|
fee.setCurrency("USD");
|
|
fee.setTotalAmount(new BigDecimal("200.00"));
|
|
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
|
doThrow(new DuplicateKeyException("duplicate source fee"))
|
|
.when(repository).insertDetail(any());
|
|
|
|
InboundFeeDetailEntity result = details.publishFreight(
|
|
10001L, 21L, 501L, 9L, "admin", LocalDateTime.of(2026, 8, 5, 12, 0));
|
|
|
|
assertThat(result).isSameAs(concurrent);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("拒绝绑定未启用仓储应收的项目")
|
|
void rejectsInapplicableItem() {
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
|
when(repository.findItem(11L)).thenReturn(item(false, true));
|
|
|
|
assertThatThrownBy(() -> details.create(10001L, 21L, 9L,
|
|
detailRequest("RECEIVABLE")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("仓储应收/应付");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("私人仓尾程派送费只允许PRIVATE目的地")
|
|
void restrictsPrivateLastMileDeliveryFeeByDestinationType() {
|
|
InboundOrderEntity fbaInbound = inbound();
|
|
fbaInbound.setDestinationType("FBA");
|
|
FeeItemEntity privateDelivery = item(true, true);
|
|
privateDelivery.setFeeCode("PRIVATE_LAST_MILE_DELIVERY");
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(fbaInbound);
|
|
when(repository.findItem(11L)).thenReturn(privateDelivery);
|
|
|
|
assertThatThrownBy(() -> details.create(10001L, 21L, 9L,
|
|
detailRequest("RECEIVABLE")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("目的地类型为PRIVATE");
|
|
verify(repository, never()).insertDetail(any());
|
|
|
|
fbaInbound.setDestinationType("PRIVATE");
|
|
doAnswer(invocation -> { ((InboundFeeDetailEntity) invocation.getArgument(0)).setId(32L); return null; })
|
|
.when(repository).insertDetail(any());
|
|
assertThat(details.create(10001L, 21L, 9L, detailRequest("RECEIVABLE")).getId())
|
|
.isEqualTo(32L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("审核只更新待审核明细")
|
|
void approvesPendingDetail() {
|
|
InboundFeeDetailEntity pending = new InboundFeeDetailEntity();
|
|
pending.setId(31L);
|
|
pending.setTenantId(10001L);
|
|
pending.setInboundId(21L);
|
|
pending.setFeeItemId(11L);
|
|
pending.setDirection("RECEIVABLE");
|
|
pending.setSourceType("MANUAL");
|
|
pending.setSourceFeeVersion(1);
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(pending);
|
|
when(repository.findItem(11L)).thenReturn(item(true, true));
|
|
when(repository.updateDetail(pending)).thenReturn(1);
|
|
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
|
|
|
details.approve(10001L, 21L, 31L, 9L, "admin");
|
|
|
|
verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
|
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
|
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("审核通过的人工费用自动写入Finance事件")
|
|
void publishesApprovedSettlementFee() {
|
|
InboundFeeDetailEntity approved = new InboundFeeDetailEntity();
|
|
approved.setId(31L);
|
|
approved.setTenantId(10001L);
|
|
approved.setInboundId(21L);
|
|
approved.setFeeItemId(11L);
|
|
approved.setDirection("RECEIVABLE");
|
|
approved.setSourceType("MANUAL");
|
|
approved.setSourceFeeVersion(1);
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
|
when(repository.findItem(11L)).thenReturn(item(true, true));
|
|
when(repository.updateDetail(approved)).thenReturn(1);
|
|
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
|
|
|
details.approve(10001L, 21L, 31L, 9L, "admin");
|
|
|
|
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
|
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("自动头程运费派生明细不能在费用明细中二次审核")
|
|
void rejectsReviewingDerivedFreight() {
|
|
InboundFeeDetailEntity derived = new InboundFeeDetailEntity();
|
|
derived.setSourceType("FBA_PRICING");
|
|
derived.setReviewStatus("PENDING");
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(derived);
|
|
|
|
assertThatThrownBy(() -> details.approve(10001L, 21L, 31L, 9L, "admin"))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("只读派生");
|
|
|
|
verify(repository, never()).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("已审核人工费用调整保留旧版本并创建待审核新版本")
|
|
void versionsApprovedManualAdjustment() {
|
|
InboundFeeDetailEntity approved = new InboundFeeDetailEntity();
|
|
approved.setId(31L);
|
|
approved.setTenantId(10001L);
|
|
approved.setInboundId(21L);
|
|
approved.setSourceType("MANUAL");
|
|
approved.setSourceFeeVersion(1);
|
|
approved.setReviewStatus("APPROVED");
|
|
approved.setPublicationStatus("PUBLISHED");
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
|
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
|
when(repository.findItem(11L)).thenReturn(item(true, true));
|
|
when(repository.supersedeApprovedManual(eq(10001L), eq(21L), eq(31L),
|
|
eq(1), eq(9L), any(LocalDateTime.class))).thenReturn(1);
|
|
doAnswer(invocation -> {
|
|
((InboundFeeDetailEntity) invocation.getArgument(0)).setId(32L);
|
|
return null;
|
|
}).when(repository).insertDetail(any());
|
|
|
|
InboundFeeDetailEntity next = details.adjust(
|
|
10001L, 21L, 31L, 1, 9L, detailRequest("RECEIVABLE"));
|
|
|
|
assertThat(next.getId()).isEqualTo(32L);
|
|
assertThat(next.getSourceFeeId()).isEqualTo(31L);
|
|
assertThat(next.getSourceFeeVersion()).isEqualTo(2);
|
|
assertThat(next.getReviewStatus()).isEqualTo("PENDING");
|
|
assertThat(next.getPublicationStatus()).isEqualTo("DRAFT");
|
|
verify(repository).insertDetail(next);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("作废已入池人工费用引用当前版本并发布VOIDED")
|
|
void voidsPublishedSettlementFee() {
|
|
InboundFeeDetailEntity current = new InboundFeeDetailEntity();
|
|
current.setId(31L);
|
|
current.setTenantId(10001L);
|
|
current.setInboundId(21L);
|
|
current.setFeeItemId(11L);
|
|
current.setSourceType("MANUAL");
|
|
current.setSourceFeeVersion(1);
|
|
InboundFeeDetailEntity voided = new InboundFeeDetailEntity();
|
|
voided.setId(31L);
|
|
voided.setTenantId(10001L);
|
|
voided.setInboundId(21L);
|
|
voided.setFeeItemId(11L);
|
|
voided.setSourceType("MANUAL");
|
|
voided.setSourceFeeVersion(1);
|
|
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(current, voided);
|
|
when(repository.voidApproved(10001L, 21L, 31L, 1, 9L)).thenReturn(1);
|
|
|
|
details.voidApproved(10001L, 21L, 31L, 1, 9L);
|
|
|
|
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.VOIDED),
|
|
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
|
}
|
|
|
|
/** 构造带默认价格的公共费用项目。 */
|
|
private static FeeItemDtos.SaveRequest itemRequest() {
|
|
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", null, null, "仓库租赁费",
|
|
false, false, true, true, true, "应付仓库租赁费", new BigDecimal("60.00"));
|
|
}
|
|
|
|
private static InboundFeeDetailDtos.SaveRequest detailRequest(String direction) {
|
|
return new InboundFeeDetailDtos.SaveRequest(11L, direction, LocalDate.of(2026, 8, 5),
|
|
new BigDecimal("12.3456"), "CNY", new BigDecimal("123.46"), "联调费用");
|
|
}
|
|
|
|
private static FeeItemEntity item(boolean receivable, boolean payable) {
|
|
FeeItemEntity item = new FeeItemEntity();
|
|
item.setId(11L);
|
|
item.setCategory("仓储成本");
|
|
item.setTradeScope("INTERNATIONAL");
|
|
item.setFeeTypeName("仓库租赁费");
|
|
item.setWarehouseReceivable(receivable);
|
|
item.setWarehousePayable(payable);
|
|
item.setStatus("ACTIVE");
|
|
return item;
|
|
}
|
|
|
|
private static InboundOrderEntity inbound() {
|
|
InboundOrderEntity inbound = new InboundOrderEntity();
|
|
inbound.setId(21L);
|
|
inbound.setInboundCode("RK-20260805-001");
|
|
inbound.setChannelCode("SEA");
|
|
inbound.setChannelName("海运");
|
|
inbound.setDestinationCode("ONT8");
|
|
inbound.setDestinationName("ONT8");
|
|
inbound.setTotalPackageCount(10);
|
|
inbound.setActualGrossWeightKg(new BigDecimal("10.5000"));
|
|
inbound.setActualNetWeightKg(new BigDecimal("10.5000"));
|
|
inbound.setActualVolumeCbm(new BigDecimal("1.250000"));
|
|
inbound.setVolumeWeightKg(new BigDecimal("208.3333"));
|
|
inbound.setChargeableWeightKg(new BigDecimal("208.3333"));
|
|
inbound.setChargeableVolumeCbm(new BigDecimal("1.250000"));
|
|
return inbound;
|
|
}
|
|
}
|