fix(consignment): 撤销寄售履约按选择回库,不再因加好友强制作废扣押金

买家退款时应能把现货退回仓库继续匹配;卖家押金只在客服显式选择扣除时才没收。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-10-05 14:28:38 +08:00
co-authored by Cursor
parent 64110aa214
commit dbdafd3dc4
6 changed files with 45 additions and 75 deletions
@@ -21,49 +21,22 @@ test('押金处置选项归一:非法值一律退回按规则自动', () => {
assert.equal(normalizeConsignmentDepositHandling(null), 'auto')
})
test('打手履约押金:自动规则跟随"是否已确认加好友"', () => {
// 已确认加好友 → 自动没收
assert.equal(resolveWorkerDepositMode('auto', true), 'forfeit')
// 没确认加好友 → 自动全额退还
assert.equal(resolveWorkerDepositMode('auto', false), 'release')
assert.equal(resolveWorkerDepositMode(undefined, false), 'release')
test('打手履约押金:自动与退还都全额退还,只有显式扣除才没收', () => {
assert.equal(resolveWorkerDepositMode('auto'), 'release')
assert.equal(resolveWorkerDepositMode(undefined), 'release')
assert.equal(resolveWorkerDepositMode('refund'), 'release')
assert.equal(resolveWorkerDepositMode('forfeit'), 'forfeit')
})
test('打手履约押金:运营显式选择可以推翻自动规则', () => {
// 已确认加好友仍然选择退还
assert.equal(resolveWorkerDepositMode('refund', true), 'release')
// 没确认加好友也选择扣除
assert.equal(resolveWorkerDepositMode('forfeit', false), 'forfeit')
test('卖家发布押金:自动只看现货去向,加过好友也不再没收', () => {
assert.equal(resolveConsignmentDepositAction('auto', { archive: true }), 'release')
assert.equal(resolveConsignmentDepositAction('auto', { archive: false }), 'keep_frozen')
})
test('卖家发布押金:自动规则 = 已确认加好友没收 / 作废退还 / 回库保持冻结', () => {
assert.equal(
resolveConsignmentDepositAction('auto', { autoForfeit: true, archive: true }),
'forfeit',
)
assert.equal(
resolveConsignmentDepositAction('auto', { autoForfeit: true, archive: false }),
'forfeit',
)
assert.equal(
resolveConsignmentDepositAction('auto', { autoForfeit: false, archive: true }),
'release',
)
assert.equal(
resolveConsignmentDepositAction('auto', { autoForfeit: false, archive: false }),
'keep_frozen',
)
})
test('卖家发布押金:显式"保留冻结"不会去退也不会去打款流水', () => {
assert.equal(
resolveConsignmentDepositAction('keep_frozen', { autoForfeit: true, archive: true }),
'keep_frozen',
)
assert.equal(
resolveConsignmentDepositAction('forfeit', { autoForfeit: false, archive: true }),
'forfeit',
)
test('卖家发布押金:显式选择覆盖自动规则', () => {
assert.equal(resolveConsignmentDepositAction('keep_frozen', { archive: true }), 'keep_frozen')
assert.equal(resolveConsignmentDepositAction('forfeit', { archive: false }), 'forfeit')
assert.equal(resolveConsignmentDepositAction('forfeit', { archive: true }), 'forfeit')
})
test('覆盖押金处置时视为需要留原因', () => {
@@ -7,14 +7,14 @@
*/
export const WORKER_DEPOSIT_HANDLING = {
/** 按规则自动:已确认加好友则没收,否则全额退还 */
/** 按规则自动:全额退还。要扣除必须显式选择 forfeit */
AUTO: 'auto',
REFUND: 'refund',
FORFEIT: 'forfeit',
} as const
export const CONSIGNMENT_DEPOSIT_HANDLING = {
/** 按规则自动:已确认加好友则没收;否则作废时退还、回库时继续冻结 */
/** 按规则自动:作废退还、回库继续冻结。买家退款不因加过好友而没收,要扣必须显式选择 forfeit */
AUTO: 'auto',
/** 保持冻结(既不退还也不没收,留给后续人工处理) */
KEEP_FROZEN: 'keep_frozen',
@@ -48,31 +48,27 @@ export function normalizeConsignmentDepositHandling(value: unknown): Consignment
}
/**
* 打手履约押金:`auto` 时跟随「是否已确认加好友」——确认加好友意味着货已不可逆,
* 取消/脱离由打手侧承担损失。
* 打手履约押金:`auto` 与 `refund` 都全额退还。
* 已确认加好友不再自动没收;要扣必须显式选择 `forfeit`。
*/
export function resolveWorkerDepositMode(
handling: unknown,
autoForfeit: boolean,
): WorkerDepositMode {
export function resolveWorkerDepositMode(handling: unknown): WorkerDepositMode {
const normalized = normalizeWorkerDepositHandling(handling)
if (normalized === WORKER_DEPOSIT_HANDLING.REFUND) return 'release'
if (normalized === WORKER_DEPOSIT_HANDLING.FORFEIT) return 'forfeit'
return autoForfeit ? 'forfeit' : 'release'
return 'release'
}
/**
* 卖家发布押金:`auto` 时——已确认加好友一律没收;否则按现货去向定,
* 作废(货不再挂卖)退还,回库(继续挂卖)保持冻结。
* 卖家发布押金:`auto` 只看现货去向,不看是否加过好友。
* 作废(货不再挂卖)退还,回库(继续挂卖)保持冻结。买家退款走回库时不扣卖家押金。
* 要扣除必须显式选择 `forfeit`。
*/
export function resolveConsignmentDepositAction(
handling: unknown,
input: { autoForfeit: boolean; archive: boolean },
input: { archive: boolean },
): ConsignmentDepositAction {
const normalized = normalizeConsignmentDepositHandling(handling)
if (normalized === CONSIGNMENT_DEPOSIT_HANDLING.KEEP_FROZEN) return 'keep_frozen'
if (normalized === CONSIGNMENT_DEPOSIT_HANDLING.FORFEIT) return 'forfeit'
if (input.autoForfeit) return 'forfeit'
return input.archive ? 'release' : 'keep_frozen'
}
@@ -27,7 +27,6 @@ import {
type WorkOrderRow,
} from '../../repositories/worker-platform/index.js'
import { createHttpError } from '../../utils/http.js'
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
import {
normalizeConsignmentDepositHandling,
normalizeWorkerDepositHandling,
@@ -1000,17 +999,16 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
// 1. 处理关联库存解绑与状态流转。
// 必须带「仍被本工单占用」条件:否则会把别的工单正在使用的现货抢过来解绑,
// 造成同一件现货被多张工单履约。
// 已确认加好友 = 买家好友已加上、货随时可能送出去:不论运营选的是回库还是作废,
// 都只能作废下架,否则这件现货会回到在库池被下一个工单再次占用(二次出货)。
const autoForfeit = isWorkOrderGiftDepositForfeitable(lockedOrder)
const archiveOnly = autoForfeit || input.action === 'archive_inventory'
// 现货去向只看运营选择:回库 / 取消工单都退回在库继续匹配,只有显式「报废该现货」才作废。
// 已确认加好友不再强制作废。买家退款时货还在,回库才能继续卖;二次出货由运营确认后再选回库。
const archiveOnly = input.action === 'archive_inventory'
// SQL 与参数必须一起构造:作废分支要 5 个参数,回库分支只用 4 个。
// 之前两条分支共用 5 元参数数组,回库时 PostgreSQL 直接报
// "bind message supplies 5 parameters, but prepared statement requires 4",
// 于是「退单回库」这条最常用的路径必定 500。
const { sql: invSql, params: invParams } = buildConsignmentRevokeInventoryUpdate({
archiveOnly,
autoForfeit,
autoForfeit: false,
reason,
now,
inventoryId,
@@ -1025,10 +1023,9 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
})
}
// 卖家发布押金:默认走规则(已确认加好友→没收;作废→退还;回库→保持冻结),
// 运营可以显式覆盖成"保留冻结"或"扣除没收"。
// 卖家发布押金按选择走:auto 时回库保持冻结、作废退还;只有显式「扣除没收」才扣。
// 买家退款不因已确认加好友而没收卖家押金。
const consignmentDepositAction = resolveConsignmentDepositAction(input.consignmentDeposit, {
autoForfeit,
archive: archiveOnly,
})
const submissionId = consignmentInfo.sourceSubmissionId || inv.source_submission_id
@@ -1054,7 +1051,7 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
// 打手履约押金:撤销寄售履约会把工单打回未指派/已取消,必须同时结算打手押金,
// 否则这笔钱会一直冻着(以前这里只清 assigned_worker_id,押金没有出口)。
const revokedWorkerId = Number(lockedOrder.assigned_worker_id || 0)
const workerDepositMode = resolveWorkerDepositMode(input.workerDeposit, autoForfeit)
const workerDepositMode = resolveWorkerDepositMode(input.workerDeposit)
let workerDepositAmount = 0
if (revokedWorkerId > 0) {
workerDepositAmount = await settleWorkOrderDepositWithClient(client, {
@@ -17,13 +17,13 @@ test('撤销寄售履约必须结算打手履约押金,不能只清 assigned_w
assert.ok(source.includes('撤销寄售履约扣除履约押金'))
})
test('撤销寄售履约的现货去向由押金规则与运营选择共同决定', async () => {
test('撤销寄售履约的现货去向和押金都按运营选择,已确认加好友不再强制', async () => {
const source = await readServiceSource()
assert.ok(source.includes('const autoForfeit = isWorkOrderGiftDepositForfeitable(lockedOrder)'))
assert.ok(
source.includes("const archiveOnly = autoForfeit || input.action === 'archive_inventory'"),
)
assert.ok(source.includes("const archiveOnly = input.action === 'archive_inventory'"))
assert.ok(!source.includes('isWorkOrderGiftDepositForfeitable'))
assert.ok(!source.includes('autoForfeit || input.action'))
assert.ok(source.includes('resolveConsignmentDepositAction(input.consignmentDeposit'))
assert.ok(source.includes('resolveWorkerDepositMode(input.workerDeposit)'))
// 押金去向要写进工单事件,方便对账
assert.ok(
source.includes('workerDeposit: { mode: workerDepositMode, amount: workerDepositAmount }'),
@@ -2489,7 +2489,11 @@ test('确认加好友后的取消/退单/作废路径都要没收卖家发布押
)
assert.ok(
revokeBody.includes('forfeitConsignmentPublishDepositBySubmissionWithClient('),
'撤销寄售履约在已确认加好友后不能再退卖家押金',
'撤销寄售履约在运营显式选择扣除时仍要没收卖家押金',
)
assert.ok(
revokeBody.includes("const archiveOnly = input.action === 'archive_inventory'"),
'撤销寄售履约不能再因已确认加好友强制作废',
)
})
@@ -3523,13 +3523,13 @@ export default function WorkOrdersPanel() {
type="warning"
message={
revokeConsignmentModal.order?.gift?.friendAddedAt
? '该单已确认加好友:撤销时现货一律作废下架,不会退回在库池'
: '撤销后,关联寄售物品将退单回库;物品的【允许匹配】开关保持原设置不变'
? '该单已确认加好友:现货去向和押金都按下面的选择执行,不会强制作废,也不会自动扣卖家押金'
: '撤销后,关联寄售物品按你选择的方式处理;回库时【允许匹配】开关保持原设置不变'
}
description={
revokeConsignmentModal.order?.gift?.friendAddedAt
? '买家好友已经加上,货随时可能送出去,回池会被下一个工单再次占用(等于二次出货)。押金默认按「已确认加好友不退」处理,可在下方按需覆盖。'
: '只适用于【进行中 / 待验收 / 问题单】。已验收订单无法撤销(现货已交付、押金已结清),买家退款或纠纷请走售后流程。'
? '买家退款请选「退单回库」,现货回到在库继续匹配。请先确认货没有送出,避免回库后被下一单再次占用。只有账号失效才选「报废该现货」。卖家押金默认回库保持冻结、作废则退还;要扣除必须手动选择「扣除没收」。'
: '只适用于【进行中 / 待验收 / 问题单】。已验收订单无法撤销(现货已交付、押金已结清),买家退款请选「退单回库并取消工单」。'
}
style={{ marginBottom: 16 }}
/>
@@ -3583,7 +3583,7 @@ export default function WorkOrdersPanel() {
<Radio value="auto">
按规则自动
<span style={{ fontSize: 12, color: '#8c8c8c' }}>
(已确认加好友则扣除,未加好友则全额退还)
(默认全额退还;要扣请选「全额扣除」)
</span>
</Radio>
<Radio value="refund">全额退还打手</Radio>
@@ -3610,7 +3610,7 @@ export default function WorkOrdersPanel() {
<Radio value="auto">
按规则自动 (推荐)
<span style={{ fontSize: 12, color: '#8c8c8c' }}>
(退单回库继续卖时保持冻结;若已确认加好友则没收;作废退单时退还)
(回库继续卖则保持冻结;作废则退还。买家退款不扣卖家押金,要扣请选「扣除没收」)
</span>
</Radio>
<Radio value="keep_frozen">