fix(consignment): 撤销寄售履约按选择回库,不再因加好友强制作废扣押金
买家退款时应能把现货退回仓库继续匹配;卖家押金只在客服显式选择扣除时才没收。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user