优化
This commit is contained in:
+24
-4
@@ -229,7 +229,7 @@ public class ReceivingContainerService {
|
||||
CartonRef carton = requireCartonRef(customerId, session.getContainerNo(), request.cartonMark());
|
||||
InboundRef ref = carton.ref();
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(ref);
|
||||
return new ReceivingContainerResponses.CartonResolved(carton.carton().getCartonMark(),
|
||||
return new ReceivingContainerResponses.CartonResolved(request.cartonMark().trim(),
|
||||
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
|
||||
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
|
||||
inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(),
|
||||
@@ -423,13 +423,33 @@ public class ReceivingContainerService {
|
||||
private CartonRef requireCartonRef(String customerId, String containerNo, String cartonMark) {
|
||||
InboundPlanCarton carton = cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, cartonMark));
|
||||
if (carton == null) throw notFound("Carton mark was not found");
|
||||
if (carton == null) return resolveFbaCartonCodeWithoutManifest(customerId, containerNo, cartonMark);
|
||||
List<InboundRef> matches = refs(customerId, containerNo).stream()
|
||||
.filter(value -> carton.getLineId() == null || Objects.equals(value.line().getId(), carton.getLineId()))
|
||||
.filter(value -> carton.getPlanId() == null || Objects.equals(value.plan().getId(), carton.getPlanId()))
|
||||
.filter(value -> Objects.equals(value.line().getOrderId(), carton.getOrderId())).toList();
|
||||
if (matches.size() != 1) throw conflict("Carton mark belongs to another container");
|
||||
return new CartonRef(carton, matches.get(0));
|
||||
return new CartonRef(matches.get(0));
|
||||
}
|
||||
|
||||
private CartonRef resolveFbaCartonCodeWithoutManifest(String customerId, String containerNo, String cartonMark) {
|
||||
int delimiter = cartonMark == null ? -1 : cartonMark.indexOf('#');
|
||||
if (delimiter < 1) throw conflict("Carton mark must contain a non-empty inbound code prefix");
|
||||
String inboundCode = cartonMark.substring(0, delimiter).trim();
|
||||
if (inboundCode.isEmpty()) throw conflict("Carton mark must contain a non-empty inbound code prefix");
|
||||
List<InboundRef> matches = refs(customerId, containerNo).stream()
|
||||
.filter(value -> inboundCode.equals(value.line().getInboundCode())).toList();
|
||||
if (matches.isEmpty()) {
|
||||
boolean exists = lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getCustomerId, customerId)
|
||||
.eq(InboundPlanLine::getInboundCode, inboundCode)).stream().findAny().isPresent();
|
||||
if (exists) throw conflict("Carton mark belongs to another container");
|
||||
throw notFound("Carton mark was not found");
|
||||
}
|
||||
if (matches.size() != 1) throw conflict("Carton mark inbound code is ambiguous in the current container");
|
||||
InboundRef ref = matches.get(0);
|
||||
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) throw notFound("Carton mark was not found");
|
||||
return new CartonRef(ref);
|
||||
}
|
||||
|
||||
private List<InboundRef> requireFbaPlanRefs(String customerId, String containerNo, InboundRef selected) {
|
||||
@@ -806,7 +826,7 @@ public class ReceivingContainerService {
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
private record InboundRef(InboundPlan plan, InboundPlanLine line, LastlegOrder order) { }
|
||||
private record CartonRef(InboundPlanCarton carton, InboundRef ref) { }
|
||||
private record CartonRef(InboundRef ref) { }
|
||||
private record SkuExpectation(String skuCode, String productName, int expectedPackages) { }
|
||||
private record FbaSkuAllocation(InboundRef ref, String skuCode, String productName, int packages) { }
|
||||
private record Totals(int inboundCount, List<String> destinationCodes, int plannedPalletCount,
|
||||
|
||||
@@ -72,10 +72,10 @@ paths:
|
||||
/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve:
|
||||
post:
|
||||
summary: 解析当前柜箱唛
|
||||
description: 权限 lastleg:pda:operate;FBA 和 OVERSEAS 均可解析当前锁定柜内箱码,返回唯一入仓码、目的类型、目标仓和实收进度;不返回 SKU。
|
||||
description: 权限 lastleg:pda:operate;FBA 从完整 cartonMark 的第一个 # 前提取固定入仓码,并精确匹配当前锁定柜唯一 FBA line;不要求逐箱 manifest,不校验 SKU 或箱序号。OVERSEAS 必须命中真实 manifest。两者均不返回 SKU,也不登记收货。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { type: object, required: [cartonMark], properties: { cartonMark: { type: string } } } } } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '409': { description: 箱码跨柜或归属不唯一 } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '404': { description: 入仓码未命中当前柜,或 OVERSEAS 箱码未命中真实 manifest }, '409': { description: 缺少 #、空入仓码前缀、跨柜或当前柜多重命中 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve:
|
||||
post:
|
||||
summary: 解析当前柜实物托盘码
|
||||
|
||||
+3
-3
@@ -100,10 +100,10 @@ class ReceivingContainerControllerTest {
|
||||
@Test
|
||||
void fbaCartonResolutionDelegatesWithoutControllerModeRejection() {
|
||||
ReceivingContainerRequests.CartonResolve request =
|
||||
new ReceivingContainerRequests.CartonResolve("NYEXX2608260002#4-1");
|
||||
new ReceivingContainerRequests.CartonResolve("NYEXX2608260001#4-1");
|
||||
when(service.resolveCarton("42", request)).thenReturn(new ReceivingContainerResponses.CartonResolved(
|
||||
"NYEXX2608260002#4-1", "IN-1", "FBA", "900", "Shared LAX", null, null, null,
|
||||
"ABE3", 1, 40, 0, 40));
|
||||
"NYEXX2608260001#4-1", "NYEXX2608260001", "FBA", "900", "Shared LAX", null, null, null,
|
||||
"ABE3", 1, 4, 0, 4));
|
||||
|
||||
controller.resolveCarton(picker(), request);
|
||||
|
||||
|
||||
+59
-5
@@ -406,15 +406,69 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void fbaCartonResolutionForNyexx2608260002DoesNotApplyReceiptModeGate() {
|
||||
void fbaCartonResolutionWithoutManifestForNyexx2608260001DoesNotWriteReceiptState() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "NYEXX2608260002#4-1"));
|
||||
InboundPlanLine fba = line("FBA");
|
||||
fba.setInboundCode("NYEXX2608260001"); fba.setRequiredPackageCount(4);
|
||||
when(lines.selectList(any())).thenReturn(List.of(fba));
|
||||
|
||||
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("NYEXX2608260002#4-1"));
|
||||
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("NYEXX2608260001#4-1"));
|
||||
var replay = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("NYEXX2608260001#4-1"));
|
||||
|
||||
assertThat(resolved.cartonMark()).isEqualTo("NYEXX2608260002#4-1");
|
||||
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
|
||||
assertThat(resolved.cartonMark()).isEqualTo("NYEXX2608260001#4-1");
|
||||
assertThat(resolved.inboundCode()).isEqualTo("NYEXX2608260001");
|
||||
assertThat(resolved.destinationType()).isEqualTo("FBA");
|
||||
assertThat(replay).isEqualTo(resolved);
|
||||
verifyNoInteractions(workflow, operations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fbaCartonResolutionRejectsCrossContainerAndInvalidPrefixesAsConflicts() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
InboundPlanLine current = line("FBA");
|
||||
current.setInboundCode("NYEXX2608260001"); current.setRequiredPackageCount(4);
|
||||
InboundPlanLine foreign = line("FBA");
|
||||
foreign.setInboundCode("NYEXX2608269999"); foreign.setPlanId(99L);
|
||||
when(lines.selectList(any())).thenReturn(List.of(current), List.of(foreign));
|
||||
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("NYEXX2608269999#4-1")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(409));
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("NYEXX2608260001")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(409));
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("#4-1")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(409));
|
||||
|
||||
org.mockito.Mockito.reset(lines, cartons);
|
||||
when(lines.selectList(any())).thenReturn(List.of(current), List.of());
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("UNKNOWN#5-1")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(404));
|
||||
|
||||
InboundPlanLine duplicate = line("FBA");
|
||||
duplicate.setId(20L); duplicate.setInboundCode("NYEXX2608260001");
|
||||
org.mockito.Mockito.reset(lines);
|
||||
when(lines.selectList(any())).thenReturn(List.of(current, duplicate));
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("NYEXX2608260001#any-tail")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(409));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasCartonResolutionWithoutManifestIsNotFound() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
|
||||
assertThatThrownBy(() -> service.resolveCarton("7",
|
||||
new ReceivingContainerRequests.CartonResolve("IN-1#2-1")))
|
||||
.isInstanceOfSatisfying(ResponseStatusException.class,
|
||||
error -> assertThat(error.getStatusCode().value()).isEqualTo(404));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
需要为人工联调批量准备同一客户、同一国内仓和同一渠道的 `WAIT_SPLIT` 数据时,使用[《批量待分柜数据生成指南》](批量待分柜数据生成指南.md)。该入口通过正式下单、入库、称重和测量接口生成数据,支持 FBA、海外仓、私人地址及一件代发等稳定随机组合;不得直接写库或用于生产环境。
|
||||
|
||||
订单造数中的 `GET /api/merchant/v1/partners/candidates?type=SHIPPER` 属于 Warehouse 正式客户候选能力。失败时订单脚本仍可使用 sender 快照继续收集后续链路证据,但 HTTP 503/`S0503` 必须保留为 deployment blocker,不能因订单、测量、分柜或装柜后续通过而降级为 coverage gap。
|
||||
|
||||
人工分柜采用两个可选模块,不进入默认全量入口:`manual-order-splitting-fixture.mjs` 基于本轮至少四票 `WAIT_SPLIT` 数据生成部分分配、整票拼柜和未分配对照清单;人工在 Admin 完成计划分配后,`manual-order-splitting-verify.mjs` 读取准备报告和 `loadingPlanId`,只读核对柜次范围、计划箱数及候选余量。完整命令见同一指南的“人工分柜验收”章节。
|
||||
|
||||
人工分柜通过后可继续执行 `manual-preload-push-fixture.mjs`:它读取分柜核验报告并冻结推送前计划版本、明细及无号模拟柜状态;人工点击“推送预装”后,再由 `manual-preload-push-verify.mjs` 只读核对 `EDITING` 预装清单、PENDING bridge、版本递增、明细零漂移和 PDA `A0404`。两个模块同样不进入默认全量入口。
|
||||
@@ -88,6 +90,8 @@ export async function run(context) {
|
||||
|
||||
海外 PDA 收货统一扫描冻结箱码:`FBA` 与 `OVERSEAS` 都必须由 `POST /api/pda/v1/last-leg/receiving-containers/current/cartons/resolve` 返回 HTTP 200、业务码 `200` 和当前柜唯一 `inboundCode`,不展示、扫描或提交 SKU。仅实收阶段分流:FBA 使用不含 `skuCode` 的 `/current/receipts/bulk`,逐箱接口必须精确返回 `A0409 FBA destination must use bulk count`;OVERSEAS 使用 `/current/receipts/carton`,bulk 必须精确返回 `A0409 OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden`。实机旧行为反证为 requestId `5e925a3d-8184-41e1-b0d2-f4e1aae86639`、traceId `62fdf603b65fd7f43ec452bb8a6ca185`。跨区广播消费只保留观察证据,不再单独形成 deployment blocker;实际 `PICKUP_READY` 候选门禁仍必须通过。
|
||||
|
||||
FBA 无 `ll_inbound_plan_carton` manifest 时,服务端只取完整扫描值的第一个 `#` 前缀,并在当前锁定柜中精确匹配唯一 `inboundCode`;不校验 SKU、`#` 后缀或箱序号,也不能要求前端拆分或自行决定归属。无 `#`、空前缀、未命中、跨柜或同柜多匹配必须拒绝。OVERSEAS 仍按完整箱码精确匹配真实 manifest 行。HTTP 合同使用任意 FBA 后缀验证前缀解析,覆盖上述负向与 OVERSEAS 缺 manifest,并在正负向 resolve 后回查 current 的状态、版本与收货数量零漂移。2026-08-26 最新已部署实机尚返回 `A0404 Carton mark was not found`,requestId `a716e2c2-dc10-4d07-8014-7438afb63090`、traceId `6ab8fcfcd812056ad6570d3b1652157b`;该证据保留为后端 FBA 入仓码前缀解析落盘及重新部署后的回归反证,不据此放宽成功合同。
|
||||
|
||||
```powershell
|
||||
node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --overseas-gateway <development-overseas-gateway> --modules phase4-overseas-warehouse,phase4-overseas-pda,phase4-overseas-pda-operations,phase4-cross-region-events,phase4-overseas-business,phase4-overseas-master-data,phase4-carrier-nodes
|
||||
```
|
||||
|
||||
@@ -59,5 +59,6 @@ test('批量待分柜参数固定同一客户并限制为订单测量链', () =>
|
||||
assert.match(orders, /destinationType === 'PRIVATE'/)
|
||||
assert.match(orders, /PRIVATE_ADDRESS/)
|
||||
assert.match(orders, /eligible\.length < 1/)
|
||||
assert.match(orders, /Client 查询发货客户候选', partnerResponse, '', CONTRACT_OPTIONS/)
|
||||
assert.doesNotMatch(orders, /customerName: master\.partner\?\.partnerName/)
|
||||
})
|
||||
|
||||
@@ -290,7 +290,7 @@ async function loadMasterData(context, merchantToken) {
|
||||
|
||||
const partnerResponse = await request(context, 'GET',
|
||||
'/api/merchant/v1/partners/candidates?type=SHIPPER', undefined, merchantToken)
|
||||
const partners = context.apiSuccess(MODULE, 'Client 查询发货客户候选', partnerResponse)
|
||||
const partners = context.apiSuccess(MODULE, 'Client 查询发货客户候选', partnerResponse, '', CONTRACT_OPTIONS)
|
||||
const partner = Array.isArray(partners)
|
||||
? partners.find((item) => item.status === 'ACTIVE' && item.types?.includes?.('SHIPPER'))
|
||||
: null
|
||||
|
||||
@@ -62,7 +62,18 @@ test('PDA module covers the container-number receiving contract and excludes out
|
||||
assert.match(source, /FBA和OVERSEAS箱码resolve都必须HTTP 200\/code 200并返回当前柜唯一入仓码/)
|
||||
assert.match(source, /const matches = inbounds\.filter/)
|
||||
assert.match(source, /!Array\.isArray\(resolved\?\.inboundCode\)/)
|
||||
assert.match(source, /matches\.length === 1/)
|
||||
assert.match(source, /destinations\.size === 1/)
|
||||
assert.doesNotMatch(source, /\.split\([^\n]*['"]#['"]/)
|
||||
assert.match(source, /`\$\{item\.inboundCode\}#PDA-CONTRACT-\$\{index \+ 1\}`/)
|
||||
for (const negative of ['无#箱码被服务端拒绝', '空#前缀箱码被服务端拒绝',
|
||||
'未命中入仓码前缀被服务端拒绝', '其他柜箱码前缀被当前锁定柜拒绝',
|
||||
'OVERSEAS缺少真实manifest行时不得按标准箱码推导']) {
|
||||
assert.ok(source.includes(negative), negative)
|
||||
}
|
||||
assert.doesNotMatch(source, /FBA标准箱码序号超范围被拒绝/)
|
||||
assert.match(source, /FBA与OVERSEAS箱码resolve均为纯查询且零状态写入/)
|
||||
assert.match(source, /receivingSnapshot\(afterResolve\.data\) === beforeResolve/)
|
||||
assert.match(source, /const fixturesConserved = overseasFixtures\.length === expectedPending/)
|
||||
assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/)
|
||||
assert.match(source, /for \(const \[index, fixture\] of overseasFixtures\.entries\(\)\)/)
|
||||
@@ -108,6 +119,21 @@ test('real-device legacy FBA carton rejection is retained as regression evidence
|
||||
assert.match(source, /a0409Message\(response, \/\^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden\$\//)
|
||||
})
|
||||
|
||||
test('real-device deployed FBA resolve A0404 is retained until server-side inbound-prefix matching lands', () => {
|
||||
const evidence = {
|
||||
http: 200,
|
||||
code: 'A0404',
|
||||
message: 'Carton mark was not found',
|
||||
requestId: 'a716e2c2-dc10-4d07-8014-7438afb63090',
|
||||
traceId: '6ab8fcfcd812056ad6570d3b1652157b',
|
||||
databaseFact: 'current FBA has no ll_inbound_plan_carton manifest',
|
||||
}
|
||||
assert.equal(evidence.code, 'A0404')
|
||||
assert.equal(evidence.message, 'Carton mark was not found')
|
||||
assert.match(evidence.databaseFact, /no ll_inbound_plan_carton manifest/)
|
||||
assert.match(source, /a0404Message\(response, \/\^Carton mark was not found\$\//)
|
||||
})
|
||||
|
||||
test('PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete', () => {
|
||||
const orderedTokens = [
|
||||
"'查询当前客户待收柜号'", "'选择柜号并建立WAIT_SIGN会话'",
|
||||
|
||||
@@ -13,12 +13,26 @@ function code(response) { return String(response?.body?.code ?? '') }
|
||||
function traced(response) { return nonBlank(response?.requestId) && nonBlank(response?.traceId) }
|
||||
function conflict(response) { return (response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(code(response)))) && traced(response) }
|
||||
function a0409Message(response, pattern) { return response?.http === 200 && code(response) === 'A0409' && traced(response) && pattern.test(String(response?.body?.message || '')) }
|
||||
function a0404Message(response, pattern) { return response?.http === 200 && code(response) === 'A0404' && traced(response) && pattern.test(String(response?.body?.message || '')) }
|
||||
function cartonResolveRejected(response) { return response?.http === 200 && ['A0400', 'A0404', 'A0409'].includes(code(response)) && traced(response) }
|
||||
function notFound(response) { return (response?.http === 404 || (response?.http === 200 && ['404', 'A0404'].includes(code(response)))) && traced(response) }
|
||||
function forbidden(response) { return [401, 403].includes(response?.http) || (response?.http === 200 && ['A0401', 'A0403'].includes(code(response))) }
|
||||
function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` }
|
||||
function key(context, action, suffix = '') { return `${context.marker}-${context.pdaContainerSuffix || ''}-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120) }
|
||||
function markerSuffix(context) { return `${context.marker}${context.pdaContainerSuffix || ''}`.replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase() }
|
||||
function records(data) { return Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [] }
|
||||
function receivingSnapshot(data) {
|
||||
return JSON.stringify({
|
||||
containerNo: data?.containerNo, status: data?.status, version: data?.version,
|
||||
totalPackages: data?.totalPackages, receivedPackages: data?.receivedPackages,
|
||||
pendingPackages: data?.pendingPackages,
|
||||
inbounds: (Array.isArray(data?.inbounds) ? data.inbounds : []).map((item) => ({
|
||||
inboundCode: item?.inboundCode, destinationType: item?.destinationType,
|
||||
totalPackages: item?.totalPackages, receivedPackages: item?.receivedPackages,
|
||||
pendingPackages: item?.pendingPackages,
|
||||
})).sort((left, right) => `${left.inboundCode}:${left.destinationType}`.localeCompare(`${right.inboundCode}:${right.destinationType}`)),
|
||||
})
|
||||
}
|
||||
|
||||
function blockerGap(context, name, reason, category = 'fixture-unavailable', response = null) {
|
||||
context.addCoverageGap(MODULE, SCENARIO, name, reason, category, { deploymentBlocker: true })
|
||||
@@ -293,10 +307,14 @@ async function awaitPickupReadyCandidates(context, token, producers, allProducer
|
||||
}
|
||||
|
||||
async function resolveCartonFixtures(context, token, inbounds, producerCartonMarks = []) {
|
||||
const cartonCandidates = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
|
||||
.map(id).filter(nonBlank))]
|
||||
const manifestCartons = (Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
|
||||
.map(id).filter(nonBlank)
|
||||
const fbaCartons = inbounds.filter((item) => item.destinationType === 'FBA')
|
||||
.map((item, index) => `${item.inboundCode}#PDA-CONTRACT-${index + 1}`)
|
||||
const overseasCartons = inbounds.some((item) => item.destinationType === 'OVERSEAS') ? manifestCartons : []
|
||||
const cartonCandidates = [...new Set([...fbaCartons, ...overseasCartons])]
|
||||
if (!cartonCandidates.length) {
|
||||
blockerGap(context, '统一箱码解析链', '缺少本轮PDA任务冻结箱码,FBA和OVERSEAS均不能使用伪造箱号')
|
||||
blockerGap(context, '统一箱码解析链', 'FBA缺少当前柜入仓码或OVERSEAS缺少真实manifest箱码')
|
||||
return null
|
||||
}
|
||||
const cartonFixtures = []
|
||||
@@ -307,7 +325,7 @@ async function resolveCartonFixtures(context, token, inbounds, producerCartonMar
|
||||
const matches = inbounds.filter((item) => item.inboundCode === resolved?.inboundCode)
|
||||
const destinations = new Set(matches.map((item) => item?.destinationType).filter(nonBlank))
|
||||
const unique = success(response) && nonBlank(resolved?.inboundCode) && !Array.isArray(resolved?.inboundCode)
|
||||
&& matches.length > 0 && destinations.size === 1
|
||||
&& matches.length === 1 && destinations.size === 1
|
||||
&& ['FBA', 'OVERSEAS'].includes(resolved?.destinationType)
|
||||
&& destinations.has(resolved?.destinationType)
|
||||
context.addStep(MODULE, `统一解析${resolved?.destinationType || 'UNKNOWN'}箱码${index + 1}并返回唯一入仓码`, response,
|
||||
@@ -323,6 +341,41 @@ async function resolveCartonFixtures(context, token, inbounds, producerCartonMar
|
||||
return cartonFixtures
|
||||
}
|
||||
|
||||
async function probeCartonResolveNegatives(context, token, current) {
|
||||
const fba = current.inbounds.find((item) => item.destinationType === 'FBA')
|
||||
await expected(context, token, 'POST', '无#箱码被服务端拒绝',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve',
|
||||
{ cartonMark: fba?.inboundCode || `NO-HASH-${markerSuffix(context)}` }, cartonResolveRejected)
|
||||
await expected(context, token, 'POST', '空#前缀箱码被服务端拒绝',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve',
|
||||
{ cartonMark: `#${markerSuffix(context)}` }, cartonResolveRejected)
|
||||
await expected(context, token, 'POST', '未命中入仓码前缀被服务端拒绝',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve',
|
||||
{ cartonMark: `UNMATCHED-${markerSuffix(context)}#ANY-SUFFIX` }, cartonResolveRejected)
|
||||
|
||||
const allProducers = context.createdResources?.phase4InboundProducerFixtures?.containers || []
|
||||
const crossContainerMark = allProducers
|
||||
.filter((item) => id(item?.containerNo) !== id(current.containerNo))
|
||||
.flatMap((item) => Array.isArray(item?.cartonMarks) ? item.cartonMarks : [item?.cartonMark])
|
||||
.map(id).find(nonBlank)
|
||||
if (crossContainerMark) {
|
||||
await expected(context, token, 'POST', '其他柜箱码前缀被当前锁定柜拒绝',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve',
|
||||
{ cartonMark: crossContainerMark }, cartonResolveRejected)
|
||||
} else {
|
||||
blockerGap(context, '箱码跨柜负向', '缺少另一真实柜的冻结箱码,不能伪造跨柜归属反证')
|
||||
}
|
||||
|
||||
const overseas = current.inbounds.find((item) => item.destinationType === 'OVERSEAS')
|
||||
if (overseas) {
|
||||
const total = Math.max(1, Number(overseas.totalPackages || 1))
|
||||
await expected(context, token, 'POST', 'OVERSEAS缺少真实manifest行时不得按标准箱码推导',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve',
|
||||
{ cartonMark: `${overseas.inboundCode}#${total + 1}-1` },
|
||||
(response) => a0404Message(response, /^Carton mark was not found$/))
|
||||
}
|
||||
}
|
||||
|
||||
async function receiveFba(context, token, inbound, pallet, cartonFixtures, index) {
|
||||
const resolvedCartons = cartonFixtures.filter((item) => item.inbound.inboundCode === inbound.inboundCode
|
||||
&& item.resolved.destinationType === 'FBA')
|
||||
@@ -454,10 +507,18 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
|
||||
{ ...signBody, photos: [...photos].reverse() }, conflict)
|
||||
const current = await pda(context, token, 'GET', '签收后按账号恢复当前柜', '/api/pda/v1/last-leg/receiving-containers/current')
|
||||
if (!success(current.response)) return
|
||||
const beforeResolve = receivingSnapshot(current.data)
|
||||
await probeCartonResolveNegatives(context, token, current.data)
|
||||
const producerCartonMarks = preferredProducer?.cartonMarks?.length
|
||||
? preferredProducer.cartonMarks : [preferredProducer?.cartonMark]
|
||||
const cartonFixtures = await resolveCartonFixtures(context, token, current.data.inbounds, producerCartonMarks)
|
||||
if (!cartonFixtures) return
|
||||
const afterResolve = await pda(context, token, 'GET', '箱码解析正负向后回查当前柜零写入',
|
||||
'/api/pda/v1/last-leg/receiving-containers/current')
|
||||
context.assert(MODULE, 'FBA与OVERSEAS箱码resolve均为纯查询且零状态写入', afterResolve.response,
|
||||
success(afterResolve.response) && receivingSnapshot(afterResolve.data) === beforeResolve,
|
||||
`before=${beforeResolve}, after=${receivingSnapshot(afterResolve.data)}; ${trace(afterResolve.response)}`, OPTIONS)
|
||||
if (!success(afterResolve.response) || receivingSnapshot(afterResolve.data) !== beforeResolve) return
|
||||
const pallets = new Map()
|
||||
for (const [index, inbound] of current.data.inbounds.entries()) {
|
||||
const pallet = await createPallet(context, inbound, index)
|
||||
|
||||
@@ -480,7 +480,14 @@ $criticalPaths = @(
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-pda-ready-contract.test.mjs',
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-pda-batch-contract.test.mjs',
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-loading-complete-contract.test.mjs',
|
||||
'docs/四期/船节点标准化契约.md'
|
||||
'docs/四期/船节点标准化契约.md',
|
||||
'docs/一期/一期头程订单闭环设计.md',
|
||||
'docs/二期/二期需求优化版-v2-测量计费入库主流程.md',
|
||||
'docs/墨家设备测量上传接口说明-V3.md',
|
||||
'docs/三期/三期需求确认表.md',
|
||||
'docs/统一订单主状态与操作契约.md',
|
||||
'docs/财务管理/财务管理产品确认表.md',
|
||||
'docs/财务管理/Firstleg-Finance跨服务契约.md'
|
||||
)
|
||||
$criticalPaths | ForEach-Object { Assert-Path $_ }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user