Files
baiyue-firstleg/scripts/fba-remaining-nodes-acceptance-transfer.mjs
T

329 lines
26 KiB
JavaScript

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'},
unit: {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
'unit-carton': {actor:'pda',permission:'lastleg:pda:operate',replay:'KEYED'},
bind: {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','unit-carton','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'){
demand(task?.relabelNo===plan.transfer.relabelNo&&task.customerOrderNo===TRANSFER
&&task.taskStatus==='PROCESSING'&&Number(task.expectedPackages)===1&&Number(task.relabelledPackages)===1
&&Number(task.skippedPackages)===0&&Number(task.exceptionPackages)===0,'COMPLETE_UNIT_FACTS_BEFORE_BIND_REQUIRED')
const selectedItems=task.items?.filter(i=>context.observedCartonMarks?.includes(i.cartonMark))||[]
demand(selectedItems.length===1&&selectedItems.every(i=>{
if(!Number.isInteger(i.expectedPackages)||i.expectedPackages!==1)return false
const facts=(task.unitFacts||[]).filter(f=>String(f.itemId)===String(i.itemId)&&f.result==='RELABELLED')
return facts.length===1&&Number.isInteger(facts[0].quantity)&&facts[0].quantity===1
}), 'OWN_RELABELLED_UNIT_FACT_BEFORE_BIND_REQUIRED')
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==='unit-carton'){
const item=task.items?.find(i=>String(i.itemId)===String(context.itemId))
demand(task.taskStatus==='PROCESSING'&&task.relabelledPackages===0&&item?.operationType==='CARTON_AND_SKU'
&&item.expectedPackages===1&&item.skuProcessedPackages===1&&item.cartonProcessedPackages===0
&&!(task.unitFacts||[]).some(f=>String(f.itemId)===String(item.itemId)), 'OWN_SKU_ONLY_PARTIAL_UNIT_REQUIRED')
demand(context.observedOldLabel===item.cartonMark&&context.observedNewLabel===item.fbaCartonMark,'OBSERVED_LABELS_MUST_MATCH_OWN_ITEM')
command.path=pdaBase+'/cartons/scan';command.body={cartonMark:context.observedOldLabel,expectedVersion:version(task.version),idempotencyKey:key}
}
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 function redactTransferMessage(message) {
if(typeof message!=='string')return undefined
return message.replace(/https?:\/\/\S+/gi,'[url redacted]')
.replace(/Bearer\s+\S+/gi,'Bearer [redacted]')
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,'[token redacted]')
.replace(/\b(password|accessToken|refreshToken|username|loginId)\s*[:=]\s*[^\s,;]+/gi,'$1=[redacted]').slice(0,1000)
}
export async function runPreparedTransferStage({command,release,actor,transport,journalDirectory,binary,replay=false,recovery}) {
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 originalFile=path.join(journalDirectory,`${command.runId}-${command.stage}.json`)
let file=originalFile
if(recovery){
demand(recovery.authorization==='F33_BIND_A0409_AFTER_UNIT'&&command.stage==='bind'
&&command.runId==='f33-original-20261005-r1'
&&command.preparedHash==='bb1ccb10ebaef5f37d8f16782ae6f8f2051d31061142533a61e8ab020c1e2a0d', 'ONLY_RELEASED_ORIGINAL_F33_BIND_RECOVERY_ALLOWED')
const readProof=(name,sha)=>{
demand(typeof name==='string'&&path.basename(name)===name&&/^[a-f0-9]{64}$/.test(sha),'EXACT_LOCAL_RECOVERY_PROOF_REQUIRED')
const bytes=fs.readFileSync(path.join(journalDirectory,name))
demand(crypto.createHash('sha256').update(bytes).digest('hex')===sha,'RECOVERY_PROOF_CHANGED')
return JSON.parse(bytes)
}
demand(fs.existsSync(originalFile),'ORIGINAL_REJECTED_BIND_JOURNAL_REQUIRED')
const original=readProof(path.basename(originalFile),recovery.originalJournalSha256)
demand(original.phase==='ACKNOWLEDGED'&&original.http===200&&original.code==='A0409'
&&original.success===false&&!original.replay&&original.command.preparedHash===command.preparedHash
&&commandHash(original.command)===command.preparedHash,'ONLY_KNOWN_ACKNOWLEDGED_A0409_BIND_CAN_RECOVER')
const rollback=readProof(recovery.rollbackProofFile,recovery.rollbackProofSha256)
demand(rollback.verified===true&&['transfer','positions','ledger','linked','order','task']
.every(k=>rollback.unchanged?.includes(k)),'COMPLETE_REJECTION_ROLLBACK_PROOF_REQUIRED')
const fresh=readProof(recovery.freshProofFile,recovery.freshProofSha256)
demand(fresh.runId===command.runId&&fresh.transferNo===TRANSFER&&fresh.relabelNo===command.body.relabelNo
&&fresh.originalKeyNotExecuted===true&&fresh.unitFactsVerified===true
&&fresh.unitFactCount===1&&fresh.relabelledPackages===1&&fresh.stockAndLedgerUnchangedByUnit===true
&&fresh.sourcePalletCode===command.body.source.palletCode&&fresh.targetPalletCode===command.body.target.palletCode
&&fresh.sourcePalletVersion===command.body.source.expectedVersion&&fresh.targetPalletVersion===command.body.target.expectedVersion
&&String(fresh.sourcePositionId)===String(command.body.actualSource.positionId)
&&fresh.sourcePositionVersion===command.body.actualSource.expectedPositionVersion,'FRESH_UNIT_AND_UNCHANGED_ORIGINAL_VERSIONS_REQUIRED')
file=path.join(journalDirectory,`${command.runId}-bind-recovery.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,
...(recovery?{recovery:{authorization:recovery.authorization,originalJournalFile:path.basename(originalFile),originalJournalSha256:recovery.originalJournalSha256,
rollbackProofFile:recovery.rollbackProofFile,rollbackProofSha256:recovery.rollbackProofSha256,freshProofFile:recovery.freshProofFile,freshProofSha256:recovery.freshProofSha256}}:{})}
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),message:redactTransferMessage(response.message),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}
}