feat: complete overseas customer and PDA workflows

This commit is contained in:
2026-09-04 12:59:37 +08:00
parent a274d029bb
commit 54cb9ceade
60 changed files with 1598 additions and 277 deletions
@@ -0,0 +1,101 @@
package com.baiyue.lastleg.merchant;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.controller.MerchantInboundPlanController;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
import com.baiyue.lastleg.looseboard.controller.MerchantLocalLooseboardController;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
import com.baiyue.lastleg.procurement.controller.MerchantOverseasProcurementController;
import com.baiyue.lastleg.security.OverseasPermissions;
import com.baiyue.lastleg.trucking.controller.MerchantUsTruckOrderController;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import java.util.Arrays;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class MerchantOverseasCustomerApiContractTest {
@Mock LocalLooseboardService looseboardService;
@Mock InboundPlanService inboundPlanService;
private MerchantLocalLooseboardController looseboard;
private MerchantInboundPlanController inbound;
@BeforeEach
void setUp() {
looseboard = new MerchantLocalLooseboardController();
inbound = new MerchantInboundPlanController();
ReflectionTestUtils.setField(looseboard, "service", looseboardService);
ReflectionTestUtils.setField(inbound, "service", inboundPlanService);
}
@Test
void merchantControllersUseFrozenPathsAndExistingClientPermissionCodes() {
assertController(MerchantOverseasProcurementController.class,
"/api/merchant/v1/last-leg/overseas-procurements", OverseasPermissions.MERCHANT_PROCUREMENT);
assertController(MerchantUsTruckOrderController.class,
"/api/merchant/v1/last-leg/us-truck-orders", OverseasPermissions.MERCHANT_TRUCKING);
assertController(MerchantLocalLooseboardController.class,
"/api/merchant/v1/last-leg/local-looseboard-orders", OverseasPermissions.MERCHANT_LOOSEBOARD);
assertController(MerchantInboundPlanController.class,
"/api/merchant/v1/last-leg/inbound-plans", OverseasPermissions.MERCHANT_INBOUND_LIST);
}
@Test
void looseboardAndInboundQueriesUseAuthenticatedCustomerScope() {
AuthenticatedUser merchant = user(101L, "MERCHANT");
LocalLooseboardRequests.MerchantQuery looseboardQuery = new LocalLooseboardRequests.MerchantQuery(1, 20,
null, null, null, null, null, null);
InboundPlanRequests.Query inboundQuery = new InboundPlanRequests.Query(1, 20, null, null, null,
null, null, null, null, null, null, null, null);
looseboard.page(merchant, looseboardQuery);
inbound.page(merchant, inboundQuery);
verify(looseboardService).page("101", looseboardQuery);
verify(inboundPlanService).page("101", inboundQuery);
}
@Test
void merchantQueryDtosDoNotAcceptCustomerScope() {
for (Class<?> type : Set.of(LocalLooseboardRequests.MerchantQuery.class,
com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests.MerchantQuery.class,
com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests.MerchantQuery.class,
InboundPlanRequests.Query.class)) {
assertThat(Arrays.stream(type.getRecordComponents()).map(component -> component.getName()))
.doesNotContain("customerId", "warehouseId", "operatorId");
}
}
@Test
void merchantEndpointsRejectPlatformIdentityBeforeCallingServices() {
AuthenticatedUser platform = user(0L, "PLATFORM");
assertThatThrownBy(() -> looseboard.detail(platform, "LB-1"))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
assertThatThrownBy(() -> inbound.detail(platform, "IP-1"))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
}
private static void assertController(Class<?> type, String path, String permission) {
assertThat(type.getAnnotation(RequestMapping.class).value()).containsExactly(path);
assertThat(type.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + permission + "')");
}
private static AuthenticatedUser user(long customerId, String appCode) {
return new AuthenticatedUser(7L, customerId, customerId, 8L, "operator", appCode, "WEB", Set.of());
}
}
@@ -0,0 +1,47 @@
package com.baiyue.lastleg.merchant;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class MerchantOverseasOpenApiContractTest {
@Test
void documentsFrozenMerchantPathsPermissionsResponsesAndConflictSemantics() throws Exception {
String yaml;
try (var input = getClass().getResourceAsStream("/openapi/overseas-business-v1.yaml")) {
assertThat(input).isNotNull();
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
for (String path : new String[] {
"/api/merchant/v1/last-leg/overseas-procurements:",
"/api/merchant/v1/last-leg/overseas-procurements/{orderNo}:",
"/api/merchant/v1/last-leg/us-truck-orders:",
"/api/merchant/v1/last-leg/us-truck-orders/{orderNo}:",
"/api/merchant/v1/last-leg/local-looseboard-orders:",
"/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}:",
"/api/merchant/v1/last-leg/inbound-plans:",
"/api/merchant/v1/last-leg/inbound-plans/{planNo}:"}) {
assertThat(yaml).contains(path);
assertThat(pathBlock(yaml, path)).doesNotContain("name: customerId");
}
assertThat(yaml).contains("merchant:cross-border:procurement")
.contains("merchant:cross-border:domestic-trucking")
.contains("merchant:cross-border:pallet-shipping")
.contains("merchant:warehouse:inbound:list")
.contains("#/components/schemas/ProcurementDetailResponse")
.contains("#/components/schemas/TruckDetailResponse")
.contains("#/components/schemas/LooseboardDetailResponse")
.contains("#/components/schemas/InboundPlanDetailResponse")
.contains("PENDING_REVIEW").contains("PENDING_ACCEPTANCE")
.contains("version 必填").contains("幂等冲突");
}
private static String pathBlock(String yaml, String path) {
int start = yaml.indexOf(" " + path);
int end = yaml.indexOf("\n /api/", start + path.length());
return yaml.substring(start, end < 0 ? yaml.length() : end);
}
}