This commit is contained in:
2026-08-15 11:05:58 +08:00
parent c4282020b5
commit 557ab75319
50 changed files with 591 additions and 394 deletions
@@ -70,7 +70,7 @@ public class OrderApplicationService {
long orderId = IdWorker.getId();
order.setId(orderId); order.setTenantId(tenantId); order.setOrderNo(nextNo(orderId));
order.setStatus(OrderStatus.DRAFT);
apply(order, request, snapshots);
apply(order, tenantId, request, snapshots);
order.setInboundCode(nextInboundCode(inboundCodePrefix));
order.setCreatedBy(userId); order.setUpdatedBy(userId);
repository.insertOrder(order);
@@ -108,7 +108,7 @@ public class OrderApplicationService {
}
pricingChannelGuard.requireAvailable(tenantId, request.receivingWarehouseId(), request.channelCode(),
request.destinationType());
apply(order, request, snapshots); order.setStatus(OrderStatus.DRAFT); order.setUpdatedBy(userId);
apply(order, tenantId, request, snapshots); order.setStatus(OrderStatus.DRAFT); order.setUpdatedBy(userId);
if (before == OrderStatus.SUBMITTED) {
order.setStatus(OrderStatus.SUBMITTED);
}
@@ -411,9 +411,9 @@ public class OrderApplicationService {
default -> false;
};
}
private void apply(OrderEntity order, OrderRequests.SaveRequest request, OrderMasterDataGuard.Snapshots snapshots) {
order.setCustomerId(snapshots.partner() == null ? null : snapshots.partner().id());
order.setCustomerName(snapshots.partner() == null ? request.customerName() : snapshots.partner().partnerName());
private void apply(OrderEntity order, long tenantId, OrderRequests.SaveRequest request, OrderMasterDataGuard.Snapshots snapshots) {
order.setCustomerId(tenantId);
order.setCustomerName(request.customerName().trim());
order.setTransportMode(request.transportMode()); order.setTradeTerm(request.tradeTerm());
order.setDestinationCountryCode(snapshots.destination() == null ? request.destinationCountryCode()
: snapshots.destination().warehouse().countryCode());
@@ -5,7 +5,6 @@ import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.PartnerClient;
import com.baiyue.order.client.WarehouseClient;
import com.baiyue.order.dto.OrderRequests;
import feign.FeignException;
@@ -21,13 +20,11 @@ import org.slf4j.LoggerFactory;
public class OrderMasterDataGuard {
private static final Logger log = LoggerFactory.getLogger(OrderMasterDataGuard.class);
@Resource private WarehouseClient warehouses;
@Resource private PartnerClient partners;
@Value("${baiyue.security.internal-service-token}") private String token;
@Value("${baiyue.warehouse.public-tenant-id:0}")
private long publicTenantId = 0L;
public Snapshots validate(long tenantId, OrderRequests.SaveRequest request) {
try {
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(request.customerId());
var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库");
requireWarehouseType(receiving, "DOMESTIC", "收货仓库");
if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull))
@@ -47,7 +44,7 @@ public class OrderMasterDataGuard {
destination=loadWarehouse(request.destinationWarehouseId(),"目的仓库");
requireWarehouseType(destination, request.destinationType(), "目的仓库");
}
return new Snapshots(partner,receiving,destination);
return new Snapshots(receiving,destination);
} catch (FeignException exception) {
throw downstreamFailure(exception);
} catch (BizException exception) {
@@ -56,10 +53,12 @@ public class OrderMasterDataGuard {
throw downstreamFailure(exception);
}
}
public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId,
public void validatePersisted(long tenantId, Long customerId, Long receivingWarehouseId,
Long destinationWarehouseId, String destinationType) {
try {
if (partnerId!=null) loadPartner(partnerId);
if (customerId != null && customerId != tenantId) {
throw new BizException(ResultCode.FORBIDDEN,"订单客户必须是当前登录企业");
}
if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用");
WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库");
requireWarehouseType(receiving,"DOMESTIC","收货仓库");
@@ -78,25 +77,6 @@ public class OrderMasterDataGuard {
throw downstreamFailure(exception);
}
}
private PartnerClient.Partner loadPartner(long id) {
ApiResponse<PartnerClient.Partner> response;
try {
response=partners.active(token,publicTenantId,id);
} catch (FeignException.NotFound exception) {
throw new BizException(ResultCode.NOT_FOUND,"平台往来单位不存在或不可用");
}
requireSuccessful(response,"往来单位");
if (response.data()==null) throw unavailable("往来单位");
var value=response.data();
if (!Long.valueOf(publicTenantId).equals(value.tenantId()))
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于平台公共主数据");
if (!"ACTIVE".equals(value.status()))
throw new BizException(ResultCode.CONFLICT,"往来单位已停用");
if (value.types() == null || !value.types().contains("SHIPPER"))
throw new BizException(ResultCode.VALIDATION_FAILED,"所选往来单位不具备发货单位类型");
if (!StringUtils.hasText(value.partnerName())) throw unavailable("往来单位");
return value;
}
private WarehouseClient.Detail loadWarehouse(long id,String name) {
ApiResponse<WarehouseClient.Detail> response;
try {
@@ -139,6 +119,5 @@ public class OrderMasterDataGuard {
throw new BizException(code, message);
}
}
public record Snapshots(PartnerClient.Partner partner, WarehouseClient.Detail receiving,
WarehouseClient.Detail destination) { }
public record Snapshots(WarehouseClient.Detail receiving, WarehouseClient.Detail destination) { }
}
@@ -2,7 +2,6 @@ package com.baiyue.order.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.PartnerClient;
import com.baiyue.order.client.WarehouseClient;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderEntity;
@@ -558,15 +557,15 @@ class OrderApplicationServiceStateTest {
}
private OrderMasterDataGuard.Snapshots snapshots() {
return snapshots(new PartnerClient.Partner(7L, 0L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER")));
return snapshots(null);
}
private OrderMasterDataGuard.Snapshots snapshots(PartnerClient.Partner partner) {
private OrderMasterDataGuard.Snapshots snapshots(Object ignored) {
var warehouse = new WarehouseClient.Warehouse(8L, 0L, "CN-SZX-01", "收货仓", "DOMESTIC", "CN", "ACTIVE");
var address = new WarehouseClient.Address("CN", "广东", "深圳", "南山", "518000", "测试路1号", null);
var capability = new WarehouseClient.Capability("RECEIVING", true, LocalTime.of(16, 0));
var receiving = new WarehouseClient.Detail(warehouse, List.of(address), List.of(capability));
return new OrderMasterDataGuard.Snapshots(partner, receiving, null);
return new OrderMasterDataGuard.Snapshots(receiving, null);
}
private OrderEntity persistedDraft(String pickupMethod, String destinationType) {
@@ -3,241 +3,92 @@ package com.baiyue.order.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.PartnerClient;
import com.baiyue.order.client.WarehouseClient;
import com.baiyue.order.dto.OrderRequests;
import feign.FeignException;
import feign.Request;
import feign.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@DisplayName("订单主数据校验")
import java.math.BigDecimal;
import java.util.List;
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.when;
@SpringJUnitConfig(OrderMasterDataGuard.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=token")
class OrderMasterDataGuardTest {
@MockBean private WarehouseClient warehouses;
@MockBean private PartnerClient partners;
@Resource private OrderMasterDataGuard guard;
private static final long PUBLIC_TENANT_ID = 0L;
@Test
void validMasterDataReturnsTrustedServerSnapshot() {
var partner=new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
var address=new WarehouseClient.Address("CN","广东省","深圳市","宝安区","518000","示例路1号",null);
var capability=new WarehouseClient.Capability("RECEIVING",true,java.time.LocalTime.of(16,0));
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,PUBLIC_TENANT_ID,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),List.of(address),List.of(capability));
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(partner));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(wh));
var result=guard.validate(10001,request(7L,8L));
assertThat(result.partner().partnerName()).isEqualTo("服务端客户名");
assertThat(result.receiving().warehouse().warehouseName()).isEqualTo("深圳仓");
}
@Test
void inactiveOrCrossTenantMasterDataIsRejected() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(null));
assertThatThrownBy(() -> guard.validate(10002,request(7L,8L)))
.isInstanceOf(BizException.class).hasMessageContaining("服务暂时不可用");
}
@Test
void downstreamHttp200BusinessFailureKeepsBusinessCodeAndMessage() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(
ApiResponse.<PartnerClient.Partner>failure(ResultCode.NOT_FOUND,"往来单位不存在",null,null));
@MockBean private WarehouseClient warehouses;
@Resource private OrderMasterDataGuard guard;
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
@Test
void validatesWarehousesWithoutTreatingCustomerAsWarehousePartner() {
when(warehouses.active("token", PUBLIC_TENANT_ID, 8L))
.thenReturn(ApiResponse.success(warehouse(8L, "DOMESTIC", true)));
var result = guard.validate(20002L, request(8L, "PRIVATE", null));
assertThat(result.receiving().warehouse().warehouseName()).isEqualTo("仓库8");
verify(warehouses).active("token", PUBLIC_TENANT_ID, 8L);
}
@Test
void persistedCustomerMustMatchAuthenticatedEnterprise() {
assertThatThrownBy(() -> guard.validatePersisted(20002L, 30003L, 8L, null, "PRIVATE"))
.isInstanceOf(BizException.class)
.hasMessage("往来单位不存在")
.hasMessage("订单客户必须是当前登录企业")
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
verifyNoInteractions(warehouses);
.isEqualTo(ResultCode.FORBIDDEN);
}
@Test
void masterDataResponseWithNullElementReturnsControlledBusinessError() {
var partner=new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,PUBLIC_TENANT_ID,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),
Collections.singletonList(null), Collections.singletonList(null));
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(partner));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(wh));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.VALIDATION_FAILED);
@Test
void customerTenantCanUsePublicDomesticReceivingWarehouse() {
when(warehouses.active("token", PUBLIC_TENANT_ID, 8L))
.thenReturn(ApiResponse.success(warehouse(8L, "DOMESTIC", true)));
guard.validate(20002L, request(8L, "PRIVATE", null));
verify(warehouses).active("token", PUBLIC_TENANT_ID, 8L);
}
@Test
void masterDataResponseMissingSnapshotNameIsServiceUnavailable() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,null,"ACTIVE","SHIPPER",List.of("SHIPPER"))));
void destinationWarehouseTypeMustMatchDestinationType() {
when(warehouses.active("token", PUBLIC_TENANT_ID, 8L))
.thenReturn(ApiResponse.success(warehouse(8L, "DOMESTIC", true)));
when(warehouses.active("token", PUBLIC_TENANT_ID, 9L))
.thenReturn(ApiResponse.success(warehouse(9L, "OVERSEAS", false)));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
verifyNoInteractions(warehouses);
}
@Test
void downstreamNonFeignRuntimeExceptionMapsToServiceUnavailable() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(new IllegalStateException("解码失败"));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
@Test
void downstreamForbiddenMapsToServiceUnavailable() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(403));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
@Test
void downstreamHttpNotFoundReturnsPlatformPartnerUnavailableMessage() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(404));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.hasMessage("平台往来单位不存在或不可用")
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
}
@Test
void wrappedDownstreamHttpConflictMapsToServiceUnavailable() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(new IllegalStateException("wrapped", feign(409)));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
@Test
void downstreamServerErrorMapsToServiceUnavailable() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(500));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
@Test
void inactiveMasterDataInSuccessfulResponseMapsToConflict() {
when(partners.active("token",PUBLIC_TENANT_ID,7))
.thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","INACTIVE","SHIPPER",List.of("SHIPPER"))));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
}
@Test
void partnerWithoutShipperTypeIsRejected() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"收货单位","ACTIVE","CONSIGNEE",List.of("CONSIGNEE"))));
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.VALIDATION_FAILED);
verifyNoInteractions(warehouses);
}
@Test
void customerTenantCanUseDomesticWarehouseFromPublicTenant() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
warehouse(8L,"DOMESTIC",true)));
guard.validate(20002,request(7L,8L));
verify(warehouses).active("token",PUBLIC_TENANT_ID,8);
}
@Test
void customersAAndBUseTheSamePlatformWarehouseAndPartner() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"平台共享单位","ACTIVE","SHIPPER",List.of("SHIPPER"))));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
warehouse(8L,"DOMESTIC",true)));
guard.validate(20002,request(7L,8L));
guard.validate(30003,request(7L,8L));
verify(partners,times(2)).active("token",PUBLIC_TENANT_ID,7);
verify(warehouses,times(2)).active("token",PUBLIC_TENANT_ID,8);
}
@Test
void orderWithoutPartnerUsesPublicWarehouseWithoutPartnerLookup() {
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
warehouse(8L,"DOMESTIC",true)));
var result=guard
.validate(20002,request(null,8L));
assertThat(result.partner()).isNull();
verifyNoInteractions(partners);
verify(warehouses).active("token",PUBLIC_TENANT_ID,8);
}
@Test
void fbaDestinationMustReferenceMatchingPublicWarehouse() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
warehouse(8L,"DOMESTIC",true)));
when(warehouses.active("token",PUBLIC_TENANT_ID,9)).thenReturn(ApiResponse.success(
warehouse(9L,"OVERSEAS",false)));
assertThatThrownBy(() -> guard
.validate(20002,request(7L,8L,"FBA",9L)))
assertThatThrownBy(() -> guard.validate(20002L, request(8L, "FBA", 9L)))
.isInstanceOf(BizException.class)
.hasMessageContaining("目的仓库类型必须为 FBA");
}
@Test
void privateDestinationCannotReferencePublicWarehouse() {
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
warehouse(8L,"DOMESTIC",true)));
assertThatThrownBy(() -> guard
.validate(20002,request(7L,8L,"PRIVATE",9L)))
.isInstanceOf(BizException.class)
.hasMessageContaining("私人地址不能引用公共仓库");
verify(warehouses,never()).active("token",PUBLIC_TENANT_ID,9);
private OrderRequests.SaveRequest request(long receivingWarehouseId, String destinationType,
Long destinationWarehouseId) {
var party = new OrderRequests.Party("公司", "联系人", "13800138000", null,
"CN", null, "深圳", null, "地址", null);
var cargo = new OrderRequests.Cargo("货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
null, null, null, null, null, false, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null);
return new OrderRequests.SaveRequest(null, "当前企业", "SEA", null, "CN", destinationWarehouseId,
"目的仓", null, null, party, party, List.of(cargo), "PICKUP", receivingWarehouseId,
"收货仓", "CH", "渠道", null, null, "BUY_DECLARATION", null, "LCL", false,
destinationType, null, null, null);
}
private FeignException feign(int status) {
Request request = Request.create(Request.HttpMethod.GET, "http://warehouse-service/internal/test",
Map.of(), null, StandardCharsets.UTF_8, null);
Response response = Response.builder().status(status).reason("test").request(request)
.headers(Map.of()).build();
return FeignException.errorStatus("masterData", response);
}
private OrderRequests.SaveRequest request(Long partner,Long warehouse) {
return request(partner,warehouse,"PRIVATE",null);
}
private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) {
var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null);
var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE,
null,null,null,null,null,false,null,null,null,null,null,null,null,null,null,null,null,null,null,
null,null,null,null,null,null);
return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道",null,null,"BUY_DECLARATION",null,"LCL",false,destinationType,null,null,null);
}
private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {
var value=new WarehouseClient.Warehouse(id,PUBLIC_TENANT_ID,"WH-"+id,"仓库"+id,type,"CN","ACTIVE");
var address=new WarehouseClient.Address("CN","广东省","深圳市",null,null,"示例地址",null);
var capabilities=receiving
? List.of(new WarehouseClient.Capability("RECEIVING",true,null))
private WarehouseClient.Detail warehouse(long id, String type, boolean receiving) {
var value = new WarehouseClient.Warehouse(id, PUBLIC_TENANT_ID, "WH-" + id, "仓库" + id,
type, "CN", "ACTIVE");
var address = new WarehouseClient.Address("CN", "广东省", "深圳市", null, null, "示例地址", null);
var capabilities = receiving
? List.of(new WarehouseClient.Capability("RECEIVING", true, null))
: List.<WarehouseClient.Capability>of();
return new WarehouseClient.Detail(value,List.of(address),capabilities);
return new WarehouseClient.Detail(value, List.of(address), capabilities);
}
}