Files
baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java
T

142 lines
12 KiB
Java
Raw Normal View History

2026-10-19 17:56:36 +08:00
package com.baiyue.lastleg.delivery;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.lastleg.client.FinanceFeeCoverageClient;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.model.DeliveryCargoGroups;
import com.baiyue.lastleg.delivery.repository.DispatchDeliveryRepository;
import com.baiyue.lastleg.delivery.service.DispatchDeliveryFees;
import com.baiyue.lastleg.dispatch.port.DispatchContracts.CargoLine;
2026-10-20 10:35:52 +08:00
import com.baiyue.lastleg.dispatch.port.DispatchContracts.SourceOrigin;
2026-10-19 17:56:36 +08:00
import com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos;
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
import com.baiyue.lastleg.fleet.trip.FleetBookingFreight;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import java.util.List;
import static com.baiyue.lastleg.delivery.DeliveryDispatchFixtures.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** 新整车收费逻辑直接验证真实客户、明确覆盖、零约定和逐组归属。 */
class DispatchDeliveryFeesTest {
private final FleetBookingFreight external=mock(FleetBookingFreight.class);
private final FinanceFeeCoverageClient finance=mock(FinanceFeeCoverageClient.class);
private final OverseasFeeSourceService sources=mock(OverseasFeeSourceService.class);
private final DispatchDeliveryRepository repository=mock(DispatchDeliveryRepository.class);
private final DeliveryCargoGroups grouping=new DeliveryCargoGroups(new ObjectMapper().findAndRegisterModules());
private final DispatchDeliveryFees service=new DispatchDeliveryFees(external,finance,sources,grouping,repository,"offline-test-token");
/** A客户原费不能免除B客户;全部覆盖只禁止应收,应付仍独立。 */
@Test void coverageRequiresCurrentCustomerVersionAndKeepsPayablesIndependent() {
var row=firstleg(cargo(1,"101","ONT8",51,2));var group=grouping.groups(context(List.of(row),false)).get(0);
var fee=original(101);when(finance.sources(anyString(),any())).thenReturn(ApiResponse.success(List.of(fee)));
var input=input(group,List.of(new CoverageSelection(71L,1,"FEE-HASH",List.of("DELIVERY_FEE"))),List.of());
var evidence=service.coverage(group,input,"OP",ordinary());
service.validate(group,input,evidence,ordinary());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.validate(group,input(group,input.feeCoverage(),List.of(fee("RECEIVABLE"))),evidence,ordinary()));
service.validate(group,input(group,input.feeCoverage(),List.of(fee("PAYABLE"))),evidence,ordinary());
when(finance.sources(anyString(),any())).thenReturn(ApiResponse.success(List.of(original(102))));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.coverage(group,input,"OP",ordinary()));
when(finance.sources(anyString(),any())).thenReturn(ApiResponse.success(List.of(fee)));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.coverage(group,
input(group,List.of(new CoverageSelection(71L,2,"FEE-HASH",List.of("DELIVERY_FEE"))),List.of()),"OP",ordinary()));
}
/** 无引用时不强制Finance可用,已有外接约定组禁止新增派送应收。 */
2026-10-26 13:41:43 +08:00
@Test void ordinaryAllowsLaterManualFeeAndOldReceivableInputIsRejected() {
2026-10-19 17:56:36 +08:00
var row=cargo(1,"101","ONT8",51,2);var group=grouping.groups(context(List.of(row),false)).get(0);
var input=input(group,List.of(),List.of());
assertNull(service.coverage(group,input,"OP",ordinary()));verifyNoInteractions(finance);
2026-10-26 13:41:43 +08:00
service.validate(group,input,null,ordinary());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.validate(group,input(group,List.of(),List.of(fee("RECEIVABLE"))),null,ordinary()));
2026-10-19 17:56:36 +08:00
var quote=new FleetBookingFreight.Quote("BOOK-1",1,"INC-1","IN-1","客户101","FBA:ONT8","USD",BigDecimal.ZERO,BigDecimal.ONE,"单",BigDecimal.ZERO);
var allExternal=new FleetBookingFreight.Preview("freight",false,List.of(quote),List.of());
service.validate(group,input,null,allExternal);
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.validate(group,input(group,List.of(),List.of(fee("RECEIVABLE"))),null,allExternal));
var mixed=grouping.groups(context(List.of(row,cargo(2,"101","ONT8",51,3)),false)).get(0);
2026-10-26 13:41:43 +08:00
service.validate(mixed,input(mixed,List.of(),List.of()),null,allExternal);
2026-10-19 17:56:36 +08:00
}
/** 正数报价发布到自己的客户目的地,零约定仅保存商业操作不造零费用。 */
2026-10-26 13:41:43 +08:00
@Test void zeroAndPositiveQuotesKeepFactsWithoutGeneratingReceivables() {
2026-10-19 17:56:36 +08:00
var row=cargo(1,"101","ONT8",51,2);var context=context(List.of(row),false);var group=grouping.groups(context).get(0);
var delivery=new DeliveryOrder();delivery.setId(81L);delivery.setDeliveryNo("DL-101");delivery.setCustomerId("101");delivery.setOverseasWarehouseId("1001");
var zero=new FleetBookingFreight.Quote("BOOK-ZERO",1,"INC-1","IN-1","客户101","FBA:ONT8","USD",BigDecimal.ZERO,BigDecimal.ONE,"单",BigDecimal.ZERO);
service.publish(context,group,input(group,List.of(),List.of()),delivery,null,new FleetBookingFreight.Preview("freight",false,List.of(zero),List.of()),"OP","ARR-1");
verify(repository).operation(any());verifyNoInteractions(sources);
var positive=new FleetBookingFreight.Quote("BOOK-POSITIVE",1,"INC-1","IN-1","客户101","FBA:ONT8","USD",BigDecimal.TEN,BigDecimal.ONE,"单",BigDecimal.TEN);
service.publish(context,group,input(group,List.of(),List.of()),delivery,null,new FleetBookingFreight.Preview("freight",false,List.of(positive),List.of()),"OP","ARR-1");
2026-10-26 13:41:43 +08:00
verify(repository,times(2)).operation(any()); verifyNoInteractions(sources);
2026-10-19 17:56:36 +08:00
}
/** 合托两票一笔原费只能覆盖对应来源,不得免掉另一票新增应收。 */
@Test void mixedOriginalSourcesAreBatchedAndPartialCoverageStillRequiresFee() {
var first=firstleg(cargo(1,"101","ONT8",51,2));var raw=firstleg(cargo(2,"101","ONT8",51,3));
var second=new CargoLine(raw.dispatchItemId(),raw.customerId(),raw.customerName(),raw.orderId(),raw.orderNo(),raw.sourceKey(),raw.sourceStage(),
raw.sourceRegion(),"502",raw.inboundCode(),raw.planPalletId(),raw.warehousePalletId(),raw.palletNo(),raw.pickId(),raw.attributionId(),raw.inventoryPositionId(),
2026-10-20 10:35:52 +08:00
raw.receiptId(),raw.returnReceiptId(),raw.returnCartonId(),raw.cartonMark(),raw.relabelNo(),raw.destination(),raw.packages(),raw.productQuantity(),raw.grossWeightKg(),raw.volumeCbm(),raw.batchAllocations(),raw.sourceOrigin());
2026-10-19 17:56:36 +08:00
var group=grouping.groups(context(List.of(first,second),false)).get(0);
when(finance.sources(anyString(),any())).thenReturn(ApiResponse.success(List.of(original(101))));
var input=input(group,List.of(new CoverageSelection(71L,1,"FEE-HASH",List.of("DELIVERY_FEE"))),List.of());
var evidence=service.coverage(group,input,"OP",ordinary());
assertEquals(2,evidence.cargo().size());
verify(finance).sources("offline-test-token",new FinanceFeeCoverageClient.Query(101L,List.of(501L,502L)));
2026-10-26 13:41:43 +08:00
service.validate(group,input,evidence,ordinary());
2026-10-19 17:56:36 +08:00
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.coverage(group,
input(group,List.of(input.feeCoverage().get(0),input.feeCoverage().get(0)),List.of()),"OP",ordinary()));
}
2026-10-20 10:35:52 +08:00
/** 多个换标原货源共组费用只归真实派送,不借第一票原货源或履约单伪造单一归属。 */
@Test void relabelFeeRetainsDeliveryReferenceWithoutInventingOneOrigin() {
var first=new SourceOrigin(10001L,201L,301L,401L,701L,"IN-701","CODE-701",null);
var second=new SourceOrigin(10002L,202L,302L,402L,702L,"IN-702","CODE-702",null);
var context=context(List.of(relabelCargo(1,"101","ONT8",51,2,first),
relabelCargo(3,"101","ONT8",51,3,second)),false);
var group=grouping.groups(context).get(0);
var delivery=new DeliveryOrder();delivery.setId(81L);delivery.setDeliveryNo("DL-101");
delivery.setCustomerId("101");delivery.setOverseasWarehouseId("1001");
2026-10-26 13:41:43 +08:00
service.publish(context,group,input(group,List.of(),List.of(fee("PAYABLE"))),delivery,null,ordinary(),"OP","ARR-RELABEL");
2026-10-20 10:35:52 +08:00
var saved=ArgumentCaptor.forClass(OverseasFeeSourceDtos.SaveRequest.class);
verify(sources).publishApproved(eq("OP"),anyString(),saved.capture());
assertEquals("DELIVERY_ORDER",saved.getValue().businessType());
assertEquals(81L,saved.getValue().businessId());assertEquals("DL-101",saved.getValue().businessNo());
assertEquals("101",saved.getValue().customerId());
assertNull(saved.getValue().orderId());assertNull(saved.getValue().inboundId());assertNull(saved.getValue().inboundCode());
assertEquals(List.of(first,second),group.cargo().stream().map(CargoLine::sourceOrigin).toList());
assertTrue(group.cargo().stream().allMatch(c->c.sourceOrigin().productQuantity()==null&&c.grossWeightKg()==null&&c.volumeCbm()==null));
assertEquals(5,group.packages());assertEquals(1,group.palletCount());
}
2026-10-19 17:56:36 +08:00
/** 只读平台候选接口仍拒绝客户身份,不能借组结果跨客户访问。 */
@Test void readOnlyCoverageControllerRejectsMerchantBeforeQuery() throws Exception {
var delivery=mock(com.baiyue.lastleg.delivery.service.DispatchDeliveryService.class);
var controller=new com.baiyue.lastleg.delivery.controller.PlatformDeliveryFeeCoverageController(delivery);
var merchant=new com.baiyue.common.security.AuthenticatedUser(1L,101L,101L,1L,"test","MERCHANT","WEB",java.util.Set.of());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->controller.preview(merchant,"DP-10"));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->controller.preview(null,"DP-10"));
var method=controller.getClass().getMethod("preview",com.baiyue.common.security.AuthenticatedUser.class,String.class);
assertTrue(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value().contains("dispatch:list"));
verifyNoInteractions(delivery);
}
/** 构造当前客户原费用证据。 */
private FinanceFeeCoverageClient.Fee original(long customer) { return new FinanceFeeCoverageClient.Fee(71L,72L,"INBOUND_FEE",1,customer,501L,"IN-1","FIRSTLEG","原费用","CNY",BigDecimal.TEN,"FEE-HASH","AVAILABLE"); }
/** 真实头程关联只在来源行声明,不从展示编号推断。 */
private CargoLine firstleg(CargoLine c) { return new CargoLine(c.dispatchItemId(),c.customerId(),c.customerName(),c.orderId(),c.orderNo(),c.sourceKey(),c.sourceStage(),
"FIRSTLEG","501",c.inboundCode(),c.planPalletId(),c.warehousePalletId(),c.palletNo(),c.pickId(),c.attributionId(),c.inventoryPositionId(),c.receiptId(),
2026-10-20 10:35:52 +08:00
c.returnReceiptId(),c.returnCartonId(),c.cartonMark(),c.relabelNo(),c.destination(),c.packages(),c.productQuantity(),c.grossWeightKg(),c.volumeCbm(),c.batchAllocations(),c.sourceOrigin()); }
2026-10-19 17:56:36 +08:00
/** 输入仅使用共同ArrangeGroup,不保留旧单客户协议别名。 */
private ArrangeGroup input(DeliveryCargoGroups.Group group,List<CoverageSelection> coverage,List<FeeItem> fees) {
return new ArrangeGroup(group.key(),group.cargoHash(),DEPARTED,"TRUCK",null,null,"WAREHOUSE_PICKUP","PALLET",null,null,null,null,null,"freight",group.cargoHash(),coverage,fees);
}
/** 无外接约定仍需按普通未覆盖部分收费。 */
private FleetBookingFreight.Preview ordinary() { return new FleetBookingFreight.Preview("freight",true,List.of(),List.of()); }
/** 同一费用类型的应收/应付保留独立规则。 */
private FeeItem fee(String direction) { return new FeeItem("DELIVERY_FEE","派送费",direction,"USD",BigDecimal.TEN,null,null,null,null,null,null,null,null,null,null,null,null,null,null); }
}