费用提交

This commit is contained in:
2026-10-28 11:29:38 +08:00
parent ca61f79b6d
commit 96ffc0925a
10 changed files with 180 additions and 9 deletions
@@ -1055,8 +1055,8 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
boolean localTruck="LOCAL_TRUCK".equals(row.getTaskType()) && "ACTIVE".equals(row.getStatus());
for(var action:Action.values())if(action!=Action.ARRANGE_DELIVERY && action!=Action.DEPART) {
boolean state=(pickup || localTruck) && switch(action) {
case EDIT -> Set.of("BEFORE_LOADING","LOADING").contains(row.getStage());
case CANCEL -> localTruck ? "BEFORE_LOADING".equals(row.getStage()) : Set.of("BEFORE_LOADING","LOADING").contains(row.getStage());
case EDIT -> pickup ? "BEFORE_LOADING".equals(row.getStage()) && !"APPROVED".equals(row.getReviewState()) : Set.of("BEFORE_LOADING","LOADING").contains(row.getStage());
case CANCEL -> "BEFORE_LOADING".equals(row.getStage()) && !"APPROVED".equals(row.getReviewState());
case SUBMIT_REVIEW -> "BEFORE_LOADING".equals(row.getStage()) && canSubmitReview(row.getReviewState());
case DISPATCH_REVIEW -> "DISPATCH_PENDING".equals(row.getReviewState());
case LOADING_REVIEW -> "LOADING_PENDING".equals(row.getReviewState());
@@ -131,6 +131,7 @@ public class TransportTaskService {
var row=roots.lock(number);
if(!Objects.equals(trip.getRecordVersion(),request.recordVersion()) || request.dispatchRecordVersion()==null
|| !Objects.equals(row.getRecordVersion(),request.dispatchRecordVersion()))throw conflict("排车或审核信息已变化,请刷新后重试");
if("PICKUP".equals(trip.getTripPurpose()) && (!"BEFORE_LOADING".equals(row.getStage()) || "APPROVED".equals(row.getReviewState())))throw conflict("理货审核通过后不能维护车辆");
if(!"PLANNED".equals(trip.getStatus()) || "PICKUP".equals(trip.getTripPurpose()) && pickups.tasks(trip.getTripNo()).stream().anyMatch(task->!"ASSIGNED".equals(task.status())))throw conflict("已开始执行的任务不能更换资源照片");
// 主档照片缺省带入本车;显式本趟照片仍由原归属校验复核。
var supplied=request.reviewPhotos();
@@ -179,10 +180,10 @@ public class TransportTaskService {
@Transactional
public TransportTaskModels.Detail cancel(String number,String operator,FleetTripModels.Cancel request) {
var trip=transport(number,true);
// 本土卡车理货通过即进入装车,后续阶段禁止撤销运输安排。
if("LOCAL_TRUCK".equals(trip.getTripPurpose())) {
// 运输任务理货通过即进入装车,后续阶段禁止撤销运输安排。
if(Set.of("PICKUP","LOCAL_TRUCK").contains(trip.getTripPurpose())) {
var row=roots.lock(number);
if(row==null || !"BEFORE_LOADING".equals(row.getStage()))throw conflict("装车中及后续阶段不能取消排车");
if(row==null || !"BEFORE_LOADING".equals(row.getStage()) || "APPROVED".equals(row.getReviewState()))throw conflict("理货审核通过及后续阶段不能取消排车");
}
trips.cancel(trip.getTripNo(),operator,request);return detail(number);
}
@@ -109,7 +109,7 @@ public final class LastlegOrderRequests {
@Min(1) Long supplierId,
@Size(max = 200) String supplierName,
@Size(max = 32) String supplierCategory,
@Pattern(regexp = "ON_ACCOUNT|DIRECT_PAID") String settlementMode,
@Pattern(regexp = "BILL|WALLET|ON_ACCOUNT|DIRECT_PAID") String settlementMode,
LocalDateTime paidAt,
@Pattern(regexp = "ONLINE|BANK_TRANSFER|CASH|OTHER") String paymentMethod,
@Size(max = 128) String paymentReferenceNo,
@@ -14,6 +14,26 @@ import static org.mockito.ArgumentMatchers.*;
/** 创建时间公共投影、实际货件来源及司机换证保存的直接回归。 */
class DispatchListAndResourceRegressionTest {
/** 取货理货通过后动作快照关闭资源维护和取消,不受尚未上传装货照片影响。 */
@Test void pickupActionsLockResourcesAndCancellationAfterApproval() throws Exception {
var f=new DispatchAggregatePersistenceTest.Fixture();
var row=new com.baiyue.lastleg.dispatch.entity.DispatchAggregate();
row.setTaskType("PICKUP");row.setStatus("ACTIVE");row.setStage("BEFORE_LOADING");row.setReviewState("DRAFT");
var security=org.springframework.security.core.context.SecurityContextHolder.getContext();var previous=security.getAuthentication();
security.setAuthentication(new org.springframework.security.authentication.UsernamePasswordAuthenticationToken("operator-test","",List.of(
new org.springframework.security.core.authority.SimpleGrantedAuthority("platform:overseas-warehouse:dispatch:edit"),
new org.springframework.security.core.authority.SimpleGrantedAuthority("platform:overseas-warehouse:dispatch:cancel"))));
try {
assertThat(f.service.actions(f.actor,row).get(Action.EDIT).allowed()).isTrue();
assertThat(f.service.actions(f.actor,row).get(Action.CANCEL).allowed()).isTrue();
row.setReviewState("APPROVED");
for(var stage:Stage.values()) {
row.setStage(stage.name());
assertThat(f.service.actions(f.actor,row).get(Action.EDIT).allowed()).isFalse();
assertThat(f.service.actions(f.actor,row).get(Action.CANCEL).allowed()).isFalse();
}
} finally { security.setAuthentication(previous); }
}
/** 各阶段、审核分区和任务类型均取同一主单创建时间,不借用计划或更新时间。 */
@Test void creationTimeIsPreservedAcrossAllListScopes() throws Exception {
var f=new DispatchAggregatePersistenceTest.Fixture();
@@ -22,6 +22,26 @@ class TransportTaskServiceTest {
DispatchAggregateRepository roots;TransportTaskRepository links;FleetTripRepository tripRepo;FleetTripService trips;
UsTruckOrderRepository trucks;TripOperationsEvidenceGateway files;TransportTaskService service;FleetTripEntity trip;UsTruckOrder order;
final Map<String,String> commands=new HashMap<>();
/** 理货通过后的旧页面不能更换资源或取消取货,尚未上传装货照片也不能例外。 */
@Test void approvedPickupRejectsResourceChangesAndCancellation() {
var root=roots.find("PC1");root.setTaskType("PICKUP");root.setRecordVersion(9L);root.setReviewState("APPROVED");
when(roots.lock("PC1")).thenReturn(root);
trip.setTripPurpose("PICKUP");trip.setStatus("PLANNED");
for(var stage:List.of("LOADING","AFTER_LOADING","DELIVERING","DELIVERED")) {
root.setStage(stage);
assertThatThrownBy(()->service.assign("PC1","OP",new TransportTaskModels.Assign(3L,7L,8L,"保存",9L,null,null))).hasMessageContaining("不能维护车辆");
assertThatThrownBy(()->service.cancel("PC1","OP",new FleetTripModels.Cancel(3L,"取消"))).hasMessageContaining("不能取消排车");
}
verifyNoInteractions(trips,files);
}
/** 审核前仍可取消,沿用原运输取消与审计路径。 */
@Test void pickupBeforeLoadingCanStillCancel() {
var root=roots.find("PC1");root.setTaskType("PICKUP");root.setStage("BEFORE_LOADING");root.setReviewState("DRAFT");
when(roots.lock("PC1")).thenReturn(root);trip.setTripPurpose("PICKUP");trip.setStatus("PLANNED");
doThrow(new IllegalStateException("已进入运输取消")).when(trips).cancel(eq("TR1"),eq("OP"),any());
assertThatThrownBy(()->service.cancel("PC1","OP",new FleetTripModels.Cancel(3L,"取消"))).hasMessage("已进入运输取消");
verify(trips).cancel(eq("TR1"),eq("OP"),any());
}
/** 本土卡车审核仍拦截执行中任务、无效时间和未维护车辆,不因分流而放松门禁。 */
@Test void localTruckReviewRejectsInvalidExecutionTimeAndMissingResources() {
assertThatThrownBy(()->service.verifyLocalTruckReview("PC1")).hasMessageContaining("未发车");
@@ -0,0 +1,45 @@
package com.baiyue.lastleg.dto;
import jakarta.validation.Validation;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class LastlegOrderRequestsTest {
/** 入口接受应收、应付及默认结算方式,方向组合仍由费用服务校验。 */
@ParameterizedTest
@NullSource
@ValueSource(strings = {"BILL", "WALLET", "ON_ACCOUNT", "DIRECT_PAID"})
void acceptsSupportedSettlementModes(String settlementMode) {
try (var factory = Validation.buildDefaultValidatorFactory()) {
assertTrue(factory.getValidator().validate(fee(settlementMode)).isEmpty());
}
}
/** 未知或空白结算代码必须在请求入口拒绝。 */
@ParameterizedTest
@ValueSource(strings = {"", "CASH", "wallet"})
void rejectsUnsupportedSettlementModes(String settlementMode) {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var violations = factory.getValidator().validate(fee(settlementMode));
assertEquals(1, violations.size());
assertEquals("settlementMode", violations.iterator().next().getPropertyPath().toString());
}
}
/** 构造订单详情提交的有效费用,覆盖截图中的美元余额扣费请求。 */
private static LastlegOrderRequests.CreateFee fee(String settlementMode) {
boolean payable = "ON_ACCOUNT".equals(settlementMode) || "DIRECT_PAID".equals(settlementMode);
return new LastlegOrderRequests.CreateFee(
"FEE_TYPE_071", "装车费", null, payable ? "PAYABLE" : "RECEIVABLE", "USD",
new BigDecimal("10.00"), LocalDate.of(2026, 10, 28),
null, null, null, settlementMode, null, null, null, null,
BigDecimal.ONE, "天", null, null, null);
}
}