费用提交

This commit is contained in:
2026-10-26 13:41:43 +08:00
parent 0fe1d10566
commit 761fe3686e
171 changed files with 8750 additions and 386 deletions
@@ -43,6 +43,7 @@ class OverseasProcurementServiceTest {
@Mock OverseasProcurementOrderMapper orders;
@Mock OverseasProcurementStatusHistoryMapper histories;
@Mock ProcurementWarehouseDirectory warehouseDirectory;
@Mock com.baiyue.lastleg.procurement.repository.ProcurementDirectionRepository directionRepository;
@Mock com.baiyue.lastleg.service.LastlegDocumentNumberGenerator orderNoGenerator;
@Mock OverseasProcurementFileMapper procurementFiles;
@Mock OverseasProcurementFeeMapper procurementFees;
@@ -57,6 +58,7 @@ class OverseasProcurementServiceTest {
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
ReflectionTestUtils.setField(service, "directionRepository", directionRepository);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
ReflectionTestUtils.setField(service, "procurementFiles", procurementFiles);
@@ -190,4 +192,51 @@ class OverseasProcurementServiceTest {
assertEquals("label.pdf",service.downloadFile("100","HC1",42).fileName());
verify(fileGateway).download("100",1,"LABEL",42);
}
@Test void exportReviewPersistsDirectionAndCannotReplayWithChangedDirection() {
var order = order(OverseasProcurementService.PENDING_REVIEW,0,0);
when(orders.lockNo("100","HC1")).thenReturn(order);
when(orders.findNo("100","HC1")).thenReturn(order);
when(histories.listOrder("100",1L)).thenReturn(List.of());
when(warehouseDirectory.required(10L,ProcurementDirection.EXPORT)).thenReturn(
new com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient.ReceivingWarehouse(10L,"SZX","国内采购仓","CN","ACTIVE"));
var request = new OverseasProcurementRequests.Review(0L,"APPROVE","export-review","已核对海外收件资料",10L,ProcurementDirection.EXPORT);
var result = service.review("100","HC1","7",request);
assertEquals(ProcurementDirection.EXPORT,result.summary().procurementDirection());
assertEquals("国内采购仓",result.summary().purchaseWarehouseName());
assertEquals(1L,service.review("100","HC1","7",request).summary().version());
assertThrows(ResponseStatusException.class, () -> service.review("100","HC1","7",
new OverseasProcurementRequests.Review(0L,"APPROVE","export-review","已核对海外收件资料",10L,ProcurementDirection.IMPORT)));
verify(warehouseDirectory,never()).required(10L);
}
@Test void oldRequestHashDoesNotGainNullDirectionField() throws Exception {
var request = new OverseasProcurementRequests.Review(0L,"APPROVE","old-key","checked",10L);
var json = new ObjectMapper().writeValueAsString(request);
assertEquals(false,json.contains("procurementDirection"));
assertEquals(ProcurementDirection.IMPORT,ProcurementDirection.normalize(null));
}
@Test void pendingReviewCannotReverseDirectionOfAnAlreadyRecordedPurchase() {
var order = order(OverseasProcurementService.PENDING_REVIEW,0,0);
when(orders.lockNo("100","HC1")).thenReturn(order);
when(directionRepository.hasStarted(1L)).thenReturn(true);
assertThrows(ResponseStatusException.class, () -> service.review("100","HC1","7",
new OverseasProcurementRequests.Review(0L,"APPROVE","export-after-purchase","checked",10L,ProcurementDirection.EXPORT)));
verify(warehouseDirectory,never()).required(10L,ProcurementDirection.EXPORT);
assertEquals(OverseasProcurementService.PENDING_REVIEW,order.getStatus());
}
@Test void clientCanCreateDomesticPurchaseForAnOverseasRecipient() {
AtomicReference<OverseasProcurementOrder> saved = new AtomicReference<>();
when(orders.insert(any(OverseasProcurementOrder.class))).thenAnswer(invocation -> { saved.set(invocation.getArgument(0)); return 1; });
when(orders.findNo(anyString(),anyString())).thenAnswer(ignored -> saved.get());
when(histories.listOrder(anyString(),any())).thenReturn(List.of());
var detail=service.create("100","7",new OverseasProcurementRequests.Create("Acme","Office","Sample","A",null,10L,
BigDecimal.TEN,"CNY","US, California, test destination","Buyer","+1 1234567890",null,"export-create",ProcurementDirection.EXPORT));
assertEquals(ProcurementDirection.EXPORT,detail.summary().procurementDirection());
assertEquals("US, California, test destination",detail.receivingAddress());
assertEquals(OverseasProcurementService.PENDING_REVIEW,detail.summary().status());
}
}
@@ -81,9 +81,8 @@ class ProcurementFinancePersistenceTest {
var saved=fees.selectById(fee.getId());assertThat(saved.getWalletStatus()).isEqualTo("SETTLED");
assertThat(saved.getSourceFeeId()).isEqualTo(901L);
verify(sources).publishSettled(eq("7"),anyString(),any(),any(),argThat(ref -> ref.commandEventId().equals(settled.commandEventId())));
assertThat(service.sourceLinked(saved)).isFalse();
jdbc.update("INSERT INTO ll_event_outbox VALUES('OVERSEAS_PROCUREMENT_FEE','901','PUBLISHED')");
assertThat(service.sourceLinked(saved)).isTrue();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_event_outbox", Integer.class)).isZero();
}
@Test void fullInboundNeverSettlesEstimateAndActualUsesOriginalHold() {
@@ -205,10 +204,8 @@ class ProcurementFinancePersistenceTest {
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_procurement_wallet_attempt WHERE fee_id=?",Integer.class,fee.getId())).isEqualTo(2);
verify(sources,times(2)).publishSettled(anyString(),anyString(),any(),any(),argThat(ref->ref.commandEventId().equals(result.commandEventId())));
verify(commands,times(2)).publish(anyString(),anyLong(),anyString(),anyString(),any(),anyString(),anyLong());
assertThat(service.sourceLinked(recovered)).isFalse();
assertThatThrownBy(()->service.requireShipmentReady(11)).hasMessageContaining("来源关联");
jdbc.update("INSERT INTO ll_event_outbox VALUES('OVERSEAS_PROCUREMENT_FEE','901','PUBLISHED')");
assertThat(service.sourceLinked(recovered)).isTrue();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_event_outbox", Integer.class)).isZero();
service.requireShipmentReady(11);
}
@@ -260,8 +257,7 @@ class ProcurementFinancePersistenceTest {
assertThatThrownBy(()->service.requireShipmentReady(11)).hasMessageContaining("未决资金");
handle(result(fee,"FREEZE","SUCCEEDED","FROZEN","100"));
handle(result(fees.selectById(fee.getId()),"SETTLE","SUCCEEDED","SETTLED","100"));
assertThatThrownBy(()->service.requireShipmentReady(11)).hasMessageContaining("来源关联");
jdbc.update("INSERT INTO ll_event_outbox VALUES('OVERSEAS_PROCUREMENT_FEE','901','PUBLISHED')");
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_event_outbox", Integer.class)).isZero();
service.requireShipmentReady(11);
tx.execute(s->service.refund("9","PO-1","7",fee.getId(),new BigDecimal("20"),1,"correction","pending-refund"));
assertThatThrownBy(()->service.requireShipmentReady(11)).hasMessageContaining("未决资金");
@@ -65,4 +65,13 @@ class ProcurementQueryServiceTest {
fixture.jdbc.update("INSERT INTO ll_procurement_carton VALUES(201,11,301,'OLD',20,30,40,5,10),(202,11,302,'DRAFT',20,30,40,5,10)");
assertThat(query.customer("9","PO-1").boxes()).isEmpty();
}
@Test void exportProjectionExposesDirectionAndLabelsWithoutRewritingHistoricalLegs() {
fixture.jdbc.update("UPDATE ll_overseas_procurement_order SET procurement_direction=? WHERE id=11",ProcurementDirection.EXPORT);
var result=query.platform("9","PO-1",Set.of("platform:overseas-business:purchase:receive-overseas"));
assertThat(result.customerView().order().procurementDirection()).isEqualTo(ProcurementDirection.EXPORT);
assertThat(result.customerView().progress()).extracting(ProcurementQueryService.Progress::title).contains("国内收货完成","海外收货完成","海外费用核定");
assertThat(result.actions()).filteredOn(a -> a.key().equals("RECEIVE_OVERSEAS")).singleElement().satisfies(a -> assertThat(a.label()).isEqualTo("登记国内实收"));
}
}
@@ -30,6 +30,7 @@ import static org.mockito.ArgumentMatchers.*;
class ProcurementTransportPersistenceTest {
JdbcTemplate jdbc;TransactionTemplate tx;OverseasProcurementOrderMapper orders;
ProcurementWarehouseDirectory warehouses;
ProcurementTransportService service;ProcurementFinanceService finance;OverseasProcurementFee estimate;
final Address warehouse=new Address("Overseas","1234567890","US operating address");
final Address domestic=new Address("Domestic","13800000000","Shanghai receiving address");
@@ -60,7 +61,7 @@ class ProcurementTransportPersistenceTest {
jdbc.update("INSERT INTO ll_procurement_carton VALUES(201,11,301,'C1',20,30,40,6.125,6),(202,11,301,'C2',20,30,40,4.125,4)");
jdbc.update("INSERT INTO ll_procurement_carton_source VALUES(201,101,6),(202,101,4)");
jdbc.update("INSERT INTO ll_overseas_procurement_file(id,order_id,customer_id,category,file_id,file_name,content_type,size_bytes,created_by) VALUES(501,11,'9','LABEL',1001,'labels.pdf','application/pdf',10,'7'),(502,11,'9','EXCEPTION',1002,'proof.pdf','application/pdf',10,'7')");
var warehouses=mock(ProcurementWarehouseDirectory.class);when(warehouses.sender(91)).thenReturn(warehouse);
warehouses=mock(ProcurementWarehouseDirectory.class);when(warehouses.sender(91)).thenReturn(warehouse);
finance=mock(ProcurementFinanceService.class);var payment=new OverseasProcurementFee();payment.setWalletStatus("SETTLED");
when(finance.current(11,ProcurementFeePurpose.SAMPLE_PAYMENT,"SAMPLE_PAYMENT")).thenReturn(payment);when(finance.sourceLinked(payment)).thenReturn(true);
estimate=new OverseasProcurementFee();estimate.setId(801L);estimate.setAmount(BigDecimal.ZERO);estimate.setWalletStatus("ZERO");
@@ -172,4 +173,32 @@ class ProcurementTransportPersistenceTest {
private void apply(){tx.execute(s->service.applyLabels("9","PO-1","7",new ApplyLabels(version(),"apply-"+version(),labelVersion(Leg.OVERSEAS),List.of(201L,202L),start.plusMinutes(30),"attached")));}
private Ship shipRequest(Leg leg,String key){return new Ship(version(),key,leg,labelVersion(leg),List.of(201L,202L),start.plusMinutes(leg==Leg.OVERSEAS?60:100),"actual handoff");}
private ProcurementTransportService.Snapshot ship(Leg leg,String key){return tx.execute(s->service.ship("9","PO-1","7",shipRequest(leg,key)));}
@Test void exportUsesDomesticSenderAndOverseasDestinationThenEndsAfterLocalHandoff() {
var sender = new Address("Domestic warehouse","13800000000","Shenzhen domestic origin");
var receiver = new Address("Overseas warehouse","+1 1234567890","Los Angeles overseas receiving");
var finalReceiver = new Address("Overseas customer","+1 9876543210","San Diego customer destination");
jdbc.update("UPDATE ll_overseas_procurement_order SET procurement_direction=?,contact_name=?,contact_phone=?,receiving_address=? WHERE id=11",
ProcurementDirection.EXPORT,finalReceiver.name(),finalReceiver.phone(),finalReceiver.address());
when(warehouses.sender(91,ProcurementDirection.EXPORT)).thenReturn(sender);
tx.execute(s -> service.saveLabels("9","PO-1","7",new Labels(version(),"export-labels",Leg.OVERSEAS,1,receiver,null,List.of(label(201),label(202)),"export manifest")));
review(BigDecimal.ZERO); apply();
assertThat(ship(Leg.OVERSEAS,"export-international").status()).isEqualTo("INTERNATIONAL_TRANSIT");
String snapshot = jdbc.queryForObject("SELECT address_snapshot FROM ll_procurement_shipment WHERE leg='OVERSEAS'",String.class);
assertThat(snapshot).contains(sender.address(),receiver.address()).doesNotContain(warehouse.address());
jdbc.update("INSERT INTO ll_procurement_receipt(id,order_id,customer_id,receipt_no,leg,source_carton_id,good_quantity,damaged_quantity,extra_quantity,received_at,created_by) VALUES(102,11,'9','PR-2','DOMESTIC',201,6,0,0,?,'7'),(103,11,'9','PR-3','DOMESTIC',202,4,0,0,?,'7')",start.plusMinutes(65),start.plusMinutes(65));
jdbc.update("UPDATE ll_procurement_flow SET domestic_received_at=? WHERE order_id=11",start.plusMinutes(70));
tx.execute(s -> service.saveLabels("9","PO-1","7",new Labels(version(),"export-local-labels",Leg.DOMESTIC,1,finalReceiver,null,List.of(label(201),label(202)),"overseas delivery")));
assertThatThrownBy(() -> ship(Leg.DOMESTIC,"before-charges")).hasMessageContaining("费用尚未确认");
jdbc.update("UPDATE ll_procurement_flow SET charges_confirmed_at=? WHERE order_id=11",start.plusMinutes(80));
var request = shipRequest(Leg.DOMESTIC,"export-local");
assertThat(tx.execute(s -> service.ship("9","PO-1","7",request)).status()).isEqualTo("SHIPPED_TO_CUSTOMER");
tx.execute(s -> service.ship("9","PO-1","7",request));
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_procurement_shipment",Integer.class)).isEqualTo(2);
String local = jdbc.queryForObject("SELECT address_snapshot FROM ll_procurement_shipment WHERE leg='DOMESTIC'",String.class);
assertThat(local).contains(receiver.address(),finalReceiver.address());
verify(warehouses,never()).sender(91);
verify(finance).requireShipmentReady(11);
}
}
@@ -32,4 +32,25 @@ class ProcurementWarehouseDirectoryTest {
receiving();when(client.activeWarehouse(91,"test-internal")).thenReturn(ApiResponse.success(new Detail(new Warehouse(92L,9L,"LAX","Other","OVERSEAS","US","ACTIVE","Receiver","1234567890"),List.of(address))));
assertThatThrownBy(()->directory.sender(91)).hasMessageContaining("暂不可用");
}
@Test void domesticDirectionUsesDomesticCatalogAndChecksMasterScopeTypeAndStatus() {
ReflectionTestUtils.setField(directory,"token","test-internal");
when(client.domesticWarehouses("test-internal")).thenReturn(ApiResponse.success(List.of(new DomesticWarehouse(91L,"SZX","深圳仓"))));
assertThat(directory.options(ProcurementDirection.EXPORT)).singleElement().satisfies(w -> assertThat(w.warehouseId()).isEqualTo(91L));
var domesticAddress = new Address("OPERATING","CN","广东","深圳",null,"518000","测试国内作业地址",null);
when(client.activeWarehouse(91,"test-internal")).thenReturn(ApiResponse.success(new Detail(
new Warehouse(91L,0L,"SZX","深圳仓","DOMESTIC","CN","ACTIVE","仓管","13800000000"),List.of(domesticAddress))));
assertThat(directory.sender(91,ProcurementDirection.EXPORT).address()).contains("深圳","测试国内作业地址");
verify(client,never()).receivingWarehouse(anyLong(),anyString());
for (var invalid : List.of(
new Warehouse(91L,0L,"LAX","海外仓","OVERSEAS","US","ACTIVE","仓管","13800000000"),
new Warehouse(91L,9L,"SZX","客户仓","DOMESTIC","CN","ACTIVE","仓管","13800000000"),
new Warehouse(91L,0L,"SZX","停用仓","DOMESTIC","CN","INACTIVE","仓管","13800000000"),
new Warehouse(92L,0L,"SZX","其他仓","DOMESTIC","CN","ACTIVE","仓管","13800000000"))) {
when(client.activeWarehouse(91,"test-internal")).thenReturn(ApiResponse.success(new Detail(invalid,List.of(domesticAddress))));
assertThatThrownBy(() -> directory.required(91,ProcurementDirection.EXPORT)).hasMessageContaining("国内公共仓");
}
assertThatThrownBy(() -> directory.options("INVALID")).hasMessageContaining("方向无效");
}
}