删除库存和旧回调管理
This commit is contained in:
@@ -31,58 +31,12 @@ function createTaskFixture(patch = {}) {
|
||||
requires_claim: true,
|
||||
claim_token: '',
|
||||
primary_claim_token_id: null,
|
||||
primary_inventory_item_id: null,
|
||||
last_error: '',
|
||||
context_json: JSON.stringify({
|
||||
inventorySkuCode: 'dnf-cdk-a',
|
||||
primaryRequirement: {
|
||||
roleKey: 'primary_code',
|
||||
credentialType: 'claim_code',
|
||||
},
|
||||
}),
|
||||
context_json: '{}',
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
|
||||
test('syncDeliveryTasksForOrderWithDeps moves paid claim task to waiting inventory when reservation fails', async () => {
|
||||
const updates = []
|
||||
const reserveCalls = []
|
||||
|
||||
const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, orderItems, {
|
||||
listTasksByOrderId: async () => [createTaskFixture()],
|
||||
reserveInventoryForTask: async (payload) => {
|
||||
reserveCalls.push(payload)
|
||||
return null
|
||||
},
|
||||
updateTask: async (taskId, patch) => {
|
||||
updates.push({ taskId, patch })
|
||||
return { id: taskId, ...patch }
|
||||
},
|
||||
nowIso: () => '2026-04-14T12:00:00.000Z',
|
||||
})
|
||||
|
||||
assert.deepEqual(reserveCalls, [
|
||||
{
|
||||
skuCode: 'dnf-cdk-a',
|
||||
taskId: 30,
|
||||
credentialType: 'claim_code',
|
||||
roleKey: 'primary_code',
|
||||
},
|
||||
])
|
||||
assert.deepEqual(updates, [
|
||||
{
|
||||
taskId: 30,
|
||||
patch: {
|
||||
task_status: 'waiting_inventory',
|
||||
inventory_status: 'pending',
|
||||
last_error: '库存不足,等待可用库存凭据',
|
||||
updated_at: '2026-04-14T12:00:00.000Z',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(result[0]?.task_status, 'waiting_inventory')
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrderWithDeps sends manual dispatch tasks to manual review', async () => {
|
||||
const notifications = []
|
||||
const updates = []
|
||||
@@ -150,7 +104,6 @@ test('syncDeliveryTasksForOrderWithDeps prepares kuaishou cloud task with claim
|
||||
taskId: 30,
|
||||
patch: {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
claim_token: 'claim-token',
|
||||
claim_expires_at: '2026-04-15T12:00:00.000Z',
|
||||
user_action_status: 'pending_claim',
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import {
|
||||
listFulfillmentProfileRequirements,
|
||||
resolveFulfillmentBinding,
|
||||
} from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { reserveInventoryForTask } from './inventory-service.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import type { InventoryItemRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
@@ -31,15 +29,6 @@ type FulfillmentBindingLike = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type FulfillmentRequirementLike = {
|
||||
role_key?: string
|
||||
roleKey?: string
|
||||
credential_type?: string
|
||||
credentialType?: string
|
||||
is_required?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type DeliveryTaskRow = TaskRow & {
|
||||
skuCode?: string
|
||||
skuName?: string
|
||||
@@ -49,21 +38,12 @@ type DeliveryTaskDeps = {
|
||||
createTask?: typeof createTask
|
||||
listTasksByOrderId?: typeof listTasksByOrderId
|
||||
updateTask?: typeof updateTask
|
||||
listFulfillmentProfileRequirements?: (
|
||||
profileId: number | string,
|
||||
) => Promise<FulfillmentRequirementLike[]>
|
||||
resolveFulfillmentBinding?: (input: {
|
||||
skuCode: string
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
}) => Promise<FulfillmentBindingLike | null>
|
||||
reserveInventoryForTask?: (input: {
|
||||
skuCode: string
|
||||
taskId: number | string
|
||||
credentialType?: string
|
||||
roleKey?: string
|
||||
}) => Promise<InventoryItemRow | null>
|
||||
createTaskClaimToken?: (taskId: number | string) => Promise<ClaimTokenLike>
|
||||
notifyTaskAutoManualReview?: (payload: {
|
||||
task: unknown
|
||||
@@ -77,18 +57,12 @@ type DeliveryTaskDeps = {
|
||||
type RuntimeDeliveryTaskDeps = Required<Pick<
|
||||
DeliveryTaskDeps,
|
||||
'updateTask'
|
||||
| 'reserveInventoryForTask'
|
||||
| 'createTaskClaimToken'
|
||||
| 'notifyTaskAutoManualReview'
|
||||
| 'nowIso'
|
||||
>>
|
||||
|
||||
type TaskContext = {
|
||||
inventorySkuCode?: string
|
||||
primaryRequirement?: {
|
||||
roleKey?: string
|
||||
credentialType?: string
|
||||
} | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -108,9 +82,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
createTask: createDeliveryTask = createTask,
|
||||
listTasksByOrderId: listTasks = listTasksByOrderId,
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
listFulfillmentProfileRequirements: listProfileRequirements = listFulfillmentProfileRequirements,
|
||||
resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding,
|
||||
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
@@ -119,7 +91,6 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
|
||||
const runtimeDeps: RuntimeDeliveryTaskDeps = {
|
||||
updateTask: updateDeliveryTask,
|
||||
reserveInventoryForTask: reserveInventory,
|
||||
createTaskClaimToken: createClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview,
|
||||
nowIso: getNowIso,
|
||||
@@ -154,8 +125,6 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
continue
|
||||
}
|
||||
const profile = binding
|
||||
const requirements = await listProfileRequirements(profile.profile_id || profile.id)
|
||||
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
||||
@@ -181,7 +150,6 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
profileId: Number(profile.profile_id || profile.id),
|
||||
executorKey: String(profile.executor_key || 'manual_dispatch'),
|
||||
taskStatus: initialStatus,
|
||||
inventoryStatus: profile.requires_claim ? 'pending' : 'not_required',
|
||||
deliveryStatus: 'pending',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
@@ -197,13 +165,6 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
profileName: String(profile.profile_name || profile.name || ''),
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
inventorySkuCode: item.sku_code,
|
||||
primaryRequirement: primaryRequirement
|
||||
? {
|
||||
roleKey: String(primaryRequirement.role_key || primaryRequirement.roleKey || 'primary_code'),
|
||||
credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'claim_code'),
|
||||
}
|
||||
: null,
|
||||
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
|
||||
? {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
@@ -303,16 +264,12 @@ async function preparePaidTask(
|
||||
): Promise<TaskRow | DeliveryTaskRow | null> {
|
||||
const {
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
nowIso: getNowIso = nowIso,
|
||||
} = deps
|
||||
|
||||
const now = getNowIso()
|
||||
const taskContext = parseTaskContext(task)
|
||||
const primaryRequirement = taskContext.primaryRequirement || null
|
||||
const inventorySkuCode = String(taskContext.inventorySkuCode || task.skuCode || '').trim()
|
||||
|
||||
if (['link_generated', 'claimed', 'role_confirmed', 'redeeming', 'redeemed', 'closed'].includes(task.task_status)) {
|
||||
return task
|
||||
@@ -344,7 +301,6 @@ async function preparePaidTask(
|
||||
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
@@ -357,7 +313,6 @@ async function preparePaidTask(
|
||||
const lastError = task.last_error || '当前任务需要人工履约处理'
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'not_required',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
@@ -370,71 +325,7 @@ async function preparePaidTask(
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
if (!inventorySkuCode) {
|
||||
const lastError = '未匹配到 SKU,无法为任务分配库存凭据'
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'missing_sku',
|
||||
})
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
if (task.primary_inventory_item_id && task.primary_claim_token_id) {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (!primaryRequirement?.credentialType) {
|
||||
const lastError = '履约档案未配置库存要求,无法自动分配库存'
|
||||
const updatedTask = await updateDeliveryTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
await notifyManualReview({
|
||||
task: updatedTask || task,
|
||||
reason: lastError,
|
||||
source: 'missing_inventory_requirement',
|
||||
})
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
const reserved = await reserveInventory({
|
||||
skuCode: inventorySkuCode,
|
||||
taskId: task.id,
|
||||
credentialType: primaryRequirement.credentialType,
|
||||
roleKey: primaryRequirement.roleKey || 'primary_code',
|
||||
})
|
||||
|
||||
if (!reserved) {
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'waiting_inventory',
|
||||
inventory_status: 'pending',
|
||||
last_error: '库存不足,等待可用库存凭据',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createClaimToken(task.id)
|
||||
|
||||
return updateDeliveryTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
inventory_status: 'reserved',
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
return task
|
||||
}
|
||||
|
||||
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
releaseReservedInventoryItemsWithDeps,
|
||||
reserveInventoryForTaskWithDeps,
|
||||
} from './inventory-service.js'
|
||||
|
||||
test('reserveInventoryForTaskWithDeps returns null when skuCode is missing', async () => {
|
||||
let findCalled = false
|
||||
|
||||
const result = await reserveInventoryForTaskWithDeps(
|
||||
{ skuCode: '', taskId: 12 },
|
||||
{
|
||||
findFirstAvailableInventoryItemBySkuCode: async () => {
|
||||
findCalled = true
|
||||
return null
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(result, null)
|
||||
assert.equal(findCalled, false)
|
||||
})
|
||||
|
||||
test('reserveInventoryForTaskWithDeps reserves first available inventory item with defaults', async () => {
|
||||
const calls = []
|
||||
const result = await reserveInventoryForTaskWithDeps(
|
||||
{ skuCode: 'dnf-cdk-a', taskId: 88 },
|
||||
{
|
||||
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
|
||||
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
|
||||
return { id: 321 }
|
||||
},
|
||||
assignReservedInventoryItem: async (inventoryItemId, taskId, updatedAt, roleKey) => {
|
||||
calls.push(['assign', inventoryItemId, taskId, updatedAt, roleKey])
|
||||
return { id: inventoryItemId, reserved_by_task_id: taskId, role_key: roleKey, updated_at: updatedAt }
|
||||
},
|
||||
nowIso: () => '2026-04-14T12:00:00.000Z',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['find', 'dnf-cdk-a', 'claim_code', null],
|
||||
['assign', 321, 88, '2026-04-14T12:00:00.000Z', 'primary_code'],
|
||||
])
|
||||
assert.equal(result?.id, 321)
|
||||
assert.equal(result?.reserved_by_task_id, 88)
|
||||
})
|
||||
|
||||
test('reserveInventoryForTaskWithDeps forwards inventory group filters', async () => {
|
||||
const calls = []
|
||||
|
||||
await reserveInventoryForTaskWithDeps(
|
||||
{ skuCode: 'dnf-cdk-a', taskId: 88, inventoryGroupCodes: ['A组', 'B组'] },
|
||||
{
|
||||
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
|
||||
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
|
||||
return { id: 321 }
|
||||
},
|
||||
assignReservedInventoryItem: async () => ({ id: 321 }),
|
||||
nowIso: () => '2026-04-14T12:00:00.000Z',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['find', 'dnf-cdk-a', 'claim_code', ['A组', 'B组']],
|
||||
])
|
||||
})
|
||||
|
||||
test('reserveInventoryForTaskWithDeps returns null when restricted inventory groups are empty', async () => {
|
||||
let findCalled = false
|
||||
|
||||
const result = await reserveInventoryForTaskWithDeps(
|
||||
{ skuCode: 'dnf-cdk-a', taskId: 12, inventoryGroupCodes: [] },
|
||||
{
|
||||
findFirstAvailableInventoryItemBySkuCode: async () => {
|
||||
findCalled = true
|
||||
return null
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(result, null)
|
||||
assert.equal(findCalled, false)
|
||||
})
|
||||
|
||||
test('releaseReservedInventoryItemsWithDeps skips empty ids and keeps successful releases only', async () => {
|
||||
const calls = []
|
||||
const result = await releaseReservedInventoryItemsWithDeps(
|
||||
[0, null, 11, 12, '', 13],
|
||||
'2026-04-14T13:00:00.000Z',
|
||||
{
|
||||
releaseReservedInventoryItem: async (inventoryItemId, updatedAt) => {
|
||||
calls.push([inventoryItemId, updatedAt])
|
||||
return inventoryItemId === 12 ? null : { id: inventoryItemId }
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
[11, '2026-04-14T13:00:00.000Z'],
|
||||
[12, '2026-04-14T13:00:00.000Z'],
|
||||
[13, '2026-04-14T13:00:00.000Z'],
|
||||
])
|
||||
assert.deepEqual(result, [
|
||||
{ id: 11 },
|
||||
{ id: 13 },
|
||||
])
|
||||
})
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
assignReservedInventoryItem,
|
||||
findFirstAvailableInventoryItemBySkuCode,
|
||||
releaseReservedInventoryItem,
|
||||
} from '../../repositories/inventory-repo.js'
|
||||
import type { InventoryItemRow } from '../../types/repository-rows.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
type InventoryItemId = number | string | null | undefined
|
||||
type InventoryGroupCodesInput = string | string[] | null | undefined
|
||||
|
||||
type ReserveInventoryForTaskInput = {
|
||||
skuCode?: string
|
||||
taskId: number | string
|
||||
credentialType?: string
|
||||
roleKey?: string
|
||||
inventoryGroupCodes?: InventoryGroupCodesInput
|
||||
}
|
||||
|
||||
type ReserveInventoryForTaskDeps = {
|
||||
assignReservedInventoryItem?: (
|
||||
inventoryItemId: number | string,
|
||||
taskId: number | string,
|
||||
updatedAt: string,
|
||||
roleKey?: string,
|
||||
) => Promise<InventoryItemRow | null>
|
||||
findFirstAvailableInventoryItemBySkuCode?: (
|
||||
skuCode: string,
|
||||
credentialType?: string,
|
||||
inventoryGroupCodes?: InventoryGroupCodesInput,
|
||||
) => Promise<InventoryItemRow | null>
|
||||
nowIso?: () => string
|
||||
}
|
||||
|
||||
type ReleaseReservedInventoryItemsDeps = {
|
||||
releaseReservedInventoryItem?: (
|
||||
inventoryItemId: number | string,
|
||||
updatedAt: string,
|
||||
) => Promise<InventoryItemRow | null>
|
||||
}
|
||||
|
||||
export async function reserveInventoryForTask({
|
||||
skuCode,
|
||||
taskId,
|
||||
credentialType = 'claim_code',
|
||||
roleKey = 'primary_code',
|
||||
inventoryGroupCodes = null,
|
||||
}: ReserveInventoryForTaskInput): Promise<InventoryItemRow | null> {
|
||||
return reserveInventoryForTaskWithDeps(
|
||||
{ skuCode, taskId, credentialType, roleKey, inventoryGroupCodes },
|
||||
{
|
||||
assignReservedInventoryItem,
|
||||
findFirstAvailableInventoryItemBySkuCode,
|
||||
nowIso,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function reserveInventoryForTaskWithDeps(
|
||||
{
|
||||
skuCode,
|
||||
taskId,
|
||||
credentialType = 'claim_code',
|
||||
roleKey = 'primary_code',
|
||||
inventoryGroupCodes = null,
|
||||
}: ReserveInventoryForTaskInput,
|
||||
{
|
||||
assignReservedInventoryItem: assignReserved = assignReservedInventoryItem,
|
||||
findFirstAvailableInventoryItemBySkuCode: findAvailable = findFirstAvailableInventoryItemBySkuCode,
|
||||
nowIso: getNowIso = nowIso,
|
||||
}: ReserveInventoryForTaskDeps = {},
|
||||
): Promise<InventoryItemRow | null> {
|
||||
if (!skuCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(inventoryGroupCodes) && inventoryGroupCodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const available = await findAvailable(skuCode, credentialType, inventoryGroupCodes)
|
||||
|
||||
if (!available) {
|
||||
return null
|
||||
}
|
||||
|
||||
return assignReserved(available.id, taskId, getNowIso(), roleKey)
|
||||
}
|
||||
|
||||
export async function releaseReservedInventoryItems(
|
||||
inventoryItemIds: InventoryItemId[] = [],
|
||||
updatedAt = nowIso(),
|
||||
): Promise<InventoryItemRow[]> {
|
||||
return releaseReservedInventoryItemsWithDeps(
|
||||
inventoryItemIds,
|
||||
updatedAt,
|
||||
{ releaseReservedInventoryItem },
|
||||
)
|
||||
}
|
||||
|
||||
export async function releaseReservedInventoryItemsWithDeps(
|
||||
inventoryItemIds: InventoryItemId[] = [],
|
||||
updatedAt = nowIso(),
|
||||
{
|
||||
releaseReservedInventoryItem: releaseReserved = releaseReservedInventoryItem,
|
||||
}: ReleaseReservedInventoryItemsDeps = {},
|
||||
): Promise<InventoryItemRow[]> {
|
||||
const released: InventoryItemRow[] = []
|
||||
|
||||
for (const inventoryItemId of inventoryItemIds) {
|
||||
if (!inventoryItemId) {
|
||||
continue
|
||||
}
|
||||
|
||||
const item = await releaseReserved(inventoryItemId, updatedAt)
|
||||
if (item) {
|
||||
released.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return released
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { mergeWebhookOrderState } from './order-service.js'
|
||||
import { mergeSourceOrderState } from './order-service.js'
|
||||
|
||||
test('mergeWebhookOrderState keeps paid state when a late trade_create webhook arrives', () => {
|
||||
const merged = mergeWebhookOrderState(
|
||||
test('mergeSourceOrderState keeps paid state when a late created source event arrives', () => {
|
||||
const merged = mergeSourceOrderState(
|
||||
{
|
||||
order_status: 'paid',
|
||||
pay_status: 'paid',
|
||||
@@ -22,8 +22,8 @@ test('mergeWebhookOrderState keeps paid state when a late trade_create webhook a
|
||||
assert.equal(merged.paidAt, '2026-04-14T10:06:10.000Z')
|
||||
})
|
||||
|
||||
test('mergeWebhookOrderState upgrades unpaid orders when payment webhook arrives later', () => {
|
||||
const merged = mergeWebhookOrderState(
|
||||
test('mergeSourceOrderState upgrades unpaid orders when payment source event arrives later', () => {
|
||||
const merged = mergeSourceOrderState(
|
||||
{
|
||||
order_status: 'created',
|
||||
pay_status: 'unpaid',
|
||||
@@ -41,8 +41,8 @@ test('mergeWebhookOrderState upgrades unpaid orders when payment webhook arrives
|
||||
assert.equal(merged.paidAt, '2026-04-14T10:06:12.000Z')
|
||||
})
|
||||
|
||||
test('mergeWebhookOrderState preserves refunded status over older paid webhook', () => {
|
||||
const merged = mergeWebhookOrderState(
|
||||
test('mergeSourceOrderState preserves refunded status over older paid source event', () => {
|
||||
const merged = mergeSourceOrderState(
|
||||
{
|
||||
order_status: 'refunded',
|
||||
pay_status: 'refunded',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
|
||||
import { resolveOrderItemForFulfillment } from './product-match-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js'
|
||||
|
||||
type SourceOrderItem = {
|
||||
@@ -83,10 +83,6 @@ type OrderStateMergeResult = {
|
||||
|
||||
type StatusPriorityMap = Record<string, number>
|
||||
|
||||
export async function upsertOrderFromWebhook(event: SourceOrderEvent): Promise<UpsertOrderResult> {
|
||||
return upsertOrderFromSource(event, { sourceLabel: 'webhook' })
|
||||
}
|
||||
|
||||
export async function upsertOrderFromSource(
|
||||
event: SourceOrderEvent,
|
||||
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
|
||||
@@ -105,7 +101,7 @@ export async function upsertOrderFromSource(
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
logWebhook('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
|
||||
logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
@@ -126,7 +122,7 @@ export async function upsertOrderFromSource(
|
||||
const configuredItems = (resolvedItems as FulfillmentOrderItem[]).filter((item) => item.isConfigured)
|
||||
|
||||
if (configuredItems.length === 0) {
|
||||
logWebhook('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, {
|
||||
logIntegration('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
@@ -161,7 +157,7 @@ export async function upsertOrderFromSource(
|
||||
rawPayloadJson: JSON.stringify(event.rawPayload),
|
||||
paidAt: event.paidAt,
|
||||
}
|
||||
const mergedPayload = mergeWebhookOrderState(existing, basePayload)
|
||||
const mergedPayload = mergeSourceOrderState(existing, basePayload)
|
||||
|
||||
const order = existing
|
||||
? await updateOrder(existing.id, {
|
||||
@@ -195,7 +191,7 @@ export async function upsertOrderFromSource(
|
||||
|
||||
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
|
||||
|
||||
logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
|
||||
logIntegration('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
|
||||
orderId: order.id,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
@@ -235,7 +231,7 @@ const PAY_STATUS_PRIORITY = {
|
||||
refunded: 2,
|
||||
}
|
||||
|
||||
export function mergeWebhookOrderState(
|
||||
export function mergeSourceOrderState(
|
||||
existing: OrderStateInput | null | undefined,
|
||||
incoming: OrderStateInput | null | undefined,
|
||||
): OrderStateMergeResult {
|
||||
|
||||
Reference in New Issue
Block a user