fix(lastleg): inspect occupied pallets for PDA queries
This commit is contained in:
+17
-4
@@ -117,6 +117,17 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WarehousePalletReference resolvePallet(String palletCode) {
|
public WarehousePalletReference resolvePallet(String palletCode) {
|
||||||
|
WarehousePalletData data = palletData(palletCode);
|
||||||
|
requireReservable(data);
|
||||||
|
return pallet(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WarehousePalletReference inspectPallet(String palletCode) {
|
||||||
|
return pallet(palletData(palletCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WarehousePalletData palletData(String palletCode) {
|
||||||
WarehousePalletResponse response = client.get()
|
WarehousePalletResponse response = client.get()
|
||||||
.uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode)
|
.uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode)
|
||||||
.header("X-Internal-Service-Token", internalToken)
|
.header("X-Internal-Service-Token", internalToken)
|
||||||
@@ -130,10 +141,12 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
|||||||
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
"Warehouse pallet service returned no usable response");
|
"Warehouse pallet service returned no usable response");
|
||||||
}
|
}
|
||||||
requireReservable(response.data());
|
return response.data();
|
||||||
return new WarehousePalletReference(response.data().id(), response.data().palletCode(),
|
}
|
||||||
response.data().warehouseId(), response.data().locationId(), response.data().status(),
|
|
||||||
response.data().fbaWarehouseCode(), response.data().unitPrice(), response.data().version());
|
private WarehousePalletReference pallet(WarehousePalletData data) {
|
||||||
|
return new WarehousePalletReference(data.id(), data.palletCode(), data.warehouseId(), data.locationId(),
|
||||||
|
data.status(), data.fbaWarehouseCode(), data.unitPrice(), data.version());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+3
@@ -9,6 +9,9 @@ public interface WarehouseResourceGateway {
|
|||||||
void scrapPallet(String customerId, long palletId);
|
void scrapPallet(String customerId, long palletId);
|
||||||
WarehousePalletReference resolvePallet(String palletCode);
|
WarehousePalletReference resolvePallet(String palletCode);
|
||||||
WarehousePalletReference resolvePallet(long palletId);
|
WarehousePalletReference resolvePallet(long palletId);
|
||||||
|
default WarehousePalletReference inspectPallet(String palletCode) {
|
||||||
|
return resolvePallet(palletCode);
|
||||||
|
}
|
||||||
default WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) {
|
default WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) {
|
||||||
throw new UnsupportedOperationException("Warehouse location resolution is not configured");
|
throw new UnsupportedOperationException("Warehouse location resolution is not configured");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ public class PdaWarehouseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PdaWarehouseResponses.Pallet pallet(long warehouseId, PdaWarehouseRequests.Scan request) {
|
public PdaWarehouseResponses.Pallet pallet(long warehouseId, PdaWarehouseRequests.Scan request) {
|
||||||
WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.resolvePallet(code(request.code()));
|
WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.inspectPallet(code(request.code()));
|
||||||
if (pallet == null) throw notFound("Warehouse pallet was not found");
|
if (pallet == null) throw notFound("Warehouse pallet was not found");
|
||||||
if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse");
|
if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse");
|
||||||
WarehouseResourceGateway.WarehouseLocationReference location = warehouse.resolveLocation(warehouseId,
|
WarehouseResourceGateway.WarehouseLocationReference location = warehouse.resolveLocation(warehouseId,
|
||||||
|
|||||||
+18
@@ -64,6 +64,24 @@ class HttpWarehouseResourceGatewayTest {
|
|||||||
server.verify();
|
server.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inspectAllowsOccupiedPalletForReadOnlyInventoryQueries() {
|
||||||
|
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-OCCUPIED"))
|
||||||
|
.andRespond(withSuccess("""
|
||||||
|
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-OCCUPIED",
|
||||||
|
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||||
|
"status":"OCCUPIED","fbaWarehouseCode":null,"version":4,
|
||||||
|
"locationType":"STORAGE","locationStatus":"OCCUPIED","reservable":false,
|
||||||
|
"reservationBlockReason":"PALLET_NOT_IDLE"}}
|
||||||
|
""", MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
WarehouseResourceGateway.WarehousePalletReference result = gateway.inspectPallet("PAL-OCCUPIED");
|
||||||
|
|
||||||
|
assertThat(result.palletId()).isEqualTo(2092040891261427713L);
|
||||||
|
assertThat(result.status()).isEqualTo("OCCUPIED");
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveFailsClosedWhenWarehouseOmitsReservationFacts() {
|
void resolveFailsClosedWhenWarehouseOmitsReservationFacts() {
|
||||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN"))
|
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN"))
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ class PdaWarehouseServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void palletFromAnotherWarehouseIsRejected() {
|
void palletFromAnotherWarehouseIsRejected() {
|
||||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
when(warehouse.inspectPallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
||||||
1L, "PAL-1", 901L, 2L, "OCCUPIED", null, 1));
|
1L, "PAL-1", 901L, 2L, "OCCUPIED", null, 1));
|
||||||
assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("PAL-1")))
|
assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("PAL-1")))
|
||||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
@@ -50,7 +50,7 @@ class PdaWarehouseServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void missingPalletIsReportedAsNotFound() {
|
void missingPalletIsReportedAsNotFound() {
|
||||||
when(warehouse.resolvePallet("MISSING")).thenReturn(null);
|
when(warehouse.inspectPallet("MISSING")).thenReturn(null);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("MISSING")))
|
assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("MISSING")))
|
||||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("404");
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("404");
|
||||||
|
|||||||
+7
-1
@@ -29,13 +29,19 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释
|
|||||||
"'RELEASED'",
|
"'RELEASED'",
|
||||||
"terminalStatus === 'COMPLETED'",
|
"terminalStatus === 'COMPLETED'",
|
||||||
"'CANCELLED'",
|
"'CANCELLED'",
|
||||||
'relabelPayload(context, warehouseId, suffix, 0.01)',
|
'relabelPayload(context, warehouseId, source, suffix, 0.01)',
|
||||||
|
'/relabel-applications/inventory-candidates',
|
||||||
|
'operationWarehouseId: id(warehouseId)',
|
||||||
|
'sourceInventoryBalanceId:',
|
||||||
|
'sourceInventoryPositionId:',
|
||||||
|
'sourceInboundOrderId:',
|
||||||
'cartonMark:',
|
'cartonMark:',
|
||||||
'fbaCartonMark:',
|
'fbaCartonMark:',
|
||||||
]) assert.ok(source.includes(token), token)
|
]) assert.ok(source.includes(token), token)
|
||||||
assert.match(source, /actualBalance[\s\S]*frozenBalance[\s\S]*availableBalance[\s\S]*creditLimit[\s\S]*creditUsed[\s\S]*availableCredit/)
|
assert.match(source, /actualBalance[\s\S]*frozenBalance[\s\S]*availableBalance[\s\S]*creditLimit[\s\S]*creditUsed[\s\S]*availableCredit/)
|
||||||
assert.match(source, /voucherFileId: id\(files\.find\(\(item\) => item\.category === 'VOUCHER'\)\?\.fileId\)/)
|
assert.match(source, /voucherFileId: id\(files\.find\(\(item\) => item\.category === 'VOUCHER'\)\?\.fileId\)/)
|
||||||
assert.doesNotMatch(source, /voucherFileId:\s*Number\(/)
|
assert.doesNotMatch(source, /voucherFileId:\s*Number\(/)
|
||||||
|
assert.doesNotMatch(source, /customerOrderNo:[^\n]+overseasWarehouseId:/)
|
||||||
assert.doesNotMatch(source, /internal\/finance|jdbc|mysql|process\.env|https?:\/\//i)
|
assert.doesNotMatch(source, /internal\/finance|jdbc|mysql|process\.env|https?:\/\//i)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -169,14 +169,17 @@ async function procurementFlow(context, fixture, overseasToken) {
|
|||||||
return { orderNo, feeNo, freezePollRequests: frozen.attempts, settlePollRequests: settled.attempts }
|
return { orderNo, feeNo, freezePollRequests: frozen.attempts, settlePollRequests: settled.attempts }
|
||||||
}
|
}
|
||||||
|
|
||||||
function relabelPayload(context, warehouseId, suffix, chargeAmount) {
|
function relabelPayload(context, warehouseId, source, suffix, chargeAmount) {
|
||||||
const value = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
|
const value = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
|
||||||
return {
|
return {
|
||||||
customerOrderNo: `${context.marker}-${suffix}`.slice(0, 128), overseasWarehouseId: id(warehouseId),
|
customerOrderNo: `${context.marker}-${suffix}`.slice(0, 128), operationWarehouseId: id(warehouseId),
|
||||||
labelDeadline: new Date(Date.now() + 86_400_000).toISOString().slice(0, 19),
|
labelDeadline: new Date(Date.now() + 86_400_000).toISOString().slice(0, 19),
|
||||||
contactName: '全量验收联系人', contactPhone: '+1-213-555-0199', chargeAmount,
|
contactName: '全量验收联系人', contactPhone: '+1-213-555-0199', chargeAmount,
|
||||||
remark: context.marker, idempotencyKey: `${context.marker}-${suffix}`.slice(0, 255),
|
remark: context.marker, idempotencyKey: `${context.marker}-${suffix}`.slice(0, 255),
|
||||||
items: [{ skuCode: `OLD-${value}-${suffix}`.slice(0, 128), productNameZh: `${context.marker} 换标商品`,
|
items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId),
|
||||||
|
sourceInventoryPositionId: id(source.position.positionId), sourceInboundOrderId: id(source.inboundOrderId),
|
||||||
|
skuCode: source.skuCode || `OLD-${value}-${suffix}`.slice(0, 128),
|
||||||
|
productNameZh: source.productNameZh || `${context.marker} 换标商品`,
|
||||||
totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128),
|
totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128),
|
||||||
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker,
|
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker,
|
||||||
cartonMark: `CM-${value}-${suffix}`.slice(0, 255),
|
cartonMark: `CM-${value}-${suffix}`.slice(0, 255),
|
||||||
@@ -185,7 +188,20 @@ function relabelPayload(context, warehouseId, suffix, chargeAmount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function relabelFlow(context, overseasToken, warehouseId, suffix, terminalStatus) {
|
async function relabelFlow(context, overseasToken, warehouseId, suffix, terminalStatus) {
|
||||||
const payload = relabelPayload(context, warehouseId, suffix, 0.01)
|
const candidatesResponse = await context.requestOverseas('GET', query(
|
||||||
|
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', { operationWarehouseId: warehouseId }),
|
||||||
|
undefined, {}, overseasToken)
|
||||||
|
const candidates = success(candidatesResponse) && Array.isArray(candidatesResponse.body?.data)
|
||||||
|
? candidatesResponse.body.data : []
|
||||||
|
const source = candidates.find((candidate) => id(candidate.inboundOrderId)
|
||||||
|
&& id(candidate.inventoryBalanceId)
|
||||||
|
&& candidate.positions?.some((position) => id(position.positionId) && Number(position.availablePackages) > 0))
|
||||||
|
if (source) source.position = source.positions.find((position) => id(position.positionId)
|
||||||
|
&& Number(position.availablePackages) > 0)
|
||||||
|
assert(context, `${suffix}换标读取当前客户权威库存候选`, candidatesResponse,
|
||||||
|
Boolean(source?.position), `warehouseId=${warehouseId}, candidates=${candidates.length}`)
|
||||||
|
if (!source?.position) return null
|
||||||
|
const payload = relabelPayload(context, warehouseId, source, suffix, 0.01)
|
||||||
const created = await context.requestOverseas('POST', '/api/merchant/v1/last-leg/relabel-applications', payload, {}, overseasToken)
|
const created = await context.requestOverseas('POST', '/api/merchant/v1/last-leg/relabel-applications', payload, {}, overseasToken)
|
||||||
const relabelNo = created?.body?.data?.application?.relabelNo
|
const relabelNo = created?.body?.data?.application?.relabelNo
|
||||||
assert(context, `${suffix}换标单创建并携带收费金额`, created,
|
assert(context, `${suffix}换标单创建并携带收费金额`, created,
|
||||||
|
|||||||
Reference in New Issue
Block a user