重构订单履约发货流程

This commit is contained in:
yml2213
2026-07-08 21:57:34 +08:00
parent 7b215a66a5
commit 9a24268b5a
17 changed files with 1415 additions and 810 deletions
@@ -2,38 +2,26 @@ import { createTask, listTasksByOrderId, updateTask } from '../../repositories/t
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createTaskClaimToken } from '../claim/claim-service.js'
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
import { prepareKuaishouFeifeiTask } from '../fulfillment/kuaishou-feifei/index.js'
import {
planFulfillmentTaskForOrderItem,
type FulfillmentBindingLike,
} from '../fulfillment/planner.js'
import { preparePaidFulfillmentTask } from '../fulfillment/executors/registry.js'
import type { FulfillmentPrepareDeps } from '../fulfillment/executors/types.js'
import { nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import {
TASK_STATUS,
isPaidPreparationStableStatus,
resolveInitialPaidTaskStatus,
shouldEnsureKuaishouCloudClaimLink,
} from '../../domain/task-status.js'
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
type JsonObject = Record<string, unknown>
type ClaimTokenLike = {
token: string
expired_at: string
[key: string]: unknown
}
type FulfillmentBindingLike = {
id: number
profile_id?: number
profile_key?: string
profile_name?: string
name?: string
executor_key?: string
requires_claim?: boolean
auto_dispatch?: boolean
config_json?: string | JsonObject
[key: string]: unknown
}
type DeliveryTaskRow = TaskRow & {
skuCode?: string
skuName?: string
@@ -54,17 +42,6 @@ type DeliveryTaskDeps = {
randomId?: (prefix?: string) => string
}
type RuntimeDeliveryTaskDeps = Required<
Pick<
DeliveryTaskDeps,
'updateTask' | 'createTaskClaimToken' | 'notifyTaskAutoManualReview' | 'nowIso'
>
>
type TaskContext = {
[key: string]: unknown
}
export async function syncDeliveryTasksForOrder(
order: OrderRow,
orderItems: OrderItemRow[],
@@ -88,7 +65,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
randomId: createRandomId = randomId,
} = deps
const runtimeDeps: RuntimeDeliveryTaskDeps = {
const runtimeDeps: FulfillmentPrepareDeps = {
updateTask: updateDeliveryTask,
createTaskClaimToken: createClaimToken,
notifyTaskAutoManualReview: notifyManualReview,
@@ -102,53 +79,30 @@ export async function syncDeliveryTasksForOrderWithDeps(
return existingTasks
}
const itemMap = new Map(orderItems.map((item) => [item.id, item]))
const preparedTasks = await Promise.all(
existingTasks.map((task) =>
preparePaidTask(
{
...task,
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
},
runtimeDeps,
),
),
)
return preparedTasks.filter(isTaskRow)
return preparePaidTasks(existingTasks, runtimeDeps)
}
const tasks: DeliveryTaskRow[] = []
for (const item of orderItems) {
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
const plan = await planFulfillmentTaskForOrderItem({
order,
item,
getProfileByKey,
})
if (!profile) {
if (!plan) {
continue
}
const itemSnapshot = parseJsonObject(item.item_snapshot_json)
const quantity = Math.max(1, Number(item.quantity || 1))
const fulfillmentConfig = parseJsonObject(profile.config_json)
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
const kuaishouFeifeiConfig = parseJsonObject(fulfillmentConfig.kuaishouFeifei)
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
const cloudSourceKeys = normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys)
const resolvedCloudSourceKey =
cloudSourceKeys.length === 1
? String(cloudtentaclesConfig.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
: ''
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
const primaryDeliveryItem = deliveryItems[0] || {
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
cloudSkuName: String(cloudtentaclesConfig.skuName || '').trim(),
quantity: 1,
}
for (let index = 0; index < quantity; index += 1) {
const createdAt = getNowIso()
const initialStatus =
order.pay_status === 'paid' ? resolvePaidTaskStatus(profile) : TASK_STATUS.PENDING_PAYMENT
order.pay_status === 'paid'
? resolvePaidTaskStatus(plan.profile)
: TASK_STATUS.PENDING_PAYMENT
const task = await createDeliveryTask({
orderId: order.id,
@@ -160,131 +114,20 @@ export async function syncDeliveryTasksForOrderWithDeps(
shopName: order.shop_name,
platformOrderId: order.platform_order_id,
taskNo: createRandomId('DT'),
profileId: Number(profile.profile_id || profile.id),
executorKey: String(profile.executor_key || 'manual_dispatch'),
profileId: plan.profileId,
executorKey: plan.executorKey,
taskStatus: initialStatus,
deliveryStatus: 'pending',
resultCode: '',
resultMessage: '',
claimToken: '',
claimExpiresAt: null,
automationMode: profile.auto_dispatch ? 'automatic' : 'manual',
requiresClaim: Boolean(profile.requires_claim),
userActionStatus: profile.requires_claim ? 'pending_claim' : 'not_required',
automationMode: plan.autoDispatch ? 'automatic' : 'manual',
requiresClaim: plan.requiresClaim,
userActionStatus: plan.requiresClaim ? 'pending_claim' : 'not_required',
attemptCount: 0,
lastError: '',
contextJson: JSON.stringify({
profileKey: String(profile.profile_key || ''),
profileName: String(profile.profile_name || profile.name || ''),
skuCode: item.sku_code,
skuName: item.sku_name,
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
? {
flowType: 'kuaishou_cloud_fulfillment',
configId: String(fulfillmentConfig.configId || '').trim(),
internalSkuCode: item.sku_code,
internalSkuName: item.sku_name,
deliveryItems,
ticket: {
code: '',
status: 'pending',
capturedAt: null,
capturedBy: null,
verifiedAt: null,
oid: '',
formToken: '',
leftCount: 0,
goodsTitle: '',
},
binding: {
prepareStatus: 'pending',
cloudSourceKeys,
resolvedSourceKey: resolvedCloudSourceKey,
skuId: primaryDeliveryItem.cloudSkuId,
skuName: primaryDeliveryItem.cloudSkuName,
vnKey: '1',
vnId: 0,
vnPhone: '',
bindUrl: '',
bindPreparedAt: null,
bindExpiresAt: null,
bindProbeAt: null,
bindProbeStatus: '',
bindProbeMessage: '',
},
role: {
status: 'pending',
name: '',
rid: '',
refreshedAt: null,
errorMessage: '',
rawInfo: null,
},
purchase: {
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
usedKnapsack: false,
purchaseTriggered: false,
assetBefore: 0,
assetAfter: 0,
purchaseAt: null,
},
dispatch: {
status: 'pending',
dispatchAt: null,
dispatchBy: null,
sendType: 0,
note: '',
},
returnNumber: {
status: 'pending',
returnedAt: null,
returnedBy: null,
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
},
consume: {
status: 'pending',
shopId: String(
kuaishouConsumeConfig.shopId ||
kuaishouShopConfig.shopId ||
itemSnapshot.shopId ||
order.shop_id ||
'',
).trim(),
shopName: String(
kuaishouConsumeConfig.shopName ||
kuaishouShopConfig.kshopName ||
itemSnapshot.shopName ||
order.shop_name ||
'',
).trim(),
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
consumedAt: null,
errorMessage: '',
},
notes: String(fulfillmentConfig.notes || '').trim(),
}
: null,
kuaishouFeifei: isKuaishouFeifeiExecutor(profile.executor_key)
? {
flowType: 'kuaishou_feifei',
productCode: String(kuaishouFeifeiConfig.productCode || '').trim(),
productName: String(kuaishouFeifeiConfig.productName || item.sku_name || '').trim(),
platformOrderNo: '',
orderNo: '',
rechargeStatus: 0,
rechargeStatusLabel: '',
rechargeResultMessage: '',
claimUrl: '',
consumeStatus: 'pending',
h5: {
entryUrl: '',
rechargeUrl: '',
},
lastSyncedAt: null,
}
: null,
}),
contextJson: JSON.stringify(plan.context),
createdAt,
updatedAt: createdAt,
})
@@ -303,336 +146,26 @@ export async function syncDeliveryTasksForOrderWithDeps(
return tasks
}
const preparedTasks = await Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
const preparedTasks = await Promise.all(
tasks.map((task) => preparePaidFulfillmentTask(task, runtimeDeps)),
)
return preparedTasks.filter(isTaskRow)
}
async function preparePaidTask(
task: DeliveryTaskRow,
deps: Partial<RuntimeDeliveryTaskDeps> = {},
): Promise<TaskRow | DeliveryTaskRow | null> {
const {
updateTask: updateDeliveryTask = updateTask,
createTaskClaimToken: createClaimToken = createTaskClaimToken,
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
nowIso: getNowIso = nowIso,
} = deps
const now = getNowIso()
if (isPaidPreparationStableStatus(task.task_status)) {
return task
}
if (isKuaishouCloudExecutor(task.executor_key)) {
if (shouldEnsureKuaishouCloudClaimLink(task.task_status)) {
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
const claimToken = await createClaimToken(task.id)
return updateDeliveryTask(task.id, {
claim_token: claimToken.token,
claim_expires_at: claimToken.expired_at,
user_action_status: 'pending_claim',
updated_at: now,
})
}
return task
}
const claimToken = await createClaimToken(task.id)
return updateDeliveryTask(task.id, {
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
claim_token: claimToken.token,
claim_expires_at: claimToken.expired_at,
user_action_status: 'pending_claim',
last_error: task.last_error || '领取链接已生成,等待客户提交核销码',
updated_at: now,
})
}
if (isKuaishouFeifeiExecutor(task.executor_key)) {
try {
return await prepareKuaishouFeifeiTask(task as TaskRow)
} catch (error) {
const message = error instanceof Error ? error.message : 'kuaishou-feifei 订单创建失败'
const updatedTask = await updateDeliveryTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
user_action_status: 'not_required',
last_error: message,
result_code: 'kuaishou_feifei_prepare_failed',
result_message: message,
updated_at: now,
})
await notifyManualReview({
task: updatedTask || task,
reason: message,
source: 'kuaishou_feifei_prepare_failed',
})
return updatedTask
}
}
if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
const lastError = task.last_error || '当前任务需要人工履约处理'
const updatedTask = await updateDeliveryTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
user_action_status: 'not_required',
last_error: lastError,
updated_at: now,
})
await notifyManualReview({
task: updatedTask || task,
reason: lastError,
source: 'manual_dispatch_profile',
})
return updatedTask
}
return task
async function preparePaidTasks(
tasks: TaskRow[],
deps: FulfillmentPrepareDeps,
) {
const preparedTasks = await Promise.all(
tasks.map((task) => preparePaidFulfillmentTask(task, deps)),
)
return preparedTasks.filter(isTaskRow)
}
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
return resolveInitialPaidTaskStatus(profile)
}
function parseTaskContext(task: { context_json?: unknown } | null | undefined): TaskContext {
const value = task?.context_json
if (!value) {
return {}
}
if (typeof value === 'object') {
return value as TaskContext
}
try {
const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as TaskContext)
: {}
} catch {
return {}
}
}
function parseJsonObject(value: unknown): JsonObject {
if (!value) {
return {}
}
if (typeof value === 'object' && !Array.isArray(value)) {
return value as JsonObject
}
try {
const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as JsonObject)
: {}
} catch {
return {}
}
}
async function resolveDynamicCloudtentaclesProfile(
item: OrderItemRow,
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
): Promise<FulfillmentBindingLike | null> {
const snapshot = parseJsonObject(item.item_snapshot_json)
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
const cloudSourceKeys = normalizeStringArray(cloudtentacles.cloudSourceKeys)
const deliveryItems = normalizeCloudDeliveryItems({
deliveryItems: cloudtentacles.deliveryItems,
skuId: cloudtentacles.cloudSkuId,
skuName: cloudtentacles.cloudSkuName || item.sku_name,
})
const primaryDeliveryItem = deliveryItems[0] || {
cloudSkuId: 0,
cloudSkuName: '',
quantity: 1,
}
if (
!primaryDeliveryItem.cloudSkuId ||
!primaryDeliveryItem.cloudSkuName ||
cloudSourceKeys.length === 0
) {
return null
}
const profile = await getProfileByKey('kuaishou_ct_assisted')
if (!profile) {
return null
}
return {
...profile,
profile_id: Number(profile.id || profile.profile_id || 0),
profile_key: 'kuaishou_ct_assisted',
profile_name: String(profile.profile_name || profile.name || '快手 cloud 履约').trim(),
executor_key: 'kuaishou_ct_assisted',
requires_claim: false,
auto_dispatch: false,
config_json: {
flowType: 'kuaishou_cloud_fulfillment',
configId: `${String(cloudtentacles.matchMode || 'cloudtentacles_name').trim()}:${String(cloudtentacles.normalizedProductName || primaryDeliveryItem.cloudSkuId).trim()}`,
cloudtentacles: {
cloudSourceKeys,
skuId: primaryDeliveryItem.cloudSkuId,
skuName: primaryDeliveryItem.cloudSkuName,
resolvedSourceKey:
cloudSourceKeys.length === 1
? String(cloudtentacles.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
: '',
deliveryItems,
vnKey: '1',
autoBuyEnabled: true,
minAssetReserve: 0,
autoReturnNumberAfterDispatch: true,
},
kuaishouConsume: {
shopId: String(snapshot.shopId || '').trim(),
shopName: String(snapshot.shopName || '').trim(),
autoConsumeAfterDispatch: false,
},
notes:
cloudtentacles.matchMode === 'cloudtentacles_override'
? '91卡券商品名命中 cloudtentacles 覆盖规则'
: '91卡券商品名自动匹配 cloudtentacles 商品',
},
}
}
async function resolveDynamicFulfillmentProfile(
item: OrderItemRow,
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
): Promise<FulfillmentBindingLike | null> {
return (
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
)
}
async function resolveDynamicKuaishouFeifeiProfile(
item: OrderItemRow,
getProfileByKey: (profileKey: string) => Promise<FulfillmentBindingLike | null>,
): Promise<FulfillmentBindingLike | null> {
const snapshot = parseJsonObject(item.item_snapshot_json)
const feifei = parseJsonObject(snapshot.kuaishouFeifei)
const productCode = String(feifei.productCode || '').trim()
if (!productCode) {
return null
}
const profile = await getProfileByKey('kuaishou_feifei')
if (!profile) {
return null
}
return {
...profile,
profile_id: Number(profile.id || profile.profile_id || 0),
profile_key: 'kuaishou_feifei',
profile_name: String(profile.profile_name || profile.name || 'kuaishou-feifei 履约').trim(),
executor_key: 'kuaishou_feifei',
requires_claim: true,
auto_dispatch: false,
config_json: {
flowType: 'kuaishou_feifei',
configId: `kuaishou_feifei:${productCode}`,
kuaishouFeifei: {
productCode,
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
matchMode: String(feifei.matchMode || 'kuaishou_feifei_name').trim(),
},
notes: '91卡券商品名自动匹配 kuaishou-feifei 商品映射',
},
}
}
function normalizeStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return []
}
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
}
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
cloudSkuId: number
cloudSkuName: string
quantity: number
}> {
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
const items = rawItems
.map((item) => normalizeCloudDeliveryItem(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
Boolean(item),
)
if (items.length > 0) {
return mergeCloudDeliveryItems(items)
}
const cloudSkuId = Number(value.skuId || 0) || 0
if (!cloudSkuId) {
return []
}
return [
{
cloudSkuId,
cloudSkuName: String(value.skuName || '').trim(),
quantity: 1,
},
]
}
function normalizeCloudDeliveryItem(value: unknown) {
const source =
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
return null
}
const quantity = Number(source.quantity || 1) || 1
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
}
}
function mergeCloudDeliveryItems(
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
) {
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
for (const item of items) {
const existing = merged.get(item.cloudSkuId)
if (existing) {
existing.quantity += item.quantity
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
continue
}
merged.set(item.cloudSkuId, { ...item })
}
return Array.from(merged.values())
}
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
return Boolean(task && Number(task.id || 0) > 0)
}
function isKuaishouCloudExecutor(value: unknown): boolean {
return String(value || '').trim() === 'kuaishou_ct_assisted'
}
function isKuaishouFeifeiExecutor(value: unknown): boolean {
return String(value || '').trim() === 'kuaishou_feifei'
}
@@ -4,14 +4,12 @@ import {
updateOrder,
} from '../../repositories/order-repo.js'
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
import { resolveOrderItemForFulfillment } from './product-match-service.js'
import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js'
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
import {
isKuaishouIndustryVoucherSendCallbackSuccess,
resolveKuaishouIndustryVoucherSendCallbackMessage,
} from '../platforms/kuaishou-industry/voucher-service.js'
resolveOrderFulfillmentReadiness,
syncOrderFulfillmentAttachments,
} from '../fulfillment/order-fulfillment-readiness-service.js'
import { nowIso } from '../../utils/time.js'
import { logIntegration } from '../../utils/logger.js'
import { createHttpError } from '../../utils/http.js'
@@ -189,19 +187,19 @@ export async function upsertOrderFromSource(
})),
)
const industryVoucherGate = await resolveKuaishouIndustryVoucherGate(order.platform_order_id)
if (!industryVoucherGate.allowed) {
const readiness = await resolveOrderFulfillmentReadiness(order)
if (!readiness.allowed) {
logIntegration('[order-service]', `${sourceLabel} 订单暂停履约:电子凭证发码回调未确认`, {
orderId: order.id,
provider: order.provider,
platform: order.platform,
platformOrderId: order.platform_order_id,
voucherCount: industryVoucherGate.voucherCount,
reason: industryVoucherGate.reason,
voucherCount: readiness.voucherCount,
reason: readiness.reason,
}, { level: 'warn' })
return {
ignoreReason: 'kuaishou_industry_send_callback_unconfirmed',
ignoreReason: readiness.blockReason || 'fulfillment_not_ready',
order,
orderItems,
tasks: [],
@@ -209,7 +207,7 @@ export async function upsertOrderFromSource(
}
const tasks = await syncDeliveryTasksForOrder(order, orderItems)
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
await syncOrderFulfillmentAttachments(order, tasks, {
source: `${sourceLabel}_order_upsert`,
now,
})
@@ -233,36 +231,6 @@ export async function upsertOrderFromSource(
}
}
async function resolveKuaishouIndustryVoucherGate(platformOrderId: unknown) {
const oid = String(platformOrderId || '').trim()
if (!oid) {
return {
allowed: true,
voucherCount: 0,
reason: '',
}
}
const vouchers = await listKuaishouIndustryVouchersByOid(oid)
if (vouchers.length === 0 || vouchers.every(isKuaishouIndustryVoucherSendCallbackSuccess)) {
return {
allowed: true,
voucherCount: vouchers.length,
reason: '',
}
}
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
return {
allowed: false,
voucherCount: vouchers.length,
reason: blockedVoucher
? resolveKuaishouIndustryVoucherSendCallbackMessage(blockedVoucher)
: '电子凭证发码回调未确认',
}
}
const ORDER_STATUS_PRIORITY = {
created: 0,
paid: 1,
@@ -1,236 +1,9 @@
import {
normalizeCloudtentaclesMatchName,
resolveCloudtentaclesSkuByProductName,
type CloudtentaclesNameMatchResult,
} from './cloudtentacles-name-match-service.js'
import {
resolveKuaishouFeifeiProductByName,
type KuaishouFeifeiProductMatch,
} from '../platforms/kuaishou-feifei/product-rule-service.js'
export type FulfillmentItem = {
itemId?: string
externalItemId?: string
skuCode?: string
skuName?: string
externalSkuCode?: string
externalSkuName?: string
quantity?: number
spec?: Record<string, unknown>
snapshot?: Record<string, unknown>
[key: string]: unknown
}
type ResolveOrderItemForFulfillmentInput = {
provider?: string
platform?: string
item?: FulfillmentItem
}
type HasConfiguredOrderItemsInput = Omit<ResolveOrderItemForFulfillmentInput, 'item'> & {
items?: FulfillmentItem[]
}
type FulfillmentItemCandidate = {
externalItemId: string
externalSkuCode: string
externalSkuName: string
externalSkuNameNormalized: string
resolvedSkuCode: string
cloudtentaclesNameMatch: CloudtentaclesNameMatchResult | null
kuaishouFeifeiMatch: KuaishouFeifeiProductMatch | null
isConfigured: boolean
}
export type ResolvedFulfillmentItem = FulfillmentItem & {
itemId: string
externalItemId: string
externalSkuCode: string
externalSkuName: string
skuCode: string
skuName: string
snapshot: Record<string, unknown>
isConfigured: boolean
}
export async function resolveOrderItemForFulfillment({
provider = '',
platform = '',
item = {},
}: ResolveOrderItemForFulfillmentInput): Promise<ResolvedFulfillmentItem> {
const candidate = await resolveConfiguredItemCandidate({
provider,
platform,
item,
})
const {
externalItemId,
externalSkuCode,
externalSkuName,
externalSkuNameNormalized,
cloudtentaclesNameMatch,
kuaishouFeifeiMatch,
} = candidate
const resolvedSkuCode = pickFirstNonEmpty([
cloudtentaclesNameMatch?.cloudSkuName,
kuaishouFeifeiMatch?.productCode,
externalSkuCode,
externalItemId,
])
const resolvedSkuName = pickFirstNonEmpty([
cloudtentaclesNameMatch?.cloudSkuName,
kuaishouFeifeiMatch?.skuName,
item.skuName,
externalSkuName,
resolvedSkuCode,
])
const snapshot = {
...(isPlainObject(item.snapshot) ? item.snapshot : {}),
externalItemId,
externalSkuCode,
externalSkuName,
externalSkuNameNormalized,
resolvedSkuCode,
matchMode: cloudtentaclesNameMatch?.matchMode || '',
cloudtentacles: cloudtentaclesNameMatch
? {
matchMode: cloudtentaclesNameMatch.matchMode,
productName: cloudtentaclesNameMatch.productName,
normalizedProductName: cloudtentaclesNameMatch.normalizedProductName,
cloudSkuId: cloudtentaclesNameMatch.cloudSkuId,
cloudSkuName: cloudtentaclesNameMatch.cloudSkuName,
cloudSkuPrice: cloudtentaclesNameMatch.cloudSkuPrice,
cloudSkuInventory: cloudtentaclesNameMatch.cloudSkuInventory,
cloudSourceKeys: cloudtentaclesNameMatch.cloudSourceKeys,
resolvedSourceKey: cloudtentaclesNameMatch.resolvedSourceKey,
deliveryItems: cloudtentaclesNameMatch.deliveryItems,
skuSnapshot: cloudtentaclesNameMatch.skuSnapshot,
}
: null,
kuaishouFeifei: kuaishouFeifeiMatch
? {
matchMode: kuaishouFeifeiMatch.matchMode,
productName: kuaishouFeifeiMatch.productName,
normalizedProductName: kuaishouFeifeiMatch.normalizedProductName,
productCode: kuaishouFeifeiMatch.productCode,
skuName: kuaishouFeifeiMatch.skuName,
}
: null,
isConfigured: candidate.isConfigured,
}
return {
...item,
itemId: externalItemId,
externalItemId,
externalSkuCode,
externalSkuName,
skuCode: resolvedSkuCode,
skuName: resolvedSkuName,
snapshot,
isConfigured: candidate.isConfigured,
}
}
export async function hasConfiguredOrderItems({
provider = '',
platform = '',
items = [],
}: HasConfiguredOrderItemsInput): Promise<boolean> {
const candidates = await Promise.all(
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
provider,
platform,
item,
})),
)
return candidates.some((item) => item.isConfigured)
}
export function normalizeProductName(value: unknown): string {
return normalizeCloudtentaclesMatchName(value)
}
function pickFirstNonEmpty(values: unknown[]): string {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized) {
return normalized
}
}
return ''
}
async function resolveConfiguredItemCandidate({
provider = '',
platform = '',
item = {},
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
const externalItemId = pickFirstNonEmpty([
item.externalItemId,
item.itemId,
])
const externalSkuCode = pickFirstNonEmpty([
item.externalSkuCode,
item.skuCode,
externalItemId,
])
const externalSkuName = pickFirstNonEmpty([
item.externalSkuName,
item.skuName,
externalSkuCode,
])
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
if (isOpen91KuaishouOrder(provider, platform)) {
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
const kuaishouFeifeiMatch = cloudtentaclesNameMatch
? null
: resolveKuaishouFeifeiProductByName(externalSkuName)
const resolvedSkuCode = pickFirstNonEmpty([
cloudtentaclesNameMatch?.cloudSkuName,
kuaishouFeifeiMatch?.productCode,
externalSkuCode,
externalItemId,
])
return {
externalItemId,
externalSkuCode,
externalSkuName,
externalSkuNameNormalized,
resolvedSkuCode,
cloudtentaclesNameMatch,
kuaishouFeifeiMatch,
isConfigured: Boolean(cloudtentaclesNameMatch || kuaishouFeifeiMatch),
}
}
const resolvedSkuCode = pickFirstNonEmpty([
externalSkuCode,
externalItemId,
])
return {
externalItemId,
externalSkuCode,
externalSkuName,
externalSkuNameNormalized,
resolvedSkuCode,
cloudtentaclesNameMatch: null,
kuaishouFeifeiMatch: null,
isConfigured: false,
}
}
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
return String(provider || '').trim() === '91kaquan'
&& String(platform || '').trim() === 'kuaishou'
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]'
}
export {
hasConfiguredOrderItems,
normalizeProductName,
resolveOrderItemForFulfillment,
} from '../fulfillment/product-resolution-service.js'
export type {
FulfillmentItem,
ResolvedFulfillmentItem,
} from '../fulfillment/product-resolution-service.js'