寄卖手动匹配改走内部现货并统一展示与超时口径
- 手动匹配不再指派供货打手:停在待加好友,清空截止时间与超时分钟 - 物品名模板去掉「0 璀璨」残留;寄卖单号无提报号时回退库存编号 - 状态列按寄卖库存显示内部匹配,打手列保留供货打手标签 - 超时配置增加启用开关,默认关闭(不限时) - 补充前后端回归测试
This commit is contained in:
@@ -41,6 +41,8 @@ export type ConsignmentInventoryRow = {
|
||||
source_submission_id: string | number | null
|
||||
/** 唯一锚点:来源提报单号(客服手工入库的现货没有) */
|
||||
source_submission_no?: string | null
|
||||
/** 来源提报单创建时间(打手提报时间) */
|
||||
source_submission_created_at?: string | null
|
||||
/** 提报凭证(卖家提报时上传的物品/主页截图) */
|
||||
submission_screenshots_json?: string | unknown[] | null
|
||||
/** 赠送凭证:冷却结束后上传的赠送完成截图(存在绑定工单的验收资料里) */
|
||||
@@ -331,6 +333,8 @@ export async function listConsignmentInventories(filters: {
|
||||
ci.*,
|
||||
wc.name AS category_name,
|
||||
cs.submission_no AS source_submission_no,
|
||||
|
||||
cs.created_at AS source_submission_created_at,
|
||||
cs.screenshots_json AS submission_screenshots_json,
|
||||
COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name,
|
||||
supplier_worker.phone AS live_worker_phone,
|
||||
@@ -496,6 +500,8 @@ export async function getConsignmentInventoryById(
|
||||
ci.*,
|
||||
wc.name AS category_name,
|
||||
cs.submission_no AS source_submission_no,
|
||||
|
||||
cs.created_at AS source_submission_created_at,
|
||||
cs.screenshots_json AS submission_screenshots_json,
|
||||
COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name,
|
||||
supplier_worker.phone AS live_worker_phone,
|
||||
@@ -531,6 +537,8 @@ export async function listConsignmentInventoriesByIds(
|
||||
ci.*,
|
||||
wc.name AS category_name,
|
||||
cs.submission_no AS source_submission_no,
|
||||
|
||||
cs.created_at AS source_submission_created_at,
|
||||
cs.screenshots_json AS submission_screenshots_json,
|
||||
COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name,
|
||||
supplier_worker.phone AS live_worker_phone,
|
||||
@@ -1166,6 +1174,8 @@ export async function listActiveConsignmentInventoriesForMatching(options?: {
|
||||
SELECT
|
||||
ci.*,
|
||||
cs.submission_no AS source_submission_no,
|
||||
|
||||
cs.created_at AS source_submission_created_at,
|
||||
COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name,
|
||||
supplier_worker.phone AS live_worker_phone,
|
||||
ccs.name AS contact_supporter_name
|
||||
@@ -1537,6 +1547,8 @@ export async function getConsignmentInventoryBySubmissionId(
|
||||
ci.*,
|
||||
wc.name AS category_name,
|
||||
cs.submission_no AS source_submission_no,
|
||||
|
||||
cs.created_at AS source_submission_created_at,
|
||||
COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name,
|
||||
supplier_worker.phone AS live_worker_phone,
|
||||
ccs.name AS contact_supporter_name
|
||||
|
||||
@@ -289,6 +289,7 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) {
|
||||
sourceSubmissionId: row.source_submission_id ? Number(row.source_submission_id) : null,
|
||||
/** 唯一锚点:卖家端看到的「寄卖单号」就是它,客服端也用同一个号 */
|
||||
sourceSubmissionNo: row.source_submission_no || '',
|
||||
sourceSubmissionCreatedAt: row.source_submission_created_at || null,
|
||||
...evidence,
|
||||
...(showingMatchedBuyer
|
||||
? {}
|
||||
|
||||
@@ -66,3 +66,30 @@ test('超时重匹配:立即尝试一次,失败回大厅', async () => {
|
||||
assert.ok(source.includes('fallbackConsignmentWorkOrderToHall'), '失败必须显式回大厅')
|
||||
assert.ok(!source.includes('reenterConsignmentInternalMatching'), '不得再回内部匹配池')
|
||||
})
|
||||
|
||||
test('超时默认不限时:配置缺省 0 分钟,匹配引擎不得回落到 30 分钟', async () => {
|
||||
const [engine, profile] = await Promise.all([
|
||||
readFile(ENGINE, 'utf8'),
|
||||
readFile(new URL('./consignment-profile-service.ts', import.meta.url), 'utf8'),
|
||||
])
|
||||
assert.ok(engine.includes('config?.timeout.minutes ?? 0'), '匹配引擎缺省必须是不限时(0)')
|
||||
assert.ok(!engine.includes('config?.timeout.minutes ?? 30'), '不得再默认 30 分钟')
|
||||
assert.ok(
|
||||
profile.includes('minutes: integer(timeout.minutes, 0,'),
|
||||
'配置归一化的缺省值必须是不限时',
|
||||
)
|
||||
})
|
||||
|
||||
test('手动匹配走内部现货:不指派供货打手、不生成截止时间', async () => {
|
||||
const source = await readFile(ENGINE, 'utf8')
|
||||
assert.ok(
|
||||
source.includes('if (!manual && !inventory.worker_id) return false'),
|
||||
'手动匹配必须允许没有打手账号的库存',
|
||||
)
|
||||
assert.ok(
|
||||
source.includes('parkWorkOrderWaitingFriendAddWithClient(client, current'),
|
||||
'手动匹配必须停在待加好友,由客服履约',
|
||||
)
|
||||
assert.ok(source.includes("fulfillmentType: 'internal_stock'"), 'material 必须标记内部现货')
|
||||
assert.ok(source.includes('timeout_minutes = 0'), '内部现货不设时限')
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isFriendGiftAcceptanceMode } from '../../domain/work-order-acceptance-m
|
||||
import {
|
||||
createWorkOrderEventWithClient,
|
||||
getWorkOrderById,
|
||||
parkWorkOrderWaitingFriendAddWithClient,
|
||||
type WorkOrderRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import {
|
||||
@@ -783,9 +784,11 @@ async function applyWholeOrderMatch(
|
||||
): Promise<boolean> {
|
||||
const { workOrder, profile } = input
|
||||
const inventory = candidate.inventory
|
||||
if (!workOrder.id || !inventory.worker_id) return false
|
||||
const now = nowIso()
|
||||
const manual = context.matchType === 'manual'
|
||||
if (!workOrder.id) return false
|
||||
// 自动匹配仍只认有供货打手的库存;手动匹配允许无打手库存(内部现货,客服代履约)。
|
||||
if (!manual && !inventory.worker_id) return false
|
||||
const now = nowIso()
|
||||
|
||||
type ApplyResult = { applied: boolean; sms?: ConsignmentMatchSmsPayload }
|
||||
const result = await withTransaction<ApplyResult>(async (client: PoolClient) => {
|
||||
@@ -845,8 +848,9 @@ async function applyWholeOrderMatch(
|
||||
(unitPriceFen || Math.max(0, Number(current.reward_amount || 0))) * requiredQuantity
|
||||
const serviceFeeFen = config ? buildServiceFeeFen(config, grossRewardFen) : 0
|
||||
const netRewardFen = Math.max(0, grossRewardFen - serviceFeeFen)
|
||||
const timeoutMinutes = config?.timeout.minutes ?? 30
|
||||
const timeoutMinutes = config?.timeout.minutes ?? 0
|
||||
const timeoutPolicy = config?.timeout.policy ?? 'reopen'
|
||||
const consignmentWorkerId = Number(inventory.worker_id || 0)
|
||||
const nextMaterial = {
|
||||
...material,
|
||||
consignment: {
|
||||
@@ -866,25 +870,114 @@ async function applyWholeOrderMatch(
|
||||
serviceFeeFen,
|
||||
netRewardFen,
|
||||
previousRewardAmountFen: originalRewardFen,
|
||||
workerId: Number(inventory.worker_id),
|
||||
workerName: inventory.worker_name || '',
|
||||
matchedAt: now,
|
||||
matchType: manual ? 'manual' : 'auto',
|
||||
...(manual
|
||||
? {
|
||||
manualMatch: true,
|
||||
// 手动匹配 = 内部现货履约:不把供货打手指派成接单人,
|
||||
// 工单停在「待加好友」,由客服使用库存账号完成加好友与赠送。
|
||||
workerPhone: inventory.worker_phone || '',
|
||||
buyerName: inventory.buyer_name || '',
|
||||
sellerGameId: inventory.seller_game_id || '',
|
||||
sellerContact: inventory.seller_contact || '',
|
||||
sellerPhone: inventory.seller_phone || '',
|
||||
contactSupporterId: inventory.contact_supporter_id
|
||||
? Number(inventory.contact_supporter_id)
|
||||
: null,
|
||||
contactSupporterName: inventory.contact_supporter_name || '',
|
||||
remark: inventory.remark || '',
|
||||
inboundBatch: inventory.inbound_batch,
|
||||
credentials: safeParseJson(inventory.credentials_json),
|
||||
screenshots: safeParseJson(
|
||||
inventory.screenshots_json || inventory.submission_screenshots_json || [],
|
||||
),
|
||||
costPriceFen: inventory.cost_price_fen,
|
||||
sourceSubmissionId: inventory.source_submission_id
|
||||
? Number(inventory.source_submission_id)
|
||||
: null,
|
||||
sourceSubmissionNo: inventory.source_submission_no || '',
|
||||
fulfillmentType: 'internal_stock',
|
||||
fulfilledAt: now,
|
||||
fulfilledBy: context.actorName || '',
|
||||
manualMatch: true,
|
||||
...(context.reason ? { matchNote: context.reason } : {}),
|
||||
...(context.allowNonExact ? { allowNonExact: true } : {}),
|
||||
...(context.specConflicts?.length
|
||||
? { specConflicts: [...context.specConflicts] }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
: {
|
||||
workerId: consignmentWorkerId,
|
||||
}),
|
||||
...(context.partial ? { partialMatch: true } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
if (manual) {
|
||||
// 内部现货履约:占库成功但不算已加好友,工单停在「待加好友」;
|
||||
// 不写接单人、不生成截止时间,后续由客服确认好友并走赠送冷却。
|
||||
const parked = await parkWorkOrderWaitingFriendAddWithClient(client, current, {
|
||||
now,
|
||||
actorName: context.actorName || 'admin',
|
||||
eventType: 'consignment_manual_matched',
|
||||
materialJson: nextMaterial,
|
||||
rewardAmountFen: netRewardFen,
|
||||
captureOriginalReward: netRewardFen !== originalRewardFen,
|
||||
extraPayload: {
|
||||
inventoryId: Number(inventory.id),
|
||||
inventoryNo: inventory.inventory_no,
|
||||
profileId: profile?.profileId ?? null,
|
||||
salePriceFen: inventorySalePriceFen(inventory),
|
||||
grossRewardFen,
|
||||
serviceFeeFen,
|
||||
netRewardFen,
|
||||
allocationId: allocation.id,
|
||||
matchType: 'manual',
|
||||
fulfillmentType: 'internal_stock',
|
||||
actorName: context.actorName || '',
|
||||
reason: context.reason || '',
|
||||
allowNonExact: Boolean(context.allowNonExact),
|
||||
specConflicts: context.specConflicts || [],
|
||||
...(context.partial ? { partialMatch: true } : {}),
|
||||
},
|
||||
})
|
||||
if (!parked) return { applied: false }
|
||||
|
||||
// 内部现货没有接单打手,也不参与超时扫描(deadline_at = NULL)。
|
||||
const refreshed = await client.query<WorkOrderRow>(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET assigned_worker_id = NULL,
|
||||
assigned_at = NULL,
|
||||
deadline_at = NULL,
|
||||
timeout_minutes = 0,
|
||||
timeout_policy = $2,
|
||||
hall_visible_at = NULL,
|
||||
internal_match_deadline_at = NULL,
|
||||
internal_match_attempts = COALESCE(internal_match_attempts, 0) + 1,
|
||||
rematch_excluded_worker_id = NULL,
|
||||
rematch_excluded_inventory_id = NULL,
|
||||
updated_at = $1
|
||||
WHERE id = $3
|
||||
RETURNING *
|
||||
`,
|
||||
[now, timeoutPolicy, current.id],
|
||||
)
|
||||
const updatedRow = refreshed.rows[0] || parked
|
||||
return {
|
||||
applied: true,
|
||||
sms: {
|
||||
workOrder: updatedRow,
|
||||
inventory,
|
||||
quantity: requiredQuantity,
|
||||
priceFen: unitPriceFen,
|
||||
matchType: 'manual',
|
||||
matchedAt: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await client.query<WorkOrderRow>(
|
||||
`
|
||||
UPDATE work_orders
|
||||
@@ -1064,7 +1157,7 @@ async function applySharingMatch(
|
||||
}
|
||||
|
||||
const config = profile?.config
|
||||
const timeoutMinutes = config?.timeout.minutes ?? 30
|
||||
const timeoutMinutes = config?.timeout.minutes ?? 0
|
||||
const timeoutPolicy = config?.timeout.policy ?? 'reopen'
|
||||
const deadline = addMinutesIso(timeoutMinutes, now)
|
||||
// 库存没有售价时退回订单单价,避免份额报酬为 0;有售价时始终以库存净售价为准。
|
||||
|
||||
@@ -791,7 +791,13 @@ async function main() {
|
||||
])
|
||||
const row = wo.rows[0]!
|
||||
assert(row.status === 'in_progress', `status=${row.status}`)
|
||||
assert(Number(row.assigned_worker_id) === sellerId, `assigned=${row.assigned_worker_id}`)
|
||||
assert(row.assigned_worker_id === null, '手动匹配走内部现货,不能把供货打手指派成接单人')
|
||||
assert(row.acceptance_mode === 'friend_gift', `mode=${row.acceptance_mode}`)
|
||||
assert(row.gift_phase === 'material_required', `gift=${row.gift_phase}`)
|
||||
assert(
|
||||
row.material_json.consignment.fulfillmentType === 'internal_stock',
|
||||
'material 未标记 internal_stock',
|
||||
)
|
||||
assert(Number(row.reward_amount) === 8550, `reward=${row.reward_amount}`)
|
||||
assert(
|
||||
Number(row.consignment_original_reward_fen) === 10000,
|
||||
|
||||
@@ -15,13 +15,24 @@ test('寄卖配置:缺省值必须可安全用于发布(保证金/服务费
|
||||
assert.equal(config.deposit.enabled, false)
|
||||
assert.equal(config.serviceFee.enabled, false)
|
||||
assert.equal(config.matching.autoMatchEnabled, false)
|
||||
assert.equal(config.timeout.minutes, 30)
|
||||
assert.equal(config.timeout.minutes, 0)
|
||||
assert.ok(Number.isFinite(Date.parse(config.matching.orderEffectiveFrom)))
|
||||
assert.equal(config.priceDrop.enabled, true)
|
||||
assert.equal(config.priceDrop.hours, 24)
|
||||
assert.equal(config.fields.quantity, true)
|
||||
})
|
||||
|
||||
test('寄卖配置:超时默认不限时,显式分钟数原样保留', () => {
|
||||
const unlimited = normalizeConsignmentProfileConfig({ timeout: { minutes: 0, policy: 'reopen' } })
|
||||
assert.equal(unlimited.timeout.minutes, 0)
|
||||
|
||||
const limited = normalizeConsignmentProfileConfig({
|
||||
timeout: { minutes: 45, policy: 'cancel_release' },
|
||||
})
|
||||
assert.equal(limited.timeout.minutes, 45)
|
||||
assert.equal(limited.timeout.policy, 'cancel_release')
|
||||
})
|
||||
|
||||
test('寄卖配置:非法枚举与越界数字回落到安全值', () => {
|
||||
const config = normalizeConsignmentProfileConfig({
|
||||
deposit: { mode: 'bad', fixedAmountFen: -1, unitAmountFen: 'x' },
|
||||
|
||||
@@ -180,7 +180,8 @@ export const DEFAULT_CONSIGNMENT_PROFILE_CONFIG: ConsignmentProfileConfig = {
|
||||
orderEffectiveFrom: '',
|
||||
},
|
||||
timeout: {
|
||||
minutes: 30,
|
||||
// 默认不限时(0 分钟):订单进入代练中后不设截止时间,也不会自动超时处置。
|
||||
minutes: 0,
|
||||
policy: 'reopen',
|
||||
},
|
||||
priceDrop: {
|
||||
@@ -348,7 +349,7 @@ export function normalizeConsignmentProfileConfig(value: unknown): ConsignmentPr
|
||||
orderEffectiveFrom,
|
||||
},
|
||||
timeout: {
|
||||
minutes: integer(timeout.minutes, 30, 0, 7 * 24 * 60),
|
||||
minutes: integer(timeout.minutes, 0, 0, 7 * 24 * 60),
|
||||
policy: timeoutPolicy,
|
||||
},
|
||||
priceDrop: {
|
||||
|
||||
@@ -263,6 +263,24 @@ test('动态规格:物品名模板与规格展示文本', () => {
|
||||
assert.equal(fallback, '苏尔南手枪')
|
||||
})
|
||||
|
||||
test('动态规格:璀璨数为 0 时物品名不残留「璀璨」单位', () => {
|
||||
const specs = surnanSpecs()
|
||||
const template = '{system}{series}{diamondLevel}钻{brillianceCount}璀璨 苏尔南'
|
||||
const zero = buildConsignmentItemName({
|
||||
template,
|
||||
specs,
|
||||
values: { ...surnanValues, brillianceCount: '0' },
|
||||
})
|
||||
assert.equal(zero, '苹果暗潮3钻 苏尔南')
|
||||
|
||||
const three = buildConsignmentItemName({
|
||||
template,
|
||||
specs,
|
||||
values: { ...surnanValues, brillianceCount: '3' },
|
||||
})
|
||||
assert.equal(three, '苹果暗潮3钻3璀璨 苏尔南', '璀璨数大于 0 时单位必须保留')
|
||||
})
|
||||
|
||||
test('动态规格:商品下拉不带进物品名时,用选中的商品名作物品名', () => {
|
||||
const productSpec: ConsignmentSpecField = {
|
||||
key: 'product',
|
||||
|
||||
@@ -752,6 +752,27 @@ export function buildConsignmentSpecText(
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 物品名模板里跟在占位符后面的固定单位。
|
||||
* 数值字段为 0 时 formatConsignmentSpecValue 返回空串,单位必须一并省略,
|
||||
* 否则会留下「2钻璀璨」这种没有数字的残句。
|
||||
*/
|
||||
const ITEM_NAME_UNIT_SUFFIX: Record<string, string> = {
|
||||
brillianceCount: '璀璨',
|
||||
diamondLevel: '钻',
|
||||
}
|
||||
|
||||
function fillItemNameTemplate(template: string, tokens: Map<string, string>): string {
|
||||
return template.replace(
|
||||
/\{([a-zA-Z][a-zA-Z0-9_]*)\}(璀璨|钻)?/g,
|
||||
(_, key: string, unit?: string) => {
|
||||
const value = tokens.get(key) || ''
|
||||
if (value) return unit ? `${value}${unit}` : value
|
||||
return ITEM_NAME_UNIT_SUFFIX[key] === unit ? '' : unit || ''
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function buildConsignmentItemName(input: {
|
||||
template: string
|
||||
specs: ConsignmentSpecField[]
|
||||
@@ -772,10 +793,7 @@ export function buildConsignmentItemName(input: {
|
||||
|
||||
let result = String(template || '').trim()
|
||||
if (result) {
|
||||
result = result.replace(
|
||||
/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g,
|
||||
(_, key: string) => tokenValues.get(key) || '',
|
||||
)
|
||||
result = fillItemNameTemplate(result, tokenValues)
|
||||
} else {
|
||||
// 优先用显式勾选「进物品名」的字段;模板驱动的商品下拉没有维度可拼,
|
||||
// 退化为取第一个选择型字段的选项名(就是商品名本身),否则物品名只剩分类兜底。
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { WorkOrderShareRow } from '../../repositories/worker-platform/types.js'
|
||||
import type { WorkOrderRow, WorkOrderShareRow } from '../../repositories/worker-platform/types.js'
|
||||
import {
|
||||
isWorkerOrderVipEvidencePending,
|
||||
mapWorkOrderAdmin,
|
||||
mapWorkOrderMaterialWithConsignmentWorker,
|
||||
pickWorkerRepresentativeShare,
|
||||
resolveWorkerOrderStatusTabs,
|
||||
@@ -298,3 +299,81 @@ test('寄售供货人改名后,工单 material 展示使用库存实时昵称'
|
||||
) as { consignment: { workerName: string } }
|
||||
assert.equal(noSnapshot.consignment.workerName, '')
|
||||
})
|
||||
|
||||
/**
|
||||
* 手动匹配改走内部现货后,工单上没有 `assigned_worker_id`。
|
||||
* 后台操作区必须按这个口径收敛:关闭订单/撤单依赖接单打手,内部现货单不能出现;
|
||||
* 客服仍需补资料、标问题、确认加好友。
|
||||
*/
|
||||
function internalStockOrder(overrides: Record<string, unknown> = {}): WorkOrderRow {
|
||||
return {
|
||||
id: 1001,
|
||||
work_order_no: 'WK26111323404271',
|
||||
platform_order_id: 'P-1001',
|
||||
product_name: '安卓怒嚎2钻 苏尔南',
|
||||
status: 'in_progress',
|
||||
acceptance_mode: 'friend_gift',
|
||||
gift_phase: 'material_required',
|
||||
gift_cooldown_hours: 72,
|
||||
gift_cooldown_minutes: 0,
|
||||
assigned_worker_id: null,
|
||||
reward_amount: 8550,
|
||||
timeout_minutes: 0,
|
||||
deadline_at: null,
|
||||
material_json: JSON.stringify({
|
||||
consignment: {
|
||||
inventoryId: 88,
|
||||
inventoryNo: 'INV20261113001',
|
||||
sourceSubmissionNo: 'SUB20261113001',
|
||||
fulfillmentType: 'internal_stock',
|
||||
workerName: '苏尔南2号小号',
|
||||
fulfilledAt: '2026-11-13T10:00:00.000Z',
|
||||
salePriceFen: 8550,
|
||||
quantity: 1,
|
||||
},
|
||||
}),
|
||||
created_at: '2026-11-13T09:00:00.000Z',
|
||||
updated_at: '2026-11-13T10:00:00.000Z',
|
||||
...overrides,
|
||||
} as unknown as WorkOrderRow
|
||||
}
|
||||
|
||||
test('内部现货手动匹配:操作区不出现关闭订单/撤单,保留客服跟进动作', () => {
|
||||
const mapped = mapWorkOrderAdmin(internalStockOrder())
|
||||
|
||||
assert.equal(mapped.status, 'in_progress')
|
||||
assert.equal(mapped.worker, null, '内部现货单没有接单打手')
|
||||
assert.equal(mapped.deadlineAt, null, '默认不限时')
|
||||
|
||||
// 关闭订单与撤单都以接单打手为前提,内部现货单必须隐藏(与正确示例图一致)
|
||||
assert.equal(mapped.capabilities.canCloseOrder, false)
|
||||
assert.equal(mapped.capabilities.canCancel, false)
|
||||
// 也不能再按「未匹配单」重新指派/编辑/删除
|
||||
assert.equal(mapped.capabilities.canAssign, false)
|
||||
assert.equal(mapped.capabilities.canEdit, false)
|
||||
assert.equal(mapped.capabilities.canDelete, false)
|
||||
assert.equal(mapped.capabilities.canReopen, false)
|
||||
|
||||
// 客服履约入口保留:更新资料、标记问题单、确认加好友
|
||||
assert.equal(mapped.capabilities.canUpdateMaterial, true)
|
||||
assert.equal(mapped.capabilities.canMarkProblem, true)
|
||||
assert.equal(mapped.capabilities.canConfirmFriend, true)
|
||||
assert.equal(mapped.capabilities.canCompleteGiftCooldown, false)
|
||||
assert.equal(mapped.capabilities.canAccept, false)
|
||||
})
|
||||
|
||||
test('内部现货进入赠送冷却:只出现结束冷却,仍无关闭订单/撤单', () => {
|
||||
const mapped = mapWorkOrderAdmin(
|
||||
internalStockOrder({
|
||||
gift_phase: 'friend_countdown',
|
||||
gift_available_at: '2099-01-01T00:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(mapped.capabilities.canCompleteGiftCooldown, true)
|
||||
assert.equal(mapped.capabilities.canConfirmFriend, false)
|
||||
assert.equal(mapped.capabilities.canCloseOrder, false)
|
||||
assert.equal(mapped.capabilities.canCancel, false)
|
||||
assert.equal(mapped.capabilities.canUpdateMaterial, true)
|
||||
assert.equal(mapped.capabilities.canMarkProblem, true)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user