Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
5 changed files with 101 additions and 8 deletions
Showing only changes of commit 6dbcb2128c - Show all commits
@@ -92,7 +92,7 @@ public class FinanceFeeSourceEventService {
snapshot.sourceType(), snapshot.feeId());
jdbc.update("UPDATE fin_cost SET status='VOIDED',voided_at=CURRENT_TIMESTAMP(3),void_reason='来源版本已替代',version=version+1,updated_by=0 WHERE cost_origin='FIRSTLEG_DIRECT_PAID' AND source_system='FIRSTLEG' AND source_type=? AND source_fee_id=? AND status='CONFIRMED'",
snapshot.sourceType(), snapshot.feeId());
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,container_booking_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')",
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,container_booking_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,review_status,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'APPROVED','AVAILABLE')",
snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(),
snapshot.inboundCode() == null || snapshot.inboundCode().isBlank() ? snapshot.containerNo() : snapshot.inboundCode(),
snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
@@ -50,7 +50,8 @@ class FinanceFeeSourceEventServiceTest {
assertThat(service.consume(event)).isEqualTo(101L);
verify(firstleg).inbound("test-token", 21, 31, 1);
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class));
verify(jdbc).update(argThat(sql -> sql.startsWith("INSERT INTO fin_fee_source")
&& sql.contains("review_status,status") && sql.contains("'APPROVED','AVAILABLE'")), any(Object[].class));
}
@Test
@@ -63,7 +64,8 @@ class FinanceFeeSourceEventServiceTest {
verify(firstleg).container("test-token", 41, 31, 1);
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("CONTAINER_FEE"), eq(31L));
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
verify(jdbc).update(argThat(sql -> sql.startsWith("INSERT INTO fin_fee_source")
&& sql.contains("review_status,status") && sql.contains("'APPROVED','AVAILABLE'")), values.capture());
assertThat(values.getValue()[8]).as("fin_fee_source.container_id keeps loadingPlanId")
.isEqualTo(41L);
assertThat(values.getValue()[9]).as("fin_fee_source.container_booking_id stores physical container")
@@ -107,7 +109,8 @@ class FinanceFeeSourceEventServiceTest {
verify(supplierValidator).requireActive(71L, "TRUCKING");
verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class));
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
verify(jdbc).update(argThat(sql -> sql.startsWith("INSERT INTO fin_fee_source")
&& sql.contains("review_status,status") && sql.contains("'APPROVED','AVAILABLE'")), values.capture());
assertThat(values.getValue()[20].toString())
.contains("\"supplierId\":71", "\"supplierName\":\"现有供应商\"", "\"supplierCategory\":\"TRUCKING\"");
}
@@ -145,7 +148,8 @@ class FinanceFeeSourceEventServiceTest {
assertThat(service.consume(event)).isEqualTo(101L);
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
verify(jdbc).update(argThat(sql -> sql.startsWith("INSERT INTO fin_fee_source")
&& sql.contains("review_status,status") && sql.contains("'APPROVED','AVAILABLE'")), values.capture());
assertThat(values.getValue()[14]).isEqualTo("DIRECT_PAID");
assertThat(values.getValue()[16]).isEqualTo("BANK_TRANSFER");
assertThat(values.getValue()[18]).isEqualTo("[901]");
@@ -171,7 +175,8 @@ class FinanceFeeSourceEventServiceTest {
assertThat(service.consume(event)).isEqualTo(101L);
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("INBOUND_FEE"), eq(31L));
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class));
verify(jdbc).update(argThat(sql -> sql.startsWith("INSERT INTO fin_fee_source")
&& sql.contains("review_status,status") && sql.contains("'APPROVED','AVAILABLE'")), any(Object[].class));
}
@Test
@@ -5,6 +5,15 @@ import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
const source = readFileSync(new URL('./dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
test('dedicated outbound uses exact source cartons and verifies completion and stock deduction', () => {
assert.match(source, /await outboundOverseas\(group, picker.token\)/)
assert.match(source, /one-click-dropship\?customerId=/)
assert.match(source, /position.inboundCode === candidate.inboundCode/)
assert.match(source, /cartonMarks: inputs\[index\].cartonMarks/)
assert.match(source, /outbound.outbound\?\.status === 'OUTBOUNDED'/)
assert.match(source, /!remaining.some/)
})
test('dropship matrix has a real customer prerequisite without generic dispatch fixtures', () => {
const modules = normalizeModuleOrder(['dropship-pallet-matrix'])
assert.ok(modules.includes('merchant-qualification-fixture'))
@@ -321,7 +321,8 @@ export async function run(context) {
async function receiveOverseas(group) {
const record = group[0]
const label = record.label
context.marker = `${marker}-${label}-RECEIVE`
// The PDA account helper keeps the marker tail; retain the run identity there.
context.marker = `${label}-RECEIVE-${marker}`
const base = '/api/pda/v1/last-leg/receiving-containers'
const access = await resolvePickerAccess(context)
check(`${label}海外PDA角色可用`, null, Boolean(access))
@@ -376,10 +377,75 @@ export async function run(context) {
check(`${label}海外入库完成且重放稳定`, null, completed.status === 'COMPLETED'
&& JSON.stringify(replay) === JSON.stringify(completed))
for (const item of group) item.overseas.receiving = { completed, current, receivingVerified: true }
await outboundOverseas(group, picker.token)
} finally {
await disablePicker(context, picker, records.indexOf(record) + 1)
}
}
async function outboundOverseas(group, token) {
const label = group[0].label
const base = '/api/platform/v1/last-leg/outbound-orders'
const customerId = String(group[0].task.tenantId)
const warehouseId = group[0].overseas.warehouseId
async function call(name, method, path, body, pda = false) {
const response = await context.requestOverseas(method, path, body, {}, pda ? token : context.overseasToken)
const value = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
check(`${label}${name}成功`, response, response?.body?.code === '200' && value != null)
return value
}
const candidatePath = `${base}/one-click-dropship/candidates?customerId=${customerId}&warehouseId=${warehouseId}`
const all = await call('读取专用出库可用库存', 'GET', candidatePath)
const selected = all.filter((candidate) => group.some((record) => record.source.inboundCode === candidate.inboundCode))
const inputs = selected.map((candidate) => {
const record = group.find((item) => item.source.inboundCode === candidate.inboundCode)
const pallet = record.task.pallets.find((item) => item.palletNo === candidate.palletNo)
const cartonMarks = (pallet?.cartons || []).filter((carton) =>
String(carton.inboundId) === String(record.source.inboundId)).map((carton) => carton.boxCode)
check(`${label}专用出库候选逐票逐托箱数准确`, null,
cartonMarks.length > 0 && Number(candidate.availablePackages) === cartonMarks.length
&& String(candidate.customerId) === customerId && String(candidate.overseasWarehouseId) === warehouseId)
return { inventoryPositionId: candidate.inventoryPositionId, packages: cartonMarks.length,
sku: candidate.skuCode, cartonMarks }
})
check(`${label}专用出库候选覆盖全部票和箱`, null, group.every((record) =>
selected.filter((item) => item.inboundCode === record.source.inboundCode)
.reduce((sum, item) => sum + Number(item.availablePackages), 0) === record.expectedCartonMarks.length))
const body = { recipient: `HTTP acceptance ${context.marker}`, items: inputs,
remark: context.marker, idempotencyKey: `${context.marker}-outbound-create` }
const task = await call('创建一件代发专用出库', 'POST', `${base}/one-click-dropship?customerId=${customerId}`, body)
for (const record of group) record.outbound = { task, selected, completed: false }
const replay = await call('专用出库创建同键重放', 'POST', `${base}/one-click-dropship?customerId=${customerId}`, body)
check(`${label}专用出库重放不重复建单`, null, JSON.stringify(replay) === JSON.stringify(task))
const pdaPath = `/api/pda/v1/last-leg/outbound-tasks/${task.taskNo}`
const detail = await call('PDA读取专用出库任务', 'GET', pdaPath, undefined, true)
check(`${label}PDA出库明细完整`, null, detail.items?.length === inputs.length)
for (const [index, candidate] of selected.entries()) {
const item = detail.items.find((row) => row.palletNo === candidate.palletNo && row.skuCode === candidate.skuCode)
check(`${label}PDA拣货来源仓位和入仓码一致`, null, Boolean(item)
&& item.positions?.some((position) => String(position.inventoryPositionId) === String(candidate.inventoryPositionId)
&& position.inboundCode === candidate.inboundCode))
const pick = { outboundPalletItemId: item.outboundPalletItemId,
inventoryPositionId: candidate.inventoryPositionId, packages: inputs[index].packages,
cartonMarks: inputs[index].cartonMarks, idempotencyKey: `${context.marker}-outbound-pick-${index}` }
const picked = await call('PDA专用出库拣货', 'POST', `${pdaPath}/pick`, pick, true)
const repeated = await call('PDA出库拣货同键重放', 'POST', `${pdaPath}/pick`, pick, true)
check(`${label}PDA出库拣货重放不重复扣减`, null, JSON.stringify(repeated) === JSON.stringify(picked))
}
const completion = { idempotencyKey: `${context.marker}-outbound-complete` }
const completed = await call('PDA完成专用出库', 'POST', `${pdaPath}/complete`, completion, true)
const repeated = await call('PDA专用出库完成重放', 'POST', `${pdaPath}/complete`, completion, true)
check(`${label}专用出库任务完成且重放稳定`, null,
completed.status === 'COMPLETED' && JSON.stringify(repeated) === JSON.stringify(completed))
const outbound = await call('回查专用出库单终态', 'GET', `${base}/${task.outboundNo}`)
check(`${label}专用出库单真实完成且箱数核平`, null,
outbound.outbound?.status === 'OUTBOUNDED' && outbound.outbound?.outboundType === 'ONE_CLICK_DROPSHIP'
&& Number(outbound.outbound.packageCount) === inputs.reduce((sum, item) => sum + item.packages, 0)
&& outbound.pallets.every((item) => Number(item.packageCount) === Number(item.pickedPackageCount)))
const remaining = await call('回查专用出库可用库存扣减', 'GET', candidatePath)
check(`${label}本轮每票库存全部扣减`, null,
!remaining.some((candidate) => group.some((record) => record.source.inboundCode === candidate.inboundCode)))
for (const record of group) record.outbound = { task: completed, selected, detail: outbound, completed: true }
}
try {
await verify(await prepare('SINGLE-MULTI', 2), 2)
await verify(await prepare('SINGLE-SINGLE', 1), 1)
@@ -402,7 +468,7 @@ export async function run(context) {
await convertOverseas(group)
await receiveOverseas(group)
}
context.createdResources.dropshipPalletMatrix.stage = 'OVERSEAS_RECEIVING_COMPLETED'
context.createdResources.dropshipPalletMatrix.stage = 'DEDICATED_OUTBOUND_COMPLETED'
} finally {
context.marker = marker
context.options.prepareDropshipManualCount = savedOptions
@@ -91,6 +91,19 @@ async function verifyTruckDeparturePayables(context, aggregate) {
&& row.direction === 'PAYABLE' && row.reviewStatus === 'APPROVED'
&& Math.round(Number(row.amount) * 100) === Math.round(Number(fees.get(id(row.sourceFeeId)).amount) * 100)),
`expected=${fees.size}, imported=${imported.size}`, OPTIONS)
const candidates = new Map()
for (const customer of new Set([...fees.values()].map(fee => id(fee.tenantId)))) {
const response = await request(context, 'GET', `/api/platform/finance/v1/fee-pool?customerId=${customer}&direction=PAYABLE&supplierId=${id(truck.carrierId)}`)
const rows = context.apiSuccess(MODULE, '回查拖车费可用于应付结算', response, '', OPTIONS)
for (const row of Array.isArray(rows) ? rows : []) {
if (row.sourceType === 'CONTAINER_FEE' && fees.has(id(row.sourceId))) candidates.set(id(row.sourceId), row)
}
}
context.assert(MODULE, '拖车费已进入应付结算候选且未付款', null,
candidates.size === fees.size && [...candidates.values()].every(row => row.direction === 'PAYABLE'
&& row.settlementMode === 'ON_ACCOUNT' && row.status === 'AVAILABLE' && !row.paidAt
&& Math.round(Number(row.amount) * 100) === Math.round(Number(fees.get(id(row.sourceId)).amount) * 100)),
`expected=${fees.size}, candidates=${candidates.size}`, OPTIONS)
}
export async function run(context) {