diff --git a/apps/backend/src/domain/consignment-deposit-policy.test.ts b/apps/backend/src/domain/consignment-deposit-policy.test.ts index 2db1094a..3215adba 100644 --- a/apps/backend/src/domain/consignment-deposit-policy.test.ts +++ b/apps/backend/src/domain/consignment-deposit-policy.test.ts @@ -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('覆盖押金处置时视为需要留原因', () => { diff --git a/apps/backend/src/domain/consignment-deposit-policy.ts b/apps/backend/src/domain/consignment-deposit-policy.ts index 9c49e19e..16998605 100644 --- a/apps/backend/src/domain/consignment-deposit-policy.ts +++ b/apps/backend/src/domain/consignment-deposit-policy.ts @@ -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' } diff --git a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts index 63e1a7c8..930ba81b 100644 --- a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts @@ -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, { diff --git a/apps/backend/src/services/worker-platform/consignment-revoke-policy.test.ts b/apps/backend/src/services/worker-platform/consignment-revoke-policy.test.ts index be325973..c6a8ec43 100644 --- a/apps/backend/src/services/worker-platform/consignment-revoke-policy.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-revoke-policy.test.ts @@ -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 }'), diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 2ebb2476..932ed2c5 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -2489,7 +2489,11 @@ test('确认加好友后的取消/退单/作废路径都要没收卖家发布押 ) assert.ok( revokeBody.includes('forfeitConsignmentPublishDepositBySubmissionWithClient('), - '撤销寄售履约在已确认加好友后不能再退卖家押金', + '撤销寄售履约在运营显式选择扣除时仍要没收卖家押金', + ) + assert.ok( + revokeBody.includes("const archiveOnly = input.action === 'archive_inventory'"), + '撤销寄售履约不能再因已确认加好友强制作废', ) }) diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 5fa730da..b97ae37f 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -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() { 按规则自动 - (已确认加好友则扣除,未加好友则全额退还) + (默认全额退还;要扣请选「全额扣除」) 全额退还打手 @@ -3610,7 +3610,7 @@ export default function WorkOrdersPanel() { 按规则自动 (推荐) - (退单回库继续卖时保持冻结;若已确认加好友则没收;作废退单时退还) + (回库继续卖则保持冻结;作废则退还。买家退款不扣卖家押金,要扣请选「扣除没收」)