fix: expose phase2 terminal inbound billing

This commit is contained in:
2026-08-08 11:46:20 +08:00
parent ef4e1d6015
commit 0113d19274
8 changed files with 52 additions and 11 deletions
@@ -29,7 +29,7 @@ public class MerchantInboundBillingController {
@GetMapping("/{orderId}/inbound-billing")
@PreAuthorize("hasAuthority('merchant:order:view')")
@Operation(summary = "查询订单入库结果和最新费用",
description = "客户只读查看客服审核通过后发布的费用,不需要确认费用。")
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
public ApiResponse<MerchantInboundBillingResponse> find(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
@@ -20,6 +20,8 @@ public final class InboundProcessStatus {
public static final String INBOUNDING = "INBOUNDING";
/** 已入库完成,费用已发送给客户。 */
public static final String INBOUNDED = "INBOUNDED";
/** 二期处理完成,等待三期分柜。 */
public static final String WAIT_SPLIT = "WAIT_SPLIT";
/** 已进入装柜作业,PDA 可执行拣货。 */
public static final String LOADING = "LOADING";
/** 装柜完成,等待派送完成。 */
@@ -31,7 +33,7 @@ public final class InboundProcessStatus {
public static final Set<String> VALUES = Set.of(
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT,
LOADING, LOADED, DELIVERED, VOIDED
);
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.dto.InboundOrderDetail;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.domain.InboundProcessStatus;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.repository.InboundOrderRepository;
@@ -37,7 +38,7 @@ public class MerchantInboundBillingService {
if (inbound == null) {
throw new BizException(ResultCode.NOT_FOUND, "订单尚未生成入库单");
}
if (!"INBOUNDED".equals(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) {
if (!isPublished(inbound)) {
throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布");
}
InboundOrderDetail platformDetail = orders.detail(tenantId, inbound);
@@ -106,12 +107,18 @@ public class MerchantInboundBillingService {
private InboundOrderEntity requirePublished(long tenantId, long orderId) {
InboundOrderEntity inbound = orders.findByOrderId(tenantId, orderId);
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "订单尚未生成入库单");
if (!"INBOUNDED".equals(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) {
if (!isPublished(inbound)) {
throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布");
}
return inbound;
}
private static boolean isPublished(InboundOrderEntity inbound) {
return InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
&& "CONFIRMED".equals(inbound.getFeeStatus());
}
private String previewUrl(long orderId, long fileId) {
return "/api/merchant/v1/first-leg/orders/" + orderId
+ "/inbound-billing/images/" + fileId;
@@ -434,7 +434,7 @@ paths:
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
get:
summary: 客户查询已完成入库结果和最新已发布费用
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,未完成客服审核发布时不可见。
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。
parameters:
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
responses:
@@ -53,7 +53,8 @@ class InboundOrderControllerContractTest {
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
.contains("platform:firstleg:inbound:list")
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
.doesNotContain("WAIT_CUSTOMER_CONFIRM")
.contains("WAIT_SPLIT");
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
assertThat(taskList).doesNotContain("InboundOrderPageEnvelope");
}
@@ -8,7 +8,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,6 +34,11 @@ class MerchantInboundBillingControllerContractTest {
.isEqualTo("hasAuthority('merchant:order:view')");
assertThat(MerchantInboundBillingController.class.getDeclaredMethods())
.noneMatch(candidate -> candidate.getName().toLowerCase().contains("confirm"));
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
.contains("仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见");
}
@Test
@@ -40,6 +40,18 @@ class InboundOrderQueryServiceTest {
verify(repository).page(null, query);
}
@Test
void shouldAcceptCurrentPhase2TerminalStatusFilters() {
InboundOrderQuery query = new InboundOrderQuery(null, null, "WAIT_SPLIT", null,
"APPROVED", "CONFIRMED", null, null, null, null, null, null, null,
null, null, null, null, 1, 200);
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 200, 0, 0);
when(repository.page(10001L, query)).thenReturn(expected);
assertThat(service.page(10001L, query)).isSameAs(expected);
verify(repository).page(10001L, query);
}
@Test
void shouldKeepV2TabMappingWithoutCustomerConfirmation() {
assertThat(InboundListTab.WAIT_MEASURE.processStatuses())
@@ -36,7 +36,7 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldHandleCase01() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
"客户备注", List.of(), List.of(
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
@@ -83,7 +83,7 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldHandleCase02() {
InboundOrderEntity inbound = inbound("FEE_CONFIRMING", "CONFIRMED");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "PENDING", "CONFIRMED");
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
assertThatThrownBy(() -> service.find(10001L, 77L))
@@ -104,7 +104,7 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldHandleCase04() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
InboundMeasurementResponses.Item item = mock(InboundMeasurementResponses.Item.class);
when(item.intelligentImageFileId()).thenReturn(41L);
@@ -127,7 +127,7 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldHandleCase05() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
when(measurement.items()).thenReturn(List.of());
when(measurement.weightResults()).thenReturn(List.of());
@@ -140,10 +140,22 @@ class MerchantInboundBillingServiceTest {
verify(imageClient, never()).download("internal-token", 10001L, 88L, 99L);
}
private static InboundOrderEntity inbound(String process, String feeStatus) {
@Test
void shouldRejectHistoricalPublishedStatusCombination() {
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "SENT");
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
assertThatThrownBy(() -> service.find(10001L, 77L))
.isInstanceOf(BizException.class)
.hasMessageContaining("尚未由客服审核并发布");
verify(pricing, never()).latest(10001L, 88L);
}
private static InboundOrderEntity inbound(String process, String reviewStatus, String feeStatus) {
InboundOrderEntity inbound = new InboundOrderEntity();
inbound.setId(88L);
inbound.setProcessStatus(process);
inbound.setMeasureReviewStatus(reviewStatus);
inbound.setFeeStatus(feeStatus);
return inbound;
}