Files
order_site/apps/frontend/tests/after-sales-amount-and-modal.test.ts
T
zhaoxing 787b8e2771 feat(consignment): 接单模板反推规格字段,打手端大批量选项改下拉
- 新增 consignment-template-spec-service:按「分类 + 子树」解析接单模板商品名,
  全部可拆成统一维度且无组合碰撞时生成多字段矩阵,否则整类降级为商品名下拉;
  选项只存 ruleKey,取价时实时回查模板,模板停用/删除即无价(missingPricePolicy=block)。
- 匹配引擎:模板驱动配置强制使用 profile specs(不再回退库存旧快照),订单侧按
  product_rule_id 反解规格;解析失败按 unknown 处理,不参与自动匹配。
- 管理端:寄卖配置新增「从接单模板生成」,弹窗给出推荐模式(规格字段 / 商品名下拉)、
  降级原因与组合→模板价覆盖矩阵。
- 打手端:选项超过 8 个改用带搜索的下拉(地铁逃生 100 个商品),规格网格按字段数
  自适应铺满;物品名改用商品名,去掉硬编码「苏尔南」后缀。
- 迁移重编号:合并 main 后寄卖线顺延为 147–151,migrate.ts 保留 legacy→new 幂等映射。
- 同工作区的拼单份额报酬口径、超时重匹配兜底、整单/拼单匹配弹窗调整一并提交。

验证:前端 254/254、后端寄卖相关 41/41、双端 tsc 通过;开发库已实跑
发布→审核入库→自动匹配(模板价 1200 → 匹配报酬 1200)。
2026-11-07 12:26:10 +08:00

110 lines
5.3 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
function readSource(relativePath: string) {
return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8')
}
test('工单跳到售后:先记录待打开工单,等面板挂载后再弹窗(避免点两次)', () => {
const source = readSource('../src/pages/admin/AdminWorkerPlatformPage.tsx')
assert.ok(source.includes('pendingAfterSalesOrder'), '必须用 pending state 承接待打开的售后工单')
assert.ok(
source.includes('setPendingAfterSalesOrder(state.createOrder)'),
'收到路由 state 时只记录待打开工单,不能直接调用尚未挂载的 ref',
)
assert.ok(
!source.includes('afterSalesPanelRef.current?.openCreateModal(state.createOrder)'),
'不能在 setActiveTab 的同一次同步执行里调用 ref(此时面板还没挂载,调用会被静默吞掉)',
)
assert.ok(
source.includes('}, [visibleActiveTab, pendingAfterSalesOrder])'),
'第二个 effect 必须在售后 Tab 激活后重跑',
)
assert.ok(
source.includes("if (visibleActiveTab !== 'after-sales' || !pendingAfterSalesOrder) return"),
'只有切到售后 Tab 且存在待打开工单时才处理',
)
assert.ok(
source.includes('afterSalesPanelRef.current.openCreateModal(pendingAfterSalesOrder)'),
'面板挂载后必须真正打开建单弹窗',
)
})
test('售后处置弹窗:默认金额取后端建议值,并展示发单金额与拼单份数', () => {
const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx')
assert.ok(
source.includes('suggestedRecoveryAmount'),
'默认追缴金额必须使用 suggestedRecoveryAmount',
)
assert.ok(
source.includes('existingRecoveryAmount > 0'),
'已追缴过的售后必须沿用原有追缴金额,不能被建议值覆盖',
)
assert.ok(source.includes('afterSalesCase.workOrderRewardAmount'), '整单必须展示工单发单金额')
assert.ok(source.includes('afterSalesCase.shareUnitReward'), '拼单必须展示一份价格')
assert.ok(source.includes('afterSalesCase.shareRewardAmount'), '拼单必须展示份额合计金额')
assert.ok(source.includes('afterSalesCase.shareQuantity'), '拼单必须展示已拼份数')
assert.ok(source.includes('(整单)'), '整单场景要有明确标识')
})
test('拼单处置:默认一份,可输入份数并按「一份价格 × 份数」联动追缴金额;整单不出现份数输入', () => {
const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx')
assert.ok(source.includes('recoveryQuantity?: number'), '表单值类型必须支持追缴份数')
assert.ok(source.includes('label="追缴份数"'), '拼单必须提供追缴份数输入框')
assert.ok(source.includes('name="recoveryQuantity"'), '份数输入必须绑定 recoveryQuantity')
assert.ok(source.includes('min={1}'), '份数至少 1 份')
assert.ok(
source.includes('Math.max(1, Number(afterSalesCase.shareQuantity || 1))'),
'份数上限必须是该责任份额的份数',
)
assert.ok(source.includes('onValuesChange'), '份数变化必须联动追缴金额')
assert.ok(
source.includes('changedValues.recoveryQuantity === undefined'),
'只有份数变化才重算金额,手动改金额不能被覆盖',
)
assert.ok(
source.includes('Number(afterSalesCase.shareUnitReward || 0)) * quantity') ||
source.includes('Number(afterSalesCase.shareUnitReward || 0)'),
'联动金额必须按一份价格乘以份数计算',
)
assert.ok(source.includes('resolutionRecoveryQuantity'), '弹窗必须回显当前追缴份数')
// 份数输入必须只在 shareId 分支(拼单)渲染,整单只展示发单金额。
const shareBranch = source.indexOf('afterSalesCase.shareId ? (')
const quantityLabel = source.indexOf('label="追缴份数"')
assert.ok(shareBranch > 0 && quantityLabel > shareBranch, '份数输入必须在拼单分支内')
const wholeOrderAlert = source.indexOf('(整单)')
const nextRecoveryField = source.indexOf('label="追缴金额"', wholeOrderAlert)
assert.ok(wholeOrderAlert > quantityLabel, '整单分支应在拼单分支之后')
assert.ok(
!source.slice(wholeOrderAlert, nextRecoveryField).includes('recoveryQuantity'),
'整单分支不能包含份数输入',
)
})
test('建单份额下拉:每个份额要显示份数、一份价格与合计金额', () => {
const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx')
assert.ok(source.includes('`${share.worker?.displayName'), '份额下拉仍要显示打手名')
assert.ok(source.includes('一份 ${formatMoney(share.unitReward)}'), '份额下拉必须显示一份价格')
assert.ok(source.includes('合计 ${formatMoney(share.shareReward)}'), '份额下拉必须显示合计金额')
})
test('售后单类型必须包含金额字段,否则前端读取为 undefined', () => {
const source = readSource('../src/types/worker-platform.ts')
for (const field of [
'workOrderRewardAmount: number',
'sharingEnabled: boolean',
'shareUnitReward: number',
'shareRewardAmount: number',
'suggestedRecoveryAmount: number',
]) {
assert.ok(source.includes(field), `WorkerAfterSalesCase 缺少 ${field}`)
}
})