优化
This commit is contained in:
@@ -7,8 +7,9 @@ import { cleanupSucceeded, isAcceptanceFeeItem } from './fee-items.mjs'
|
||||
|
||||
const source = readFileSync(new URL('./fee-items.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('fee detail binding explicitly sends the settlement inclusion contract field', () => {
|
||||
assert.match(source, /const detailPayload = \{[\s\S]*settlementIncluded: false/)
|
||||
test('fee detail binding does not send the removed settlement switch', () => {
|
||||
assert.match(source, /const detailPayload = \{/)
|
||||
assert.doesNotMatch(source, /settlementIncluded/)
|
||||
})
|
||||
|
||||
test('referenced fee-item deletion remains a conflict assertion', () => {
|
||||
|
||||
@@ -215,7 +215,7 @@ export async function run(context) {
|
||||
const feeDetailPath = `/api/platform/v1/inbounds/${encodeURIComponent(inbound.inboundId)}/fee-details?accountId=${encodeURIComponent(inbound.tenantId)}`
|
||||
const detailPayload = {
|
||||
feeItemId, direction: 'RECEIVABLE', businessDate: new Date().toISOString().slice(0, 10),
|
||||
unitPrice: 1.25, currency: 'CNY', amount: 1.25, settlementIncluded: false,
|
||||
unitPrice: 1.25, currency: 'CNY', amount: 1.25,
|
||||
remark: context.marker,
|
||||
}
|
||||
const disabledBindingResponse = await context.request('POST', feeDetailPath, detailPayload)
|
||||
|
||||
@@ -22,7 +22,7 @@ test('覆盖费用、对账单、AR 和 AP 页面分层状态', () => {
|
||||
for (const status of ['DRAFT', 'PENDING_REVIEW', 'ISSUED']) assert.match(source, new RegExp(`'${status}'`))
|
||||
assert.match(source, /reviewStatus === 'PENDING'/)
|
||||
assert.match(source, /reviewStatus === 'APPROVED'/)
|
||||
assert.match(source, /settlementIncluded === true/)
|
||||
assert.doesNotMatch(source, /settlementIncluded|settlement-inclusion/)
|
||||
})
|
||||
|
||||
test('平台列表默认全量查询不带 customerId,未部署时登记 blocker', () => {
|
||||
@@ -36,7 +36,7 @@ test('所有财务写操作使用 marker 幂等键并上传真实凭证', () =>
|
||||
assert.match(source, /fixture\.voucherFileId/)
|
||||
assert.match(source, /finance-full-fixtures\.mjs/)
|
||||
assert.match(source, /context\.createdResources\.financeDemo = matrix/)
|
||||
assert.match(source, /`source-\$\{role\}-\$\{source\.feeSourceId\}-include`/)
|
||||
assert.match(source, /`source-\$\{role\}-\$\{source\.feeSourceId\}-submit`/)
|
||||
assert.match(source, /`ap-\$\{role\}-\$\{source\.feeSourceId\}-\$\{source\.supplierId\}-create`/)
|
||||
})
|
||||
|
||||
@@ -49,8 +49,8 @@ test('费用来源使用 operationVersion,应付供应商长整型保持字符
|
||||
assert.match(source, /expectedVersion: operationVersion\(current\.value\)/)
|
||||
assert.match(source, /supplierId: id\(source\.supplierId\)/)
|
||||
assert.doesNotMatch(source, /supplierId: Number\(source\.supplierId\)/)
|
||||
assert.match(source, /partnerId, partnerName:/)
|
||||
assert.doesNotMatch(source, /partnerId: Number\(partnerId\)/)
|
||||
assert.match(source, /supplierId, remark:/)
|
||||
assert.doesNotMatch(source, /partnerId|partnerName/)
|
||||
})
|
||||
|
||||
test('恢复模式只续补固定报告来源并校验 AR AP 金额状态', () => {
|
||||
|
||||
@@ -52,22 +52,21 @@ async function sourceDetail(context, feeSourceId) {
|
||||
return { response, value: data(response) }
|
||||
}
|
||||
|
||||
async function approveAndInclude(context, source, role) {
|
||||
async function approveSource(context, source, role) {
|
||||
let current = await sourceDetail(context, source.feeSourceId)
|
||||
if (current.value?.reviewStatus === 'DRAFT' || current.value?.reviewStatus === 'REJECTED') {
|
||||
const response = await write(context, 'POST', `${BASE}/fee-sources/${source.feeSourceId}/submit`,
|
||||
{ reason: `${MARKER}-${role}`, expectedVersion: operationVersion(current.value) }, `source-${role}-${source.feeSourceId}-submit`)
|
||||
current = { response, value: data(response) }
|
||||
}
|
||||
if (current.value?.reviewStatus === 'PENDING') {
|
||||
const response = await write(context, 'POST', `${BASE}/fee-sources/${source.feeSourceId}/review`,
|
||||
{ action: 'APPROVE', reason: `${MARKER}-${role}`, expectedVersion: operationVersion(current.value) }, `source-${role}-${source.feeSourceId}-approve`)
|
||||
current = { response, value: data(response) }
|
||||
}
|
||||
if (current.value?.settlementIncluded !== true) {
|
||||
const response = await write(context, 'PUT', `${BASE}/fee-sources/${source.feeSourceId}/settlement-inclusion`,
|
||||
{ included: true, reason: `${MARKER}-${role}`, expectedVersion: operationVersion(current.value) }, `source-${role}-${source.feeSourceId}-include`)
|
||||
current = { response, value: data(response) }
|
||||
}
|
||||
record(context, `${role} 来源已审核并纳入结算`, current.response,
|
||||
current.value?.reviewStatus === 'APPROVED' && current.value?.settlementIncluded === true
|
||||
&& current.value?.sourceStatus === 'AVAILABLE',
|
||||
`feeSourceId=${source.feeSourceId}, review=${current.value?.reviewStatus}, included=${current.value?.settlementIncluded}`)
|
||||
record(context, `${role} 来源已审核并自动进入结算候选`, current.response,
|
||||
current.value?.reviewStatus === 'APPROVED' && current.value?.sourceStatus === 'AVAILABLE',
|
||||
`feeSourceId=${source.feeSourceId}, review=${current.value?.reviewStatus}`)
|
||||
return current.value
|
||||
}
|
||||
|
||||
@@ -75,20 +74,20 @@ async function createExtraPayable(context, fixture, index) {
|
||||
const itemResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&pageSize=200')
|
||||
const feeItem = rows(data(itemResponse)).find((item) => item.status === 'ACTIVE' && item.warehousePayable === true)
|
||||
const inboundId = id(fixture.orders.at(-1)?.inboundId)
|
||||
const partnerId = id(fixture.supplier?.partnerId ?? fixture.supplier?.supplierId)
|
||||
if (!feeItem || !inboundId || !partnerId) return null
|
||||
const supplierId = id(fixture.supplier?.supplierId)
|
||||
if (!feeItem || !inboundId || !supplierId) return null
|
||||
const body = { feeItemId: Number(feeItem.id), direction: 'PAYABLE', businessDate: new Date().toISOString().slice(0, 10),
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY', settlementIncluded: true,
|
||||
partnerId, partnerName: fixture.supplier?.supplierName || null, remark: `${MARKER}-AP-${index}` }
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY',
|
||||
supplierId, remark: `${MARKER}-AP-${index}` }
|
||||
const basePath = `/api/platform/v1/inbounds/${inboundId}/fee-details`
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=10001`, body, `extra-payable-${index}`)
|
||||
const created = data(createdResponse)
|
||||
if (!ok(createdResponse) || !created?.id) return null
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=10001`, undefined, `extra-payable-${index}-approve`)
|
||||
record(context, `创建第 ${index} 条额外应付来源`, approveResponse, ok(approveResponse),
|
||||
`sourceFeeId=${created.id}, inboundId=${inboundId}, partnerId=${partnerId}`)
|
||||
`sourceFeeId=${created.id}, inboundId=${inboundId}, supplierId=${supplierId}`)
|
||||
return { sourceFeeId: id(created.id), direction: 'PAYABLE', businessDate: body.businessDate,
|
||||
supplierId: partnerId, supplierName: body.partnerName }
|
||||
supplierId, supplierName: fixture.supplier?.supplierName || null }
|
||||
}
|
||||
|
||||
async function createStatement(context, source, role, targetStatus) {
|
||||
@@ -206,7 +205,7 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
'invalid-finance-demo-recovery-source')
|
||||
continue
|
||||
}
|
||||
const approved = await approveAndInclude(context, { feeSourceId: payableIds[index] }, role)
|
||||
const approved = await approveSource(context, { feeSourceId: payableIds[index] }, role)
|
||||
const existing = await recoverPayable(context, matrix.payableSettlements[role], payableIds[index])
|
||||
const recoverySupplierId = role === 'apPartial'
|
||||
? id(matrix.payableSettlements?.apPaid?.supplierId)
|
||||
@@ -234,7 +233,7 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
matrix.receivableSettlements[role] = value
|
||||
verifySettlementState(context, 'RECEIVABLE', role, response, value, expectedStatus)
|
||||
}
|
||||
matrix.feeSources.approvedUnbilled = await approveAndInclude(context, { feeSourceId: '300' }, 'approved-unbilled')
|
||||
matrix.feeSources.approvedUnbilled = await approveSource(context, { feeSourceId: '300' }, 'approved-unbilled')
|
||||
context.createdResources.financeFullFixtures = fixture
|
||||
context.createdResources.financeDemo = matrix
|
||||
}
|
||||
@@ -294,7 +293,7 @@ export async function run(context) {
|
||||
const poolBySource = new Map(pool.map((item) => [id(item.sourceId), item]))
|
||||
const sources = produced.map((source) => ({ ...source,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker,
|
||||
supplierId: id(source.supplierId ?? source.partnerId ?? fixture.supplier?.supplierId) }))
|
||||
supplierId: id(source.supplierId ?? fixture.supplier?.supplierId) }))
|
||||
.filter((source) => source.feeSourceId)
|
||||
const receivables = sources.filter((source) => source.direction === 'RECEIVABLE')
|
||||
const payables = sources.filter((source) => source.direction === 'PAYABLE' && source.supplierId)
|
||||
@@ -311,10 +310,10 @@ export async function run(context) {
|
||||
|
||||
const pending = await sourceDetail(context, receivables[0].feeSourceId)
|
||||
record(context, '保留费用待审核', pending.response,
|
||||
pending.value?.reviewStatus === 'PENDING' && pending.value?.settlementIncluded === false,
|
||||
pending.value?.reviewStatus === 'PENDING',
|
||||
`feeSourceId=${receivables[0].feeSourceId}, review=${pending.value?.reviewStatus}`)
|
||||
matrix.feeSources.pendingReview = pending.value
|
||||
matrix.feeSources.approvedUnbilled = await approveAndInclude(context, receivables[1], 'approved-unbilled')
|
||||
matrix.feeSources.approvedUnbilled = await approveSource(context, receivables[1], 'approved-unbilled')
|
||||
|
||||
const statementRoles = [
|
||||
['draft', 'DRAFT'], ['pendingReview', 'PENDING_REVIEW'], ['arUnpaid', 'ISSUED'],
|
||||
@@ -322,7 +321,7 @@ export async function run(context) {
|
||||
]
|
||||
for (let index = 0; index < statementRoles.length; index += 1) {
|
||||
const [role, status] = statementRoles[index]
|
||||
await approveAndInclude(context, receivables[index + 2], role)
|
||||
await approveSource(context, receivables[index + 2], role)
|
||||
const statement = await createStatement(context, receivables[index + 2], role, status)
|
||||
if (statement.value) matrix.statements[role] = statement.value
|
||||
}
|
||||
@@ -344,7 +343,7 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
for (const [index, role] of ['apUnpaid', 'apPartial', 'apPaid'].entries()) {
|
||||
await approveAndInclude(context, payables[index], role)
|
||||
await approveSource(context, payables[index], role)
|
||||
const settlement = await createAp(context, fixture, payables[index], role)
|
||||
if (!settlement) continue
|
||||
if (role === 'apUnpaid') matrix.payableSettlements[role] = settlement
|
||||
|
||||
@@ -35,23 +35,23 @@ async function loadFeeItem(context, direction) {
|
||||
const data = context.apiSuccess(MODULE, `读取 ${direction} ACTIVE 费用项目`, response, '', OPTIONS)
|
||||
return rows(data).find((item) => item?.status === 'ACTIVE' && (direction === 'RECEIVABLE'
|
||||
? item.warehouseReceivable === true
|
||||
: item.warehousePayable === true && /BOOKING|FREIGHT|OCEAN|SHIPPING|订舱|海运|运费/i.test(`${item.feeCode || item.code || ''} ${item.feeName || item.name || ''}`))) || null
|
||||
: item.warehousePayable === true)) || null
|
||||
}
|
||||
|
||||
async function loadActivePartner(context) {
|
||||
const response = await context.request('GET', '/api/platform/v1/suppliers?page=1&size=200&status=ACTIVE&category=BOOKING')
|
||||
const data = context.apiSuccess(MODULE, '读取 Warehouse ACTIVE 订舱供应商', response, 'category=BOOKING', OPTIONS)
|
||||
return rows(data).find((item) => item?.status === 'ACTIVE' && item?.category === 'BOOKING') || null
|
||||
async function loadActiveSupplier(context, category) {
|
||||
const suffix = category ? `&category=${encodeURIComponent(category)}` : ''
|
||||
const response = await context.request('GET', `/api/platform/v1/suppliers?page=1&size=200&status=ACTIVE${suffix}`)
|
||||
const data = context.apiSuccess(MODULE, '读取 Warehouse ACTIVE 供应商', response, category ? `category=${category}` : 'all categories', OPTIONS)
|
||||
return rows(data).find((item) => item?.status === 'ACTIVE' && (!category || item?.category === category)) || null
|
||||
}
|
||||
|
||||
async function createFee(context, resource, item, spec, partner) {
|
||||
async function createFee(context, resource, item, spec, supplier) {
|
||||
const base = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details`
|
||||
const path = `${base}?accountId=${encodeURIComponent(resource.tenantId)}`
|
||||
const body = {
|
||||
feeItemId: Number(item.id), direction: spec.direction, businessDate: spec.businessDate,
|
||||
unitPrice: spec.amount, currency: 'CNY', amount: spec.amount, settlementIncluded: true,
|
||||
partnerId: spec.direction === 'PAYABLE' ? String(partner.id) : null,
|
||||
partnerName: spec.direction === 'PAYABLE' ? (partner.name || null) : null,
|
||||
unitPrice: spec.amount, currency: 'CNY', amount: spec.amount,
|
||||
supplierId: spec.direction === 'PAYABLE' ? String(supplier.id) : null,
|
||||
remark: `${FIXED_MARKER}-${spec.name}`,
|
||||
}
|
||||
const response = await request(context, 'POST', path, body, `create-${spec.name}`)
|
||||
@@ -127,10 +127,10 @@ export async function run(context) {
|
||||
|
||||
const receivableItem = await loadFeeItem(context, 'RECEIVABLE')
|
||||
const payableItem = await loadFeeItem(context, 'PAYABLE')
|
||||
const partner = await loadActivePartner(context)
|
||||
if (!receivableItem || !payableItem || !partner?.id) {
|
||||
const supplier = await loadActiveSupplier(context, payableItem?.supplierCategory || null)
|
||||
if (!receivableItem || !payableItem || !supplier?.id) {
|
||||
blocker(context, '费用状态矩阵前置主数据完整',
|
||||
`receivableFeeItem=${id(receivableItem?.id) || 'missing'}, payableFeeItem=${id(payableItem?.id) || 'missing'}, activePartner=${id(partner?.id) || 'missing'}`)
|
||||
`receivableFeeItem=${id(receivableItem?.id) || 'missing'}, payableFeeItem=${id(payableItem?.id) || 'missing'}, activeSupplier=${id(supplier?.id) || 'missing'}`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export async function run(context) {
|
||||
for (const [index, spec] of specs.entries()) {
|
||||
const resource = resources[index % resources.length]
|
||||
const fixture = await createFee(context, resource,
|
||||
spec.direction === 'RECEIVABLE' ? receivableItem : payableItem, spec, partner)
|
||||
spec.direction === 'RECEIVABLE' ? receivableItem : payableItem, spec, supplier)
|
||||
if (!fixture) continue
|
||||
if (spec.action === 'reject') {
|
||||
const response = await request(context, 'POST', `${fixture.base}/${fixture.feeId}/reject?accountId=${encodeURIComponent(resource.tenantId)}`,
|
||||
@@ -168,8 +168,7 @@ export async function run(context) {
|
||||
const conflictTarget = fixtures.find((item) => item.name === 'AR-APPROVED')
|
||||
if (conflictTarget) {
|
||||
const adjustBody = { feeItemId: Number(receivableItem.id), direction: 'RECEIVABLE', businessDate: conflictTarget.businessDate,
|
||||
unitPrice: 115, currency: 'CNY', amount: 115, settlementIncluded: true,
|
||||
partnerId: null, partnerName: null, remark: `${FIXED_MARKER}-CONFLICT` }
|
||||
unitPrice: 115, currency: 'CNY', amount: 115, remark: `${FIXED_MARKER}-CONFLICT` }
|
||||
const conflict = await request(context, 'POST',
|
||||
`${conflictTarget.base}/${conflictTarget.feeId}/adjust?accountId=${encodeURIComponent(conflictTarget.resource.tenantId)}&expectedSourceVersion=999`,
|
||||
adjustBody, 'stale-adjust')
|
||||
@@ -182,8 +181,7 @@ export async function run(context) {
|
||||
if (replayTarget) {
|
||||
const replayPath = `${replayTarget.base}?accountId=${encodeURIComponent(replayTarget.resource.tenantId)}`
|
||||
const replayBody = { feeItemId: Number(receivableItem.id), direction: 'RECEIVABLE', businessDate: replayTarget.businessDate,
|
||||
unitPrice: 116, currency: 'CNY', amount: 116, settlementIncluded: true,
|
||||
partnerId: null, partnerName: null, remark: `${FIXED_MARKER}-IDEMPOTENT` }
|
||||
unitPrice: 116, currency: 'CNY', amount: 116, remark: `${FIXED_MARKER}-IDEMPOTENT` }
|
||||
const first = await request(context, 'POST', replayPath, replayBody, 'idempotent-create')
|
||||
const second = await request(context, 'POST', replayPath, replayBody, 'idempotent-create')
|
||||
const firstData = first?.body?.data; const secondData = second?.body?.data
|
||||
|
||||
@@ -47,7 +47,7 @@ test('扩展财务验收输出可回传资源与柜次部署阻断', () => {
|
||||
})
|
||||
|
||||
test('按柜 PAYABLE 费用携带 Warehouse 供应商', () => {
|
||||
assert.match(fixtureSource, /partnerId: id\(supplier\.id\)/)
|
||||
assert.match(fixtureSource, /partnerName: supplier\.name \|\| null/)
|
||||
assert.match(fixtureSource, /supplierId: id\(supplier\.id\)/)
|
||||
assert.doesNotMatch(fixtureSource, /partnerId|partnerName/)
|
||||
assert.match(fixtureSource, /\/api\/platform\/v1\/suppliers\?page=1&size=200&status=ACTIVE&category=BOOKING/)
|
||||
})
|
||||
|
||||
@@ -68,8 +68,8 @@ async function createWaybillPayable(context, fixture, order, input, index) {
|
||||
if (!inboundId) return null
|
||||
const path = `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details?accountId=${encodeURIComponent(fixture.customerId)}`
|
||||
const body = { feeItemId: Number(input.feeItem.id), direction: 'PAYABLE', businessDate: dateOffset(-3 - index),
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY', settlementIncluded: true,
|
||||
partnerId: id(input.supplier.id), partnerName: input.supplier.name || null,
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY',
|
||||
supplierId: id(input.supplier.id),
|
||||
remark: `${context.marker}-WAYBILL-PAYABLE-${index + 1}` }
|
||||
const response = await write(context, 'POST', path, body, `waybill-payable-${index + 1}`)
|
||||
const detail = context.apiSuccess(MODULE, `创建第 ${index + 1} 条独占运单 PAYABLE 来源`, response,
|
||||
@@ -262,7 +262,7 @@ export async function run(context) {
|
||||
|
||||
const legacyPayable = { ...pool.resolved[0], feeSourceId: pool.resolved[0].sourceFeeId,
|
||||
direction: 'PAYABLE', currency: 'CNY', sourceType: 'MANUAL',
|
||||
marker: context.marker, partnerId: pool.resolved[0].supplierId }
|
||||
marker: context.marker, supplierId: pool.resolved[0].supplierId }
|
||||
await runReconciliation(context, fixture, legacyPayable)
|
||||
const receivables = fixture.receivableSettlements || []
|
||||
const firstPayable = fixture.payableSettlements?.[0]
|
||||
|
||||
@@ -31,15 +31,13 @@ test('fee writes use verified Firstleg paths and do not fabricate historical IDs
|
||||
assert.match(source, /chargeUnit === 'CBM'/)
|
||||
})
|
||||
|
||||
test('PAYABLE fee selection uses the formal feeCode and feeTypeName fields', () => {
|
||||
test('PAYABLE fee selection uses only the explicit nullable supplierCategory field', () => {
|
||||
const selector = source.slice(source.indexOf('async function loadFeeItem'), source.indexOf('async function createManualFee'))
|
||||
assert.match(selector, /item\.warehousePayable === true/)
|
||||
assert.match(selector, /supplierCategoryForFeeItem\(item\)/)
|
||||
assert.equal(supplierCategoryForFeeItem({ feeCode: 'FBA_HEAD_FREIGHT', feeTypeName: '头程运费' }), 'BOOKING')
|
||||
assert.equal(supplierCategoryForFeeItem({ feeCode: 'FEE_TYPE_001', feeTypeName: '拖车费2' }), 'TRUCKING')
|
||||
assert.equal(supplierCategoryForFeeItem({ feeCode: 'CUSTOMS_CLEARANCE', feeTypeName: '报关费' }), 'CUSTOMS')
|
||||
assert.equal(supplierCategoryForFeeItem({ feeCode: 'UNSUPPORTED', feeTypeName: '操作费' }), null)
|
||||
assert.equal(supplierCategoryForFeeItem({ feeCode: 'FEE_TYPE_001', feeName: '拖车费2' }), null)
|
||||
assert.doesNotMatch(selector, /Boolean\(supplierCategoryForFeeItem/)
|
||||
assert.equal(supplierCategoryForFeeItem({ supplierCategory: 'BOOKING', feeTypeName: '操作费' }), 'BOOKING')
|
||||
assert.equal(supplierCategoryForFeeItem({ supplierCategory: null, feeTypeName: '拖车费' }), null)
|
||||
assert.equal(supplierCategoryForFeeItem({ supplierCategory: 'INVALID' }), null)
|
||||
})
|
||||
|
||||
test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => {
|
||||
@@ -232,7 +230,7 @@ test('automatic fixture reuses the published review-chain source without recalcu
|
||||
assert.match(source, /sourceType === 'FBA_PRICING'/)
|
||||
assert.match(source, /reviewStatus === 'APPROVED'/)
|
||||
assert.match(source, /publicationStatus === 'PUBLISHED'/)
|
||||
assert.match(source, /settlementIncluded === true/)
|
||||
assert.doesNotMatch(source, /settlementIncluded/)
|
||||
assert.match(source, /const feeSourceId = id\(published\.sourceFeeId\)/)
|
||||
assert.doesNotMatch(source, /\/fees\/calculate/)
|
||||
assert.doesNotMatch(source, /\/fees\/confirm/)
|
||||
|
||||
@@ -28,12 +28,8 @@ export function normalizeFinanceFixtureSources(sources, orders, allocations, cus
|
||||
}
|
||||
|
||||
export function supplierCategoryForFeeItem(item) {
|
||||
const value = `${item?.feeCode ?? ''} ${item?.feeTypeName ?? ''}`.toUpperCase()
|
||||
if (['TRUCK', 'TRAILER', 'DRAYAGE', '拖车', '卡车'].some((candidate) => value.includes(candidate))) return 'TRUCKING'
|
||||
if (['CUSTOMS', 'CLEARANCE', 'DECLARATION', '报关', '清关'].some((candidate) => value.includes(candidate))) return 'CUSTOMS'
|
||||
if (['CERTIFICATE', 'CERTIFICATION', 'CERT_', '认证', '证书'].some((candidate) => value.includes(candidate))) return 'CERTIFICATE'
|
||||
if (['BOOKING', 'FREIGHT', 'OCEAN', 'SHIPPING', '订舱', '海运', '运费'].some((candidate) => value.includes(candidate))) return 'BOOKING'
|
||||
return null
|
||||
return ['BOOKING', 'CUSTOMS', 'TRUCKING', 'CERTIFICATE'].includes(item?.supplierCategory)
|
||||
? item.supplierCategory : null
|
||||
}
|
||||
|
||||
export function buildSupplierBody(marker, category) {
|
||||
@@ -145,16 +141,16 @@ async function loadFeeItem(context, direction) {
|
||||
const list = context.apiSuccess(MODULE, `查询${direction}人工费用项目`, response, '', OPTIONS)
|
||||
return rows(list).find((item) => item?.status === 'ACTIVE' && (direction === 'RECEIVABLE'
|
||||
? item.warehouseReceivable === true
|
||||
: item.warehousePayable === true && Boolean(supplierCategoryForFeeItem(item)))) || null
|
||||
: item.warehousePayable === true)) || null
|
||||
}
|
||||
|
||||
async function createManualFee(context, resource, feeItem, direction, amount, basis, businessDate, partner = null) {
|
||||
async function createManualFee(context, resource, feeItem, direction, amount, basis, businessDate, supplier = null) {
|
||||
if (context.financeFixtureHooks?.createManualFee) return context.financeFixtureHooks.createManualFee(context, resource, feeItem, direction, amount, basis, businessDate)
|
||||
const basePath = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details`
|
||||
const listPath = `${basePath}?accountId=${encodeURIComponent(resource.tenantId)}`
|
||||
const body = { feeItemId: String(feeItem.id), direction, businessDate,
|
||||
unitPrice: amount, currency: 'CNY', amount, settlementIncluded: true,
|
||||
partnerId: partner?.id ? String(partner.id) : null, partnerName: partner?.name || null,
|
||||
unitPrice: amount, currency: 'CNY', amount,
|
||||
supplierId: supplier?.id ? String(supplier.id) : null,
|
||||
remark: `${context.marker}-${basis}` }
|
||||
const response = await request(context, 'POST', listPath, body)
|
||||
const detail = context.apiSuccess(MODULE, `${basis}人工费用明细`, response, `inboundId=${resource.inboundId}`, OPTIONS)
|
||||
@@ -214,8 +210,7 @@ async function loadPublishedAutomaticFee(context, resource) {
|
||||
const details = context.apiSuccess(MODULE, '回读审核链已发布自动费用来源', listResponse,
|
||||
`inboundId=${resource.inboundId}`, OPTIONS)
|
||||
const published = rows(details).find((item) => item?.sourceType === 'FBA_PRICING'
|
||||
&& item?.reviewStatus === 'APPROVED' && item?.publicationStatus === 'PUBLISHED'
|
||||
&& item?.settlementIncluded === true)
|
||||
&& item?.reviewStatus === 'APPROVED' && item?.publicationStatus === 'PUBLISHED')
|
||||
if (!published) {
|
||||
blocker(context, '审核链已生成可结算自动费用来源',
|
||||
`inboundId=${resource.inboundId}, processStatus=${pricingSummary.processStatus || pricingInbound?.processStatus || 'missing'}, feeId=${id(latest.feeId)}, feeStatus=${latest.feeStatus}, feeDetails=${rows(details).length};确认和发送成功后应同步生成 APPROVED + PUBLISHED 的 FBA_PRICING 来源`)
|
||||
@@ -241,20 +236,20 @@ async function loadPublishedAutomaticFee(context, resource) {
|
||||
|
||||
async function loadSupplier(context, category) {
|
||||
if (context.financeFixtureHooks?.loadSupplier) return context.financeFixtureHooks.loadSupplier(context, category)
|
||||
if (!category) return null
|
||||
const body = buildSupplierBody(context.marker, category)
|
||||
const effectiveCategory = category || 'BOOKING'
|
||||
const body = buildSupplierBody(context.marker, effectiveCategory)
|
||||
const response = await request(context, 'POST', '/api/platform/v1/suppliers?tenantId=0', body)
|
||||
const supplierId = id(context.apiSuccess(MODULE, '创建本轮 Finance 隔离供应商', response,
|
||||
`category=${category}`, OPTIONS))
|
||||
`category=${effectiveCategory}`, OPTIONS))
|
||||
if (!supplierId) return null
|
||||
const detailResponse = await context.request('GET', `/api/platform/v1/suppliers/${encodeURIComponent(supplierId)}?tenantId=0`)
|
||||
const detail = context.apiSuccess(MODULE, '回读本轮 Finance 隔离供应商', detailResponse,
|
||||
`supplierId=${supplierId}`, OPTIONS)
|
||||
context.assert(MODULE, 'Finance 供应商为 ACTIVE 且类别匹配费用项目', detailResponse,
|
||||
id(detail?.id) === supplierId && detail?.status === 'ACTIVE' && detail?.category === category
|
||||
id(detail?.id) === supplierId && detail?.status === 'ACTIVE' && (!category || detail?.category === category)
|
||||
&& detail?.name === body.name,
|
||||
`supplierId=${supplierId}, status=${detail?.status || 'missing'}, category=${detail?.category || 'missing'}`, OPTIONS)
|
||||
return detail?.status === 'ACTIVE' && detail?.category === category ? detail : null
|
||||
return detail?.status === 'ACTIVE' && (!category || detail?.category === category) ? detail : null
|
||||
}
|
||||
|
||||
async function uploadVoucher(context, customerId) {
|
||||
@@ -405,7 +400,7 @@ export async function run(context) {
|
||||
const payableSupplierCategory = supplierCategoryForFeeItem(feeItems.PAYABLE)
|
||||
const supplier = await loadSupplier(context, payableSupplierCategory)
|
||||
if (!supplier?.id) blocker(context, '取得 Warehouse PAYABLE 供应商',
|
||||
`未找到 ACTIVE ${payableSupplierCategory || 'SUPPORTED'} 供应商或 feeCode + feeTypeName 无法映射正式类别的 PAYABLE 费用项;不能伪造 supplierId/partnerId`)
|
||||
`未找到 ACTIVE ${payableSupplierCategory || '任意类别'} 供应商;不能伪造 supplierId`)
|
||||
for (const direction of ['RECEIVABLE', 'PAYABLE']) {
|
||||
if (!feeItems[direction]) context.addCoverageGap(MODULE, `missing-${direction.toLowerCase()}-fee-item`, `${direction} 人工费用 fixture`,
|
||||
`没有找到带 warehouse${direction === 'RECEIVABLE' ? 'Receivable' : 'Payable'}=true 的 ACTIVE 费用项目,禁止猜测费用项目 ID`, 'missing-deployed-fee-item')
|
||||
@@ -434,7 +429,7 @@ export async function run(context) {
|
||||
for (const [index, manualResource] of manualResources.entries()) {
|
||||
const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80 + index * 10,
|
||||
'MANUAL_ORDER_FEE', dateOffset(-3 - index), supplier)
|
||||
if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id),
|
||||
if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id),
|
||||
supplierName: supplier.name || null, supplierCategory: supplier.category || payableSupplierCategory })
|
||||
}
|
||||
}
|
||||
@@ -448,8 +443,7 @@ export async function run(context) {
|
||||
const basePath = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details`
|
||||
const path = `${basePath}?accountId=${encodeURIComponent(container.tenantId)}`
|
||||
const body = { feeItemId: String(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2),
|
||||
amount: 300, currency: 'CNY', settlementIncluded: true, partnerId: id(supplier.id),
|
||||
partnerName: supplier.name || null, remark: `${context.marker}-CONTAINER_SHARED` }
|
||||
amount: 300, currency: 'CNY', supplierId: id(supplier.id), remark: `${context.marker}-CONTAINER_SHARED` }
|
||||
const response = await request(context, 'POST', path, body)
|
||||
if (response?.http === 404) {
|
||||
blocker(context, '按柜费用来源接口已部署并可路由',
|
||||
@@ -460,7 +454,7 @@ export async function run(context) {
|
||||
if (feeId) { const approval = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(container.tenantId)}`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) }
|
||||
containerFixture = { feeSourceId: feeId || null, sourceType: 'MANUAL', direction: 'PAYABLE', amount: 300,
|
||||
businessDate: body.businessDate, currency: 'CNY', billingBasis: 'CONTAINER_SHARED',
|
||||
supplierId: supplier?.id ? id(supplier.id) : null, partnerId: supplier?.id ? id(supplier.id) : null,
|
||||
supplierId: supplier?.id ? id(supplier.id) : null,
|
||||
supplierName: supplier?.name || null, supplierCategory: supplier?.category || payableSupplierCategory,
|
||||
containerBookingId: id(container.containerBookingId), containerNo: container.containerNo || null,
|
||||
loadingPlanId: id(container.loadingPlanId) }
|
||||
@@ -471,7 +465,7 @@ export async function run(context) {
|
||||
'当前上下文没有同 marker 的已完成柜次及真实 Firstleg 体积快照,禁止借用历史柜次或模拟 Finance 分摊成功', 'no-marker-owned-container-snapshot')
|
||||
} else {
|
||||
context.addCoverageGap(MODULE, 'container-shared-cost-allocation', '按柜添加费用并按订单/运单体积分摊',
|
||||
'已识别同 marker 的 COMPLETED 柜次,但 PAYABLE 费用项目无法按 feeCode + feeTypeName 映射类别或缺少 ACTIVE 供应商', 'missing-payable-supplier')
|
||||
'已识别同 marker 的 COMPLETED 柜次,但缺少 PAYABLE 费用项目或符合其显式可空类别约束的 ACTIVE 供应商', 'missing-payable-supplier')
|
||||
}
|
||||
|
||||
if (containerFixture) fixtures.push(containerFixture)
|
||||
@@ -521,7 +515,7 @@ export async function run(context) {
|
||||
feeSources: fixtureSources,
|
||||
container: containerFixture ? { containerBookingId: containerFixture.containerBookingId,
|
||||
containerNo: containerFixture.containerNo } : { containerBookingId: null, containerNo: null },
|
||||
allocations, supplier: supplier?.id ? { supplierId: id(supplier.id), partnerId: id(supplier.id),
|
||||
allocations, supplier: supplier?.id ? { supplierId: id(supplier.id),
|
||||
supplierName: supplier.name || null, category: supplier.category || payableSupplierCategory } : null,
|
||||
voucherFileId, pricingEvidence,
|
||||
deploymentBlocker: context.steps.slice(start).filter((step) => step.deploymentBlocker).map((step) => step.evidence),
|
||||
|
||||
@@ -38,12 +38,12 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
]
|
||||
const feeSources = [...receivables,
|
||||
{ feeSourceId: '105', orderId: '205', direction: 'PAYABLE', currency: 'CNY',
|
||||
businessDate: '2026-08-11', partnerId: '501', partnerName: 'Fixture Partner' },
|
||||
businessDate: '2026-08-11', supplierId: '501', supplierName: 'Fixture Supplier' },
|
||||
{ feeSourceId: null, sourceType: 'MANUAL', direction: 'PAYABLE', currency: 'CNY',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', partnerId: '501',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', supplierId: '501',
|
||||
containerBookingId: 'CB-FAILED' }]
|
||||
feeSources.push({ feeSourceId: '106', sourceType: 'MANUAL', direction: 'PAYABLE', currency: 'CNY',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', partnerId: '501',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', supplierId: '501',
|
||||
containerBookingId: 'CB-COST-RESPONSE' })
|
||||
const fixture = { marker, customerId: '11',
|
||||
orders: feeSources.filter((item) => item.orderId)
|
||||
@@ -109,7 +109,7 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
return ok(feeSources.filter((item) => item.feeSourceId && (!omitPayableSource || item.feeSourceId !== '105')).map((item, index) => {
|
||||
const { feeSourceId, ...rest } = item
|
||||
return { ...rest, poolItemId: `${item.direction}:${1001 + index}`, sourceId: feeSourceId,
|
||||
reviewStatus: 'APPROVED', settlementIncluded: true, sourceStatus: 'AVAILABLE' }
|
||||
reviewStatus: 'APPROVED', sourceStatus: 'AVAILABLE' }
|
||||
}))
|
||||
}
|
||||
if (method === 'POST' && requestPath.endsWith('/statements')) {
|
||||
@@ -252,7 +252,7 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张
|
||||
assert.equal(new Set(fixture.receivableSettlements.map((item) => item.statementId)).size, 3)
|
||||
assert.equal(fixture.payableSettlements.length, 1)
|
||||
assert.equal(fixture.payableSettlements[0].supplierId, '501')
|
||||
assert.equal(fixture.payableSettlements[0].partnerId, '501')
|
||||
assert.equal(fixture.payableSettlements[0].supplierId, '501')
|
||||
assert.equal(fixture.payableSettlements[0].supplierName, 'Fixture Partner')
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'incomplete-container-cost-attempt'
|
||||
&& gap.reason.includes('containerBookingId=CB-FAILED')
|
||||
@@ -292,12 +292,12 @@ test('run 在 fixture 缺失时记录 blocker 且不发送 HTTP 请求', async (
|
||||
assert.equal(context.coverageGaps[0].category, 'missing-marker-owned-finance-fixture')
|
||||
})
|
||||
|
||||
test('PAYABLE 接受 partnerId 及名称快照,不依赖 SUPPLIER 类型', async () => {
|
||||
test('PAYABLE 只接受 supplierId 及 Warehouse 名称快照', async () => {
|
||||
const context = fakeContext()
|
||||
await run(context)
|
||||
const payableRequest = context.calls.find((call) => call.path.endsWith('/payable-settlements'))
|
||||
assert.equal(payableRequest.body.supplierId, '501')
|
||||
assert.doesNotMatch(source, /partnerType|types\?\.includes|SUPPLIER.*阻断/)
|
||||
assert.doesNotMatch(source, /partnerId|partnerName|partnerType/)
|
||||
})
|
||||
|
||||
test('来源缺失逐条 customerId 时不得由 fixture 根字段静默补齐', async () => {
|
||||
|
||||
@@ -206,8 +206,8 @@ async function normalizeFixture(context) {
|
||||
raw, feeSourceId: financePoolId(financeSource), sourceFeeId: sourceId(raw), customerId: id(raw.customerId),
|
||||
direction: String(raw.direction ?? ''), currency: String(raw.currency ?? raw.currencyCode ?? '').toUpperCase(),
|
||||
businessDate: sourceDate(raw), waybillId: sourceWaybill(raw), marker: raw.marker,
|
||||
supplierId: id(raw.supplierId ?? raw.partnerId ?? root.supplierId ?? root.partnerId),
|
||||
supplierName: raw.supplierName ?? raw.partnerName ?? root.supplierName ?? root.partnerName ?? null,
|
||||
supplierId: id(raw.supplierId ?? root.supplierId),
|
||||
supplierName: raw.supplierName ?? root.supplierName ?? null,
|
||||
}
|
||||
})
|
||||
const hasContainerAllocations = list(root.allocations).length > 0
|
||||
@@ -526,7 +526,7 @@ export async function run(context) {
|
||||
const body = { customerId: fixture.customerId, supplierId,
|
||||
feeSourceIds: payables.map((source) => source.feeSourceId), dueDate, currency: 'CNY' }
|
||||
const response = await write(context, 'POST', `${BASE}/payable-settlements`, body, 'payable-settlement')
|
||||
payableSettlement = context.apiSuccess(MODULE, '按 Fixture PAYABLE 与 partnerId/supplierId 生成应付结算单',
|
||||
payableSettlement = context.apiSuccess(MODULE, '按 Fixture PAYABLE 与 supplierId 生成应付结算单',
|
||||
response, `sources=${payables.length}, supplierId=${supplierId}, supplierName=${supplierName ?? 'null'}`, OPTIONS)
|
||||
if (payableSettlement) {
|
||||
const listed = await findSettlementInOfficialList(context, 'PAYABLE', fixture.customerId,
|
||||
@@ -543,7 +543,7 @@ export async function run(context) {
|
||||
}
|
||||
} else {
|
||||
blocker(context, 'PAYABLE 结算 Fixture 门禁',
|
||||
`需要至少一条同客户 CNY PAYABLE,且全部提供相同 partnerId/supplierId;sources=${payables.length}, suppliers=${payableSupplierIds.size}`,
|
||||
`需要至少一条同客户 CNY PAYABLE,且全部提供相同 supplierId;sources=${payables.length}, suppliers=${payableSupplierIds.size}`,
|
||||
'insufficient-payable-supplier-fixture')
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ export async function run(context) {
|
||||
fixture.root.receivableSettlementMap = { byStatementId: Object.fromEntries(arRows.map((row) => [row.statementId, row])) }
|
||||
fixture.root.payableSettlements = payableSettlement ? [{
|
||||
settlementId: settlementId(payableSettlement), direction: payableSettlement.direction,
|
||||
supplierId: id(payableSettlement.supplierId), partnerId: id(payableSettlement.partnerId ?? payableSettlement.supplierId),
|
||||
supplierId: id(payableSettlement.supplierId),
|
||||
supplierName: payableSettlement.supplierName ?? payables[0]?.supplierName ?? null,
|
||||
status: payableSettlement.status,
|
||||
paymentStatus: payableSettlement.paymentStatus, version: payableSettlement.version,
|
||||
|
||||
@@ -53,19 +53,19 @@ export async function run(context) {
|
||||
const feeSourceId = id(source.feeSourceId || source.id)
|
||||
let detailResponse = await context.request('GET', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}`)
|
||||
let detail = context.apiSuccess(MODULE, '读取本轮费用来源详情', detailResponse, '', OPTIONS)
|
||||
if (detail?.reviewStatus === 'DRAFT' || detail?.reviewStatus === 'REJECTED') {
|
||||
const submitBody = { reason: context.marker, expectedVersion: Number(detail.operationVersion ?? detail.version) }
|
||||
detailResponse = await write(context, 'POST', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}/submit`, submitBody, 'fee-submit')
|
||||
detail = context.apiSuccess(MODULE, '提交本轮费用来源审核', detailResponse, '', OPTIONS)
|
||||
}
|
||||
if (detail?.reviewStatus === 'PENDING') {
|
||||
const reviewBody = { action: 'APPROVE', reason: context.marker, expectedVersion: Number(detail.operationVersion ?? detail.version) }
|
||||
detailResponse = await write(context, 'POST', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}/review`, reviewBody, 'fee-review')
|
||||
detail = context.apiSuccess(MODULE, '审核本轮费用来源', detailResponse, '', OPTIONS)
|
||||
}
|
||||
if (detail?.reviewStatus === 'APPROVED' && detail?.settlementIncluded !== true) {
|
||||
const inclusionBody = { included: true, reason: context.marker, expectedVersion: Number(detail.operationVersion ?? detail.version) }
|
||||
detailResponse = await write(context, 'PUT', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}/settlement-inclusion`, inclusionBody, 'fee-include')
|
||||
detail = context.apiSuccess(MODULE, '将本轮费用来源纳入结算', detailResponse, '', OPTIONS)
|
||||
}
|
||||
context.assert(MODULE, '本轮来源满足对账单生成条件', detailResponse,
|
||||
detail?.reviewStatus === 'APPROVED' && detail?.settlementIncluded === true && detail?.sourceStatus === 'AVAILABLE' && detail?.currency === 'CNY',
|
||||
`review=${detail?.reviewStatus}, included=${detail?.settlementIncluded}, status=${detail?.sourceStatus}`, OPTIONS)
|
||||
detail?.reviewStatus === 'APPROVED' && detail?.sourceStatus === 'AVAILABLE' && detail?.currency === 'CNY',
|
||||
`review=${detail?.reviewStatus}, status=${detail?.sourceStatus}`, OPTIONS)
|
||||
|
||||
const businessDate = detail?.businessDate || new Date().toISOString().slice(0, 10)
|
||||
const createBody = { customerId, currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: [feeSourceId], periodStart: businessDate, periodEnd: businessDate, remark: context.marker }
|
||||
|
||||
@@ -17,8 +17,9 @@ test('inbound fee lifecycle covers frozen source, version, publication and WAIT_
|
||||
for (const token of [
|
||||
'/fees/latest', '/fees/published', '/fees/confirm', '/fees/send', '/fee-details',
|
||||
'FBA_PRICING', 'MANUAL', 'CALCULATED', 'CONFIRMED', 'SENT', 'PUBLISHED',
|
||||
'sourceFeeVersion', 'reviewStatus', 'publicationStatus', 'settlementIncluded', 'WAIT_SPLIT',
|
||||
'sourceFeeVersion', 'reviewStatus', 'publicationStatus', 'WAIT_SPLIT',
|
||||
]) assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
assert.match(source, /人工发布头程费用并触发 Finance Outbox/)
|
||||
assert.match(source, /对客合计仅含已发布头程与已审核其他应收且其他应付单列/)
|
||||
assert.doesNotMatch(source, /settlementIncluded|是否结算|参与结算/)
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function run(context) {
|
||||
{ feeItemId: initialDerived.feeItemId, direction: 'RECEIVABLE',
|
||||
businessDate: initialDerived.businessDate, unitPrice: initialDerived.unitPrice,
|
||||
currency: initialDerived.currency, amount: initialDerived.amount,
|
||||
settlementIncluded: initialDerived.settlementIncluded, remark: `${context.marker}-禁止修改派生` })
|
||||
remark: `${context.marker}-禁止修改派生` })
|
||||
context.assert(MODULE, 'FBA_PRICING 派生详情拒绝通用编辑', derivedUpdateResponse,
|
||||
derivedUpdateResponse.http === 200 && !success(derivedUpdateResponse)
|
||||
&& String(derivedUpdateResponse.body?.message || '').includes('只读派生'),
|
||||
@@ -108,8 +108,7 @@ export async function run(context) {
|
||||
&& id(row.sourceFeeId) === id(sent?.feeId))
|
||||
context.assert(MODULE, '仅人工发布后费用为 SENT 且派生明细为 PUBLISHED', sendResponse,
|
||||
sent?.feeStatus === 'SENT' && sent?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
&& published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED'
|
||||
&& published?.settlementIncluded === true,
|
||||
&& published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED',
|
||||
`status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const feeItemsResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&size=500&status=ACTIVE')
|
||||
@@ -125,7 +124,7 @@ export async function run(context) {
|
||||
const detailsBase = `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details`
|
||||
const manualPayload = { feeItemId: manualItem.id, direction: 'RECEIVABLE',
|
||||
businessDate: new Date().toISOString().slice(0, 10), unitPrice: 12.34, currency: 'CNY', amount: 12.34,
|
||||
settlementIncluded: true, remark: `${context.marker}-其他应收` }
|
||||
remark: `${context.marker}-其他应收` }
|
||||
const manualCreateResponse = await context.request('POST', `${detailsBase}?accountId=${encodeURIComponent(tenantId)}`, manualPayload)
|
||||
const manual = context.apiSuccess(MODULE, '新增独立 MANUAL 其他应收', manualCreateResponse, '', OPTIONS)
|
||||
context.assert(MODULE, 'MANUAL 初始版本待审核且不冒充 FBA_PRICING', manualCreateResponse,
|
||||
@@ -139,10 +138,10 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, '审核通过 MANUAL 其他应收', approveResponse, '', OPTIONS)
|
||||
const afterApproval = await listDetails(context, tenantId, inboundId, '回查已审核 MANUAL 其他应收')
|
||||
const approved = afterApproval.rows.find((row) => id(row.id) === id(manual.id))
|
||||
context.assert(MODULE, '已审核 MANUAL 纳入对客应收但与头程费用独立', afterApproval.response,
|
||||
context.assert(MODULE, '已审核 MANUAL 自动进入对客应收候选且与头程费用独立', afterApproval.response,
|
||||
approved?.sourceType === 'MANUAL' && approved?.reviewStatus === 'APPROVED'
|
||||
&& approved?.settlementIncluded === true,
|
||||
`review=${approved?.reviewStatus || 'missing'}, settlementIncluded=${approved?.settlementIncluded ?? 'missing'}`, OPTIONS)
|
||||
&& approved?.publicationStatus === 'PUBLISHED',
|
||||
`review=${approved?.reviewStatus || 'missing'}, publication=${approved?.publicationStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const adjustedManualPayload = { ...manualPayload, unitPrice: 15.67, amount: 15.67,
|
||||
remark: `${context.marker}-其他应收版本调整` }
|
||||
|
||||
Reference in New Issue
Block a user