优化
This commit is contained in:
@@ -51,14 +51,13 @@ class FbaPricingContractTest {
|
||||
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
|
||||
.contains("区域阶梯渠道可按平台 FBA 仓或海外中转仓的邮编区域")
|
||||
.contains("不再以 carrierId、业务 customerId 或客户计费规则作为前置")
|
||||
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
|
||||
.contains("仅此动作将费用置为 SENT")
|
||||
.contains("写入 Finance Outbox")
|
||||
.contains("不回退 WAIT_SPLIT 状态")
|
||||
.contains("expectedFeeVersion")
|
||||
.contains("chargeableQuantity")
|
||||
.contains("unitPrice")
|
||||
.doesNotContain("required: [subtotalAmount, additionalAmount, reason]")
|
||||
.doesNotContain("应收")
|
||||
.doesNotContain("应付")
|
||||
.doesNotContain("待客户确认");
|
||||
String customerRules = openapi.substring(
|
||||
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules:"),
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundFeeOpenApiContractTest {
|
||||
@Test
|
||||
void documentsEveryInboundFeeControllerPathAndReadOnlySourceContract() throws Exception {
|
||||
String source = new ClassPathResource("openapi/fba-pricing-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = map(new Yaml().load(source));
|
||||
Map<String, Object> paths = map(document.get("paths"));
|
||||
assertThat(paths).containsKeys(
|
||||
"/api/platform/v1/inbounds/{inboundId}/fees/calculate",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fees/confirm",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fees/send",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fees/latest",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fees/published",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details/{detailId}",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details/{detailId}/approve",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details/{detailId}/reject",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details/{detailId}/adjust",
|
||||
"/api/platform/v1/inbounds/{inboundId}/fee-details/{detailId}/void");
|
||||
assertThat(source).contains("includeHistory", "FBA_PRICING", "MANUAL", "只读派生",
|
||||
"Finance Outbox", "CALCULATED", "CONFIRMED", "SENT", "SUPERSEDED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseAndRequestSchemasTrackActualDtoAndEntityFields() throws Exception {
|
||||
Map<String, Object> document = map(new Yaml().load(new ClassPathResource(
|
||||
"openapi/fba-pricing-v1.yaml").getContentAsString(StandardCharsets.UTF_8)));
|
||||
Map<String, Object> schemas = map(map(document.get("components")).get("schemas"));
|
||||
|
||||
assertThat(properties(schemas, "FeeSnapshotResponse").keySet())
|
||||
.containsExactlyInAnyOrder(recordFields(FbaPricingDtos.FeeSnapshotResponse.class).toArray(String[]::new));
|
||||
assertThat(properties(schemas, "InboundFeeDetailSaveRequest").keySet())
|
||||
.containsExactlyInAnyOrder(recordFields(InboundFeeDetailDtos.SaveRequest.class).toArray(String[]::new));
|
||||
assertThat(properties(schemas, "InboundFeeDetailVersionRequest").keySet())
|
||||
.containsExactlyInAnyOrder(recordFields(InboundFeeDetailDtos.VersionRequest.class).toArray(String[]::new));
|
||||
assertThat(properties(schemas, "InboundFeeDetailRejectRequest").keySet())
|
||||
.containsExactlyInAnyOrder(recordFields(InboundFeeDetailDtos.RejectRequest.class).toArray(String[]::new));
|
||||
|
||||
Set<String> entityFields = new LinkedHashSet<>();
|
||||
for (Class<?> type = InboundFeeDetailEntity.class; type != Object.class; type = type.getSuperclass()) {
|
||||
Arrays.stream(type.getDeclaredFields()).map(Field::getName).forEach(entityFields::add);
|
||||
}
|
||||
assertThat(properties(schemas, "InboundFeeDetailResource").keySet())
|
||||
.containsExactlyInAnyOrder(entityFields.toArray(String[]::new));
|
||||
}
|
||||
|
||||
private static Set<String> recordFields(Class<?> type) {
|
||||
Set<String> fields = new LinkedHashSet<>();
|
||||
Arrays.stream(type.getRecordComponents()).map(RecordComponent::getName).forEach(fields::add);
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static Map<String, Object> properties(Map<String, Object> schemas, String schema) {
|
||||
return map(map(schemas.get(schema)).get("properties"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> map(Object value) {
|
||||
return (Map<String, Object>) value;
|
||||
}
|
||||
}
|
||||
+10
@@ -22,6 +22,16 @@ class InboundOrderStatusSqlContractTest {
|
||||
.contains("process_status = 'INBOUNDING'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsConfirmedWeightNetSumAuthoritativeWhenMeasurementIsApproved() {
|
||||
assertThat(sql("approveMeasurement"))
|
||||
.contains("FROM fl_inbound_weight_result weight")
|
||||
.contains("weight.recognition_status = 'CONFIRMED'")
|
||||
.contains("weight.deleted = 0")
|
||||
.contains("SUM(weight.net_weight_kg)")
|
||||
.contains("ELSE (SELECT COALESCE(SUM(item.total_weight_kg), 0)");
|
||||
}
|
||||
|
||||
private static String sql(String methodName) {
|
||||
Method method = Arrays.stream(InboundOrderMapper.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
|
||||
@@ -223,6 +223,37 @@ class FeeManagementServiceTest {
|
||||
assertThat(saved.getSettlementIncluded()).isTrue();
|
||||
}
|
||||
|
||||
@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() {
|
||||
@@ -321,6 +352,53 @@ class FeeManagementServiceTest {
|
||||
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() {
|
||||
|
||||
+9
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -48,6 +49,14 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:1");
|
||||
}
|
||||
|
||||
@Test void snapshotOnlyPublishesFinalizedLoadedInbounds() throws Exception {
|
||||
String sql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("rows", long.class)
|
||||
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
|
||||
|
||||
assertThat(sql).contains("p.plan_status = 'COMPLETED'")
|
||||
.contains("o.process_status = 'LOADED'");
|
||||
}
|
||||
|
||||
private Row row(long account,long inbound,long detail) {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
|
||||
@@ -176,6 +176,10 @@ class LoadingListServiceTest {
|
||||
verify(repository).appendHistory(eq(10L), eq("PDA-COMPLETE-10-21"),
|
||||
eq("PDA_COMPLETE_LOADING"), eq("LOADING"), eq("COMPLETED"), anyString(), anyString(),
|
||||
eq(9L), eq("operator"), eq("PDA已人工确认装柜完成"));
|
||||
verify(loadingCompletedOutboxService, never()).publish(10L);
|
||||
|
||||
service.publishCompletedEvent(10L);
|
||||
|
||||
verify(loadingCompletedOutboxService).publish(10L);
|
||||
}
|
||||
|
||||
|
||||
@@ -523,6 +523,12 @@ class LoadingPickServiceTest {
|
||||
verify(truckAppointmentService).completeLoadingFromPda(99L, 20L, 9L, "operator");
|
||||
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 9L, "operator",
|
||||
"PDA-complete-1-50", "checked");
|
||||
verify(loadingListService).publishCompletedEvent(10L);
|
||||
InOrder finalization = inOrder(loadingListService, loadingDeliveryService);
|
||||
finalization.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
|
||||
finalization.verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 9L, "operator",
|
||||
"PDA-complete-1-50", "checked");
|
||||
finalization.verify(loadingListService).publishCompletedEvent(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -548,6 +554,31 @@ class LoadingPickServiceTest {
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyString(), any());
|
||||
verify(loadingListService, never()).publishCompletedEvent(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotPublishCompletedEventWhenInboundFinalizationFails() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
|
||||
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"入库单并发修改"))
|
||||
.when(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 9L, "operator",
|
||||
"PDA-complete-finalize-fails-50", "checked");
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "complete-finalize-fails", "checked"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("并发修改");
|
||||
|
||||
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
|
||||
verify(loadingListService, never()).publishCompletedEvent(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user