贴标申请,仓储转运

This commit is contained in:
2026-09-17 17:37:29 +08:00
parent 3b8b6f5aa2
commit 29f05b7b09
106 changed files with 5019 additions and 471 deletions
@@ -113,6 +113,43 @@ async function createPda(adminToken, warehouseId, onCreated) {
throw new Error('Temporary PDA security projection did not converge')
}
async function merchantSessionForTenant(adminToken, tenantId) {
const ticket = await api(domestic, adminToken, 'POST',
`/api/platform/v1/iam/tenants/${tenantId}/impersonation`, { reason: `${marker} 海外换标真实验收` })
return api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
}
async function findRelabelScope(adminToken, preferredMerchant = null) {
const sessions = []
if (preferredMerchant?.accessToken && preferredMerchant?.overseasAccessToken) sessions.push(preferredMerchant)
if (!sessions.length) {
const tenants = await api(domestic, adminToken, 'GET', '/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE')
for (const tenant of tenants.records || []) {
try { sessions.push(await merchantSessionForTenant(adminToken, id(tenant.id))) } catch {}
}
}
for (const session of sessions) {
const customerId = jwtId(session.overseasAccessToken, 'tenant_id', 'tenantId')
if (!customerId) continue
let warehouses
try { warehouses = await api(domestic, session.accessToken, 'GET', '/api/merchant/v1/warehouses?warehouseType=OVERSEAS&page=1&size=200') }
catch { continue }
for (const warehouse of warehouses.records || []) {
if (warehouse.status !== 'ACTIVE') continue
let candidates
try { candidates = await api(overseas, session.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${encodeURIComponent(id(warehouse.id))}`) }
catch { continue }
if (candidates.some((item) => String(item.inboundOrderNo || '').startsWith('DS-')
&& Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode
&& item.positions?.some((position) => position.palletNo && position.palletNo !== 'UNBOUND'
&& Number(position.availablePackages) >= 5))) {
return { merchant: session, customerId, warehouse }
}
}
}
throw new Error('No active customer has a destination warehouse and five cartons on a physical source pallet')
}
await mkdir(outputDir, { recursive: true })
let admin
let pda
@@ -124,29 +161,22 @@ try {
}
const storedMerchantToken = storedJwt('cc-client-overseas-access-token')
const storedCustomerId = jwtId(storedMerchantToken, 'tenant_id', 'tenantId')
let merchant
let preferredMerchant = null
try {
merchant = await refreshStoredSession('cc-client-refresh-token', 'merchant')
preferredMerchant = await refreshStoredSession('cc-client-refresh-token', 'merchant')
} catch {
assert(storedCustomerId, 'Current Codex merchant session does not contain a customer id')
const ticket = await api(domestic, admin.accessToken, 'POST',
`/api/platform/v1/iam/tenants/${storedCustomerId}/impersonation`, { reason: `${marker} 当前客户海外API验收` })
merchant = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
if (storedCustomerId) preferredMerchant = await merchantSessionForTenant(admin.accessToken, storedCustomerId)
}
assert(admin.accessToken && admin.overseasAccessToken && merchant.accessToken && merchant.overseasAccessToken, 'Refresh response did not contain both regional tokens')
const customerId = jwtId(merchant.overseasAccessToken, 'tenant_id', 'tenantId')
assert(customerId, 'Current merchant customer id is missing from the overseas token')
const warehouses = await api(domestic, merchant.accessToken, 'GET', '/api/merchant/v1/warehouses?warehouseType=OVERSEAS&page=1&size=200')
let selected = null
for (const warehouse of warehouses.records || []) {
if (warehouse.status !== 'ACTIVE') continue
const candidates = await api(overseas, merchant.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${encodeURIComponent(id(warehouse.id))}`)
if (candidates.some((item) => String(item.inboundOrderNo || '').startsWith('DS-') && Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode && item.positions?.some((position) => Number(position.availablePackages) >= 5))) {
selected = warehouse; break
}
}
assert(selected, 'Current merchant has no active overseas warehouse with a five-carton DS inventory source')
await disableStalePdas(admin.accessToken)
assert(admin.accessToken && admin.overseasAccessToken, 'Platform refresh response did not contain both regional tokens')
const resume = process.env.OVERSEAS_RESUME_FULFILLMENT === 'true'
if (resume && (!process.env.OVERSEAS_TEST_CUSTOMER_ID || !process.env.OVERSEAS_TEST_WAREHOUSE_ID)) throw new Error('Resume requires explicit customer and warehouse from the saved run')
const scope = resume ? {
merchant: await merchantSessionForTenant(admin.accessToken, process.env.OVERSEAS_TEST_CUSTOMER_ID),
customerId: process.env.OVERSEAS_TEST_CUSTOMER_ID,
warehouse: { id: process.env.OVERSEAS_TEST_WAREHOUSE_ID, warehouseCode: process.env.OVERSEAS_TEST_WAREHOUSE_CODE || null },
} : await findRelabelScope(admin.accessToken, preferredMerchant)
const { merchant, customerId, warehouse: selected } = scope
// Do not disable accounts from another acceptance run; cleanup owns only this run's account.
pda = await createPda(admin.accessToken, id(selected.id), (created) => { pda = created })
const relabelReport = join(outputDir, 'relabel.json')
@@ -156,7 +186,10 @@ try {
PDA_OVERSEAS_ACCESS_TOKEN: pda.token, OVERSEAS_TEST_CUSTOMER_ID: customerId,
OVERSEAS_TEST_WAREHOUSE_ID: id(selected.id), OVERSEAS_ACCEPTANCE_OUTPUT: relabelReport,
OVERSEAS_ACCEPTANCE_SOURCE: relabelReport, OVERSEAS_FULFILLMENT_OUTPUT: fulfillmentReport }
for (const script of ['overseas-relabel-real-acceptance.mjs', 'overseas-relabel-fulfillment-acceptance.mjs']) {
const scripts = process.env.OVERSEAS_CLOSURE_EXTRA === 'true' ? ['relabel-closure-local-acceptance.mjs'] : process.env.OVERSEAS_RESUME_FULFILLMENT === 'true'
? ['overseas-relabel-fulfillment-acceptance.mjs']
: ['overseas-relabel-real-acceptance.mjs', 'overseas-relabel-fulfillment-acceptance.mjs']
for (const script of scripts) {
const result = spawnSync(process.execPath, [resolve('scripts', script)], { cwd: resolve('.'), env: childEnv, stdio: 'inherit' })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`${script} failed with exit code ${result.status}`)