重构订单履约发货流程

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
@@ -0,0 +1,106 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildClaimUrl } from '../claim/claim-service.js'
import { resolveTaskDeliveryLink } from './delivery-link-service.js'
import type { TaskRow } from '../../types/repository/rows.js'
test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou_ct_assisted',
primary_claim_token: 'cloud-token',
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
}))
assert.deepEqual(result, {
claimUrl: buildClaimUrl('cloud-token'),
expireTime: '2026-07-09T00:00:00.000Z',
})
})
test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou-industry',
claim_token: 'industry-token',
claim_expires_at: '2026-07-09T08:00:00.000Z',
}))
assert.deepEqual(result, {
claimUrl: buildClaimUrl('industry-token'),
expireTime: '2026-07-09T08:00:00.000Z',
})
})
test('resolveTaskDeliveryLink 为 feifei 任务返回已保存短链', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou_feifei',
context_json: {
kuaishouFeifei: {
shortLink: {
code: 'AbCd1234',
url: 'https://ks.khhao.com/s/AbCd1234',
targetUrl: 'https://feifei.example.com/h5/bind?code=very-long',
},
h5: {
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
},
},
},
}))
assert.deepEqual(result, {
claimUrl: 'https://ks.khhao.com/s/AbCd1234',
expireTime: '',
})
})
test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'manual_dispatch',
}))
assert.equal(result, null)
})
function createTask(patch: Partial<TaskRow> = {}): TaskRow {
return {
id: 123,
order_id: 456,
order_item_id: 789,
unit_index: 1,
platform_order_id: '2614900602069169',
profile_id: 0,
task_no: 'DT-test',
executor_key: 'kuaishou_ct_assisted',
task_status: 'pending_binding_prepare',
delivery_status: 'pending',
result_code: '',
result_message: '',
automation_mode: 'manual',
requires_claim: true,
user_action_status: 'pending_claim',
attempt_count: 0,
runtime_session_id: '',
login_type: '',
nickname: '',
role_name: '',
role_id: '',
area: '',
partition_name: '',
claim_token: '',
primary_claim_token: '',
primary_claim_token_id: null,
primary_claim_token_status: '',
artifacts_json: '{}',
context_json: '{}',
screenshot_path: '',
last_error: '',
retry_count: 0,
created_at: '2026-07-08T00:00:00.000Z',
updated_at: '2026-07-08T00:00:00.000Z',
claimed_at: null,
role_confirmed_at: null,
redeemed_at: null,
...patch,
}
}
@@ -0,0 +1,11 @@
import { resolveFulfillmentDeliveryLink } from './executors/registry.js'
import type { FulfillmentDeliveryLink } from './executors/types.js'
import type { TaskRow } from '../../types/repository/rows.js'
export type TaskDeliveryLink = FulfillmentDeliveryLink
export async function resolveTaskDeliveryLink(
task: TaskRow,
): Promise<TaskDeliveryLink | null> {
return resolveFulfillmentDeliveryLink(task)
}
@@ -0,0 +1,73 @@
import { buildClaimUrl } from '../../claim/claim-service.js'
import {
shouldEnsureKuaishouCloudClaimLink,
TASK_STATUS,
} from '../../../domain/task-status.js'
import { ensureTaskClaimLink } from '../kuaishou-cloud/index.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
} from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export const kuaishouCloudExecutor: FulfillmentExecutor = {
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
preparePaidTask,
resolveDeliveryLink,
}
async function preparePaidTask(
task: TaskRow,
deps: FulfillmentPrepareDeps,
): Promise<TaskRow | null> {
const now = deps.nowIso()
if (shouldEnsureKuaishouCloudClaimLink(task.task_status)) {
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
const claimToken = await deps.createTaskClaimToken(task.id)
return deps.updateTask(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 deps.createTaskClaimToken(task.id)
return deps.updateTask(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,
})
}
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
const expireTime = task.primary_claim_expires_at || task.claim_expires_at || ''
if (primaryToken) {
return {
claimUrl: buildClaimUrl(primaryToken),
expireTime,
}
}
try {
const ensured = await ensureTaskClaimLink(task)
return {
claimUrl: String(ensured.claimUrl || '').trim(),
expireTime: ensured.expiredAt || expireTime,
}
} catch {
return null
}
}
@@ -0,0 +1,62 @@
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
ensureKuaishouFeifeiClaimShortLink,
prepareKuaishouFeifeiTask,
resolveKuaishouFeifeiClaimUrl,
} from '../kuaishou-feifei/index.js'
import { parseTaskContext } from '../../../utils/task-json.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
} from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export const kuaishouFeifeiExecutor: FulfillmentExecutor = {
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
preparePaidTask,
resolveDeliveryLink,
}
async function preparePaidTask(
task: TaskRow,
deps: FulfillmentPrepareDeps,
): Promise<TaskRow | null> {
try {
return await prepareKuaishouFeifeiTask(task)
} catch (error) {
const message = error instanceof Error ? error.message : 'kuaishou-feifei 订单创建失败'
const updatedTask = await deps.updateTask(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: deps.nowIso(),
})
await deps.notifyTaskAutoManualReview({
task: updatedTask || task,
reason: message,
source: 'kuaishou_feifei_prepare_failed',
})
return updatedTask
}
}
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
let claimUrl = await ensureKuaishouFeifeiClaimShortLink(task)
if (!claimUrl) {
const context = parseTaskContext(task)
claimUrl = resolveKuaishouFeifeiClaimUrl(context.kuaishouFeifei)
}
if (!claimUrl) {
return null
}
return {
claimUrl,
expireTime: '',
}
}
@@ -0,0 +1,33 @@
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
} from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export const manualDispatchExecutor: FulfillmentExecutor = {
key: FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH,
preparePaidTask,
}
async function preparePaidTask(
task: TaskRow,
deps: FulfillmentPrepareDeps,
): Promise<TaskRow | null> {
const lastError = task.last_error || '当前任务需要人工履约处理'
const updatedTask = await deps.updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
user_action_status: 'not_required',
last_error: lastError,
updated_at: deps.nowIso(),
})
await deps.notifyTaskAutoManualReview({
task: updatedTask || task,
reason: lastError,
source: 'manual_dispatch_profile',
})
return updatedTask
}
@@ -0,0 +1,55 @@
import { isPaidPreparationStableStatus } from '../../../domain/task-status.js'
import { kuaishouCloudExecutor } from './kuaishou-cloud-executor.js'
import { kuaishouFeifeiExecutor } from './kuaishou-feifei-executor.js'
import { manualDispatchExecutor } from './manual-executor.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
isManualDispatchExecutor,
normalizeExecutorKey,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
} from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
const EXECUTORS = new Map<string, FulfillmentExecutor>([
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD, kuaishouCloudExecutor],
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY, kuaishouCloudExecutor],
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI, kuaishouFeifeiExecutor],
[FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH, manualDispatchExecutor],
])
export function getFulfillmentExecutor(executorKey: unknown): FulfillmentExecutor | null {
return EXECUTORS.get(normalizeExecutorKey(executorKey)) || null
}
export async function preparePaidFulfillmentTask(
task: TaskRow,
deps: FulfillmentPrepareDeps,
): Promise<TaskRow | null> {
if (isPaidPreparationStableStatus(task.task_status)) {
return task
}
const executor = getFulfillmentExecutor(task.executor_key)
if (executor?.preparePaidTask) {
return executor.preparePaidTask(task, deps)
}
if (!task.requires_claim || isManualDispatchExecutor(task.executor_key)) {
return manualDispatchExecutor.preparePaidTask?.(task, deps) || task
}
return task
}
export async function resolveFulfillmentDeliveryLink(
task: TaskRow,
): Promise<FulfillmentDeliveryLink | null> {
const executor = getFulfillmentExecutor(task.executor_key)
if (!executor?.resolveDeliveryLink) {
return null
}
return executor.resolveDeliveryLink(task)
}
@@ -0,0 +1,59 @@
import type { TaskUpdatePatch } from '../../../types/repository/inputs.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export const FULFILLMENT_EXECUTOR_KEYS = {
MANUAL_DISPATCH: 'manual_dispatch',
KUAISHOU_CLOUD: 'kuaishou_ct_assisted',
KUAISHOU_INDUSTRY: 'kuaishou-industry',
KUAISHOU_FEIFEI: 'kuaishou_feifei',
} as const
export type FulfillmentExecutorKey =
(typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {})
export type FulfillmentDeliveryLink = {
claimUrl: string
expireTime?: unknown
}
export type FulfillmentPrepareDeps = {
updateTask: (taskId: number | string, patch: TaskUpdatePatch) => Promise<TaskRow | null>
createTaskClaimToken: (taskId: number | string) => Promise<{
token: string
expired_at: string
[key: string]: unknown
}>
notifyTaskAutoManualReview: (payload: {
task: unknown
reason: string
source: string
}) => Promise<unknown> | unknown
nowIso: () => string
}
export type FulfillmentExecutor = {
key: FulfillmentExecutorKey
preparePaidTask?: (
task: TaskRow,
deps: FulfillmentPrepareDeps,
) => Promise<TaskRow | null>
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
}
export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {
return String(value || '').trim() as FulfillmentExecutorKey
}
export function isKuaishouCloudExecutor(value: unknown): boolean {
const executorKey = normalizeExecutorKey(value)
return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY
}
export function isKuaishouFeifeiExecutor(value: unknown): boolean {
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
}
export function isManualDispatchExecutor(value: unknown): boolean {
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
}
@@ -1,5 +1,5 @@
import { createHttpError } from "../../../utils/http.js";
import { normalizeProductName } from "../../order/product-match-service.js";
import { normalizeProductName } from "../product-resolution-service.js";
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
@@ -0,0 +1,92 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
ORDER_FULFILLMENT_BLOCK_REASONS,
resolveOrderFulfillmentReadiness,
} from './order-fulfillment-readiness-service.js'
import type { KuaishouIndustryVoucherRow } from '../../types/repository/rows.js'
test('resolveOrderFulfillmentReadiness 无电子凭证时允许继续履约', async () => {
const readiness = await resolveOrderFulfillmentReadiness(
{ platform_order_id: '2614900602069169' },
{
listKuaishouIndustryVouchersByOid: async () => [],
},
)
assert.deepEqual(readiness, {
allowed: true,
voucherCount: 0,
reason: '',
blockReason: '',
})
})
test('resolveOrderFulfillmentReadiness 电子凭证发码成功时允许继续履约', async () => {
const readiness = await resolveOrderFulfillmentReadiness(
{ platform_order_id: '2614900602069169' },
{
listKuaishouIndustryVouchersByOid: async () => [
createVoucher({ send_callback_status: 'success' }),
],
},
)
assert.deepEqual(readiness, {
allowed: true,
voucherCount: 1,
reason: '',
blockReason: '',
})
})
test('resolveOrderFulfillmentReadiness 电子凭证发码未确认时阻止履约', async () => {
const readiness = await resolveOrderFulfillmentReadiness(
{ platform_order_id: '2614900602069169' },
{
listKuaishouIndustryVouchersByOid: async () => [
createVoucher({
send_callback_status: 'pending',
send_callback_last_error: '回调等待中',
}),
],
},
)
assert.deepEqual(readiness, {
allowed: false,
voucherCount: 1,
reason: '回调等待中',
blockReason: ORDER_FULFILLMENT_BLOCK_REASONS.KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED,
})
})
function createVoucher(patch: Partial<KuaishouIndustryVoucherRow> = {}): KuaishouIndustryVoucherRow {
return {
id: 1,
voucher_code: 'ETICKET-1',
oid: '2614900602069169',
order_id: null,
task_id: null,
unit_index: 1,
seller_id: '',
token: '',
status: 'UNUSED',
valid_start_time: 0,
valid_end_time: 0,
consume_serial_num: '',
consume_details_json: [],
consumed_at: null,
destroyed_at: null,
send_callback_status: 'success',
send_callback_attempt_count: 0,
send_callback_last_error: '',
send_callback_response_json: {},
send_callback_sent_at: null,
raw_payload_json: {},
created_at: '2026-07-08T00:00:00.000Z',
updated_at: '2026-07-08T00:00:00.000Z',
...patch,
}
}
@@ -0,0 +1,63 @@
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js'
import {
isKuaishouIndustryVoucherSendCallbackSuccess,
resolveKuaishouIndustryVoucherSendCallbackMessage,
} from '../platforms/kuaishou-industry/voucher-service.js'
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
export const ORDER_FULFILLMENT_BLOCK_REASONS = {
KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED: 'kuaishou_industry_send_callback_unconfirmed',
} as const
type FulfillmentReadinessDeps = {
listKuaishouIndustryVouchersByOid?: typeof listKuaishouIndustryVouchersByOid
}
export async function resolveOrderFulfillmentReadiness(
order: Pick<OrderRow, 'platform_order_id'> | null | undefined,
deps: FulfillmentReadinessDeps = {},
) {
const listVouchers = deps.listKuaishouIndustryVouchersByOid || listKuaishouIndustryVouchersByOid
const oid = String(order?.platform_order_id || '').trim()
if (!oid) {
return {
allowed: true,
voucherCount: 0,
reason: '',
blockReason: '',
}
}
const vouchers = await listVouchers(oid)
if (vouchers.length === 0 || vouchers.every(isKuaishouIndustryVoucherSendCallbackSuccess)) {
return {
allowed: true,
voucherCount: vouchers.length,
reason: '',
blockReason: '',
}
}
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
return {
allowed: false,
voucherCount: vouchers.length,
reason: blockedVoucher
? resolveKuaishouIndustryVoucherSendCallbackMessage(blockedVoucher)
: '电子凭证发码回调未确认',
blockReason: ORDER_FULFILLMENT_BLOCK_REASONS.KUAISHOU_INDUSTRY_SEND_CALLBACK_UNCONFIRMED,
}
}
export async function syncOrderFulfillmentAttachments(
order: OrderRow,
tasks: TaskRow[] = [],
options: {
source?: string
now?: string
} = {},
): Promise<KuaishouIndustryVoucherRow[]> {
return bindKuaishouIndustryVouchersToOrderTasks(order, tasks, options)
}
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { planFulfillmentTaskForOrderItem } from './planner.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
test('planFulfillmentTaskForOrderItem 为 cloudtentacles 商品生成 cloud 履约计划', async () => {
const plan = await planFulfillmentTaskForOrderItem({
order: createOrder(),
item: createOrderItem({
sku_code: '荣耀勋章礼包(30个)',
sku_name: '荣耀勋章礼包(30个)',
item_snapshot_json: {
cloudtentacles: {
matchMode: 'cloudtentacles_name',
normalizedProductName: '荣耀勋章礼包(30个)',
cloudSourceKeys: ['account-a'],
cloudSkuId: 74,
cloudSkuName: '荣耀勋章礼包(30个)',
},
},
}),
getProfileByKey: async (profileKey) => ({
id: 9,
profile_key: profileKey,
name: '快手 cloud 履约',
executor_key: profileKey,
requires_claim: true,
auto_dispatch: true,
}),
})
assert.ok(plan)
assert.equal(plan.executorKey, 'kuaishou_ct_assisted')
assert.equal(plan.requiresClaim, false)
assert.equal(plan.autoDispatch, false)
assert.equal(plan.context.profileKey, 'kuaishou_ct_assisted')
assert.equal(
(plan.context.kuaishouCloudFulfillment as any).binding.resolvedSourceKey,
'account-a',
)
assert.equal((plan.context.kuaishouCloudFulfillment as any).binding.skuId, 74)
assert.equal(plan.context.kuaishouFeifei, null)
})
test('planFulfillmentTaskForOrderItem 为 feifei 商品生成 feifei 履约计划', async () => {
const plan = await planFulfillmentTaskForOrderItem({
order: createOrder(),
item: createOrderItem({
sku_code: 'FF-1001',
sku_name: '测试皮肤',
item_snapshot_json: {
kuaishouFeifei: {
productCode: 'FF-1001',
skuName: '测试皮肤',
matchMode: 'kuaishou_feifei_name',
},
},
}),
getProfileByKey: async (profileKey) => ({
id: 10,
profile_key: profileKey,
name: 'kuaishou-feifei 履约',
executor_key: profileKey,
requires_claim: false,
auto_dispatch: true,
}),
})
assert.ok(plan)
assert.equal(plan.executorKey, 'kuaishou_feifei')
assert.equal(plan.requiresClaim, true)
assert.equal(plan.autoDispatch, false)
assert.equal(plan.context.kuaishouCloudFulfillment, null)
assert.equal((plan.context.kuaishouFeifei as any).productCode, 'FF-1001')
assert.equal((plan.context.kuaishouFeifei as any).productName, '测试皮肤')
})
test('planFulfillmentTaskForOrderItem 未命中平台配置时返回 null', async () => {
const plan = await planFulfillmentTaskForOrderItem({
order: createOrder(),
item: createOrderItem({
item_snapshot_json: {
cloudtentacles: null,
kuaishouFeifei: null,
},
}),
getProfileByKey: async () => null,
})
assert.equal(plan, null)
})
function createOrder(patch: Partial<OrderRow> = {}): OrderRow {
return {
id: 1,
provider: '91kaquan',
platform: 'kuaishou',
shop_id: '91kaquan',
shop_name: '91卡券',
platform_order_id: '2614900602069169',
order_status: 'paid',
pay_status: 'paid',
buyer_id: '',
buyer_name: '',
receiver_contact: '',
total_amount: 0,
currency: 'CNY',
raw_payload_json: '{}',
paid_at: '2026-07-08T00:00:00.000Z',
created_at: '2026-07-08T00:00:00.000Z',
updated_at: '2026-07-08T00:00:00.000Z',
...patch,
}
}
function createOrderItem(patch: Partial<OrderItemRow> = {}): OrderItemRow {
return {
id: 10,
order_id: 1,
sku_code: 'SKU-1',
sku_name: '测试商品',
quantity: 1,
spec_json: '{}',
item_snapshot_json: {},
created_at: '2026-07-08T00:00:00.000Z',
updated_at: '2026-07-08T00:00:00.000Z',
...patch,
}
}
@@ -0,0 +1,436 @@
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
isKuaishouCloudExecutor,
isKuaishouFeifeiExecutor,
} from './executors/types.js'
type JsonObject = Record<string, unknown>
export 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
}
export type FulfillmentProfileResolver = (
profileKey: string,
) => Promise<FulfillmentBindingLike | null>
export type FulfillmentTaskPlan = {
profile: FulfillmentBindingLike
profileId: number
profileKey: string
profileName: string
executorKey: string
requiresClaim: boolean
autoDispatch: boolean
context: JsonObject
}
export async function planFulfillmentTaskForOrderItem({
order,
item,
getProfileByKey = getFulfillmentProfileByKey,
}: {
order: OrderRow
item: OrderItemRow
getProfileByKey?: FulfillmentProfileResolver
}): Promise<FulfillmentTaskPlan | null> {
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
if (!profile) {
return null
}
const profileId = Number(profile.profile_id || profile.id || 0)
const profileKey = String(profile.profile_key || '').trim()
const profileName = String(profile.profile_name || profile.name || '').trim()
const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
.trim()
|| FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
const fulfillmentConfig = parseJsonObject(profile.config_json)
const context = buildFulfillmentTaskContext({
order,
item,
profile,
profileKey,
profileName,
executorKey,
fulfillmentConfig,
})
return {
profile,
profileId,
profileKey,
profileName,
executorKey,
requiresClaim: Boolean(profile.requires_claim),
autoDispatch: Boolean(profile.auto_dispatch),
context,
}
}
function buildFulfillmentTaskContext({
order,
item,
profileKey,
profileName,
executorKey,
fulfillmentConfig,
}: {
order: OrderRow
item: OrderItemRow
profile: FulfillmentBindingLike
profileKey: string
profileName: string
executorKey: string
fulfillmentConfig: JsonObject
}) {
const itemSnapshot = parseJsonObject(item.item_snapshot_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,
}
return {
profileKey,
profileName,
skuCode: item.sku_code,
skuName: item.sku_name,
kuaishouCloudFulfillment: isKuaishouCloudExecutor(executorKey)
? {
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(executorKey)
? {
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,
}
}
async function resolveDynamicFulfillmentProfile(
item: OrderItemRow,
getProfileByKey: FulfillmentProfileResolver,
): Promise<FulfillmentBindingLike | null> {
return (
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
)
}
async function resolveDynamicCloudtentaclesProfile(
item: OrderItemRow,
getProfileByKey: FulfillmentProfileResolver,
): 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(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)
if (!profile) {
return null
}
return {
...profile,
profile_id: Number(profile.id || profile.profile_id || 0),
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
profile_name: String(profile.profile_name || profile.name || '快手 cloud 履约').trim(),
executor_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
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 resolveDynamicKuaishouFeifeiProfile(
item: OrderItemRow,
getProfileByKey: FulfillmentProfileResolver,
): 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(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)
if (!profile) {
return null
}
return {
...profile,
profile_id: Number(profile.id || profile.profile_id || 0),
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
profile_name: String(profile.profile_name || profile.name || 'kuaishou-feifei 履约').trim(),
executor_key: FULFILLMENT_EXECUTOR_KEYS.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 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 {}
}
}
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())
}
@@ -0,0 +1,236 @@
import {
normalizeCloudtentaclesMatchName,
resolveCloudtentaclesSkuByProductName,
type CloudtentaclesNameMatchResult,
} from '../order/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]'
}
@@ -1,12 +1,7 @@
import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
import { listTasksByOrderId } from '../../repositories/task-repo.js'
import { buildClaimUrl } from '../claim/claim-service.js'
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js'
import {
ensureKuaishouFeifeiClaimShortLink,
resolveKuaishouFeifeiClaimUrl,
} from '../fulfillment/kuaishou-feifei/index.js'
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
import { logIntegration } from '../../utils/logger.js'
import {
OPEN_91_MANUAL_FAILED_STATUS,
@@ -107,28 +102,8 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
for (const task of tasks) {
let claimUrl = ''
let expireTime: unknown = task.primary_claim_expires_at || task.claim_expires_at || ''
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
if (String(task.executor_key || '').trim() === 'kuaishou_feifei') {
const context = parseJsonObject(task.context_json)
claimUrl = await ensureKuaishouFeifeiClaimShortLink(task)
if (!claimUrl) {
claimUrl = resolveKuaishouFeifeiClaimUrl(context.kuaishouFeifei)
}
expireTime = ''
} else if (primaryToken) {
claimUrl = buildClaimUrl(primaryToken)
} else if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
try {
const ensured = await ensureTaskClaimLink(task)
claimUrl = String(ensured.claimUrl || '').trim()
expireTime = ensured.expiredAt || expireTime
} catch {
claimUrl = ''
}
}
const deliveryLink = await resolveTaskDeliveryLink(task)
const claimUrl = String(deliveryLink?.claimUrl || '').trim()
if (!claimUrl) {
continue
@@ -141,7 +116,7 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
readyTaskIds.push(Number(task.id))
cardItems.push(buildOpen91CardItem({
claimUrl,
expireTime,
expireTime: deliveryLink?.expireTime || '',
}))
}
@@ -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'