Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-01 14:48:33 +08:00
7 changed files with 250 additions and 142 deletions
@@ -54,14 +54,18 @@ export async function run(context) {
return
}
const path = `/api/platform/v1/inbounds/${inboundId}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`
const path = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?warehouseId=${encodeURIComponent(warehouseId)}`
const response = await context.request('GET', path)
const data = context.apiSuccess(MODULE, '复核本轮装柜证据聚合', response, `inboundId=${fixture.inboundId}`, OPTIONS)
const data = context.apiSuccess(MODULE, '复核本轮 task 柜级装柜证据聚合', response,
`taskId=${taskId}, containerBookingId=${containerBookingId}`, OPTIONS)
const evidences = Array.isArray(data?.evidences) ? data.evidences : []
const required = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
const active = evidences.filter((item) => item.status !== 'VOID')
const voided = evidences.find((item) => id(item.id) === id(fixture.voidedEvidenceId))
const missingTypeVoided = evidences.find((item) => id(item.id) === id(fixture.missingTypeVoidedEvidenceId))
const voided = evidences.find((item) => id(item.evidenceId) === id(fixture.voidedEvidenceId))
const missingTypeVoided = evidences.find((item) => id(item.evidenceId) === id(fixture.missingTypeVoidedEvidenceId))
context.assert(MODULE, 'task 证据聚合归属稳定 PDA 任务和物理柜次', response,
id(data?.taskId) === taskId && id(data?.containerBookingId) === containerBookingId,
`taskId=${id(data?.taskId) || 'missing'}, containerBookingId=${id(data?.containerBookingId) || 'missing'}`, OPTIONS)
context.assert(MODULE, '五类有效证据齐全且同类型多图追加不覆盖历史', response,
required.every((type) => active.some((item) => item.evidenceType === type))
&& evidences.filter((item) => item.evidenceType === 'PHOTO_STEP_1').length >= 2
@@ -69,15 +73,13 @@ export async function run(context) {
&& active.every((item) => String(item.fileName || '').includes(context.marker)
&& String(item.remark || '').includes(context.marker)),
`active=${active.length}, types=${[...new Set(active.map((item) => item.evidenceType))].join(',')}`, OPTIONS)
context.assert(MODULE, '作废证据保留原因、操作者和时间且不计入有效门禁', response,
Boolean(voided) && voided.status === 'VOID' && voided.voidReason === `${context.marker}-wrong-photo`
&& Boolean(voided.voidedBy) && Boolean(voided.voidedAt)
&& !active.some((item) => id(item.id) === id(fixture.voidedEvidenceId)),
context.assert(MODULE, '作废证据保留稳定记录与更新时间且不计入有效门禁', response,
Boolean(voided) && voided.status === 'VOID' && Boolean(voided.updatedAt)
&& !active.some((item) => id(item.evidenceId) === id(fixture.voidedEvidenceId)),
`voidedEvidenceId=${id(fixture.voidedEvidenceId) || 'missing'}, status=${voided?.status || 'missing'}`, OPTIONS)
context.assert(MODULE, '缺类作废审计保留且补传证据恢复有效门禁', response,
Boolean(missingTypeVoided) && missingTypeVoided.status === 'VOID'
&& missingTypeVoided.voidReason === `${context.marker}-temporary-missing-type`
&& Boolean(missingTypeVoided.voidedBy) && Boolean(missingTypeVoided.voidedAt)
&& Boolean(missingTypeVoided.updatedAt)
&& active.some((item) => item.evidenceType === 'PHOTO_STEP_4'
&& id(item.fileId) === id(fixture.replacementEvidenceFileId)),
`voidedEvidenceId=${id(fixture.missingTypeVoidedEvidenceId) || 'missing'}, replacementFileId=${id(fixture.replacementEvidenceFileId) || 'missing'}`, OPTIONS)
@@ -19,5 +19,7 @@ test('evidence gate requires stable PDA identity and server LOADING before any e
assert.match(source, /AWAITING_REVIEW 不作为证据开放条件/)
const gate = source.slice(source.indexOf('const taskResponse'), source.indexOf('const path ='))
assert.doesNotMatch(gate, /loading\/evidence/)
assert.match(source, /const path = `\/api\/platform\/v1\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
assert.match(source, /const path = `\/api\/platform\/v1\/first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\?warehouseId=/)
assert.doesNotMatch(source, /api\/platform\/v1\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
assert.doesNotMatch(source, /loading\/evidence\?tenantId=/)
})
@@ -52,6 +52,37 @@ test('default fixture publishes a second marker-owned inbound for the same-plan
})
test('HTTP chain refreshes after stale inventory and keeps one stable PDA identity', () => {
assert.match(loading,
/async function uploadEvidence\(context, taskId, warehouseId, evidenceType, expectedVersion, keySuffix,/)
assert.match(loading,
/first-leg\/pda-loading\/tasks\/\$\{taskId\}\/evidence\?\$\{query\}/)
assert.match(loading,
/\$\{evidencePath\}\/\$\{encodeURIComponent\(evidenceItem\.id\)\}\/content\?warehouseId=/)
assert.match(loading, /\$\{evidencePath\}\/\$\{encodeURIComponent\(id\(voidTarget\.id\)\)\}\/void/)
assert.doesNotMatch(loading,
/api\/platform\/v1\/files\/loading-evidence/)
assert.doesNotMatch(loading,
/\$\{base\}\/loading\/evidence|api\/platform\/v1\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
assert.doesNotMatch(loading, /uploadEvidence\(context, (tenantId|inboundId|containerBookingId),/)
ordered(loading, [
'创建三期 DRAFT 订舱(不生成真实柜)',
'三期 DRAFT 订舱返回稳定身份和初始版本',
'三期订舱上传当前有效订舱确认文件',
'订舱文件归属当前订舱且上传递增版本',
'订舱文件同键同载荷重放',
'订舱文件精确重放不新增文件或版本',
'回查订舱文件有效归属',
'同键重放后当前订舱仅保留一条目标有效文件',
'三期订舱 RELEASED 冻结运输快照',
])
assert.match(loading, /documentType', 'BOOKING_CONFIRMATION'/)
assert.match(loading, /id\(bookingDocument\?\.document\?\.oceanBookingId\) === bookingId/)
assert.match(loading, /releaseVersion > draftVersion/)
assert.match(loading, /'Idempotency-Key': bookingDocumentKey/)
assert.equal((loading.match(/'Idempotency-Key': bookingDocumentKey/g) || []).length, 2)
assert.match(loading, /Number\(bookingDocumentReplay\?\.bookingVersion\) === releaseVersion/)
assert.match(loading, /expectedVersion: releaseVersion, reason: context\.marker/)
assert.doesNotMatch(loading, /expectedVersion: draftVersion, reason: context\.marker/)
ordered(loading, [
'PDA 原子批次完成拣货并等待人工确认',
'五类装柜证据完整且铅封号持久化',
+167 -122
View File
@@ -77,15 +77,20 @@ async function request(context, method, path, body, headers = {}) {
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
}
async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
const path = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
async function uploadEvidence(context, taskId, warehouseId, evidenceType, expectedVersion, keySuffix,
{ sealNo = null, remark = context.marker, fileSuffix = evidenceType } = {}) {
const query = new URLSearchParams({ warehouseId: String(warehouseId), expectedVersion: String(expectedVersion),
evidenceType, remark })
if (sealNo != null) query.set('sealNo', sealNo)
const path = `/api/platform/v1/first-leg/pda-loading/tasks/${taskId}/evidence?${query}`
const form = new FormData()
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
const fileName = `${context.marker}-${evidenceType.toLowerCase()}.png`
const fileName = `${context.marker}-${String(fileSuffix).toLowerCase()}.png`
form.append('file', new Blob([png], { type: 'image/png' }), fileName)
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
'Idempotency-Key': `${context.marker}-p3-evidence-${keySuffix}`.slice(0, 128) },
body: form, signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
@@ -99,17 +104,6 @@ async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
}
}
async function bindEvidence(context, base, tenantId, evidenceType, fileId, suffix, keySuffix = evidenceType) {
return request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0,
evidenceType,
fileId,
fileName: 'client-must-not-override.png',
sealNo: evidenceType === 'SEAL' ? `SEAL-${suffix}` : null,
remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${keySuffix}`.slice(0, 128) })
}
function fixture(context) {
return context.createdResources?.orchestrationFixtures?.phase3Loading
|| context.createdResources?.orchestrationFixtures?.inboundMeasurement
@@ -440,8 +434,52 @@ export async function run(context) {
const bookingDraftResponse = context.apiSuccess(MODULE, '创建三期 DRAFT 订舱(不生成真实柜)', bookingCreateResponse, '', OPTIONS)
const bookingId = id(bookingDraftResponse?.oceanBooking?.id)
const draftVersion = Number(bookingDraftResponse?.oceanBooking?.version)
context.assert(MODULE, '三期 DRAFT 订舱返回稳定身份和初始版本', bookingCreateResponse,
Boolean(bookingId) && bookingDraftResponse?.oceanBooking?.status === 'DRAFT'
&& Number.isInteger(draftVersion) && draftVersion >= 0,
`bookingId=${bookingId || 'missing'}, status=${bookingDraftResponse?.oceanBooking?.status || 'missing'}, version=${bookingDraftResponse?.oceanBooking?.version ?? 'missing'}`, OPTIONS)
if (!bookingId || !Number.isInteger(draftVersion) || draftVersion < 0) return
const bookingDocumentKey = `${context.marker}-p3-booking-document`.slice(0, 128)
const bookingDocumentForm = () => {
const form = new FormData()
form.append('expectedVersion', String(draftVersion))
form.append('documentType', 'BOOKING_CONFIRMATION')
form.append('file', new Blob([`${context.marker}-BOOKING_CONFIRMATION`], { type: 'text/plain' }),
`booking-confirmation-${context.marker}.txt`)
return form
}
const bookingDocumentResponse = await context.requestMultipart('POST',
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm(), context.token,
{ 'Idempotency-Key': bookingDocumentKey })
const bookingDocument = context.apiSuccess(MODULE, '三期订舱上传当前有效订舱确认文件', bookingDocumentResponse, '', OPTIONS)
const releaseVersion = Number(bookingDocument?.bookingVersion)
context.assert(MODULE, '订舱文件归属当前订舱且上传递增版本', bookingDocumentResponse,
Boolean(bookingDocument?.document?.id) && id(bookingDocument?.document?.oceanBookingId) === bookingId
&& bookingDocument?.document?.documentType === 'BOOKING_CONFIRMATION'
&& bookingDocument?.document?.valid === true && Number.isInteger(releaseVersion)
&& releaseVersion > draftVersion,
`documentId=${id(bookingDocument?.document?.id) || 'missing'}, oceanBookingId=${id(bookingDocument?.document?.oceanBookingId) || 'missing'}, draftVersion=${draftVersion}, bookingVersion=${bookingDocument?.bookingVersion ?? 'missing'}`, OPTIONS)
if (!bookingId || !Number.isInteger(releaseVersion) || releaseVersion <= draftVersion) return
const bookingDocumentReplayResponse = await context.requestMultipart('POST',
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm(), context.token,
{ 'Idempotency-Key': bookingDocumentKey })
const bookingDocumentReplay = context.apiSuccess(MODULE, '订舱文件同键同载荷重放', bookingDocumentReplayResponse, '', OPTIONS)
context.assert(MODULE, '订舱文件精确重放不新增文件或版本', bookingDocumentReplayResponse,
id(bookingDocumentReplay?.document?.id) === id(bookingDocument?.document?.id)
&& id(bookingDocumentReplay?.document?.oceanBookingId) === bookingId
&& Number(bookingDocumentReplay?.bookingVersion) === releaseVersion,
`firstDocumentId=${id(bookingDocument?.document?.id) || 'missing'}, replayDocumentId=${id(bookingDocumentReplay?.document?.id) || 'missing'}, firstVersion=${releaseVersion}, replayVersion=${bookingDocumentReplay?.bookingVersion ?? 'missing'}`, OPTIONS)
const bookingAfterDocumentResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${bookingId}`)
const bookingAfterDocument = context.apiSuccess(MODULE, '回查订舱文件有效归属', bookingAfterDocumentResponse, '', OPTIONS)
const activeBookingDocuments = (bookingAfterDocument?.documents || []).filter((document) => document?.valid === true)
context.assert(MODULE, '同键重放后当前订舱仅保留一条目标有效文件', bookingAfterDocumentResponse,
activeBookingDocuments.filter((document) => id(document?.id) === id(bookingDocument?.document?.id)
&& id(document?.oceanBookingId) === bookingId && document?.documentType === 'BOOKING_CONFIRMATION').length === 1,
`activeDocuments=${activeBookingDocuments.length}, targetDocumentId=${id(bookingDocument?.document?.id) || 'missing'}`, OPTIONS)
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`,
{ expectedVersion: draftVersion, reason: context.marker },
{ expectedVersion: releaseVersion, reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) })
const bookingConfirmed = context.apiSuccess(MODULE, '三期订舱 RELEASED 冻结运输快照', releaseResponse, '', OPTIONS)
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
@@ -851,46 +889,45 @@ export async function run(context) {
const evidenceTypes = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
const evidenceItems = []
const evidencePath = `${pdaBase}/tasks/${canonicalTaskId}/evidence`
const normalizedEvidence = (item) => item ? { ...item, id: id(item.evidenceId) } : null
for (const evidenceType of evidenceTypes) {
const upload = await uploadEvidence(context, tenantId, inboundId, evidenceType)
const uploaded = context.apiSuccess(MODULE, `上传本轮 ${evidenceType} 真实证据文件`, upload, '', OPTIONS)
const fileId = id(uploaded?.fileId)
context.assert(MODULE, `${evidenceType} 文件归属类型和媒体元数据正确`, upload,
Boolean(fileId) && uploaded?.businessType === `LOADING_${evidenceType}`
&& uploaded?.contentType === 'image/png' && Number(uploaded?.sizeBytes) > 0,
`fileId=${fileId || 'missing'}, businessType=${uploaded?.businessType || 'missing'}`, OPTIONS)
if (!fileId) return
const evidence = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
const evidenceData = context.apiSuccess(MODULE, `绑定本轮 ${evidenceType} 装柜证据`, evidence, '', OPTIONS)
const evidenceItem = evidenceData?.evidences?.evidences?.find?.((item) => item.evidenceType === evidenceType)
context.assert(MODULE, `${evidenceType} 证据归属、marker 和版本正确`, evidence,
evidenceData?.mainStatus === 'LOADING' && id(evidenceItem?.fileId) === fileId
const sealNo = evidenceType === 'SEAL' ? `SEAL-${suffix}` : null
const upload = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
evidenceType, { sealNo })
const evidenceData = context.apiSuccess(MODULE, `通过 PDA task 原子上传并绑定 ${evidenceType} 证据`, upload, '', OPTIONS)
const evidenceItem = normalizedEvidence(evidenceData?.evidences?.find?.((item) =>
item.evidenceType === evidenceType && item.status !== 'VOID'))
const fileId = id(evidenceItem?.fileId)
context.assert(MODULE, `${evidenceType} 证据归属 task、柜次、marker 和版本`, upload,
id(evidenceData?.taskId) === canonicalTaskId
&& id(evidenceData?.containerBookingId) === containerBookingId
&& Boolean(evidenceItem?.id) && Boolean(fileId)
&& evidenceItem?.fileName === `${context.marker}-${evidenceType.toLowerCase()}.png`
&& evidenceItem?.remark === context.marker && Number(evidenceItem?.version) === 0
&& (evidenceType !== 'SEAL' || evidenceItem?.sealNo === `SEAL-${suffix}`),
`mainStatus=${evidenceData?.mainStatus || 'missing'}, evidenceId=${id(evidenceItem?.id) || 'missing'}`, OPTIONS)
&& (evidenceType !== 'SEAL' || evidenceItem?.sealNo === sealNo),
`taskId=${id(evidenceData?.taskId) || 'missing'}, containerBookingId=${id(evidenceData?.containerBookingId) || 'missing'}, evidenceId=${id(evidenceItem?.id) || 'missing'}`, OPTIONS)
if (!evidenceItem?.id) return
evidenceItems.push(evidenceItem)
if (evidenceType === 'PHOTO_STEP_1') {
const replay = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
const replayData = context.apiSuccess(MODULE, '装柜证据绑定同键同参幂等重放', replay, '', OPTIONS)
const replayItems = replayData?.evidences?.evidences?.filter?.((item) => item.evidenceType === evidenceType) || []
context.assert(MODULE, '证据绑定重放返回同一记录且不重复追加', replay,
replayItems.length === 1 && id(replayItems[0]?.id) === id(evidenceItem.id),
`evidenceId=${id(replayItems[0]?.id) || 'missing'}, count=${replayItems.length}`, OPTIONS)
const conflict = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0, evidenceType, fileId,
fileName: 'client-must-not-override.png', sealNo: null, remark: `${context.marker}-different`,
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${evidenceType}`.slice(0, 128) })
context.addStep(MODULE, '装柜证据绑定同键异参返回冲突', conflict,
const replay = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
evidenceType)
const replayData = context.apiSuccess(MODULE, '装柜证据 task 上传同键同载荷重放', replay, '', OPTIONS)
const replayItems = replayData?.evidences?.filter?.((item) => item.evidenceType === evidenceType) || []
context.assert(MODULE, '证据 task 上传重放返回同一记录且不重复追加', replay,
replayItems.length === 1 && id(replayItems[0]?.evidenceId) === id(evidenceItem.id),
`evidenceId=${id(replayItems[0]?.evidenceId) || 'missing'}, count=${replayItems.length}`, OPTIONS)
const conflict = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
evidenceType, { remark: `${context.marker}-different` })
context.addStep(MODULE, '装柜证据 task 上传同键异载荷返回冲突', conflict,
conflict.http === 200 && codeOf(conflict) === 'A0409'
&& Boolean(conflict.requestId) && Boolean(conflict.traceId),
`HTTP=${conflict.http}, code=${codeOf(conflict)}, requestId=${conflict.requestId || 'missing'}, traceId=${conflict.traceId || 'missing'}`, OPTIONS)
}
const download = await context.requestBinary('GET', `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}/${fileId}`)
const download = await context.requestBinary('GET',
`${evidencePath}/${encodeURIComponent(evidenceItem.id)}/content?warehouseId=${encodeURIComponent(warehouseId)}`)
context.assert(MODULE, `下载本轮 ${evidenceType} 证据文件`, download,
download.http === 200 && download.contentType.toLowerCase().includes('image/png') && download.bytes.length > 0,
`bytes=${download.bytes.length}, contentType=${download.contentType || 'missing'}`, OPTIONS)
@@ -904,95 +941,100 @@ export async function run(context) {
const firstPhoto = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_1')
const sealEvidence = evidenceItems.find((item) => item.evidenceType === 'SEAL')
const appendedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_1')
const appendedFile = context.apiSuccess(MODULE, '追加同类型第二张装柜证据文件', appendedUpload, '', OPTIONS)
const appendedFileId = id(appendedFile?.fileId)
const appended = appendedFileId
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_1', appendedFileId, suffix, 'PHOTO_STEP_1_APPEND')
: appendedUpload
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张装柜证据', appended, '', OPTIONS)
const appendedItems = appendedData?.evidences?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
context.assert(MODULE, '同类型装柜证据支持多图追加且不替换历史', appended,
const appended = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_1',
Number(firstPhoto?.version), 'PHOTO_STEP_1_APPEND', { fileSuffix: 'PHOTO_STEP_1_APPEND' })
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张 task 装柜证据', appended, '', OPTIONS)
const appendedItems = appendedData?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
const appendedActive = normalizedEvidence(appendedItems.find((item) => item.status !== 'VOID'))
const appendedFileId = id(appendedActive?.fileId)
context.assert(MODULE, '同类型装柜证据追加保留历史并唯一激活新图', appended,
Boolean(appendedFileId) && appendedItems.length >= 2
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId))
&& appendedItems.some((item) => id(item.fileId) === appendedFileId),
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId) && item.status === 'VOID')
&& id(appendedActive?.fileId) === appendedFileId,
`photoStep1Count=${appendedItems.length}, original=${id(firstPhoto?.fileId) || 'missing'}, appended=${appendedFileId || 'missing'}`, OPTIONS)
const wrongType = await bindEvidence(context, base, tenantId, 'PHOTO_STEP_2', id(firstPhoto?.fileId), suffix, 'wrong-type')
context.addStep(MODULE, '不同证据类型的真实 fileId 不可交叉绑定', wrongType, failure(wrongType),
const wrongType = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_9', 0,
'wrong-type', { fileSuffix: 'wrong-type' })
context.addStep(MODULE, '不在白名单的证据类型不可上传绑定', wrongType, failure(wrongType),
`HTTP=${wrongType.http}, code=${codeOf(wrongType)}`, OPTIONS)
const forged = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: '999999999999999999',
fileName: `${context.marker}-forged.png`, sealNo: `FORGED-${suffix}`, remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-forged-evidence`.slice(0, 128) })
context.addStep(MODULE, '伪造装柜证据 fileId 归属校验拒绝', forged, failure(forged),
const forged = await context.request('GET',
`${evidencePath}/999999999999999999/content?warehouseId=${encodeURIComponent(warehouseId)}`)
context.addStep(MODULE, '伪造装柜 evidenceId 无法读取任务证据内容', forged,
(forged.http === 200 && codeOf(forged) === 'A0404') || forged.http === 404,
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
const missingSealText = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: id(sealEvidence?.fileId),
fileName: `${context.marker}-missing-seal-text.png`, sealNo: ' ', remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-missing-seal-text`.slice(0, 128) })
const beforeMissingSealResponse = await context.request('GET',
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
const beforeMissingSeal = beforeMissingSealResponse.body?.data?.evidences || []
const missingSealText = await uploadEvidence(context, canonicalTaskId, warehouseId, 'SEAL',
Number(sealEvidence?.version), 'missing-seal-text', { sealNo: ' ', fileSuffix: 'missing-seal-text' })
context.addStep(MODULE, '带锁码照片缺少锁码文本时拒绝绑定', missingSealText, failure(missingSealText),
`HTTP=${missingSealText.http}, code=${codeOf(missingSealText)}`, OPTIONS)
const afterMissingSealResponse = await context.request('GET',
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
context.assert(MODULE, '缺锁码文本失败后 task 证据零漂移', afterMissingSealResponse,
JSON.stringify(afterMissingSealResponse.body?.data?.evidences || []) === JSON.stringify(beforeMissingSeal),
`before=${beforeMissingSeal.length}, after=${afterMissingSealResponse.body?.data?.evidences?.length ?? 'missing'}`, OPTIONS)
const voidTarget = appendedItems.find((item) => id(item.fileId) === appendedFileId)
const voidTarget = appendedActive
const voidResponse = voidTarget
? await request(context, 'POST', `${base}/loading/evidence/${encodeURIComponent(id(voidTarget.id))}/void?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` },
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void`.slice(0, 128) })
? await request(context, 'POST', `${evidencePath}/${encodeURIComponent(id(voidTarget.id))}/void`,
{ warehouseId, expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` })
: appended
const voidData = context.apiSuccess(MODULE, '作废错误装柜证据并保留审计', voidResponse, '', OPTIONS)
context.assert(MODULE, '错图作废后原记录保留且不再计入有效证据', voidResponse,
Boolean(voidTarget) && voidData?.evidences?.evidences?.some?.((item) => id(item.id) === id(voidTarget.id)
&& (item.status === 'VOID' || item.valid === false) && item.voidReason === `${context.marker}-wrong-photo`),
Boolean(voidTarget) && voidData?.evidences?.some?.((item) => id(item.evidenceId) === id(voidTarget.id)
&& item.status === 'VOID' && Boolean(item.updatedAt)),
`evidenceId=${id(voidTarget?.id) || 'missing'}, status=${voidTarget?.status || 'missing'}`, OPTIONS)
const restoredPhotoResponse = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_1', 0,
'PHOTO_STEP_1_RESTORE', { fileSuffix: 'PHOTO_STEP_1_RESTORE' })
const restoredPhotoData = context.apiSuccess(MODULE, '错图作废后恢复 PHOTO_STEP_1 有效证据', restoredPhotoResponse, '', OPTIONS)
const restoredPhoto = normalizedEvidence(restoredPhotoData?.evidences?.find?.((item) =>
item.evidenceType === 'PHOTO_STEP_1' && item.status !== 'VOID'))
context.assert(MODULE, 'PHOTO_STEP_1 恢复后旧记录保留且新记录唯一有效', restoredPhotoResponse,
Boolean(restoredPhoto?.id) && restoredPhotoData?.evidences?.some?.((item) =>
id(item.evidenceId) === id(voidTarget?.id) && item.status === 'VOID'),
`restoredEvidenceId=${id(restoredPhoto?.id) || 'missing'}`, OPTIONS)
const missingTypeTarget = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_4')
const missingTypePath = `${base}/loading/evidence/${encodeURIComponent(id(missingTypeTarget?.id))}/void?tenantId=${encodeURIComponent(tenantId)}`
const missingTypePath = `${evidencePath}/${encodeURIComponent(id(missingTypeTarget?.id))}/void`
const staleMissingTypeVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath,
{ expectedVersion: Number(missingTypeTarget.version) + 1, reason: `${context.marker}-stale-version` },
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-stale`.slice(0, 128) })
{ warehouseId, expectedVersion: Number(missingTypeTarget.version) + 1, reason: `${context.marker}-stale-version` })
: voidResponse
context.addStep(MODULE, '装柜证据作废旧版本返回冲突', staleMissingTypeVoid,
Boolean(missingTypeTarget) && staleMissingTypeVoid.http === 200 && codeOf(staleMissingTypeVoid) === 'A0409',
`HTTP=${staleMissingTypeVoid.http}, code=${codeOf(staleMissingTypeVoid)}`, OPTIONS)
const missingTypeVoidKey = `${context.marker}-p3-evidence-void-missing-type`.slice(0, 128)
const missingTypeVoidBody = {
expectedVersion: Number(missingTypeTarget?.version), reason: `${context.marker}-temporary-missing-type`,
warehouseId, expectedVersion: Number(missingTypeTarget?.version), reason: `${context.marker}-temporary-missing-type`,
}
const missingTypeVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': missingTypeVoidKey })
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
: voidResponse
const missingTypeVoidData = context.apiSuccess(MODULE, '临时作废唯一 PHOTO_STEP_4 证据', missingTypeVoid, '', OPTIONS)
context.assert(MODULE, '临时作废后 PHOTO_STEP_4 不再计入有效证据', missingTypeVoid,
Boolean(missingTypeTarget) && missingTypeVoidData?.evidences?.evidences?.some?.((item) =>
id(item.id) === id(missingTypeTarget.id) && item.status === 'VOID'),
Boolean(missingTypeTarget) && missingTypeVoidData?.evidences?.some?.((item) =>
id(item.evidenceId) === id(missingTypeTarget.id) && item.status === 'VOID'),
`evidenceId=${id(missingTypeTarget?.id) || 'missing'}`, OPTIONS)
const missingTypeVoidReplay = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': missingTypeVoidKey })
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
: voidResponse
context.assert(MODULE, '证据作废同键同参重放保持同一 VOID 结果', missingTypeVoidReplay,
missingTypeVoidReplay.http === 200 && codeOf(missingTypeVoidReplay) === '200'
&& missingTypeVoidReplay.body?.data?.evidences?.evidences?.some?.((item) =>
id(item.id) === id(missingTypeTarget?.id) && item.status === 'VOID'),
context.assert(MODULE, '已作废 task 证据重复作废返回冲突', missingTypeVoidReplay,
missingTypeVoidReplay.http === 200 && codeOf(missingTypeVoidReplay) === 'A0409',
`HTTP=${missingTypeVoidReplay.http}, code=${codeOf(missingTypeVoidReplay)}`, OPTIONS)
const missingTypeVoidConflict = missingTypeTarget
? await request(context, 'POST', missingTypePath,
{ ...missingTypeVoidBody, reason: `${context.marker}-different-void-reason` },
{ 'Idempotency-Key': missingTypeVoidKey })
{ ...missingTypeVoidBody, reason: `${context.marker}-different-void-reason` })
: voidResponse
context.addStep(MODULE, '证据作废同键异参返回冲突', missingTypeVoidConflict,
context.addStep(MODULE, '已作废 task 证据异参重试仍返回冲突', missingTypeVoidConflict,
missingTypeVoidConflict.http === 200 && codeOf(missingTypeVoidConflict) === 'A0409',
`HTTP=${missingTypeVoidConflict.http}, code=${codeOf(missingTypeVoidConflict)}`, OPTIONS)
const duplicateVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-duplicate`.slice(0, 128) })
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
: voidResponse
context.addStep(MODULE, '已作废证据使用新请求重复作废返回冲突', duplicateVoid,
duplicateVoid.http === 200 && codeOf(duplicateVoid) === 'A0409',
@@ -1001,51 +1043,53 @@ export async function run(context) {
context.addStep(MODULE, '作废唯一证据类型后不提前占用异步完成命令', null, true,
'PHOTO_STEP_4 已真实作废并回读;补传恢复后才允许最终阶段提交唯一完成命令', OPTIONS)
const replacementUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_4')
const replacementFile = context.apiSuccess(MODULE, '补传缺失 PHOTO_STEP_4 真实文件', replacementUpload, '', OPTIONS)
const replacementFileId = id(replacementFile?.fileId)
const replacementBind = replacementFileId
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_4', replacementFileId, suffix, 'PHOTO_STEP_4_RESTORE')
: replacementUpload
const replacementData = context.apiSuccess(MODULE, '补传恢复 PHOTO_STEP_4 有效证据', replacementBind, '', OPTIONS)
context.assert(MODULE, '补传后五类有效证据恢复且旧 VOID 记录保留', replacementBind,
const replacementUpload = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_4', 0,
'PHOTO_STEP_4_RESTORE', { fileSuffix: 'PHOTO_STEP_4_RESTORE' })
const replacementData = context.apiSuccess(MODULE, '补传恢复 PHOTO_STEP_4 有效证据', replacementUpload, '', OPTIONS)
const replacementItem = normalizedEvidence(replacementData?.evidences?.find?.((item) =>
item.evidenceType === 'PHOTO_STEP_4' && item.status !== 'VOID'))
const replacementFileId = id(replacementItem?.fileId)
context.assert(MODULE, '补传后五类有效证据恢复且旧 VOID 记录保留', replacementUpload,
Boolean(replacementFileId)
&& replacementData?.evidences?.evidences?.some?.((item) => item.evidenceType === 'PHOTO_STEP_4'
&& replacementData?.evidences?.some?.((item) => item.evidenceType === 'PHOTO_STEP_4'
&& id(item.fileId) === replacementFileId && item.status !== 'VOID')
&& replacementData?.evidences?.evidences?.some?.((item) => id(item.id) === id(missingTypeTarget?.id)
&& replacementData?.evidences?.some?.((item) => id(item.evidenceId) === id(missingTypeTarget?.id)
&& item.status === 'VOID'),
`replacementFileId=${replacementFileId || 'missing'}`, OPTIONS)
const customerEvidence = merchantToken
? await request(context, 'GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`,
? await request(context, 'GET', `${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`,
undefined, { Authorization: `Bearer ${merchantToken}` })
: merchantLogin
context.addStep(MODULE, '客户身份无权访问平台装柜证据', customerEvidence,
customerEvidence.http === 403 && codeOf(customerEvidence) === 'A0403'
&& Boolean(customerEvidence.requestId) && Boolean(customerEvidence.traceId),
`HTTP=${customerEvidence.http}, code=${codeOf(customerEvidence)}, requestId=${customerEvidence.requestId || 'missing'}, traceId=${customerEvidence.traceId || 'missing'}`, OPTIONS)
const wrongTenantEvidence = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(Number(tenantId) + 1)}`)
context.addStep(MODULE, '错误客户作用域不可读取本轮装柜证据', wrongTenantEvidence,
wrongTenantEvidence.http === 200 && codeOf(wrongTenantEvidence) === 'A0404',
`HTTP=${wrongTenantEvidence.http}, code=${codeOf(wrongTenantEvidence)}`, OPTIONS)
const wrongInboundEvidence = await context.request('GET',
`/api/platform/v1/inbounds/999999999999999999/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
context.addStep(MODULE, '错误入库单不可读取本轮装柜证据', wrongInboundEvidence,
wrongInboundEvidence.http === 200 && codeOf(wrongInboundEvidence) === 'A0404',
`HTTP=${wrongInboundEvidence.http}, code=${codeOf(wrongInboundEvidence)}`, OPTIONS)
const wrongWarehouseEvidence = await context.request('GET',
`${evidencePath}?warehouseId=${encodeURIComponent(Number(warehouseId) + 1)}`)
context.addStep(MODULE, '错误仓库作用域不可读取本轮 task 装柜证据', wrongWarehouseEvidence,
wrongWarehouseEvidence.http === 403 && codeOf(wrongWarehouseEvidence) === 'A0403',
`HTTP=${wrongWarehouseEvidence.http}, code=${codeOf(wrongWarehouseEvidence)}`, OPTIONS)
const wrongTaskEvidence = await context.request('GET',
`${pdaBase}/tasks/999999999999999999/evidence?warehouseId=${encodeURIComponent(warehouseId)}`)
context.addStep(MODULE, '错误 taskId 不可读取本轮装柜证据', wrongTaskEvidence,
wrongTaskEvidence.http === 200 && codeOf(wrongTaskEvidence) === 'A0404',
`HTTP=${wrongTaskEvidence.http}, code=${codeOf(wrongTaskEvidence)}`, OPTIONS)
const postLoadedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_2')
const postLoadedFile = context.apiSuccess(MODULE, '预置 LOADED 后新增证据边界文件', postLoadedUpload, '', OPTIONS)
const postLoadedFileId = id(postLoadedFile?.fileId)
const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
const evidenceList = await context.request('GET',
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS)
const activeEvidenceList = evidenceListData?.evidences?.filter?.((item) => item.status !== 'VOID') || []
context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList,
evidenceListData?.evidences?.length >= evidenceTypes.length
&& evidenceTypes.every((type) => evidenceListData.evidences.some((item) => item.evidenceType === type))
&& evidenceListData.evidences.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
id(evidenceListData?.taskId) === canonicalTaskId
&& id(evidenceListData?.containerBookingId) === containerBookingId
&& evidenceTypes.every((type) => activeEvidenceList.some((item) => item.evidenceType === type))
&& activeEvidenceList.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
`evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS)
const immutablePhoto = restoredPhoto
const postLoadedFileId = null
const incrementalFixture = context.createdResources?.orchestrationFixtures?.phase3LoadingIncremental
const incrementalInboundId = id(incrementalFixture?.inboundId)
let skipBatchVerified = false
@@ -1197,7 +1241,8 @@ export async function run(context) {
const incrementedTask = context.apiSuccess(MODULE, '增量后回查稳定 PDA 四元身份', incrementedTaskResponse, '', OPTIONS)
const incrementalTaskItem = incrementedTask?.items?.find?.((item) =>
id(item?.loadingPlanItemId) === id(incrementalPlanItem?.id))
const evidenceAfterIncrementResponse = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
const evidenceAfterIncrementResponse = await context.request('GET',
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
const evidenceAfterIncrement = context.apiSuccess(MODULE, '增量后回查既有五类证据', evidenceAfterIncrementResponse, '', OPTIONS)
context.assert(MODULE, '增量保持稳定四元身份、LOADING 不回退并撤回最终完成门禁', incrementResponse,
id(incrementedPlan?.id) === loadingPlanId && incrementedPlan?.status === 'LOADING'
@@ -1373,7 +1418,7 @@ export async function run(context) {
evidenceIds: evidenceItems.map((item) => id(item.id)),
voidedEvidenceId: id(voidTarget?.id), evidenceTypes, sealNo: `SEAL-${suffix}`,
missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId,
immutableEvidenceId: id(firstPhoto?.id), immutableEvidenceVersion: Number(firstPhoto?.version),
immutableEvidenceId: id(immutablePhoto?.id), immutableEvidenceVersion: Number(immutablePhoto?.version),
postLoadedFileId,
oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId,
canonicalPdaTaskId: canonicalTaskId,
@@ -669,18 +669,18 @@ test('customs and finalize consume the same loading inbound identity', () => {
test('loading evidence acceptance covers idempotency, reversible missing-type gate and scope isolation', () => {
const loading = source('./phase3-loading.mjs')
assertOrder(loading, [
'装柜证据绑定同键同参幂等重放',
'装柜证据绑定同键异参返回冲突',
'装柜证据 task 上传同键同载荷重放',
'装柜证据 task 上传同键异载荷返回冲突',
'装柜证据作废旧版本返回冲突',
'临时作废唯一 PHOTO_STEP_4 证据',
'证据作废同键同参重放保持同一 VOID 结果',
'证据作废同键异参返回冲突',
'已作废 task 证据重复作废返回冲突',
'已作废 task 证据异参重试仍返回冲突',
'已作废证据使用新请求重复作废返回冲突',
'作废唯一证据类型后不提前占用异步完成命令',
'补传恢复 PHOTO_STEP_4 有效证据',
'客户身份无权访问平台装柜证据',
'错误客户作用域不可读取本轮装柜证据',
'错误入库单不可读取本轮装柜证据',
'错误仓库作用域不可读取本轮 task 装柜证据',
'错误 taskId 不可读取本轮装柜证据',
])
assert.match(loading, /missingTypeVoidedEvidenceId/)
assert.match(loading, /replacementEvidenceFileId/)
@@ -2,7 +2,19 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { activeCustomerIdentity, inspectOceanBookingIdentity, platformWarehouseIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes, platformWarehouseIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
test('PDF upload fixtures contain real PDF magic bytes instead of MIME-only text', () => {
const bytes = minimalPdfBytes('RUN-2-DISPATCH_ORDER')
const content = Buffer.from(bytes).toString('ascii')
assert.equal(content.slice(0, 5), '%PDF-')
assert.match(content, /%%EOF\n$/)
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /new Blob\(\[minimalPdfBytes\(`\$\{context\.marker\}-BILL_OF_LADING`\)\], \{ type: 'application\/pdf' \}\)/)
assert.match(source, /new Blob\(\[minimalPdfBytes\(`\$\{context\.marker\}-DISPATCH_ORDER`\)\], \{ type: 'application\/pdf' \}\)/)
assert.doesNotMatch(source, /new Blob\(\[\s*`\$\{context\.marker\}-(?:BILL_OF_LADING|DISPATCH_ORDER)`\s*\], \{ type: 'application\/pdf' \}\)/)
})
test('booking prerequisite resolves an ACTIVE platform customer without fixture state', () => {
assert.deepEqual(activeCustomerIdentity({ id: '21', tenantName: '现有客户' }),
@@ -14,6 +14,22 @@ function data(response) { return codeOf(response) === '200' ? response.body.data
function failure(response) { return response?.http === 0 || response?.http >= 400 || (response?.http === 200 && codeOf(response) !== '200') }
function records(value) { return Array.isArray(value) ? value : value?.records || [] }
export function minimalPdfBytes(label = 'truck-appointment-fixture') {
const safeLabel = String(label).replace(/[^\x20-\x7E]/g, '_').slice(0, 64)
return new TextEncoder().encode(`%PDF-1.4
% ${safeLabel}
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [] /Count 0 >>
endobj
trailer
<< /Root 1 0 R >>
%%EOF
`)
}
export function platformWarehouseIdentity(item) {
return { warehouseId: id(item?.warehouseId || item?.id || item?.warehouse?.id), platformTenantId: '0' }
}
@@ -136,7 +152,7 @@ async function fixture(context) {
const bookingId = id(created?.oceanBooking?.id); if (!bookingId) return null
const form = new FormData(); form.append('expectedVersion', String(created.oceanBooking.version))
form.append('documentType', 'BILL_OF_LADING')
form.append('file', new Blob([`${context.marker}-BILL_OF_LADING`], { type: 'application/pdf' }), `bill-${suffix}.pdf`)
form.append('file', new Blob([minimalPdfBytes(`${context.marker}-BILL_OF_LADING`)], { type: 'application/pdf' }), `bill-${suffix}.pdf`)
const uploadResponse = await context.requestMultipart('POST', `${BOOKINGS}/${bookingId}/documents/upload`, form,
context.token, { 'Idempotency-Key': `${context.marker}-truck-booking-file`.slice(0, 128) })
const uploaded = context.apiSuccess(MODULE, '上传当前有效 BILL_OF_LADING 文件', uploadResponse, '', OPTIONS)
@@ -186,7 +202,7 @@ export async function run(context) {
&& !Object.hasOwn(appointment, 'customerId') && !Object.hasOwn(appointment, 'customerName'), '', OPTIONS)
const path = `${TRUCKS}/${appointmentId}`
const attachmentForm = new FormData(); attachmentForm.append('attachmentType', 'DISPATCH_ORDER')
attachmentForm.append('file', new Blob([`${context.marker}-DISPATCH_ORDER`], { type: 'application/pdf' }),
attachmentForm.append('file', new Blob([minimalPdfBytes(`${context.marker}-DISPATCH_ORDER`)], { type: 'application/pdf' }),
`dispatch-order-${identity.suffix}.pdf`)
const uploadAttachmentResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
attachmentForm, context.token)