优化
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.lastleg.inventory.query.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InboundCodeRelationshipResponse;
|
||||
import com.baiyue.lastleg.inventory.query.service.InboundCodeRelationshipService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Inventory queries")
|
||||
public class InboundCodeRelationshipController {
|
||||
static final String PATH = "/api/platform/v1/last-leg/inventory/inbound-code-relationships/{inboundCode}";
|
||||
|
||||
@Resource private InboundCodeRelationshipService service;
|
||||
|
||||
@GetMapping(PATH)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
|
||||
@Operation(summary = "Resolve overseas relationships by inbound code",
|
||||
description = "Platform-only Lastleg relationship read model. Required authority: "
|
||||
+ OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<InboundCodeRelationshipResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String inboundCode) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.find(inboundCode));
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.inventory.query.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record InboundCodeRelationshipResponse(
|
||||
String inboundCode,
|
||||
boolean found,
|
||||
Customer customer,
|
||||
OverseasWarehouse overseasWarehouse,
|
||||
String overseasReceivingStatus,
|
||||
List<TaillegOrder> taillegOrders,
|
||||
List<InboundPlan> inboundPlans,
|
||||
List<String> palletCodes,
|
||||
List<InventorySku> inventory
|
||||
) {
|
||||
public record Customer(String customerId, String customerName) { }
|
||||
public record OverseasWarehouse(String warehouseId, String warehouseCode, String warehouseName) { }
|
||||
public record TaillegOrder(String orderNo, String orderStatus) { }
|
||||
public record InboundPlan(String planNo, String planStatus, String receivingStatus) { }
|
||||
public record InventorySku(String skuCode, String productNameZh, String productNameEn, long packageCount) { }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.lastleg.inventory.query.mapper;
|
||||
|
||||
import com.baiyue.lastleg.inventory.query.model.InboundCodeRelationshipRows;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InboundCodeRelationshipMapper {
|
||||
@Select("SELECT o.customer_id AS customerId,o.customer_name AS customerName,o.order_no AS orderNo," +
|
||||
"o.current_status AS orderStatus,p.plan_no AS planNo,p.status AS planStatus," +
|
||||
"COALESCE(p.status,o.current_status) AS receivingStatus," +
|
||||
"COALESCE(b.overseas_warehouse_id,o.overseas_warehouse_id,CAST(p.overseas_warehouse_id AS CHAR)) AS overseasWarehouseId," +
|
||||
"b.overseas_warehouse_code AS overseasWarehouseCode," +
|
||||
"COALESCE(b.overseas_warehouse_name,o.overseas_warehouse_name) AS overseasWarehouseName " +
|
||||
"FROM ll_order o " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.customer_id=o.customer_id AND l.order_id=o.id AND l.inbound_code=o.inbound_code " +
|
||||
"LEFT JOIN ll_inbound_plan p ON p.customer_id=o.customer_id AND p.id=l.plan_id " +
|
||||
"LEFT JOIN ll_inventory_balance b ON b.customer_id=o.customer_id AND b.inbound_order_id=o.id AND b.inbound_code=o.inbound_code " +
|
||||
"WHERE o.inbound_code=#{inboundCode} ORDER BY o.customer_id,o.id,p.id,b.id")
|
||||
List<InboundCodeRelationshipRows.Base> bases(@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT DISTINCT r.pallet_no FROM ll_inbound_stock_receipt r " +
|
||||
"JOIN ll_order o ON o.customer_id=r.customer_id AND o.id=r.order_id " +
|
||||
"WHERE o.inbound_code=#{inboundCode} AND o.customer_id=#{customerId} " +
|
||||
"AND r.pallet_no IS NOT NULL AND TRIM(r.pallet_no)<>'' ORDER BY r.pallet_no")
|
||||
List<String> palletCodes(@Param("customerId") String customerId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT r.sku_code AS skuCode,MAX(r.product_name_zh) AS productNameZh," +
|
||||
"MAX(r.product_name_en) AS productNameEn,SUM(r.received_packages) AS packageCount " +
|
||||
"FROM ll_inbound_stock_receipt r " +
|
||||
"JOIN ll_order o ON o.customer_id=r.customer_id AND o.id=r.order_id " +
|
||||
"WHERE o.inbound_code=#{inboundCode} AND o.customer_id=#{customerId} " +
|
||||
"GROUP BY r.sku_code ORDER BY r.sku_code")
|
||||
List<InboundCodeRelationshipRows.InventorySku> inventory(
|
||||
@Param("customerId") String customerId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.inventory.query.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
public final class InboundCodeRelationshipRows {
|
||||
private InboundCodeRelationshipRows() { }
|
||||
|
||||
@Data
|
||||
public static class Base {
|
||||
private String customerId;
|
||||
private String customerName;
|
||||
private String orderNo;
|
||||
private String orderStatus;
|
||||
private String planNo;
|
||||
private String planStatus;
|
||||
private String receivingStatus;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
private String overseasWarehouseName;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class InventorySku {
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private String productNameEn;
|
||||
private Long packageCount;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.baiyue.lastleg.inventory.query.service;
|
||||
|
||||
import com.baiyue.lastleg.inventory.query.dto.InboundCodeRelationshipResponse;
|
||||
import com.baiyue.lastleg.inventory.query.mapper.InboundCodeRelationshipMapper;
|
||||
import com.baiyue.lastleg.inventory.query.model.InboundCodeRelationshipRows;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class InboundCodeRelationshipService {
|
||||
@Resource private InboundCodeRelationshipMapper mapper;
|
||||
|
||||
public InboundCodeRelationshipResponse find(String inboundCode) {
|
||||
String normalized = required(inboundCode);
|
||||
List<InboundCodeRelationshipRows.Base> bases = mapper.bases(normalized);
|
||||
if (bases.isEmpty()) return empty(normalized);
|
||||
List<String> customerIds = bases.stream().map(InboundCodeRelationshipRows.Base::getCustomerId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (customerIds.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code resolves multiple customer scopes");
|
||||
}
|
||||
String customerId = customerIds.get(0);
|
||||
String customerName = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getCustomerName).toList(),
|
||||
"customer name");
|
||||
String warehouseId = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseId).toList(),
|
||||
"overseas warehouse ID");
|
||||
String warehouseCode = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseCode).toList(),
|
||||
"overseas warehouse code");
|
||||
String warehouseName = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseName).toList(),
|
||||
"overseas warehouse name");
|
||||
List<InboundCodeRelationshipResponse.TaillegOrder> orders = bases.stream()
|
||||
.filter(value -> trim(value.getOrderNo()) != null)
|
||||
.map(value -> new InboundCodeRelationshipResponse.TaillegOrder(
|
||||
value.getOrderNo().trim(), trim(value.getOrderStatus())))
|
||||
.distinct().sorted(java.util.Comparator.comparing(InboundCodeRelationshipResponse.TaillegOrder::orderNo))
|
||||
.toList();
|
||||
List<InboundCodeRelationshipResponse.InboundPlan> plans = bases.stream()
|
||||
.filter(value -> trim(value.getPlanNo()) != null)
|
||||
.map(value -> new InboundCodeRelationshipResponse.InboundPlan(
|
||||
value.getPlanNo().trim(), trim(value.getPlanStatus()), trim(value.getReceivingStatus())))
|
||||
.distinct().sorted(java.util.Comparator.comparing(InboundCodeRelationshipResponse.InboundPlan::planNo)).toList();
|
||||
List<InboundCodeRelationshipResponse.InventorySku> inventory = mapper.inventory(customerId, normalized).stream()
|
||||
.map(value -> new InboundCodeRelationshipResponse.InventorySku(value.getSkuCode(),
|
||||
value.getProductNameZh(), value.getProductNameEn(), number(value.getPackageCount()))).toList();
|
||||
String status = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getReceivingStatus).toList(),
|
||||
"overseas receiving status");
|
||||
InboundCodeRelationshipResponse.OverseasWarehouse warehouse = warehouseId == null
|
||||
&& warehouseCode == null && warehouseName == null ? null
|
||||
: new InboundCodeRelationshipResponse.OverseasWarehouse(warehouseId, warehouseCode, warehouseName);
|
||||
return new InboundCodeRelationshipResponse(normalized, true,
|
||||
new InboundCodeRelationshipResponse.Customer(customerId, customerName), warehouse, status,
|
||||
orders, plans, mapper.palletCodes(customerId, normalized), inventory);
|
||||
}
|
||||
|
||||
private static InboundCodeRelationshipResponse empty(String inboundCode) {
|
||||
return new InboundCodeRelationshipResponse(inboundCode, false, null, null, null,
|
||||
List.of(), List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private static String unique(List<String> values, String field) {
|
||||
List<String> normalized = values.stream().map(InboundCodeRelationshipService::trim)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (normalized.size() > 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code has inconsistent " + field + " snapshots");
|
||||
}
|
||||
return normalized.isEmpty() ? null : normalized.get(0);
|
||||
}
|
||||
|
||||
private static String required(String value) {
|
||||
String normalized = trim(value);
|
||||
if (normalized == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "inboundCode is required");
|
||||
if (normalized.length() > 128) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "inboundCode is too long");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
private static long number(Long value) { return value == null ? 0L : value; }
|
||||
}
|
||||
@@ -3,6 +3,17 @@ info:
|
||||
title: Lastleg Overseas Business API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/platform/v1/last-leg/inventory/inbound-code-relationships/{inboundCode}:
|
||||
get:
|
||||
summary: 按入仓码查询海外关系摘要
|
||||
description: 仅平台且要求 lastleg:inventory:list;不接收customerId,不跨国内数据库查询,未命中返回found=false和空关系。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { in: path, name: inboundCode, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
|
||||
responses:
|
||||
'200': { description: 海外订单、计划、收货、托盘和库存业务关系, content: { application/json: { schema: { $ref: '#/components/schemas/InboundCodeRelationshipApiResponse' } } } }
|
||||
'403': { description: 非平台身份或缺少库存查看权限 }
|
||||
'409': { description: 入仓码跨客户或快照关系不一致 }
|
||||
/api/pda/v1/last-leg/receiving-containers/candidates:
|
||||
get:
|
||||
summary: 查询当前客户待签收柜号
|
||||
@@ -643,6 +654,57 @@ paths:
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的换标单详情 }, '409': { description: 非法状态迁移或关联尾程单缺失 } }
|
||||
components:
|
||||
schemas:
|
||||
InboundCodeRelationship:
|
||||
type: object
|
||||
required: [inboundCode, found, taillegOrders, inboundPlans, palletCodes, inventory]
|
||||
properties:
|
||||
inboundCode: { type: string }
|
||||
found: { type: boolean }
|
||||
customer: { $ref: '#/components/schemas/InboundCodeRelationshipCustomer', nullable: true }
|
||||
overseasWarehouse: { $ref: '#/components/schemas/InboundCodeRelationshipWarehouse', nullable: true }
|
||||
overseasReceivingStatus: { type: string, nullable: true }
|
||||
taillegOrders: { type: array, items: { $ref: '#/components/schemas/InboundCodeRelationshipOrder' } }
|
||||
inboundPlans: { type: array, items: { $ref: '#/components/schemas/InboundCodeRelationshipPlan' } }
|
||||
palletCodes: { type: array, uniqueItems: true, items: { type: string } }
|
||||
inventory: { type: array, items: { $ref: '#/components/schemas/InboundCodeRelationshipInventorySku' } }
|
||||
InboundCodeRelationshipCustomer:
|
||||
type: object
|
||||
required: [customerId]
|
||||
properties:
|
||||
customerId: { type: string, description: 字符串业务隔离标识,禁止按JavaScript number处理 }
|
||||
customerName: { type: string, nullable: true }
|
||||
InboundCodeRelationshipWarehouse:
|
||||
type: object
|
||||
properties:
|
||||
warehouseId: { type: string, nullable: true, description: 字符串共享仓库ID,禁止按JavaScript number处理 }
|
||||
warehouseCode: { type: string, nullable: true }
|
||||
warehouseName: { type: string, nullable: true }
|
||||
InboundCodeRelationshipOrder:
|
||||
type: object
|
||||
required: [orderNo]
|
||||
properties:
|
||||
orderNo: { type: string }
|
||||
orderStatus: { type: string, nullable: true }
|
||||
InboundCodeRelationshipPlan:
|
||||
type: object
|
||||
required: [planNo]
|
||||
properties:
|
||||
planNo: { type: string }
|
||||
planStatus: { type: string, nullable: true }
|
||||
receivingStatus: { type: string, nullable: true }
|
||||
InboundCodeRelationshipInventorySku:
|
||||
type: object
|
||||
required: [skuCode, packageCount]
|
||||
properties:
|
||||
skuCode: { type: string }
|
||||
productNameZh: { type: string, nullable: true }
|
||||
productNameEn: { type: string, nullable: true }
|
||||
packageCount: { type: integer, format: int64, minimum: 0 }
|
||||
InboundCodeRelationshipApiResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InboundCodeRelationship' } }
|
||||
FbaTransferDestination:
|
||||
type: object
|
||||
required: [warehouseId, warehouseCode, warehouseName]
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.inventory.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class InboundCodeRelationshipOpenApiContractTest {
|
||||
|
||||
@Test
|
||||
void publishesPlatformRelationshipLookupWithoutClientCustomerScope() throws IOException {
|
||||
String yaml = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"));
|
||||
String path = "/api/platform/v1/last-leg/inventory/inbound-code-relationships/{inboundCode}:";
|
||||
int start = yaml.indexOf(path);
|
||||
int end = yaml.indexOf("\n /", start + path.length());
|
||||
|
||||
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||
String operation = yaml.substring(start, end > start ? end : yaml.length());
|
||||
assertThat(operation)
|
||||
.contains("name: inboundCode")
|
||||
.contains("$ref: '#/components/schemas/InboundCodeRelationshipApiResponse'")
|
||||
.doesNotContain("name: customerId");
|
||||
assertThat(yaml)
|
||||
.contains("InboundCodeRelationship:")
|
||||
.contains("overseasReceivingStatus:")
|
||||
.contains("taillegOrders:")
|
||||
.contains("inboundPlans:")
|
||||
.contains("orderStatus:")
|
||||
.contains("planStatus:")
|
||||
.contains("palletCodes:")
|
||||
.contains("inventory:");
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.lastleg.inventory.query.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InboundCodeRelationshipResponse;
|
||||
import com.baiyue.lastleg.inventory.query.service.InboundCodeRelationshipService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
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.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
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;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundCodeRelationshipControllerTest {
|
||||
@Mock InboundCodeRelationshipService service;
|
||||
private InboundCodeRelationshipController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new InboundCodeRelationshipController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformReadsByInboundCodeWithoutSupplyingCustomerId() {
|
||||
InboundCodeRelationshipResponse response = new InboundCodeRelationshipResponse(
|
||||
"IN-1", false, null, null, null, List.of(), List.of(), List.of(), List.of());
|
||||
when(service.find("IN-1")).thenReturn(response);
|
||||
|
||||
var result = controller.find(platform(), "IN-1");
|
||||
|
||||
assertThat(result.data()).isSameAs(response);
|
||||
verify(service).find("IN-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerAccountCannotUsePlatformRelationshipReadModel() {
|
||||
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 8L, "customer",
|
||||
"MERCHANT", "WEB", Set.of(OverseasPermissions.INVENTORY_LIST));
|
||||
|
||||
assertThatThrownBy(() -> controller.find(customer, "IN-1"))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingIdentityIsUnauthorized() {
|
||||
assertThatThrownBy(() -> controller.find(null, "IN-1"))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("401");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser platform() {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "admin", "PLATFORM", "WEB",
|
||||
Set.of(OverseasPermissions.INVENTORY_LIST));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.inventory.query.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundCodeRelationshipMapperContractTest {
|
||||
@Test
|
||||
void mapperIsExplicitlyScannableAndQueriesOnlyLastlegTables() throws Exception {
|
||||
assertThat(InboundCodeRelationshipMapper.class).hasAnnotation(Mapper.class);
|
||||
String sql = String.join(" ", InboundCodeRelationshipMapper.class
|
||||
.getMethod("bases", String.class).getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql).contains("ll_order", "ll_inbound_plan", "ll_inventory_balance");
|
||||
assertThat(sql).doesNotContain("customerId}", "warehouse.", "iam.");
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.baiyue.lastleg.inventory.query.service;
|
||||
|
||||
import com.baiyue.lastleg.inventory.query.mapper.InboundCodeRelationshipMapper;
|
||||
import com.baiyue.lastleg.inventory.query.model.InboundCodeRelationshipRows;
|
||||
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.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundCodeRelationshipServiceTest {
|
||||
@Mock InboundCodeRelationshipMapper mapper;
|
||||
private InboundCodeRelationshipService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundCodeRelationshipService();
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingInboundCodeReturnsExplicitEmptyRelationship() {
|
||||
when(mapper.bases("NYEXX2608300001")).thenReturn(List.of());
|
||||
|
||||
var result = service.find(" NYEXX2608300001 ");
|
||||
|
||||
assertThat(result.found()).isFalse();
|
||||
assertThat(result.customer()).isNull();
|
||||
assertThat(result.taillegOrders()).isEmpty();
|
||||
assertThat(result.inboundPlans()).isEmpty();
|
||||
assertThat(result.palletCodes()).isEmpty();
|
||||
assertThat(result.inventory()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void relationshipUsesBusinessNumbersAndKeepsLongIdentifiersAsStrings() {
|
||||
InboundCodeRelationshipRows.Base row = base("2092618717861048321", "Customer A");
|
||||
when(mapper.bases("NYEXX2608300001")).thenReturn(List.of(row, row));
|
||||
when(mapper.palletCodes("2092618717861048321", "NYEXX2608300001"))
|
||||
.thenReturn(List.of("PAL-LAX-001"));
|
||||
InboundCodeRelationshipRows.InventorySku sku = new InboundCodeRelationshipRows.InventorySku();
|
||||
sku.setSkuCode("SKU-001"); sku.setProductNameZh("产品一"); sku.setPackageCount(40L);
|
||||
when(mapper.inventory("2092618717861048321", "NYEXX2608300001")).thenReturn(List.of(sku));
|
||||
|
||||
var result = service.find("NYEXX2608300001");
|
||||
|
||||
assertThat(result.found()).isTrue();
|
||||
assertThat(result.customer().customerId()).isEqualTo("2092618717861048321");
|
||||
assertThat(result.overseasWarehouse().warehouseId()).isEqualTo("193000000000200003");
|
||||
assertThat(result.taillegOrders()).extracting("orderNo").containsExactly("LMO-260830-0001");
|
||||
assertThat(result.taillegOrders()).extracting("orderStatus").containsExactly("INBOUND_PLANNED");
|
||||
assertThat(result.inboundPlans()).extracting("planNo").containsExactly("LIP-260830-0001");
|
||||
assertThat(result.inboundPlans()).extracting("planStatus").containsExactly("COMPLETED");
|
||||
assertThat(result.palletCodes()).containsExactly("PAL-LAX-001");
|
||||
assertThat(result.inventory()).extracting("skuCode", "packageCount")
|
||||
.containsExactly(org.assertj.core.groups.Tuple.tuple("SKU-001", 40L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameInboundCodeAcrossCustomersIsRejectedBeforeScopedDetailQueries() {
|
||||
when(mapper.bases("DUPLICATE")).thenReturn(List.of(
|
||||
base("100", "Customer A"), base("200", "Customer B")));
|
||||
|
||||
assertThatThrownBy(() -> service.find("DUPLICATE"))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
|
||||
verify(mapper, never()).palletCodes(anyString(), anyString());
|
||||
verify(mapper, never()).inventory(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankAndOversizedInboundCodesAreRejected() {
|
||||
assertThatThrownBy(() -> service.find(" ")).isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400");
|
||||
assertThatThrownBy(() -> service.find("X".repeat(129))).isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400");
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
|
||||
private static InboundCodeRelationshipRows.Base base(String customerId, String customerName) {
|
||||
InboundCodeRelationshipRows.Base value = new InboundCodeRelationshipRows.Base();
|
||||
value.setCustomerId(customerId); value.setCustomerName(customerName);
|
||||
value.setOrderNo("LMO-260830-0001"); value.setOrderStatus("INBOUND_PLANNED");
|
||||
value.setPlanNo("LIP-260830-0001"); value.setPlanStatus("COMPLETED");
|
||||
value.setReceivingStatus("COMPLETED"); value.setOverseasWarehouseId("193000000000200003");
|
||||
value.setOverseasWarehouseCode("US-LAX-01"); value.setOverseasWarehouseName("洛杉矶一号仓");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user