test(fba): add scoped transfer lifecycle and remaining-node evidence helpers
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# 后端脚本管理与全量 HTTP 验收索引
|
# 后端脚本管理与全量 HTTP 验收索引
|
||||||
|
|
||||||
|
`fba-remaining-nodes-acceptance-transfer.mjs` 是 F33 原已有库存转运的内部验收助手,导入不执行。`freezeTransfer` 使用显式 TEST 配置建立独立会话,只读原票和来源库存后退出;`prepareOriginalTransferPlan` / `prepareTransferStage` 只准备本票真实关联换标的逐阶段命令,不能预设关联号。`runPreparedTransferStage` 默认零网络,只有总控明确 TEST、原票、客户、阶段和稳定批次并注入已核身份的单请求流 transport 后才执行;照片以原字节摘要校验,客户确认使用本票 MERCHANT 身份,不调用被禁止的 PDA 独立复核批准。报告与写入 journal 在 `reports/fba-remaining-nodes-20261004/transfer/`,写前持久化意图;只有已明确成功的原键原载荷可显式重放一次,上传不可重放,失败或结果未知先回查,不能新建替代票、解冻其他业务或重新收货。调用者须逐阶段独立核对库存、其他 HOLD、追加流水和主体归属,HTTP 成功不自动标记业务通过;退出自身会话,不执行 Firstleg 写入、SQL、部署或删除。直接测试 `fba-remaining-nodes-acceptance-transfer.test.mjs`。
|
||||||
|
|
||||||
|
`fba-remaining-nodes-acceptance.mjs` 是 FBA 剩余节点内部证据助手,显式调用 `auditRemainingNodes`,导入不运行;读取指定原报告并按 JSON pointer/SHA-256 保留证据。`live=true` 仅使用明确 TEST 配置的独立 Auth 会话和串行 GET,结束退出;不执行业务写命令、SQL、故障注入、部署或页面操作。报告位于 `reports/fba-remaining-nodes-20261004/`,拒绝覆盖既有报告,失败保留且不自动重试。对应离线测试为 `fba-remaining-nodes-acceptance.test.mjs`,只读核验不等于原业务已完成。
|
||||||
|
|
||||||
|
`fba-remaining-nodes-acceptance-pda-operator.mjs` 是 F33 海外 PDA 的一次性 TEST 账号准备助手,导入不运行。显式调用 `createF33PdaOperator({env, credentialFile, reportFile})`,凭据来自已授权 TEST 环境;仅通过正式 IAM API 创建独立洛杉矶仓作业员、授予已存在的海外 PDA 角色并验证登录/仓绑定,不改角色权限或旧账号,不操作库存。随机凭据仅写入调用方指定的仓库外环境文件,报告建议保存 `reports/fba-remaining-nodes-20261004/pda-operator/`,不包含密码或 Token;已有文件、请求未知结果或首次强制改密均停止,禁止自动重造。退出自有管理员/PDA会话,账号保留供页面验收,报告给出精确停用接口,清理由总控在验收结束后执行。
|
||||||
|
|
||||||
`fleet-same-route-cost-test-env.mjs` 是同路线自有/外接实运成本分摊的串行TEST入口,默认`--plan`零网络;执行参数为`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id <FBA目的仓ID>`,凭据仅通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。复用两新客户普通来货/PDA实收/客户确认,各一托一箱,再复用真实外接预订、来货、PDA实收与关联,三托统一目的站;逐单拣货、实际装载和审核后整车发车。登记USD1.00合成成本,以不可变DEPART实测托位、三票来源及业务号顺序独立核对最大余数分币、原币守恒、创建重放与唯一历史,再逐票签收、正式结案并分别释放车辆/司机。自有两票分别分摊,不能先合并自有托位后假设汇总一定为0.67;余分按现行逐业务号规则分配。不写业务SQL、不部署、不操作财务付款。报告沿`reports/fleet-cross-<新批次>/`,增加`same-route-source.json`和`createdResources.fleetSameRouteCost`;入口要求验收函数显式返回本轮车次VERIFIED才能结案,不借另一证据对象的状态。失败保留原请求/样本、停止且不自动重跑,finally停用自有PDA并退出会话。`fleet-same-route-cost-test-env.test.mjs`验证错误来源/目的地/冻结依据/分币及零网络计划;真实HTTP与页面结论分别记录。
|
`fleet-same-route-cost-test-env.mjs` 是同路线自有/外接实运成本分摊的串行TEST入口,默认`--plan`零网络;执行参数为`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id <FBA目的仓ID>`,凭据仅通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。复用两新客户普通来货/PDA实收/客户确认,各一托一箱,再复用真实外接预订、来货、PDA实收与关联,三托统一目的站;逐单拣货、实际装载和审核后整车发车。登记USD1.00合成成本,以不可变DEPART实测托位、三票来源及业务号顺序独立核对最大余数分币、原币守恒、创建重放与唯一历史,再逐票签收、正式结案并分别释放车辆/司机。自有两票分别分摊,不能先合并自有托位后假设汇总一定为0.67;余分按现行逐业务号规则分配。不写业务SQL、不部署、不操作财务付款。报告沿`reports/fleet-cross-<新批次>/`,增加`same-route-source.json`和`createdResources.fleetSameRouteCost`;入口要求验收函数显式返回本轮车次VERIFIED才能结案,不借另一证据对象的状态。失败保留原请求/样本、停止且不自动重跑,finally停用自有PDA并退出会话。`fleet-same-route-cost-test-env.test.mjs`验证错误来源/目的地/冻结依据/分币及零网络计划;真实HTTP与页面结论分别记录。
|
||||||
|
|
||||||
`fleet-booking-quotes-test-env.mjs` 是外接预订选用销售报价的独立 TEST HTTP 验收入口,使用显式 `node --env-file=<用户指定目录的test-acceptance.env>` 和 `--ready-commit=a6fd0ba3`,执行前须由总控核对实际部署版本与健康。只读公共仓城市/时区,创建自身全新隔离客户、销售报价及未分配资源的车次/询价草稿;验证候选日期/币种/计价、零价与缺价、手填价格、陈旧版本拒绝、刷新重选及来源快照,不创建库存、不发车、不证明承运成功或实收完成。请求前持久留证、敏感字段脱敏、未知结果停止且禁止自动重跑,独立登录并退出;保留自己的计划/报价/草稿,不删除资源。当前固定R2标识,独占报告 `reports/fleet-booking-quotes-live-20261003-r2/`。离线合同 `fleet-booking-quotes-test-env-contract.test.mjs`,运行前仍须总控放行对应健康版本。
|
`fleet-booking-quotes-test-env.mjs` 是外接预订选用销售报价的独立 TEST HTTP 验收入口,使用显式 `node --env-file=<用户指定目录的test-acceptance.env>` 和 `--ready-commit=a6fd0ba3`,执行前须由总控核对实际部署版本与健康。只读公共仓城市/时区,创建自身全新隔离客户、销售报价及未分配资源的车次/询价草稿;验证候选日期/币种/计价、零价与缺价、手填价格、陈旧版本拒绝、刷新重选及来源快照,不创建库存、不发车、不证明承运成功或实收完成。请求前持久留证、敏感字段脱敏、未知结果停止且禁止自动重跑,独立登录并退出;保留自己的计划/报价/草稿,不删除资源。当前固定R2标识,独占报告 `reports/fleet-booking-quotes-live-20261003-r2/`。离线合同 `fleet-booking-quotes-test-env-contract.test.mjs`,运行前仍须总控放行对应健康版本。
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
// Explicit one-time TEST setup. No inventory operations, existing-user changes or role changes.
|
||||||
|
export async function createF33PdaOperator({ env, credentialFile, reportFile }) {
|
||||||
|
if (String(env.CHONGCHONG_ENV).toUpperCase() !== 'TEST') throw Error('EXPLICIT_TEST_REQUIRED')
|
||||||
|
if (fs.existsSync(credentialFile) || fs.existsSync(reportFile)) throw Error('EXISTING_RESULT_RECONCILE_NO_RECREATE')
|
||||||
|
const warehouseId = '193000000000200003', roleCode = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR'
|
||||||
|
const report = { startedAt: new Date().toISOString(), warehouseId, roleCode, requests: [], success: false,
|
||||||
|
existingAccountsChanged: false, rolePermissionsChanged: false, businessOperationsIssued: 0 }
|
||||||
|
const save = () => { fs.mkdirSync(path.dirname(reportFile), {recursive:true}); fs.writeFileSync(reportFile, JSON.stringify(report,null,2)) }
|
||||||
|
let admin, pda
|
||||||
|
const call = async(method, route, body, token=admin?.accessToken) => {
|
||||||
|
const entry={method,path:route.split('?')[0],phase:'INTENT'};report.requests.push(entry);save()
|
||||||
|
let response
|
||||||
|
try { response=await fetch(env.CHONGCHONG_ADMIN_API_BASE_URL.replace(/\/$/,'')+route,{method,redirect:'error',signal:AbortSignal.timeout(20000),headers:{Accept:'application/json',...(token?{Authorization:`Bearer ${token}`} : {}),...(body?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})}) }
|
||||||
|
catch {entry.phase='OUTCOME_UNKNOWN';save();throw Error('OUTCOME_UNKNOWN_RECONCILE_NO_RETRY')}
|
||||||
|
const value=JSON.parse(await response.text());Object.assign(entry,{phase:'ACKNOWLEDGED',http:response.status,code:String(value.code),requestId:response.headers.get('x-request-id'),traceId:response.headers.get('x-trace-id')});save()
|
||||||
|
if(response.status!==200||String(value.code)!=='200')throw Error(`FORMAL_API_REJECTED_${response.status}_${value.code}`)
|
||||||
|
return value.data
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
admin=await call('POST','/auth/login',{loginId:env.CHONGCHONG_ADMIN_USERNAME,password:env.CHONGCHONG_ADMIN_PASSWORD},'')
|
||||||
|
const roles=await call('GET','/platform/v1/iam/roles')
|
||||||
|
const permissions=await call('GET','/platform/v1/iam/permissions')
|
||||||
|
const role=roles.find(r=>r.roleCode===roleCode&&r.status==='ACTIVE')
|
||||||
|
const operate=permissions.find(p=>p.permissionCode==='lastleg:pda:operate')
|
||||||
|
if(!role||!operate||!(role.permissionIds||[]).some(id=>String(id)===String(operate.id)))throw Error('EXISTING_ROLE_OPERATE_PERMISSION_REQUIRED')
|
||||||
|
let phone
|
||||||
|
for(let i=0;i<10;i++){
|
||||||
|
const candidate='18'+String(crypto.randomInt(0,1000000000)).padStart(9,'0')
|
||||||
|
if(await call('GET',`/platform/v1/iam/tenants/administrator-phone-availability?phone=${candidate}`)){phone=candidate;break}
|
||||||
|
}
|
||||||
|
if(!phone)throw Error('NO_AVAILABLE_SYNTHETIC_PHONE')
|
||||||
|
const username='pda_f33_20261004_'+crypto.randomBytes(5).toString('hex')
|
||||||
|
const password='Pda1!'+crypto.randomBytes(18).toString('hex')
|
||||||
|
const writeCredentials = membershipId => fs.writeFileSync(credentialFile,
|
||||||
|
`PDA_USERNAME=${username}\nPDA_PASSWORD=${password}\nPDA_MEMBERSHIP_ID=${membershipId}\n`,{mode:0o600})
|
||||||
|
fs.mkdirSync(path.dirname(credentialFile),{recursive:true})
|
||||||
|
fs.writeFileSync(credentialFile,`PDA_USERNAME=${username}\nPDA_PASSWORD=${password}\nPDA_MEMBERSHIP_ID=\n`,{flag:'wx',mode:0o600})
|
||||||
|
const created=await call('POST','/platform/v1/iam/users',{username,password,phone,
|
||||||
|
displayName:'F33海外PDA独立验收-20261004',email:username+'@example.com',tenantId:'0',roleIds:[String(role.id)],
|
||||||
|
overseasWarehouseId:warehouseId,domesticWarehouseId:null,status:'ACTIVE'})
|
||||||
|
report.subject={membershipId:String(created.membershipId),userId:String(created.userId),authUserId:String(created.authUserId),
|
||||||
|
tenantId:String(created.tenantId),roleIds:(created.roleIds||[]).map(String),overseasWarehouseId:String(created.overseasWarehouseId),
|
||||||
|
domesticWarehouseId:created.domesticWarehouseId??null,status:created.status,protectedAccount:created.protectedAccount}
|
||||||
|
writeCredentials(report.subject.membershipId);save()
|
||||||
|
if(report.subject.tenantId!=='0'||report.subject.overseasWarehouseId!==warehouseId||report.subject.status!=='ACTIVE'
|
||||||
|
||report.subject.domesticWarehouseId!==null||report.subject.roleIds.length!==1||report.subject.roleIds[0]!==String(role.id))throw Error('CREATED_SUBJECT_SCOPE_MISMATCH')
|
||||||
|
pda=await call('POST','/auth/login',{loginId:username,password},'')
|
||||||
|
if(pda.passwordChangeRequired||pda.mustChangePassword||pda.firstLogin||!pda.accessToken)throw Error('FIRST_PASSWORD_CHANGE_OR_NORMAL_LOGIN_BLOCKED')
|
||||||
|
if(!(pda.authorities||[]).includes('lastleg:pda:operate')||!(pda.roles||[]).includes(roleCode)||(pda.roles||[]).length!==1)throw Error('NORMAL_LOGIN_ROLE_OR_PERMISSION_MISMATCH')
|
||||||
|
const profile=await call('GET','/pda/v1/last-leg/receiving-containers/warehouse-profile',undefined,pda.accessToken)
|
||||||
|
report.profile={warehouseId:String(profile.warehouseId),warehouseCode:profile.warehouseCode,warehouseName:profile.warehouseName}
|
||||||
|
if(report.profile.warehouseId!==warehouseId||report.profile.warehouseCode!=='US-LAX-01')throw Error('FORMAL_WAREHOUSE_PROFILE_MISMATCH')
|
||||||
|
report.cleanup={method:'PUT',path:`/platform/v1/iam/users/${report.subject.membershipId}/status`,body:{status:'DISABLED'},executeNow:false}
|
||||||
|
report.status='ACTIVE_FOR_COORDINATOR_UI';report.loginVerified=true;report.warehouseProfileVerified=true;report.success=true
|
||||||
|
}catch(error){report.failure=String(error.message).replace(/https?:\/\/\S+/g,'[redacted]')}
|
||||||
|
finally{
|
||||||
|
if(pda?.refreshToken){try{await call('POST','/auth/logout',{refreshToken:pda.refreshToken},pda.accessToken);report.pdaSessionClosed=true}catch{report.pdaSessionClosed=false;report.success=false}}else report.pdaSessionClosed=!pda
|
||||||
|
if(admin?.refreshToken){try{await call('POST','/auth/logout',{refreshToken:admin.refreshToken},admin.accessToken);report.adminSessionClosed=true}catch{report.adminSessionClosed=false;report.success=false}}else report.adminSessionClosed=!admin
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import { digest } from './fba-remaining-nodes-acceptance.mjs'
|
||||||
|
export const TRANSFER = 'WT-WH-20261002-430594'
|
||||||
|
export const CUSTOMER = '2105875471489142785'
|
||||||
|
export const BASE = `/platform/v1/last-leg/warehouse-transfers/${TRANSFER}?customerId=${CUSTOMER}`
|
||||||
|
export function relabelPrerequisite(detail, order) {
|
||||||
|
if (detail.needRelabel !== true) return { ready: true, scope: 'NOT_REQUIRED' }
|
||||||
|
const linked = Boolean(detail.relabelNo && (order.relabelApplications || []).some(app =>
|
||||||
|
app.relabelNo === detail.relabelNo && String(app.lastlegOrderId) === String(detail.lastlegOrderId)))
|
||||||
|
const confirmed = linked && order.relabelStatus === 'COMPLETED'
|
||||||
|
return { ready: confirmed, scope: 'ORIGINAL_TRANSFER_RELABEL', linked,
|
||||||
|
relabelNo: detail.relabelNo || null, relabelStatus: order.relabelStatus || null,
|
||||||
|
bindingCount: detail.relabelBindings?.length || 0,
|
||||||
|
reason: confirmed ? null : 'Required relabel is not linked/completed for this original transfer; available source quantity does not waive it.' }
|
||||||
|
}
|
||||||
|
export function validateDestination(detail, warehouse, pallet, location) {
|
||||||
|
if (!warehouse || !pallet || !location) throw Error('EXPLICIT_DESTINATION_RECEIVING_RESOURCES_REQUIRED')
|
||||||
|
if (warehouse.warehouseType !== 'OVERSEAS' || warehouse.status !== 'ACTIVE') throw Error('DESTINATION_MUST_BE_ACTIVE_OVERSEAS_RECEIVING_WAREHOUSE')
|
||||||
|
if (String(pallet.warehouseId) !== String(warehouse.id) || String(location.warehouseId) !== String(warehouse.id)
|
||||||
|
|| String(pallet.locationId) !== String(location.id)) throw Error('DESTINATION_RESOURCE_OWNERSHIP_MISMATCH')
|
||||||
|
if (pallet.status !== 'IDLE' || String(pallet.customerId) !== '0' || pallet.fbaWarehouseCode !== detail.destinationWarehouseCode) throw Error('DESTINATION_RESOURCE_UNAVAILABLE_OR_WRONG_FBA')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
export function validateSource(detail, balance, position) {
|
||||||
|
if (detail.transferNo !== TRANSFER || detail.status !== 'APPROVED' || detail.packageCount !== 1 || detail.allocations.length) throw Error('ORIGINAL_APPROVED_UNALLOCATED_TICKET_REQUIRED')
|
||||||
|
if (!detail.sourceOrders.some(s => String(s.inboundOrderId) === String(balance.inboundOrderId))) throw Error('SOURCE_NOT_BOUND_TO_ORIGINAL_TICKET')
|
||||||
|
if (String(balance.customerId) !== CUSTOMER || String(position.customerId) !== CUSTOMER || String(position.balanceId) !== String(balance.id)) throw Error('CUSTOMER_OR_BALANCE_MISMATCH')
|
||||||
|
if (String(balance.overseasWarehouseId) !== String(detail.operatingWarehouseId) || !position.palletId || Number(position.availablePackages) < 1) throw Error('SOURCE_UNAVAILABLE')
|
||||||
|
if (Number(position.holdPackages || 0) || Number(position.frozenPackages || 0) || Number(position.allocatedPackages || 0) || Number(position.pickedPackages || 0)) throw Error('SOURCE_HAS_OTHER_OCCUPANCY')
|
||||||
|
return { inventoryPositionId: String(position.id), palletId: String(position.palletId), packageCount: 1 }
|
||||||
|
}
|
||||||
|
export async function withTransferSession({env, output}, work) {
|
||||||
|
if (fs.existsSync(output)) throw Error('REPORT_EXISTS_NO_OVERWRITE')
|
||||||
|
if (String(env.CHONGCHONG_ENV).toUpperCase() !== 'TEST') throw Error('EXPLICIT_TEST_REQUIRED')
|
||||||
|
const report = { transferNo: TRANSFER, customerId: CUSTOMER, startedAt: new Date().toISOString(), requests: [], success: false }
|
||||||
|
const save = () => { fs.mkdirSync(path.dirname(output), {recursive:true}); fs.writeFileSync(output, JSON.stringify(report,null,2)) }
|
||||||
|
let session
|
||||||
|
async function call(method, route, body) {
|
||||||
|
const auth = route.startsWith('/auth/')
|
||||||
|
if (!auth && !route.startsWith('/platform/v1/last-leg/') && !route.startsWith('/platform/v1/overseas-warehouse-resources/') && !route.startsWith('/platform/v1/warehouse-admin')) throw Error('FIRSTLEG_OR_UNSCOPED_ROUTE_FORBIDDEN')
|
||||||
|
const entry = { method, path: route, phase: 'INTENT', ...(!auth && body ? {body, bodyHash:digest(body)} : {}) }
|
||||||
|
report.requests.push(entry); save()
|
||||||
|
let response
|
||||||
|
try { response = await fetch(env.CHONGCHONG_ADMIN_API_BASE_URL.replace(/\/$/,'')+route, {method,redirect:'error',signal:AbortSignal.timeout(20000),headers:{Accept:'application/json',...(session?{Authorization:`Bearer ${session.accessToken}`} : {}),...(body?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})}) }
|
||||||
|
catch { entry.phase='OUTCOME_UNKNOWN'; save(); throw Error('OUTCOME_UNKNOWN_RECONCILE_ONLY_NO_RETRY') }
|
||||||
|
const json=JSON.parse(await response.text()); Object.assign(entry,{phase:'ACKNOWLEDGED',http:response.status,code:String(json.code),requestId:response.headers.get('x-request-id'),traceId:response.headers.get('x-trace-id')});save()
|
||||||
|
if(response.status!==200||String(json.code)!=='200')throw Error(`API_FAILURE_${response.status}_${json.code}`)
|
||||||
|
return json.data
|
||||||
|
}
|
||||||
|
async function pages(route) {
|
||||||
|
const records=[]; let total
|
||||||
|
for(let page=1;page<=100;page++) {const value=await call('GET',`${route}${route.includes('?')?'&':'?'}page=${page}&size=200`);if(total!==undefined&&total!==Number(value.total))throw Error('PAGINATION_TOTAL_CHANGED');total=Number(value.total);records.push(...value.records);if(records.length>=total){if(records.length!==total)throw Error('PAGINATION_COUNT_INVALID');return records}}
|
||||||
|
throw Error('PAGINATION_BOUND_EXCEEDED')
|
||||||
|
}
|
||||||
|
try {session=await call('POST','/auth/login',{loginId:env.CHONGCHONG_ADMIN_USERNAME,password:env.CHONGCHONG_ADMIN_PASSWORD});await work({call,pages,report,save});report.success=true}
|
||||||
|
catch(error){report.failure=String(error.message).replace(/https?:\/\/\S+/g,'[redacted]')}
|
||||||
|
finally { if(session){try{await call('POST','/auth/logout',{refreshToken:session.refreshToken});report.sessionClosed=true}catch{report.sessionClosed=false;report.success=false}}else report.sessionClosed=true;save() }
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
export async function freezeTransfer({env,output}) {
|
||||||
|
return withTransferSession({env,output},async({call,pages,report,save})=>{
|
||||||
|
const detail=await call('GET',BASE)
|
||||||
|
const balances=await pages(`/platform/v1/last-leg/inventory/balances?customerId=${CUSTOMER}`)
|
||||||
|
const ledger=await pages(`/platform/v1/last-leg/inventory/ledger?customerId=${CUSTOMER}`)
|
||||||
|
const sourceCandidates=[]
|
||||||
|
for(const b of balances.filter(b=>detail.items.some(i=>i.skuCode===b.skuCode)))sourceCandidates.push(await call('GET',`/platform/v1/last-leg/inventory/balances/${b.id}?customerId=${CUSTOMER}`))
|
||||||
|
const sources=sourceCandidates.filter(b=>detail.sourceOrders.some(s=>String(s.inboundOrderId)===String(b.inboundOrderId)))
|
||||||
|
const positions=[]
|
||||||
|
for(const b of sources)positions.push(...await call('GET',`/platform/v1/last-leg/inventory/balances/${b.id}/positions?customerId=${CUSTOMER}`))
|
||||||
|
Object.assign(report,{detail,balances,ledger,sources,positions,snapshotHash:digest({detail,balances,ledger,positions})});save()
|
||||||
|
report.scopeReady=sources.length>0&&positions.some(p=>Number(p.availablePackages)>=1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LA = '193000000000200003'
|
||||||
|
export const NY = '193000000000200004'
|
||||||
|
const PDA_MEMBER = '2106727769572478980'
|
||||||
|
export const TRANSFER_STAGES = Object.freeze({
|
||||||
|
approve: {actor:'platform',permission:'platform:overseas-warehouse:relabel-order:status',replay:'SAME_STATUS_NOOP'},
|
||||||
|
claim: {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
|
||||||
|
'create-work-pallet': {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
|
||||||
|
bind: {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
|
||||||
|
unit: {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
|
||||||
|
photo: {actor:'pda',permission:'lastleg:pda:operate',replay:'NONE'},
|
||||||
|
complete: {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
|
||||||
|
'review-read': {actor:'platform',permission:'platform:overseas-warehouse:relabel-order:list',replay:'READ_ONLY'},
|
||||||
|
'customer-confirm': {actor:'merchant',permission:'merchant:overseas-warehouse:relabel:confirm',replay:'KEYED'},
|
||||||
|
outbound: {actor:'platform',permission:'lastleg:warehouse-transfer:manage',replay:'KEYED'},
|
||||||
|
receipt: {actor:'platform',permission:'lastleg:warehouse-transfer:manage',replay:'KEYED'},
|
||||||
|
})
|
||||||
|
const demand = (value, message) => { if (!value) throw Error(message) }
|
||||||
|
const version = value => { demand(Number.isInteger(value)&&value>=0,'FORMAL_CURRENT_VERSION_REQUIRED');return value }
|
||||||
|
export function prepareOriginalTransferPlan(detail, runId) {
|
||||||
|
demand(/^[A-Za-z0-9_-]{1,48}$/.test(runId),'STABLE_RUN_ID_REQUIRED')
|
||||||
|
demand(detail?.transferNo===TRANSFER&&detail.needRelabel===true&&Number(detail.packageCount)===1
|
||||||
|
&&String(detail.operatingWarehouseId)===LA&&detail.destinationWarehouseCode==='ABE4','ORIGINAL_TICKET_SCOPE_REQUIRED')
|
||||||
|
return {runId,transfer:structuredClone(detail),customerId:CUSTOMER,networkIssued:false,
|
||||||
|
linked:Boolean(detail.relabelNo&&detail.relabelApplicationId),
|
||||||
|
reconcile:{method:'GET',path:BASE},
|
||||||
|
phases:Object.entries(TRANSFER_STAGES).map(([stage,rule])=>({stage,...rule})),
|
||||||
|
reviewBoundary:'PDA /review is intentionally rejected; platform review-task is read-only. Customer confirm-completion atomically completes review.',
|
||||||
|
receiptWarehouseId:NY,businessDestination:'ABE4',pdaMembershipId:PDA_MEMBER}
|
||||||
|
}
|
||||||
|
function ownRelabel(plan, linked) {
|
||||||
|
demand(plan.linked,'WAIT_FOR_ORIGINAL_ALLOCATION_NO_ASSUMED_RELABEL')
|
||||||
|
const app=linked?.application
|
||||||
|
demand(app&&app.relabelNo===plan.transfer.relabelNo&&String(app.id)===String(plan.transfer.relabelApplicationId)
|
||||||
|
&&String(app.lastlegOrderId)===String(plan.transfer.lastlegOrderId)&&app.customerOrderNo===TRANSFER
|
||||||
|
&&String(app.overseasWarehouseId)===LA,'EXACT_ORIGINAL_RELABEL_RELATION_REQUIRED')
|
||||||
|
return encodeURIComponent(app.relabelNo)
|
||||||
|
}
|
||||||
|
function commandHash(command) {
|
||||||
|
const {preparedHash,...value}=command
|
||||||
|
return digest(value)
|
||||||
|
}
|
||||||
|
export function prepareTransferStage(plan, stage, context={}) {
|
||||||
|
const rule=TRANSFER_STAGES[stage];demand(rule,'UNSUPPORTED_STAGE_NO_PDA_REVIEW_APPROVE')
|
||||||
|
const no=ownRelabel(plan,context.linked)
|
||||||
|
const key=`F33-WT-430594-${plan.runId}-${stage}`
|
||||||
|
const command={stage,runId:plan.runId,transferNo:TRANSFER,customerId:CUSTOMER,...rule,method:'POST',path:'',body:null}
|
||||||
|
const pdaBase=`/pda/v1/last-leg/relabel-tasks/${no}`
|
||||||
|
const platformBase=`/platform/v1/last-leg/relabel-applications/${no}`
|
||||||
|
const task=context.task
|
||||||
|
if(['claim','unit','complete'].includes(stage))demand(task?.relabelNo===plan.transfer.relabelNo&&task.customerOrderNo===TRANSFER,'OWN_CURRENT_PDA_TASK_REQUIRED')
|
||||||
|
if(stage==='approve'){command.method='PUT';command.path=platformBase+'/status';command.body={status:'PENDING_PROCESSING',remark:key}}
|
||||||
|
if(stage==='claim'){command.path=pdaBase+'/claim';command.body={idempotencyKey:key}}
|
||||||
|
if(stage==='create-work-pallet'){command.path='/pda/v1/last-leg/pallet-operations/relabel-pallet-label';command.body={relabelNo:plan.transfer.relabelNo,idempotencyKey:key}}
|
||||||
|
if(stage==='bind'){
|
||||||
|
const {source,target,observedCartonMarks}=context
|
||||||
|
demand(source&&target&&String(source.warehouseId)===LA&&String(target.warehouseId)===LA
|
||||||
|
&&target.destinationWarehouseCode==='ABE4'&&source.palletCode!==target.palletCode,'FORMAL_LA_WORK_PALLETS_REQUIRED')
|
||||||
|
const marks=new Set((context.linked.items||[]).map(i=>i.cartonMark))
|
||||||
|
demand(Array.isArray(observedCartonMarks)&&observedCartonMarks.length===1&&observedCartonMarks.every(m=>marks.has(m)),'OBSERVED_OWN_CARTON_REQUIRED')
|
||||||
|
demand(plan.transfer.allocations?.some(a=>String(a.inventoryPositionId)===String(source.positionId)
|
||||||
|
&&String(a.palletId)===String(source.palletId)&&Number(a.packageCount)===1),'ORIGINAL_SELECTED_POSITION_REQUIRED')
|
||||||
|
command.path='/pda/v1/last-leg/pallet-operations/relabel-disassemble'
|
||||||
|
command.body={relabelNo:plan.transfer.relabelNo,idempotencyKey:key,
|
||||||
|
source:{palletCode:source.palletCode,expectedVersion:version(source.palletVersion)},
|
||||||
|
target:{palletCode:target.palletCode,expectedVersion:version(target.palletVersion)},cartonMarks:observedCartonMarks,
|
||||||
|
actualSource:{positionId:source.positionId,expectedPositionVersion:version(source.positionVersion),packages:1},reason:key}
|
||||||
|
}
|
||||||
|
if(stage==='unit'){
|
||||||
|
const item=task.items?.find(i=>String(i.itemId)===String(context.itemId))
|
||||||
|
demand(item&&item.operationType==='CARTON_AND_SKU','OWN_COMBINED_ITEM_REQUIRED')
|
||||||
|
demand(context.observedOldLabel===item.cartonMark&&context.observedNewLabel===item.fbaCartonMark,'OBSERVED_LABELS_MUST_MATCH_OWN_ITEM')
|
||||||
|
demand(Array.isArray(context.observedSkus)&&digest(context.observedSkus)===digest((item.skuItems||[]).map(s=>({skuCode:s.skuCode,newSkuCode:s.newSkuCode,quantity:s.quantity}))), 'OBSERVED_SKU_QUANTITIES_REQUIRED')
|
||||||
|
command.path=pdaBase+'/units';command.body={itemId:item.itemId,oldLabel:context.observedOldLabel,newLabel:context.observedNewLabel,
|
||||||
|
expectedVersion:version(task.version),idempotencyKey:key,skuVerifications:context.observedSkus}
|
||||||
|
}
|
||||||
|
if(stage==='photo'){
|
||||||
|
const photo=context.photo
|
||||||
|
demand(photo&&/^[a-f0-9]{64}$/.test(photo.sha256)&&Number.isSafeInteger(photo.sizeBytes)&&photo.sizeBytes>0
|
||||||
|
&&photo.contentType?.startsWith('image/')&&photo.fileName,'EXPLICIT_PHOTO_CONTENT_REQUIRED')
|
||||||
|
command.path=pdaBase+'/photos/files';command.body=null
|
||||||
|
command.multipart={field:'file',fileName:photo.fileName,contentType:photo.contentType,sizeBytes:photo.sizeBytes,sha256:photo.sha256}
|
||||||
|
}
|
||||||
|
if(stage==='complete'){
|
||||||
|
demand(task.taskStatus==='PROCESSING'&&Number(task.expectedPackages)===1&&Number(task.relabelledPackages)===1
|
||||||
|
&&Number(task.skippedPackages)===0&&Number(task.exceptionPackages)===0,'FULL_NORMAL_WORK_FACTS_REQUIRED')
|
||||||
|
const ownMarks=new Set(task.items.map(i=>i.cartonMark))
|
||||||
|
const bound=(task.history||[]).some(h=>{
|
||||||
|
if(h.action!=='PALLET_BIND')return false
|
||||||
|
try{const proof=JSON.parse(h.detail);return proof.round===(task.reviewedAt||'INITIAL')
|
||||||
|
&&proof.cartonMarks?.some(m=>ownMarks.has(m))&&proof.targetPallets?.length>0}catch{return false}
|
||||||
|
})
|
||||||
|
demand(bound&&Array.isArray(context.photoFileIds)&&context.photoFileIds.length>0
|
||||||
|
&&context.photoFileIds.every(id=>/^[1-9]\d*$/.test(String(id)))
|
||||||
|
&&new Set(context.photoFileIds.map(String)).size===context.photoFileIds.length,'ACTUAL_BINDING_AND_OWN_PHOTOS_REQUIRED')
|
||||||
|
command.path=pdaBase+'/complete';command.body={photoFileIds:context.photoFileIds,expectedVersion:version(task.version),idempotencyKey:key,remark:key,manualCompletion:false}
|
||||||
|
}
|
||||||
|
if(stage==='review-read'){command.method='GET';command.path=platformBase+'/review-task';command.body=null}
|
||||||
|
if(stage==='customer-confirm'){
|
||||||
|
demand(task?.taskStatus==='WAIT_REVIEW'&&task.relabelNo===plan.transfer.relabelNo&&Number(task.relabelledPackages)===1
|
||||||
|
&&task.completionPhotoFileIds?.length>0,'SUBMITTED_WORK_AND_PHOTOS_REQUIRED')
|
||||||
|
command.path=`/merchant/v1/last-leg/relabel-applications/${no}/confirm-completion`;command.body={idempotencyKey:key,remark:key}
|
||||||
|
}
|
||||||
|
if(stage==='outbound'){
|
||||||
|
demand(plan.transfer.status==='ALLOCATED'&&relabelPrerequisite(plan.transfer,context.order).ready,'OWN_RELABEL_CUSTOMER_CONFIRMATION_REQUIRED')
|
||||||
|
demand(plan.transfer.allocations?.length===1&&plan.transfer.allocations[0].status==='RESERVED'
|
||||||
|
&&plan.transfer.allocations[0].skuCode===plan.transfer.relabelBindings[0].newSku,'CURRENT_NEW_SKU_ALLOCATION_REQUIRED')
|
||||||
|
command.path=`/platform/v1/last-leg/warehouse-transfers/${TRANSFER}/outbound?customerId=${CUSTOMER}`;command.body={idempotencyKey:key,remark:key}
|
||||||
|
}
|
||||||
|
if(stage==='receipt'){
|
||||||
|
const a=plan.transfer.allocations?.[0],t=context.receiptTarget
|
||||||
|
demand(plan.transfer.status==='OUTBOUNDED'&&plan.transfer.allocations.length===1&&a.status==='SHIPPED'
|
||||||
|
&&Number(a.packageCount)===1&&a.skuCode===plan.transfer.relabelBindings[0].newSku,'CURRENT_SHIPPED_NEW_SKU_ALLOCATION_REQUIRED')
|
||||||
|
demand(t&&String(t.warehouse?.id)===NY,'EXPLICIT_NEW_YORK_RECEIVING_TARGET_REQUIRED')
|
||||||
|
validateDestination(plan.transfer,t.warehouse,t.pallet,t.location)
|
||||||
|
command.path=`/platform/v1/last-leg/warehouse-transfers/${TRANSFER}/receipts?customerId=${CUSTOMER}`
|
||||||
|
command.body={sourceInventoryPositionId:a.inventoryPositionId,destinationOverseasWarehouseId:NY,
|
||||||
|
destinationOverseasWarehouseCode:t.warehouse.warehouseCode,destinationOverseasWarehouseName:t.warehouse.warehouseName,
|
||||||
|
destinationWarehouseId:'193000000000000003',destinationWarehouseCode:'ABE4',destinationWarehouseName:'ABE4',
|
||||||
|
destinationPalletId:t.pallet.id,destinationPalletNo:t.pallet.palletCode,warehouseLocationId:t.location.id,
|
||||||
|
warehouseLocationCode:t.location.locationCode,batchNo:TRANSFER,idempotencyKey:key,remark:key}
|
||||||
|
}
|
||||||
|
command.preparedHash=commandHash(command)
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
export async function runPreparedTransferStage({command,release,actor,transport,journalDirectory,binary,replay=false}) {
|
||||||
|
if(!release?.execute)return {mode:'PLAN',networkIssued:false,command}
|
||||||
|
demand(release.environment==='TEST'&&release.transferNo===TRANSFER&&release.customerId===CUSTOMER
|
||||||
|
&&release.stage===command.stage&&release.runId===command.runId,'EXACT_STAGE_RELEASE_REQUIRED')
|
||||||
|
demand(command.preparedHash===commandHash(command),'PREPARED_COMMAND_CHANGED')
|
||||||
|
const policy=TRANSFER_STAGES[command.stage];demand(policy,'UNSUPPORTED_STAGE')
|
||||||
|
demand(command.transferNo===TRANSFER&&command.customerId===CUSTOMER
|
||||||
|
&&/^[A-Za-z0-9_-]{1,48}$/.test(command.runId),'ORIGINAL_COMMAND_SCOPE_REQUIRED')
|
||||||
|
const commandFields=new Set(['stage','runId','transferNo','customerId','actor','permission','replay','method','path','body','multipart','preparedHash'])
|
||||||
|
demand(Object.keys(command).every(field=>commandFields.has(field)),'NO_CREDENTIALS_OR_EXTRA_TRANSPORT_FIELDS_IN_JOURNAL')
|
||||||
|
demand(actor?.kind===policy.actor&&Array.isArray(actor.authorities)&&actor.authorities.includes(policy.permission),'ACTOR_PERMISSION_REQUIRED')
|
||||||
|
if(policy.actor==='pda')demand(Array.isArray(actor.roles)&&actor.roles.includes('OVERSEAS_WAREHOUSE_PDA_OPERATOR')
|
||||||
|
&&actor.appCode==='PLATFORM'&&String(actor.tenantId)==='0'
|
||||||
|
&&String(actor.membershipId)===PDA_MEMBER&&String(actor.warehouseId)===LA,'OWN_PDA_IDENTITY_REQUIRED')
|
||||||
|
else demand(actor.appCode===(policy.actor==='merchant'?'MERCHANT':'PLATFORM')
|
||||||
|
&&String(actor.tenantId)===(policy.actor==='merchant'?CUSTOMER:'0'),'ACTOR_SCOPE_REQUIRED')
|
||||||
|
demand(typeof transport==='function'&&journalDirectory,'EXPLICIT_TRANSPORT_AND_JOURNAL_REQUIRED')
|
||||||
|
if(command.multipart)demand(binary&&binary.length===command.multipart.sizeBytes
|
||||||
|
&&crypto.createHash('sha256').update(binary).digest('hex')===command.multipart.sha256,'PHOTO_BYTES_CHANGED')
|
||||||
|
fs.mkdirSync(journalDirectory,{recursive:true})
|
||||||
|
const file=path.join(journalDirectory,`${command.runId}-${command.stage}.json`)
|
||||||
|
let journal
|
||||||
|
if(fs.existsSync(file)){
|
||||||
|
journal=JSON.parse(fs.readFileSync(file,'utf8'))
|
||||||
|
demand(replay&&['KEYED','SAME_STATUS_NOOP','READ_ONLY'].includes(policy.replay)
|
||||||
|
&&journal.success===true&&journal.command.preparedHash===command.preparedHash
|
||||||
|
&&!journal.replay,'ONLY_ACKNOWLEDGED_EXACT_ORIGINAL_REPLAY_ALLOWED')
|
||||||
|
journal.replay={phase:'INTENT'};fs.writeFileSync(file,JSON.stringify(journal,null,2))
|
||||||
|
}else{
|
||||||
|
demand(!replay,'ORIGINAL_ACKNOWLEDGED_COMMAND_REQUIRED')
|
||||||
|
journal={command,actor:{kind:actor.kind,membershipId:actor.membershipId,warehouseId:actor.warehouseId},phase:'INTENT',success:false,businessVerified:false}
|
||||||
|
fs.writeFileSync(file,JSON.stringify(journal,null,2),{flag:'wx'})
|
||||||
|
}
|
||||||
|
const entry=replay?journal.replay:journal
|
||||||
|
try{
|
||||||
|
const response=await transport(command,binary)
|
||||||
|
Object.assign(entry,{phase:'ACKNOWLEDGED',http:response.http,code:String(response.code),requestId:response.requestId,traceId:response.traceId,resultDigest:digest(response.data??null)})
|
||||||
|
entry.success=response.http===200&&String(response.code)==='200'
|
||||||
|
fs.writeFileSync(file,JSON.stringify(journal,null,2))
|
||||||
|
demand(entry.success,'STAGE_REJECTED_STOP_NO_RECREATE')
|
||||||
|
return {mode:'EXECUTED',journalFile:file,response}
|
||||||
|
}catch(error){
|
||||||
|
if(entry.phase==='INTENT'){entry.phase='OUTCOME_UNKNOWN';entry.success=false;fs.writeFileSync(file,JSON.stringify(journal,null,2))}
|
||||||
|
throw Error(entry.phase==='OUTCOME_UNKNOWN'?'OUTCOME_UNKNOWN_READBACK_ONLY_NO_RETRY':'STAGE_REJECTED_STOP_NO_RECREATE')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyTransferMilestone(stage, facts) {
|
||||||
|
demand(['selection','bind','unit','customer-confirm','outbound','receipt'].includes(stage),'UNKNOWN_STOCK_MILESTONE')
|
||||||
|
const count=value=>{demand((typeof value==='number'||typeof value==='string'&&/^\d+$/.test(value))
|
||||||
|
&&Number.isSafeInteger(Number(value))&&Number(value)>=0,'EXPLICIT_INTEGER_STOCK_FACT_REQUIRED');return Number(value)}
|
||||||
|
const beforeSource=count(facts.baselineSourceTotal),beforeTarget=count(facts.baselineTargetTotal)
|
||||||
|
const source=count(facts.sourceTotal),target=count(facts.targetTotal)
|
||||||
|
const departed=['outbound','receipt'].includes(stage),received=stage==='receipt'
|
||||||
|
demand(source===beforeSource-(departed?1:0)&&target===beforeTarget+(received?1:0),'ORIGINAL_ONE_CARTON_STOCK_DELTA_REQUIRED')
|
||||||
|
demand(facts.protectedBefore&&facts.protectedAfter&&digest(facts.protectedBefore)===digest(facts.protectedAfter),'OTHER_HOLD_AND_STOCK_MUST_REMAIN_IDENTICAL')
|
||||||
|
const past=new Map((facts.ledgerAfter||[]).map(row=>[String(row.id),row]))
|
||||||
|
demand(Array.isArray(facts.ledgerBefore)&&facts.ledgerBefore.every(row=>past.has(String(row.id))&&digest(past.get(String(row.id)))===digest(row)), 'PRIOR_LEDGER_FACTS_MUST_NOT_CHANGE')
|
||||||
|
demand(past.size===(facts.ledgerAfter||[]).length,'DUPLICATE_LEDGER_IDS_NOT_COMPLETE_EVIDENCE')
|
||||||
|
if(departed){
|
||||||
|
const ship=facts.ledgerAfter.filter(row=>row.businessType==='SHIP'&&row.businessOrderNo===TRANSFER)
|
||||||
|
demand(facts.expectedNewSku&&ship.length===1&&Number(ship[0].deltaPackages)===-1
|
||||||
|
&&ship[0].skuCode===facts.expectedNewSku,'UNIQUE_ORIGINAL_NEW_SKU_SHIPMENT_REQUIRED')
|
||||||
|
}
|
||||||
|
if(received){
|
||||||
|
const receipts=facts.ledgerAfter.filter(row=>row.businessType==='STOCK_IN'&&row.businessOrderNo===TRANSFER
|
||||||
|
&&String(row.overseasWarehouseId)===NY)
|
||||||
|
demand(receipts.length===1&&Number(receipts[0].deltaPackages)===1
|
||||||
|
&&receipts[0].skuCode===facts.expectedNewSku,'UNIQUE_NEW_YORK_NEW_SKU_RECEIPT_REQUIRED')
|
||||||
|
}
|
||||||
|
return {verified:true,stage,sourceTotal:source,targetTotal:target,inTransitCartons:departed&&!received?1:0}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import {test} from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import os from 'node:os'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import {validateSource,validateDestination,relabelPrerequisite,TRANSFER,CUSTOMER} from './fba-remaining-nodes-acceptance-transfer.mjs'
|
||||||
|
import {LA,NY,prepareOriginalTransferPlan,prepareTransferStage,runPreparedTransferStage,verifyTransferMilestone} from './fba-remaining-nodes-acceptance-transfer.mjs'
|
||||||
|
const detail={transferNo:TRANSFER,status:'APPROVED',packageCount:1,allocations:[],operatingWarehouseId:'w',sourceOrders:[{inboundOrderId:'o'}]}
|
||||||
|
const balance={id:'b',customerId:CUSTOMER,inboundOrderId:'o',overseasWarehouseId:'w'}
|
||||||
|
const position={id:'p',balanceId:'b',customerId:CUSTOMER,palletId:'t',availablePackages:1,frozenPackages:0,pickedPackages:0}
|
||||||
|
test('original approved one-carton source passes exact ownership',()=>assert.deepEqual(validateSource(detail,balance,position),{inventoryPositionId:'p',palletId:'t',packageCount:1}))
|
||||||
|
test('other source, customer, warehouse and unavailable or occupied cartons stop before commands',()=>{
|
||||||
|
for(const b of [{...balance,inboundOrderId:'other'},{...balance,customerId:'other'},{...balance,overseasWarehouseId:'other'}])assert.throws(()=>validateSource(detail,b,position))
|
||||||
|
for(const p of [{...position,availablePackages:0},{...position,frozenPackages:1},{...position,holdPackages:1},{...position,allocatedPackages:1},{...position,pickedPackages:1},{...position,palletId:null}])assert.throws(()=>validateSource(detail,balance,p))
|
||||||
|
assert.throws(()=>validateSource({...detail,allocations:[{}]},balance,position))
|
||||||
|
})
|
||||||
|
test('missing destination or FBA master cannot silently become a receiving overseas resource',()=>{
|
||||||
|
assert.throws(()=>validateDestination(detail))
|
||||||
|
assert.throws(()=>validateDestination(detail,{warehouseType:'FBA',status:'ACTIVE'},{},{}))
|
||||||
|
const d={destinationWarehouseCode:'ABE4'},w={id:'ny',warehouseType:'OVERSEAS',status:'ACTIVE'}
|
||||||
|
const p={warehouseId:'ny',locationId:'loc',status:'IDLE',customerId:'0',fbaWarehouseCode:'ABE4'}
|
||||||
|
const l={id:'loc',warehouseId:'ny'}
|
||||||
|
assert.equal(validateDestination(d,w,p,l),true)
|
||||||
|
assert.throws(()=>validateDestination(d,w,{...p,warehouseId:'la'},l))
|
||||||
|
assert.throws(()=>validateDestination(d,w,{...p,status:'OCCUPIED'},l))
|
||||||
|
})
|
||||||
|
test('available original-source cartons cannot waive original ticket relabel binding',()=>{
|
||||||
|
const d={needRelabel:true,lastlegOrderId:'13',relabelBindings:[{quantity:1}]}
|
||||||
|
assert.equal(relabelPrerequisite(d,{relabelStatus:'PENDING_BINDING'}).ready,false)
|
||||||
|
assert.equal(relabelPrerequisite({...d,relabelNo:'original'},{relabelApplications:[{relabelNo:'other',lastlegOrderId:'13'}],relabelStatus:'COMPLETED'}).ready,false)
|
||||||
|
assert.equal(relabelPrerequisite({...d,relabelNo:'original'},{relabelApplications:[{relabelNo:'original',lastlegOrderId:'13'}],relabelStatus:'PENDING_PROCESSING'}).ready,false)
|
||||||
|
assert.equal(relabelPrerequisite({...d,relabelNo:'original'},{relabelApplications:[{relabelNo:'original',lastlegOrderId:'other'}],relabelStatus:'COMPLETED'}).ready,false)
|
||||||
|
assert.equal(relabelPrerequisite({...d,relabelNo:'original'},{relabelApplications:[{relabelNo:'original',lastlegOrderId:'13'}],relabelStatus:'COMPLETED'}).ready,true)
|
||||||
|
})
|
||||||
|
|
||||||
|
function linkedFixture(status='WAIT_RELABEL'){
|
||||||
|
const transfer={transferNo:TRANSFER,needRelabel:true,packageCount:1,operatingWarehouseId:LA,destinationWarehouseCode:'ABE4',
|
||||||
|
relabelNo:'RL-SYNTHETIC-ONLY',relabelApplicationId:'app',lastlegOrderId:'tail',status,
|
||||||
|
relabelBindings:[{newSku:'NEW'}],allocations:[{inventoryPositionId:'selected-old',palletId:'source-pallet',packageCount:1,status:'RESERVED',skuCode:'OLD'}]}
|
||||||
|
const linked={application:{id:'app',relabelNo:transfer.relabelNo,lastlegOrderId:'tail',customerOrderNo:TRANSFER,overseasWarehouseId:LA},items:[{cartonMark:'BOX-1'}]}
|
||||||
|
const task={relabelNo:transfer.relabelNo,customerOrderNo:TRANSFER,taskStatus:'PROCESSING',version:2,
|
||||||
|
expectedPackages:1,relabelledPackages:1,skippedPackages:0,exceptionPackages:0,
|
||||||
|
items:[{itemId:'item',cartonMark:'BOX-1',fbaCartonMark:'FBA-BOX-1',operationType:'CARTON_AND_SKU',skuItems:[{skuCode:'OLD',newSkuCode:'NEW',quantity:1}]}],
|
||||||
|
history:[{action:'PALLET_BIND',detail:JSON.stringify({round:'INITIAL',cartonMarks:['BOX-1'],targetPallets:['WORK-1']})}]}
|
||||||
|
return {transfer,linked,task,plan:prepareOriginalTransferPlan(transfer,'offline-contract')}
|
||||||
|
}
|
||||||
|
const platform={kind:'platform',appCode:'PLATFORM',tenantId:'0',authorities:['platform:overseas-warehouse:relabel-order:status']}
|
||||||
|
const release=stage=>({execute:true,environment:'TEST',transferNo:TRANSFER,customerId:CUSTOMER,stage,runId:'offline-contract'})
|
||||||
|
function temporaryJournal(){const directory=fs.mkdtempSync(path.join(os.tmpdir(),'f33-stage-'));return {directory,cleanup(){for(const file of fs.readdirSync(directory))fs.unlinkSync(path.join(directory,file));fs.rmdirSync(directory)}}}
|
||||||
|
test('unallocated original ticket remains a zero-network plan without inventing a relabel number',async()=>{
|
||||||
|
const f=linkedFixture();delete f.transfer.relabelNo;delete f.transfer.relabelApplicationId
|
||||||
|
const plan=prepareOriginalTransferPlan(f.transfer,'unallocated')
|
||||||
|
assert.equal(plan.linked,false);assert.equal(plan.networkIssued,false)
|
||||||
|
assert.throws(()=>prepareTransferStage(plan,'approve',{linked:f.linked}),/WAIT_FOR_ORIGINAL_ALLOCATION/)
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'review-approve',{linked:f.linked}),/UNSUPPORTED_STAGE/)
|
||||||
|
const command=prepareTransferStage(f.plan,'approve',{linked:f.linked});let calls=0
|
||||||
|
assert.equal((await runPreparedTransferStage({command,transport:async()=>{calls++}})).mode,'PLAN');assert.equal(calls,0)
|
||||||
|
})
|
||||||
|
test('different relabel parent IDs or customer-order linkage cannot enter the original pipeline',()=>{
|
||||||
|
const f=linkedFixture()
|
||||||
|
for(const patch of [{id:'other'},{lastlegOrderId:'other'},{customerOrderNo:'OTHER-TICKET'},{overseasWarehouseId:NY}]){
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'approve',{linked:{...f.linked,application:{...f.linked.application,...patch}}}),/EXACT_ORIGINAL_RELABEL/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
test('actual labels and approved SKU piece counts are required for one combined unit fact',()=>{
|
||||||
|
const f=linkedFixture(),context={linked:f.linked,task:f.task,itemId:'item',observedOldLabel:'BOX-1',observedNewLabel:'FBA-BOX-1',observedSkus:[{skuCode:'OLD',newSkuCode:'NEW',quantity:1}]}
|
||||||
|
const command=prepareTransferStage(f.plan,'unit',context);assert.equal(command.body.expectedVersion,2);assert.match(command.path,/\/units$/)
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'unit',{...context,observedOldLabel:'OTHER'}))
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'unit',{...context,observedSkus:[{skuCode:'OLD',newSkuCode:'NEW',quantity:2}]}))
|
||||||
|
})
|
||||||
|
test('normal completion requires current-round physical binding photos and no skipped cartons',()=>{
|
||||||
|
const f=linkedFixture(),context={linked:f.linked,task:f.task,photoFileIds:['77']}
|
||||||
|
assert.equal(prepareTransferStage(f.plan,'complete',context).body.manualCompletion,false)
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'complete',{...context,task:{...f.task,reviewedAt:'NEW-ROUND'}}))
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'complete',{...context,task:{...f.task,skippedPackages:1}}))
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'complete',{...context,photoFileIds:[]}))
|
||||||
|
})
|
||||||
|
test('receipt consumes the current shipped output position and keeps New York separate from ABE4',()=>{
|
||||||
|
const f=linkedFixture('OUTBOUNDED');f.transfer.allocations=[{inventoryPositionId:'new-output',palletId:'work-pallet',packageCount:1,status:'SHIPPED',skuCode:'NEW'}]
|
||||||
|
const plan=prepareOriginalTransferPlan(f.transfer,'receipt')
|
||||||
|
const receiptTarget={warehouse:{id:NY,warehouseType:'OVERSEAS',status:'ACTIVE',warehouseCode:'US-NYC-01',warehouseName:'纽约仓'},
|
||||||
|
pallet:{id:'ny-pallet',warehouseId:NY,locationId:'ny-location',palletCode:'F33-NY',status:'IDLE',customerId:'0',fbaWarehouseCode:'ABE4'},location:{id:'ny-location',warehouseId:NY,locationCode:'F33-NY-LOC'}}
|
||||||
|
const command=prepareTransferStage(plan,'receipt',{linked:f.linked,receiptTarget})
|
||||||
|
assert.equal(command.body.sourceInventoryPositionId,'new-output');assert.equal(command.body.destinationOverseasWarehouseId,NY)
|
||||||
|
assert.equal(command.body.destinationWarehouseCode,'ABE4');assert.equal(command.body.destinationWarehouseId,'193000000000000003')
|
||||||
|
assert.throws(()=>prepareTransferStage(plan,'receipt',{linked:f.linked,receiptTarget:{...receiptTarget,warehouse:{...receiptTarget.warehouse,id:LA}}}))
|
||||||
|
})
|
||||||
|
test('write intent is durable before a single transport call; only known exact original replay is allowed',async()=>{
|
||||||
|
const f=linkedFixture(),command=prepareTransferStage(f.plan,'approve',{linked:f.linked}),temp=temporaryJournal();let count=0
|
||||||
|
try{
|
||||||
|
const transport=async c=>{count++;const j=JSON.parse(fs.readFileSync(path.join(temp.directory,`${c.runId}-${c.stage}.json`)));assert.equal(count===1?j.phase:j.replay.phase,'INTENT');return {http:200,code:'200',data:{approved:true}}}
|
||||||
|
const options={command,release:release('approve'),actor:platform,transport,journalDirectory:temp.directory}
|
||||||
|
await runPreparedTransferStage(options);await assert.rejects(runPreparedTransferStage(options));await runPreparedTransferStage({...options,replay:true})
|
||||||
|
assert.equal(count,2);const journal=JSON.parse(fs.readFileSync(path.join(temp.directory,`${command.runId}-${command.stage}.json`)));assert.equal(journal.businessVerified,false)
|
||||||
|
await assert.rejects(runPreparedTransferStage({...options,replay:true}));assert.equal(count,2)
|
||||||
|
}finally{temp.cleanup()}
|
||||||
|
})
|
||||||
|
test('unknown result or explicit rejection preserves evidence and blocks retry even with the original key',async()=>{
|
||||||
|
for(const unknown of [true,false]){
|
||||||
|
const f=linkedFixture(),command=prepareTransferStage(f.plan,'approve',{linked:f.linked}),temp=temporaryJournal();let calls=0
|
||||||
|
try{
|
||||||
|
const transport=async()=>{calls++;if(unknown)throw Error('secret diagnostics must not be recorded');return {http:409,code:'A0409',requestId:'req',traceId:'trace'}}
|
||||||
|
const options={command,release:release('approve'),actor:platform,transport,journalDirectory:temp.directory}
|
||||||
|
await assert.rejects(runPreparedTransferStage(options));await assert.rejects(runPreparedTransferStage({...options,replay:true}));assert.equal(calls,1)
|
||||||
|
const raw=fs.readFileSync(path.join(temp.directory,`${command.runId}-${command.stage}.json`),'utf8');assert.doesNotMatch(raw,/secret diagnostics/)
|
||||||
|
const j=JSON.parse(raw);assert.equal(j.phase,unknown?'OUTCOME_UNKNOWN':'ACKNOWLEDGED');if(!unknown)assert.equal(j.requestId,'req')
|
||||||
|
}finally{temp.cleanup()}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
test('stage release actor or prepared-payload mismatch cannot send a request',async()=>{
|
||||||
|
const f=linkedFixture(),command=prepareTransferStage(f.plan,'approve',{linked:f.linked});let calls=0
|
||||||
|
const options={command,release:release('approve'),actor:platform,journalDirectory:'not-created',transport:async()=>{calls++}}
|
||||||
|
await assert.rejects(runPreparedTransferStage({...options,release:release('receipt')}))
|
||||||
|
await assert.rejects(runPreparedTransferStage({...options,actor:{...platform,tenantId:CUSTOMER}}))
|
||||||
|
await assert.rejects(runPreparedTransferStage({...options,command:{...command,body:{status:'REJECTED'}}}))
|
||||||
|
assert.equal(calls,0)
|
||||||
|
})
|
||||||
|
test('photo upload bytes are frozen and an acknowledged no-key upload cannot be repeated',async()=>{
|
||||||
|
const f=linkedFixture(),binary=Buffer.from('synthetic offline transport fixture')
|
||||||
|
const command=prepareTransferStage(f.plan,'photo',{linked:f.linked,photo:{fileName:'fixture.png',contentType:'image/png',sizeBytes:binary.length,sha256:crypto.createHash('sha256').update(binary).digest('hex')}})
|
||||||
|
const actor={kind:'pda',appCode:'PLATFORM',tenantId:'0',membershipId:'2106727769572478980',warehouseId:LA,roles:['OVERSEAS_WAREHOUSE_PDA_OPERATOR'],authorities:['lastleg:pda:operate']}
|
||||||
|
const temp=temporaryJournal();let calls=0
|
||||||
|
try{
|
||||||
|
const options={command,release:release('photo'),actor,binary,journalDirectory:temp.directory,transport:async()=>{calls++;return {http:200,code:'200',data:{fileId:'77'}}}}
|
||||||
|
await assert.rejects(runPreparedTransferStage({...options,binary:Buffer.from('changed')}));assert.equal(calls,0)
|
||||||
|
await runPreparedTransferStage(options);await assert.rejects(runPreparedTransferStage({...options,replay:true}));assert.equal(calls,1)
|
||||||
|
}finally{temp.cleanup()}
|
||||||
|
})
|
||||||
|
test('physical binding uses selected source and current versions without client customer/warehouse scope in PDA body',()=>{
|
||||||
|
const f=linkedFixture(),context={linked:f.linked,source:{warehouseId:LA,positionId:'selected-old',palletId:'source-pallet',palletCode:'SOURCE',palletVersion:3,positionVersion:4},target:{warehouseId:LA,palletCode:'WORK',palletVersion:0,destinationWarehouseCode:'ABE4'},observedCartonMarks:['BOX-1']}
|
||||||
|
const c=prepareTransferStage(f.plan,'bind',context)
|
||||||
|
assert.equal(c.body.actualSource.positionId,'selected-old');assert.equal(c.body.actualSource.packages,1);assert.equal(c.body.target.expectedVersion,0)
|
||||||
|
assert.equal(c.body.customerId,undefined);assert.equal(c.body.warehouseId,undefined)
|
||||||
|
assert.throws(()=>prepareTransferStage(f.plan,'bind',{...context,target:{...context.target,warehouseId:NY}}))
|
||||||
|
})
|
||||||
|
test('one original carton stays conserved and existing HOLD and append-only facts remain untouched',()=>{
|
||||||
|
const unchanged={positionId:'protected',availablePackages:0,holdPackages:1}
|
||||||
|
const ship={id:'ship',businessType:'SHIP',businessOrderNo:TRANSFER,deltaPackages:-1,skuCode:'NEW'}
|
||||||
|
const receipt={id:'receipt',businessType:'STOCK_IN',businessOrderNo:TRANSFER,overseasWarehouseId:NY,deltaPackages:1,skuCode:'NEW'}
|
||||||
|
const base={baselineSourceTotal:4,baselineTargetTotal:0,expectedNewSku:'NEW',protectedBefore:unchanged,protectedAfter:unchanged,ledgerBefore:[{id:'old',packages:1}],ledgerAfter:[ship,receipt,{id:'old',packages:1}]}
|
||||||
|
assert.equal(verifyTransferMilestone('customer-confirm',{...base,sourceTotal:4,targetTotal:0}).verified,true)
|
||||||
|
assert.equal(verifyTransferMilestone('outbound',{...base,sourceTotal:3,targetTotal:0}).inTransitCartons,1)
|
||||||
|
assert.equal(verifyTransferMilestone('receipt',{...base,sourceTotal:3,targetTotal:1}).inTransitCartons,0)
|
||||||
|
assert.throws(()=>verifyTransferMilestone('receipt',{...base,sourceTotal:2,targetTotal:1}))
|
||||||
|
assert.throws(()=>verifyTransferMilestone('receipt',{...base,sourceTotal:3,targetTotal:1,protectedAfter:{...unchanged,holdPackages:0}}))
|
||||||
|
assert.throws(()=>verifyTransferMilestone('receipt',{...base,sourceTotal:3,targetTotal:1,ledgerAfter:[{id:'old',packages:2}]}))
|
||||||
|
assert.throws(()=>verifyTransferMilestone('receipt',{...base,sourceTotal:3,targetTotal:1,ledgerAfter:[ship,{...receipt,skuCode:'OLD'},{id:'old',packages:1}]}))
|
||||||
|
})
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Internal evidence helper; invoked explicitly by the assigned audit, never runs on import.
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
export const digest = value => crypto.createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex')
|
||||||
|
export function selectSteps(report, predicate) {
|
||||||
|
return (report.steps || []).flatMap((step, index) => predicate(step) ? [{ pointer: `/steps/${index}`, ...step }] : [])
|
||||||
|
}
|
||||||
|
export function requirePassed(steps) {
|
||||||
|
return steps.length > 0 && steps.every(s => s.passed === true)
|
||||||
|
}
|
||||||
|
export function transferConclusion(detail) {
|
||||||
|
const shipped = Boolean(detail.actualShipAt)
|
||||||
|
const terminal = detail.status === 'RECEIVED'
|
||||||
|
const received = (detail.allocations || []).length > 0 && detail.allocations.every(a => a.status === 'RECEIVED' && a.destinationInventoryPositionId)
|
||||||
|
return { status: detail.status, orderStatus: detail.orderStatus, transferNo: detail.transferNo,
|
||||||
|
allocationCount: detail.allocations?.length || 0, packageCount: detail.packageCount,
|
||||||
|
sourceCount: detail.sourceOrders?.length || 0, actualShipAt: detail.actualShipAt || null,
|
||||||
|
completed: terminal && shipped && received, requiresRealOperation: !(terminal && shipped && received) }
|
||||||
|
}
|
||||||
|
export async function auditRemainingNodes({ root, output, env, live = false }) {
|
||||||
|
if (fs.existsSync(output)) throw Error('REPORT_EXISTS_NO_OVERWRITE')
|
||||||
|
const read = relative => JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8'))
|
||||||
|
const files = ['reports/node-completion-audit-root-20261004-r1.json',
|
||||||
|
'reports/admin-full-current-20261002-r4.json', 'reports/overseas-receiving-current-20261002-r1.json',
|
||||||
|
'reports/fba-core-f40-tax-fleet-20261003-r1/root_first.json',
|
||||||
|
'reports/f26-loaded-transit-verification-20261004-r2.json',
|
||||||
|
'reports/discard-review-entry-root-20261004-r1/final-evidence.json']
|
||||||
|
const reports = Object.fromEntries(files.map(f => [f, read(f)]))
|
||||||
|
const full = reports[files[1]]
|
||||||
|
const evidence = (file, predicate) => ({ file, sha256: digest(fs.readFileSync(path.join(root, file), 'utf8')),
|
||||||
|
steps: selectSteps(reports[file], predicate) })
|
||||||
|
const result = { checkedAt: new Date().toISOString(), goalComplete: false,
|
||||||
|
scope: 'Existing TEST evidence only; no business commands, SQL, fault injection, deployment or UI operation',
|
||||||
|
sourceFiles: files.map(file => ({ file, sha256: digest(fs.readFileSync(path.join(root, file), 'utf8')) })),
|
||||||
|
integrationRequired: { file: 'scripts/README.md', owner: '总控',
|
||||||
|
reason: 'Current write boundary excludes shared script index; documentation contract identifies this new helper as undocumented.',
|
||||||
|
text: '`scripts/fba-remaining-nodes-acceptance.mjs`:本轮FBA剩余节点内部证据助手,仅显式调用auditRemainingNodes;读取指定原报告并按JSON pointer/SHA-256保留证据。live=true仅使用明确TEST环境文件的独立Auth会话与串行GET,正常退出;不执行业务写命令、SQL、故障注入、部署或页面操作。报告reports/fba-remaining-nodes-20261004/,既有报告拒绝覆盖,失败保留且不自动重试。对应离线测试fba-remaining-nodes-acceptance.test.mjs。' },
|
||||||
|
nodes: [], requests: [], sessionClosed: !live }
|
||||||
|
result.nodes.push({ node: 'F23.2', conclusion: 'HTTP actual AT_ORIGIN_PORT supported; Admin save and two-end projection remain unverified',
|
||||||
|
category: '需真实操作', evidence: [evidence(files[3], s => /AT_ORIGIN_PORT/.test(s.name))],
|
||||||
|
next: 'Use a current not-yet-at-origin-port sample; freeze query version/history and customer projection, record actual node once through Admin, capture POST and both readbacks; preserve customs/container identity and do not rewrite a later actual node.' })
|
||||||
|
result.nodes.push({ node: 'F25', conclusion: 'Clearance/DO upload and replay, fleet/appointment/empty-return supported; PU, distinct actual pickup and mail lifecycle unverified',
|
||||||
|
category: '需真实操作', evidence: [evidence(files[1], s => s.module === 'phase5-terminal-drayage'),
|
||||||
|
evidence(files[1], s => s.module === 'phase3-container-documents' && /DELIVERY_ORDER|DESTINATION_CLEARANCE/.test(s.name))],
|
||||||
|
boundaries: { mailRequestsInOriginalReport: full.steps.filter(s => /terminal-drayages.*\/emails?|terminal-drayages.*pickup-email/.test(s.request?.path || '')).length,
|
||||||
|
pickupAssertion: 'Original drayage assertion compares actualPickupAt to unloadingCompletedAt; this does not establish a distinct pickup command/time.' },
|
||||||
|
next: 'Freeze pre-pickup container and actual node history; inspect current DO/PU contract, confirm distinct PICKED_UP before unloading; capture explicit email queue/recipient snapshot, SMTP acceptance separately, fleet-change ordered cancellation/request and failed-mail retry without assuming delivery.' })
|
||||||
|
result.nodes.push({ node: 'F27', conclusion: 'Normal conversion/resources/replay supported; resource-failure retry UI unverified',
|
||||||
|
category: '需真实操作', evidence: [evidence(files[1], s => s.module === 'phase4-tail-order-controls')],
|
||||||
|
explicitRetryEvidence: selectSteps(full, s => /资源.*重试|计划.*重试|重试.*资源/.test(s.name)),
|
||||||
|
next: 'First locate a real resourcePendingPlanCount>0 container; freeze plans, reservations, resource identities and original command key. Use original container conversion UI retry once, verify only failed resources finish and no new plans/reservations; healthy completed samples cannot prove recovery. Failure injection requires controller window.' })
|
||||||
|
result.nodes.push({ node: 'F33-transfer', conclusion: 'Original stock transfer approved; outbound/target receipt terminal not established',
|
||||||
|
category: '需真实操作', originalTransferNo: 'WT-WH-20261002-430594',
|
||||||
|
evidence: [evidence(files[1], s => (s.request?.path || '').includes('WT-WH-20261002-430594'))],
|
||||||
|
originalFixture: full.createdResources.phase4OverseasClientServices,
|
||||||
|
next: 'Continue this exact original transfer only after current source stock, holds, destination and allocations are frozen. Verify allocation, actual outbound and target receipt with per-position balances/unique movements and same-command replay. Incoming transfer fixtures are separate sources and cannot replace this ticket.' })
|
||||||
|
result.nodes.push({ node: 'F33-discard', conclusion: 'Rejection release supported; approval/decrement decision pending',
|
||||||
|
category: '缺用户输入', evidenceFiles: [files[5]], next: 'Await approval-time versus PDA actual-quantity decrement decision; no approval or decrement issued.' })
|
||||||
|
result.nodes.push({ node: 'F26', conclusion: 'Existing 68-check source/transit scope is already evidenced; no repeat run',
|
||||||
|
category: '可立即关闭指定分支', evidenceFiles: [files[4]], boundary: 'Only 11 FBA sources/13 actual cartons and pre-receipt mapping; not all event faults or UI.' })
|
||||||
|
result.nodes.push({ node: 'F24-external', conclusion: 'External carrier lifecycle lacks valid input',
|
||||||
|
category: '缺用户输入', next: 'Provide valid carrier TEST input; synthetic fixture or manual nodes cannot close supplier lifecycle.' })
|
||||||
|
const save = () => { fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(result, null, 2)) }
|
||||||
|
save()
|
||||||
|
if (!live) return result
|
||||||
|
let session
|
||||||
|
const call = async (method, route, body, token = session?.accessToken) => {
|
||||||
|
if (method !== 'GET' && !['/auth/login', '/auth/logout'].includes(route)) throw Error('BUSINESS_WRITE_FORBIDDEN')
|
||||||
|
const entry = { method, path: route, phase: 'INTENT' }; result.requests.push(entry); save()
|
||||||
|
const response = await fetch(env.CHONGCHONG_ADMIN_API_BASE_URL.replace(/\/$/, '') + route, {
|
||||||
|
method, redirect: 'error', signal: AbortSignal.timeout(20000),
|
||||||
|
headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
||||||
|
...(body === undefined ? {} : { body: JSON.stringify(body) }) })
|
||||||
|
const json = JSON.parse(await response.text())
|
||||||
|
Object.assign(entry, { phase: 'ACKNOWLEDGED', http: response.status, code: String(json.code),
|
||||||
|
requestId: response.headers.get('x-request-id'), traceId: response.headers.get('x-trace-id') }); save()
|
||||||
|
if (response.status !== 200 || String(json.code) !== '200') throw Error(`READ_FAILED_${response.status}_${json.code}`)
|
||||||
|
return json.data
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (String(env.CHONGCHONG_ENV).toUpperCase() !== 'TEST' || !env.CHONGCHONG_ADMIN_API_BASE_URL || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw Error('EXPLICIT_TEST_ENV_REQUIRED')
|
||||||
|
session = await call('POST', '/auth/login', { loginId: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD }, '')
|
||||||
|
const transferRoute = '/platform/v1/last-leg/warehouse-transfers/WT-WH-20261002-430594?customerId=2105875471489142785'
|
||||||
|
const before = await call('GET', transferRoute)
|
||||||
|
if (before.transferNo !== 'WT-WH-20261002-430594') throw Error('TRANSFER_SCOPE_MISMATCH')
|
||||||
|
result.nodes.find(n => n.node === 'F33-transfer').current = transferConclusion(before)
|
||||||
|
const container = full.createdResources.phase5TerminalDrayage.containerBookingId
|
||||||
|
const drayage = await call('GET', `/platform/v1/terminal-drayages/${container}`)
|
||||||
|
const history = await call('GET', `/platform/v1/terminal-drayages/${container}/history`)
|
||||||
|
const mailbox = await call('GET', `/platform/v1/terminal-drayages/${container}/emails`)
|
||||||
|
result.nodes.find(n => n.node === 'F25').current = {
|
||||||
|
containerNo: drayage.containerNo, status: drayage.status, actualPickupAt: drayage.actualPickupAt,
|
||||||
|
unloadingCompletedAt: drayage.unloadingCompletedAt, actualEmptyReturnAt: drayage.actualEmptyReturnAt,
|
||||||
|
pickupEqualsUnloading: drayage.actualPickupAt === drayage.unloadingCompletedAt,
|
||||||
|
historyActions: history.map(h => h.action), mailboxCount: mailbox.records?.length || 0,
|
||||||
|
mailStatuses: (mailbox.records || []).map(m => ({ status: m.status, kind: m.kind })) }
|
||||||
|
const overview = await call('GET', `/platform/v1/containers/overseas/${container}/overview`)
|
||||||
|
Object.assign(result.nodes.find(n => n.node === 'F25').current, {
|
||||||
|
pickupCodePresent: Boolean(overview.container?.pickupCode),
|
||||||
|
documents: (overview.documents || []).map(d => ({ documentType: d.documentType, version: d.version,
|
||||||
|
filePresent: Boolean(d.fileId), completed: d.completed === true })) })
|
||||||
|
const preview = await call('GET', `/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${container}`)
|
||||||
|
result.nodes.find(n => n.node === 'F27').current = { containerNo: preview.containerNo,
|
||||||
|
totalOrderCount: preview.totalOrderCount, pendingOrderCount: preview.pendingOrderCount,
|
||||||
|
blockedOrderCount: preview.blockedOrderCount, totalPlannedPalletCount: preview.totalPlannedPalletCount,
|
||||||
|
plans: (preview.groups || []).flatMap(g => (g.orders || []).map(o => ({ planNo: o.existingPlanNo, planStatus: o.planStatus }))),
|
||||||
|
recoveryProven: false }
|
||||||
|
const after = await call('GET', transferRoute)
|
||||||
|
result.transferReadStable = digest(before) === digest(after)
|
||||||
|
result.liveReadSuccess = result.transferReadStable
|
||||||
|
} catch (error) { result.liveReadSuccess = false; result.failure = String(error.message).replace(/https?:\/\/\S+/g, '[redacted]') }
|
||||||
|
finally {
|
||||||
|
if (!session) result.sessionClosed = true
|
||||||
|
if (session) { try { await call('POST', '/auth/logout', { refreshToken: session.refreshToken }); result.sessionClosed = true }
|
||||||
|
catch { result.sessionClosed = false; result.liveReadSuccess = false } }
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { requirePassed, selectSteps, transferConclusion } from './fba-remaining-nodes-acceptance.mjs'
|
||||||
|
|
||||||
|
test('missing and partially failed evidence cannot close a branch', () => {
|
||||||
|
assert.equal(requirePassed([]), false)
|
||||||
|
assert.equal(requirePassed([{ passed: true }, { passed: false }]), false)
|
||||||
|
assert.equal(requirePassed([{ passed: true }, {}]), false)
|
||||||
|
assert.equal(requirePassed([{ passed: true }]), true)
|
||||||
|
})
|
||||||
|
test('approved stock transfer and a signed unrelated order do not prove transfer receipt', () => {
|
||||||
|
assert.equal(transferConclusion({ status: 'APPROVED', orderStatus: 'DELIVERED' }).completed, false)
|
||||||
|
assert.equal(transferConclusion({ status: 'RECEIVED' }).completed, false)
|
||||||
|
assert.equal(transferConclusion({ status: 'OUTBOUNDED', actualShipAt: '2026-10-04' }).completed, false)
|
||||||
|
assert.equal(transferConclusion({ status: 'RECEIVED', actualShipAt: '2026-10-04', allocations: [{ status: 'RECEIVED', destinationInventoryPositionId: '42' }] }).completed, true)
|
||||||
|
})
|
||||||
|
test('selection keeps exact original JSON pointer and failure evidence', () => {
|
||||||
|
const rows = selectSteps({ steps: [{ name: 'other', passed: true }, { name: 'original', passed: false }] }, s => s.name === 'original')
|
||||||
|
assert.deepEqual(rows, [{ pointer: '/steps/1', name: 'original', passed: false }])
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user