Files
order_site/apps/frontend/tests/consignment-manual-match-mount.test.ts
T
yml2213 b666a9c951 fix(consignment): 拼单份额验收收官补寄卖结算钩子 + 手动匹配弹窗三处复核遗留
- acceptWorkOrderShareAndSettle 最后一个份额把母单转 accepted 的分支,
  此前不释放发布押金也不结算供货货款(两钩子只在统一验收里):
  现在同口径补上;供货货款传 assignedWorkerId=0(拼单母单无单一接单人,
  份额打手已各自按 share_reward 结算,不会重复付款)
- 手动匹配弹窗:race_condition/unavailable/spec_mismatch/partial_requires_confirm
  四个错误码清空已选并自动重拉列表,数据过期不再死循环撞错
- 手动匹配弹窗库存侧:keyword+pageSize100 透传后端(原来 keyword 是死参数、
  只拉 50 条),300ms 防抖后端搜索;无供货打手时禁用指派选项并在提交前
  再拦一道(原来必 400 死路);选中订单回显 workerId/workerName
- 凭证核销反馈抽共享 utils/voucher-consume,WorkOrdersPanel 与弹窗共用,
  口径不再漂移(弹窗顺带获得"已提前核销 N 张"提示)
- docs/寄卖系统优化方案.md 记录批次 V

后端 477(+4)/ 前端 50(+1)测试全绿,typecheck / lint / format / check:sql 通过
2026-09-17 12:45:51 +08:00

100 lines
4.9 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
/**
* 手动匹配弹窗曾经被插到「编辑寄售物品」弹窗**内部**,导致外层弹窗没打开时它根本没挂载:
* 用户点「手动匹配订单」什么都不会发生。这类"点了没反应"的问题在浏览器里才看得出来,
* 这里用静态检查把它钉住:组件必须挂在顶层(前面不能有未闭合的 Modal)。
*/
function assertTopLevelMount(filePath: string, componentName: string) {
const source = readFileSync(filePath, 'utf8')
const index = source.indexOf(`<${componentName}`)
assert.ok(index > 0, `${filePath} 里找不到 ${componentName} 的挂载点`)
const before = source.slice(0, index)
const opened = (before.match(/<Modal[\s>]/g) || []).length
const closed = (before.match(/<\/Modal>/g) || []).length
assert.equal(
opened - closed,
0,
`${componentName} 必须挂在顶层:不能嵌在别的 Modal 里,否则外层弹窗未打开时它不会渲染`,
)
}
test('库存面板:手动匹配订单弹窗挂在顶层(按钮点了必须能弹出来)', () => {
assertTopLevelMount(
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
'ConsignmentManualMatchModal',
)
})
test('订单面板:手动指定现货弹窗同样挂在顶层', () => {
assertTopLevelMount(
new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname,
'ConsignmentManualMatchModal',
)
})
test('按钮点击要真的把状态设上,弹窗的 open 由该状态驱动', () => {
const source = readFileSync(
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
'utf8',
)
assert.ok(source.includes('setManualMatchItem(record)'), '行内按钮必须设置待匹配的现货')
assert.ok(source.includes('open={Boolean(manualMatchItem)}'), '弹窗 open 必须由该状态驱动')
})
test('手动匹配入口必须按 consignment.fulfill 门控(提交的是履约动作,不能只挂 manage)', () => {
const inventoryPanel = readFileSync(
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
'utf8',
)
// 库存面板:手动匹配订单按钮要与编辑/作废拆开 —— 前者 fulfill(提交打到
// /fulfill、/assign-worker,后端要求 consignment.fulfill),后者 manage
assert.ok(
inventoryPanel.includes("resolveAdminPermission(sessionPermissions, 'consignment.fulfill')"),
'库存面板必须解析 consignment.fulfill 权限',
)
const buttonIndex = inventoryPanel.indexOf('手动匹配订单')
assert.ok(buttonIndex > 0, '找不到「手动匹配订单」按钮')
assert.ok(
inventoryPanel.slice(0, buttonIndex).lastIndexOf('canFulfillConsignment ?') > 0,
'「手动匹配订单」必须挂在 canFulfillConsignment 门下',
)
const ordersPanel = readFileSync(
new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname,
'utf8',
)
// 拦截弹窗 footer 的入口同样要门控,无 fulfill 权限的人不该看到能直达 403 的按钮
const footerButtonIndex = ordersPanel.indexOf('手动指定现货匹配')
assert.ok(footerButtonIndex > 0, '找不到「手动指定现货匹配」按钮')
const beforeFooterButton = ordersPanel.slice(0, footerButtonIndex)
assert.ok(
beforeFooterButton.lastIndexOf('canFulfillConsignment ?') >
beforeFooterButton.lastIndexOf('footer={['),
'拦截弹窗 footer 的「手动指定现货匹配」必须按 canFulfillConsignment 门控',
)
})
test('手动匹配弹窗:数据过期的 409 要清选择并刷新,无打手的现货禁止走指派', () => {
const source = readFileSync(
new URL('../src/components/admin/ConsignmentManualMatchModal.tsx', import.meta.url).pathname,
'utf8',
)
// 并发占用/规格在打开弹窗后被改 → 清空选择并重新拉列表,不能让运营循环撞同一个错
assert.ok(source.includes('consignment_inventory_race_condition'), '要处理并发占用错误码')
assert.ok(source.includes('consignment_spec_mismatch'), '要处理规格变化错误码')
assert.ok(source.includes('setSelected(null)'), '数据过期时要清空已选')
// 没关联打手的现货走指派必 400 consignment_worker_required:选项禁用 + 提交前再拦一次
assert.ok(source.includes('disabled: !selected.workerId'), '无打手时指派选项必须禁用')
assert.ok(
source.includes("deliveryKind === 'assign_worker' && !selected.workerId"),
'提交前要对无打手的指派再拦一道',
)
// 凭证核销反馈与关键字搜索:用共享出口 / 透传后端,不内联复制
assert.ok(source.includes("from '@/utils/voucher-consume'"), '核销反馈必须用共享出口')
assert.ok(source.includes('keyword: keyword.trim() || undefined'), '库存侧关键字要透传后端')
})