fix(firstleg): keep published billing visible after loading
This commit is contained in:
+3
-1
@@ -29,7 +29,9 @@ public class MerchantInboundBillingController {
|
|||||||
@GetMapping("/{orderId}/inbound-billing")
|
@GetMapping("/{orderId}/inbound-billing")
|
||||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||||
@Operation(summary = "查询订单入库结果和最新费用",
|
@Operation(summary = "查询订单入库结果和最新费用",
|
||||||
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
description = "客户按登录态客户范围只读查看;入库流程达到 WAIT_SPLIT 后,"
|
||||||
|
+ "在 LOADING、LOADED、DELIVERED 仍持续可见,且始终要求 APPROVED + CONFIRMED "
|
||||||
|
+ "并存在已发布入库运费明细,不需要客户确认费用。")
|
||||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||||
|
|||||||
+9
-3
@@ -17,9 +17,16 @@ import com.baiyue.firstleg.repository.InboundOrderRepository;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MerchantInboundBillingService {
|
public class MerchantInboundBillingService {
|
||||||
|
private static final Set<String> PUBLISHED_BILLING_PROCESS_STATUSES = Set.of(
|
||||||
|
InboundProcessStatus.WAIT_SPLIT,
|
||||||
|
InboundProcessStatus.LOADING,
|
||||||
|
InboundProcessStatus.LOADED,
|
||||||
|
InboundProcessStatus.DELIVERED);
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private InboundOrderRepository orders;
|
private InboundOrderRepository orders;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -41,6 +48,7 @@ public class MerchantInboundBillingService {
|
|||||||
if (!isPublished(inbound)) {
|
if (!isPublished(inbound)) {
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布");
|
throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布");
|
||||||
}
|
}
|
||||||
|
InboundFeeDetailEntity publishedFee = feeDetails.publishedFreight(tenantId, inbound.getId());
|
||||||
InboundOrderDetail platformDetail = orders.detail(tenantId, inbound);
|
InboundOrderDetail platformDetail = orders.detail(tenantId, inbound);
|
||||||
InboundOrderDetail merchantDetail = new InboundOrderDetail(
|
InboundOrderDetail merchantDetail = new InboundOrderDetail(
|
||||||
platformDetail.summary(), platformDetail.publicRemark(), platformDetail.palletLocations(),
|
platformDetail.summary(), platformDetail.publicRemark(), platformDetail.palletLocations(),
|
||||||
@@ -49,7 +57,6 @@ public class MerchantInboundBillingService {
|
|||||||
.toList(), List.of(), List.of());
|
.toList(), List.of(), List.of());
|
||||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||||
FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId());
|
FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId());
|
||||||
InboundFeeDetailEntity publishedFee = feeDetails.publishedFreight(tenantId, inbound.getId());
|
|
||||||
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
||||||
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
||||||
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
||||||
@@ -117,8 +124,7 @@ public class MerchantInboundBillingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isPublished(InboundOrderEntity inbound) {
|
private static boolean isPublished(InboundOrderEntity inbound) {
|
||||||
return (InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
|
return PUBLISHED_BILLING_PROCESS_STATUSES.contains(inbound.getProcessStatus())
|
||||||
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
|
|
||||||
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
||||||
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -905,7 +905,7 @@ paths:
|
|||||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||||
get:
|
get:
|
||||||
summary: 客户查询已完成入库结果和最新已发布费用
|
summary: 客户查询已完成入库结果和最新已发布费用
|
||||||
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。
|
description: 需要 merchant:order:view 权限;客户仅能按登录态客户范围只读查看。入库流程达到 WAIT_SPLIT 后,费用在 LOADING、LOADED、DELIVERED 仍持续可见;始终要求 measureReviewStatus=APPROVED、feeStatus=CONFIRMED 且存在 PUBLISHED 入库运费明细,不需要客户确认费用。
|
||||||
parameters:
|
parameters:
|
||||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
+5
-1
@@ -9,6 +9,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -32,13 +33,16 @@ class MerchantInboundBillingControllerContractTest {
|
|||||||
"find", AuthenticatedUser.class, long.class);
|
"find", AuthenticatedUser.class, long.class);
|
||||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||||
.isEqualTo("hasAuthority('merchant:order:view')");
|
.isEqualTo("hasAuthority('merchant:order:view')");
|
||||||
|
assertThat(method.getAnnotation(Operation.class).description())
|
||||||
|
.contains("WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED", "APPROVED + CONFIRMED", "已发布入库运费明细");
|
||||||
assertThat(MerchantInboundBillingController.class.getDeclaredMethods())
|
assertThat(MerchantInboundBillingController.class.getDeclaredMethods())
|
||||||
.noneMatch(candidate -> candidate.getName().toLowerCase().contains("confirm"));
|
.noneMatch(candidate -> candidate.getName().toLowerCase().contains("confirm"));
|
||||||
|
|
||||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
||||||
.contains("WAIT_SPLIT + APPROVED + CONFIRMED");
|
.contains("WAIT_SPLIT", "LOADING、LOADED、DELIVERED", "measureReviewStatus=APPROVED",
|
||||||
|
"feeStatus=CONFIRMED", "PUBLISHED 入库运费明细");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+33
-3
@@ -10,6 +10,8 @@ import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
|||||||
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
@@ -34,9 +36,10 @@ class MerchantInboundBillingServiceTest {
|
|||||||
@MockBean private InboundFeeDetailService feeDetails;
|
@MockBean private InboundFeeDetailService feeDetails;
|
||||||
@Resource private MerchantInboundBillingService service;
|
@Resource private MerchantInboundBillingService service;
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
void shouldHandleCase01() {
|
@ValueSource(strings = {"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
|
void shouldKeepPublishedBillingVisibleAfterFulfillmentProgresses(String processStatus) {
|
||||||
|
InboundOrderEntity inbound = inbound(processStatus, "APPROVED", "CONFIRMED");
|
||||||
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
||||||
"客户备注", List.of(), List.of(
|
"客户备注", List.of(), List.of(
|
||||||
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
|
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
|
||||||
@@ -151,6 +154,33 @@ class MerchantInboundBillingServiceTest {
|
|||||||
verify(pricing, never()).latest(10001L, 88L);
|
verify(pricing, never()).latest(10001L, 88L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {"FEE_CONFIRMING", "VOIDED"})
|
||||||
|
void shouldRejectProcessStatusesOutsidePublishedFulfillmentLifecycle(String processStatus) {
|
||||||
|
InboundOrderEntity inbound = inbound(processStatus, "APPROVED", "CONFIRMED");
|
||||||
|
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessageContaining("尚未由客服审核并发布");
|
||||||
|
verify(feeDetails, never()).publishedFreight(10001L, 88L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRequirePublishedFreightDetailAfterLoadingCompletes() {
|
||||||
|
InboundOrderEntity inbound = inbound("LOADED", "APPROVED", "CONFIRMED");
|
||||||
|
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||||
|
when(feeDetails.publishedFreight(10001L, 88L))
|
||||||
|
.thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,
|
||||||
|
"入库运费明细尚未发布"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessageContaining("运费明细尚未发布");
|
||||||
|
verify(orders, never()).detail(10001L, inbound);
|
||||||
|
verify(pricing, never()).latest(10001L, 88L);
|
||||||
|
}
|
||||||
|
|
||||||
private static InboundOrderEntity inbound(String process, String reviewStatus, String feeStatus) {
|
private static InboundOrderEntity inbound(String process, String reviewStatus, String feeStatus) {
|
||||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||||
inbound.setId(88L);
|
inbound.setId(88L);
|
||||||
|
|||||||
Reference in New Issue
Block a user