fix(worker-platform): 内部寄售现货履约发单金额对齐寄卖成本价并补充留痕追溯

1. 内部现货履约时原子更新工单 reward_amount 为寄卖结算成本价,解决工单列表显示旧规则价与寄卖成本冲突问题;
2. 工单资料 material.consignment 与工单事件 flow 补充 previousRewardAmountFen/updatedRewardAmountFen/costPriceFen 等价格变更留痕;
3. 撤销寄售履约且退回未指派状态时支持自动还原原工单发单金额并在撤销事件中记录还原明细;
4. 新增 consignment-fulfillment-price-trace.test.ts 专项单测并通过全量测试回归。
This commit is contained in:
yml2213
2026-09-19 10:22:09 +08:00
parent f816f57eb0
commit 439888631f
3 changed files with 96 additions and 9 deletions
@@ -99,6 +99,7 @@ export async function parkWorkOrderWaitingFriendAddWithClient(
actorName?: string | undefined
eventType?: string | undefined
materialJson?: unknown
rewardAmountFen?: number | null | undefined
extraPayload?: Record<string, unknown>
},
): Promise<WorkOrderRow | null> {
@@ -121,6 +122,10 @@ export async function parkWorkOrderWaitingFriendAddWithClient(
: JSON.stringify(input.materialJson)
const materialForInternalCheck =
input.materialJson === undefined ? workOrder.material_json : input.materialJson
const targetRewardAmountFen =
input.rewardAmountFen !== undefined && input.rewardAmountFen !== null
? Math.max(0, Math.round(Number(input.rewardAmountFen)))
: null
await client.query(
`
UPDATE work_orders
@@ -134,10 +139,19 @@ export async function parkWorkOrderWaitingFriendAddWithClient(
deadline_at = NULL,
accepted_at = CASE WHEN $4 THEN NULL ELSE accepted_at END,
material_json = COALESCE($5::jsonb, material_json),
reward_amount = COALESCE($7, reward_amount),
updated_at = $2::timestamptz
WHERE id = $6
`,
[nextStatus, input.now, cooldownHours, clearAcceptedAt, materialJson, workOrder.id],
[
nextStatus,
input.now,
cooldownHours,
clearAcceptedAt,
materialJson,
workOrder.id,
targetRewardAmountFen,
],
)
await createWorkOrderEventWithClient(client, {
workOrderId: Number(workOrder.id),
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFile } from 'node:fs/promises'
const FULFILLMENT_SERVICE = new URL('./consignment-fulfillment-service.ts', import.meta.url)
const GIFT_REPO = new URL('../../repositories/worker-platform/work-order-gift-repo.ts', import.meta.url)
test('内部现货履约必须将工单发单金额更新为寄卖成本价并记录留痕', async () => {
const source = await readFile(FULFILLMENT_SERVICE, 'utf8')
assert.ok(source.includes('const previousRewardAmountFen = Number(lockedOrder.reward_amount || 0)'))
assert.ok(source.includes('const costPriceFen = Math.max(0, Math.round(Number(inv.cost_price_fen || 0)))'))
assert.ok(source.includes('const effectiveRewardAmountFen = costPriceFen > 0 ? costPriceFen : previousRewardAmountFen'))
assert.ok(source.includes('previousRewardAmountFen,'))
assert.ok(source.includes('rewardAmountFen: effectiveRewardAmountFen,'))
assert.ok(source.includes('updatedRewardAmountFen: effectiveRewardAmountFen,'))
assert.ok(source.includes("priceAdjustReason: '寄卖内部现货履约按现货结算成本价更新发单金额'"))
})
test('parkWorkOrderWaitingFriendAddWithClient 支持原子更新 reward_amount', async () => {
const source = await readFile(GIFT_REPO, 'utf8')
assert.ok(source.includes('rewardAmountFen?: number | null | undefined'))
assert.ok(source.includes('reward_amount = COALESCE($7, reward_amount)'))
})
test('指派供货打手履约记录原始发单金额并写入事件留痕', async () => {
const source = await readFile(FULFILLMENT_SERVICE, 'utf8')
assert.ok(source.includes('const previousRewardAmountFen = Number(workOrder.reward_amount || 0)'))
assert.ok(source.includes("priceAdjustReason: '指派供货打手履约按现货结算成本价更新发单金额'"))
})
test('撤销寄售履约退回未分配时必须恢复原始发单金额并记录留痕', async () => {
const source = await readFile(FULFILLMENT_SERVICE, 'utf8')
assert.ok(source.includes('const previousRewardAmountFen ='))
assert.ok(source.includes('const restoreRewardAmount ='))
assert.ok(source.includes('reward_amount = CASE WHEN $6::boolean THEN $7 ELSE reward_amount END'))
assert.ok(source.includes('restoredRewardAmountFen: previousRewardAmountFen'))
assert.ok(source.includes("priceRestoreReason: '撤销寄售履约退回未分配恢复原始发单金额'"))
})
@@ -446,6 +446,10 @@ export async function fulfillWorkOrderWithConsignment(input: {
})
}
const previousRewardAmountFen = Number(lockedOrder.reward_amount || 0)
const costPriceFen = Math.max(0, Math.round(Number(inv.cost_price_fen || 0)))
const effectiveRewardAmountFen = costPriceFen > 0 ? costPriceFen : previousRewardAmountFen
const updatedMaterial = {
...material,
consignment: {
@@ -466,6 +470,8 @@ export async function fulfillWorkOrderWithConsignment(input: {
fulfillmentType: CONSIGNMENT_FULFILLMENT_TYPE.INTERNAL_STOCK,
fulfilledAt: now,
fulfilledBy: input.actorName,
previousRewardAmountFen,
rewardAmountFen: effectiveRewardAmountFen,
...(matchNote ? { matchNote } : {}),
...(input.allowSpecMismatch === true
? {
@@ -482,11 +488,15 @@ export async function fulfillWorkOrderWithConsignment(input: {
actorName: input.actorName,
eventType: 'consignment_fulfilled',
materialJson: updatedMaterial,
rewardAmountFen: effectiveRewardAmountFen,
extraPayload: {
inventoryId: inv.id,
inventoryNo: inv.inventory_no,
itemName: inv.item_name,
costPriceFen: inv.cost_price_fen,
previousRewardAmountFen,
updatedRewardAmountFen: effectiveRewardAmountFen,
priceAdjustReason: '寄卖内部现货履约按现货结算成本价更新发单金额',
buyerName: inv.buyer_name,
shopName,
matchType:
@@ -662,6 +672,15 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
const material = orderMaterial
const previousRewardAmountFen = Number(workOrder.reward_amount || 0)
const rewardAmountFen = resolveConsignmentAssignRewardFen({
costPriceFen: claimed.cost_price_fen,
orderRewardFen: workOrder.reward_amount,
// 指派对象是供货卖家本人 → 报酬改写为结算价(货款走工单报酬渠道);
// 指派了别的打手 → 报酬保持原值(货款走验收结算的供货货款渠道),两条渠道互斥防双付
assigneeIsSupplier: Number(targetWorkerId) === Number(inventory.worker_id || 0),
})
// 3. 将寄售手枪信息写入工单 material
const updatedMaterial = {
...material,
@@ -683,6 +702,8 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
fulfillmentType: 'worker_deposit',
assignedAt: now,
assignedBy: input.actorName,
previousRewardAmountFen,
rewardAmountFen,
...(matchNote ? { matchNote } : {}),
...(input.allowSpecMismatch === true
? {
@@ -694,14 +715,6 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
},
}
const rewardAmountFen = resolveConsignmentAssignRewardFen({
costPriceFen: claimed.cost_price_fen,
orderRewardFen: workOrder.reward_amount,
// 指派对象是供货卖家本人 → 报酬改写为结算价(货款走工单报酬渠道);
// 指派了别的打手 → 报酬保持原值(货款走验收结算的供货货款渠道),两条渠道互斥防双付
assigneeIsSupplier: Number(targetWorkerId) === Number(inventory.worker_id || 0),
})
await updateWorkOrder(workOrder.id, {
material_json: updatedMaterial,
reward_amount: rewardAmountFen,
@@ -724,6 +737,9 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
...(matchNote ? { matchNote } : {}),
workerId: targetWorkerId,
costPriceFen: claimed.cost_price_fen,
previousRewardAmountFen,
updatedRewardAmountFen: rewardAmountFen,
priceAdjustReason: '指派供货打手履约按现货结算成本价更新发单金额',
sourceSubmissionId: claimed.source_submission_id
? Number(claimed.source_submission_id)
: null,
@@ -1038,6 +1054,14 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
// 工单回到「未指派 / 已取消」前,要把上一次交付留下的指派、时限、验收草稿与
// 问题单痕迹一并清干净;否则重新指派时新打手会看到上一手的 deadline 与验收内容。
const previousRewardAmountFen =
consignmentInfo.previousRewardAmountFen !== undefined &&
consignmentInfo.previousRewardAmountFen !== null
? Number(consignmentInfo.previousRewardAmountFen)
: null
const restoreRewardAmount =
nextOrderStatus === WORK_ORDER_STATUS.UNASSIGNED && previousRewardAmountFen !== null
const woSql = `
UPDATE work_orders
SET
@@ -1054,6 +1078,7 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
published_at = NULL,
pinned_at = NULL,
${RESET_GIFT_FLOW_SQL},
reward_amount = CASE WHEN $6::boolean THEN $7 ELSE reward_amount END,
updated_at = $3
WHERE id = $4 AND status = $5
RETURNING *
@@ -1064,6 +1089,8 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
now,
lockedOrder.id,
lockedOrder.status,
restoreRewardAmount,
restoreRewardAmount ? previousRewardAmountFen : lockedOrder.reward_amount,
])
const wo = woRes.rows[0]
if (!wo) {
@@ -1093,6 +1120,14 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
workerDeposit: { mode: workerDepositMode, amount: workerDepositAmount },
consignmentDeposit: { action: consignmentDepositAction },
inventoryArchived: archiveOnly,
...(restoreRewardAmount
? {
restoredRewardAmountFen: previousRewardAmountFen,
previousConsignmentCostFen:
consignmentInfo.costPriceFen || inv.cost_price_fen || null,
priceRestoreReason: '撤销寄售履约退回未分配恢复原始发单金额',
}
: {}),
}),
now,
})