fix(fulfillment): 冻结已分配订单的内部SKU事实
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { preserveAssignedOrderItemState, resolveOrderItemSyncPlan } from './order-item-repo.js'
|
||||
import {
|
||||
preserveAssignedOrderItemState,
|
||||
replaceOrderItemsWithExecutor,
|
||||
resolveOrderItemSyncPlan,
|
||||
} from './order-item-repo.js'
|
||||
|
||||
test('resolveOrderItemSyncPlan preserves matching order item ids across repeated source syncs', () => {
|
||||
const existingItems = [
|
||||
@@ -136,6 +140,8 @@ test('已分配订单重推时冻结原渠道决策与中文内部 SKU', () => {
|
||||
|
||||
const stabilized = preserveAssignedOrderItemState(existing as never, incoming)
|
||||
|
||||
assert.equal(stabilized.skuCode, 'lucky_coin_x2')
|
||||
assert.equal(stabilized.skuName, '幸运币2个')
|
||||
assert.deepEqual(stabilized.specJson, { skuNick: '幸运币2个', sourceSpec: 'new' })
|
||||
assert.deepEqual(stabilized.itemSnapshotJson, {
|
||||
externalSkuName: 'new title',
|
||||
@@ -147,3 +153,52 @@ test('已分配订单重推时冻结原渠道决策与中文内部 SKU', () => {
|
||||
affiliateDash: { sku: 'lucky_coin_x2' },
|
||||
})
|
||||
})
|
||||
|
||||
test('已分配订单重推写库时不会用平台新 SKU 覆盖冻结值', async () => {
|
||||
const existing = {
|
||||
id: '41',
|
||||
order_id: 7,
|
||||
fulfillment_status: 'assigned',
|
||||
fulfillment_channel: 'affiliate_dash',
|
||||
sku_code: 'lucky_coin_x2',
|
||||
sku_name: '幸运币2个',
|
||||
quantity: 1,
|
||||
spec_json: { skuNick: '幸运币2个' },
|
||||
item_snapshot_json: {
|
||||
internalSku: '幸运币2个',
|
||||
internalSkuChannel: 'affiliate_dash',
|
||||
isConfigured: true,
|
||||
fulfillmentRoute: { selectedExecutorKey: 'affiliate_dash' },
|
||||
affiliateDash: { sku: 'lucky_coin_x2' },
|
||||
},
|
||||
}
|
||||
const incoming = {
|
||||
skuCode: 'platform-new',
|
||||
skuName: '平台新名称',
|
||||
quantity: 1,
|
||||
specJson: JSON.stringify({ skuNick: '平台新名称' }),
|
||||
itemSnapshotJson: JSON.stringify({ externalSkuName: '平台新名称' }),
|
||||
createdAt: '2026-11-03T00:00:00.000Z',
|
||||
updatedAt: '2026-11-03T00:00:00.000Z',
|
||||
}
|
||||
const updateParams: unknown[][] = []
|
||||
let selectCount = 0
|
||||
|
||||
const executor = async (sql: string, params: unknown[] = []) => {
|
||||
if (sql.includes('SELECT *') && sql.includes('FROM order_items')) {
|
||||
selectCount += 1
|
||||
return { rows: selectCount === 1 ? [existing] : [] }
|
||||
}
|
||||
if (sql.includes('UPDATE order_items')) {
|
||||
updateParams.push(params)
|
||||
return { rows: [], rowCount: 1 }
|
||||
}
|
||||
throw new Error(`unexpected SQL: ${sql}`)
|
||||
}
|
||||
|
||||
await replaceOrderItemsWithExecutor(executor as never, 7, [incoming])
|
||||
|
||||
assert.equal(updateParams.length, 1)
|
||||
assert.equal(updateParams[0]?.[0], 'lucky_coin_x2')
|
||||
assert.equal(updateParams[0]?.[1], '幸运币2个')
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function replaceOrderItemsWithExecutor(
|
||||
|
||||
for (const update of plan.updates) {
|
||||
const effectiveItem = preserveAssignedOrderItemState(
|
||||
existingById.get(update.orderItemId),
|
||||
existingById.get(Number(update.orderItemId)),
|
||||
update.item,
|
||||
)
|
||||
await executor(
|
||||
@@ -107,8 +107,8 @@ export async function replaceOrderItemsWithExecutor(
|
||||
WHERE id = $7
|
||||
`,
|
||||
[
|
||||
update.item.skuCode,
|
||||
update.item.skuName,
|
||||
effectiveItem.skuCode,
|
||||
effectiveItem.skuName,
|
||||
update.item.quantity,
|
||||
safeJsonStringify(effectiveItem.specJson || '{}'),
|
||||
safeJsonStringify(effectiveItem.itemSnapshotJson || effectiveItem.specJson || '{}'),
|
||||
@@ -197,6 +197,8 @@ export function preserveAssignedOrderItemState(
|
||||
|
||||
return {
|
||||
...incoming,
|
||||
skuCode: existing.sku_code,
|
||||
skuName: existing.sku_name,
|
||||
specJson: {
|
||||
...incomingSpec,
|
||||
...(frozenInternalSku ? { skuNick: frozenInternalSku } : {}),
|
||||
@@ -342,25 +344,43 @@ export async function updateOrderItemSourceSnapshot(
|
||||
orderItemId: number | string,
|
||||
input: { skuName: string; itemSnapshotJson: string; updatedAt: string; specJson?: string },
|
||||
): Promise<OrderItemRow | null> {
|
||||
const specClause = input.specJson ? ', spec_json = $5::jsonb' : ''
|
||||
const params: unknown[] = [
|
||||
input.skuName,
|
||||
input.itemSnapshotJson,
|
||||
input.updatedAt,
|
||||
Number(orderItemId),
|
||||
]
|
||||
if (input.specJson) {
|
||||
params.push(input.specJson)
|
||||
}
|
||||
return withTransaction(async (client) => {
|
||||
const selected = await client.query<OrderItemRow>(
|
||||
`SELECT * FROM order_items WHERE id = $1 FOR UPDATE`,
|
||||
[Number(orderItemId)],
|
||||
)
|
||||
const existing = selected.rows[0]
|
||||
if (!existing) return null
|
||||
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
UPDATE order_items
|
||||
SET sku_name = $1, item_snapshot_json = $2::jsonb, updated_at = $3 ${specClause}
|
||||
WHERE id = $4
|
||||
RETURNING id
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return result.rows[0] ? getOrderItemById(orderItemId) : null
|
||||
const effectiveItem = preserveAssignedOrderItemState(existing, {
|
||||
skuCode: existing.sku_code,
|
||||
skuName: input.skuName,
|
||||
quantity: existing.quantity,
|
||||
specJson: input.specJson || existing.spec_json || '{}',
|
||||
itemSnapshotJson: input.itemSnapshotJson,
|
||||
createdAt: String(existing.created_at || input.updatedAt),
|
||||
updatedAt: input.updatedAt,
|
||||
})
|
||||
const specClause = input.specJson ? ', spec_json = $5::jsonb' : ''
|
||||
const params: unknown[] = [
|
||||
effectiveItem.skuName,
|
||||
effectiveItem.itemSnapshotJson,
|
||||
effectiveItem.updatedAt,
|
||||
Number(orderItemId),
|
||||
]
|
||||
if (input.specJson) {
|
||||
params.push(safeJsonStringify(effectiveItem.specJson || '{}'))
|
||||
}
|
||||
|
||||
const result = await client.query<OrderItemRow>(
|
||||
`
|
||||
UPDATE order_items
|
||||
SET sku_name = $1, item_snapshot_json = $2::jsonb, updated_at = $3 ${specClause}
|
||||
WHERE id = $4
|
||||
RETURNING *
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return result.rows[0] || null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ test(
|
||||
resolveInternalSkuMapping,
|
||||
upsertInternalSkuMapping,
|
||||
} = await import('./internal-sku-mapping-service.js')
|
||||
const { updateOrderItemSourceSnapshot } = await import('../../repositories/order-item-repo.js')
|
||||
const suffix = `${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
|
||||
const shopId = `it-routing-${suffix}`
|
||||
const manualSku = `集成测试人工-${suffix}`
|
||||
@@ -167,6 +168,15 @@ test(
|
||||
assert.equal(manualItem.rows[0]?.fulfillment_channel, 'manual')
|
||||
assert.ok(Number(manualItem.rows[0]?.fulfillment_config_id) > 0)
|
||||
|
||||
const sourceSyncedItem = await updateOrderItemSourceSnapshot(manualItemId, {
|
||||
skuName: `平台覆盖-${suffix}`,
|
||||
specJson: JSON.stringify({ skuNick: `平台覆盖-${suffix}` }),
|
||||
itemSnapshotJson: JSON.stringify({ externalSkuName: `平台覆盖-${suffix}` }),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
assert.equal(sourceSyncedItem?.sku_name, manualSku)
|
||||
assert.equal(sourceSyncedItem?.spec_json?.skuNick, manualSku)
|
||||
|
||||
const switched = await switchChannelForOrderItems({
|
||||
orderItemIds: [manualItemId],
|
||||
channel: 'third_party_voucher',
|
||||
|
||||
Reference in New Issue
Block a user