feat(finance): 统一履约事实并改造人工账单分类

This commit is contained in:
yml2213
2026-10-23 13:35:48 +08:00
parent fb80cad695
commit 5d1ba11523
18 changed files with 764 additions and 123 deletions
@@ -1,8 +1,17 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
import type { OrderItemRow, OrderListRow } from '../../types/repository/rows.js'
import {
mapAdminOrderListItem,
resolveOrderFulfillmentSummary,
resolveVoucherSummary,
summarizeOrderItems,
} from './admin-order-read-helpers.js'
import type {
KuaishouIndustryVoucherRow,
OrderItemRow,
OrderListRow,
} from '../../types/repository/rows.js'
test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly for single item', () => {
const order: OrderListRow = {
@@ -62,6 +71,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo
skuNick: '安卓血宴2钻',
matchSku: '安卓血宴2钻',
quantity: 1,
fulfillmentMode: 'pending',
},
])
})
@@ -259,3 +269,92 @@ test('拼多多匹配 SKU 优先 sku_name,不用带地区的长 spec', () => {
assert.equal(mapped.matchSku, '安卓怒嚎1钻')
assert.equal(mapped.items?.[0]?.matchSku, '安卓怒嚎1钻')
})
function buildOrderForFulfillmentTest(): OrderListRow {
return {
id: 501,
provider: '91kaquan',
platform: 'kuaishou',
shop_id: 'shop-1',
shop_name: '测试店铺',
platform_order_id: 'TEST-ORDER-501',
order_status: 'paid',
pay_status: 'paid',
buyer_id: '',
buyer_name: '',
receiver_contact: '',
total_amount: 1000,
currency: 'CNY',
raw_payload_json: {},
paid_at: '2026-10-20T02:00:00.000Z',
created_at: '2026-10-20T02:00:00.000Z',
updated_at: '2026-10-20T02:00:00.000Z',
}
}
function buildOrderItemForFulfillmentTest(
id: number,
fulfillmentMode: string | null,
): OrderItemRow {
return {
id,
order_id: 501,
sku_code: `SKU-${id}`,
sku_name: `商品-${id}`,
quantity: 1,
spec_json: { itemTitle: `商品-${id}`, skuNick: `规格-${id}` },
fulfillment_mode: fulfillmentMode,
}
}
test('人工单即使有已核销快照也仍然判为 manual,核销码单独记状态', () => {
const vouchers = [{ status: 'CONSUMED' }] as unknown as KuaishouIndustryVoucherRow[]
const mapped = mapAdminOrderListItem(buildOrderForFulfillmentTest(), {
orderItems: [buildOrderItemForFulfillmentTest(1, 'manual')],
vouchers,
})
assert.equal(mapped.fulfillmentMode, 'manual')
assert.equal(mapped.manualItemCount, 1)
assert.equal(mapped.hasVoucher, true)
assert.equal(mapped.voucherState, 'consumed')
assert.equal(mapped.canClaimH5, false)
})
test('自动发货与历史平台按 fulfillment_mode 区分,不再按 provider 猜', () => {
const affiliate = resolveOrderFulfillmentSummary({
items: [buildOrderItemForFulfillmentTest(1, 'affiliate_dash')],
})
const legacy = resolveOrderFulfillmentSummary({
items: [buildOrderItemForFulfillmentTest(2, 'legacy_platform')],
})
assert.equal(affiliate.fulfillmentMode, 'affiliate_dash')
assert.equal(affiliate.canClaimH5, true)
assert.equal(legacy.fulfillmentMode, 'legacy_platform')
assert.equal(legacy.canClaimH5, false)
})
test('多商品订单出现不同履约方式时显示 mixed,并保留人工项计数', () => {
const summary = resolveOrderFulfillmentSummary({
items: [
buildOrderItemForFulfillmentTest(1, 'work_order'),
buildOrderItemForFulfillmentTest(2, 'manual'),
],
})
assert.equal(summary.fulfillmentMode, 'mixed')
assert.equal(summary.manualItemCount, 1)
assert.equal(summary.pendingItemCount, 0)
})
test('核销码状态汇总:无、单一、混合', () => {
assert.deepEqual(resolveVoucherSummary([]), { hasVoucher: false, voucherState: 'none' })
assert.deepEqual(
resolveVoucherSummary([
{ status: 'UNUSED' },
{ status: 'consumed' },
] as unknown as KuaishouIndustryVoucherRow[]),
{ hasVoucher: true, voucherState: 'mixed' },
)
})
@@ -7,7 +7,13 @@ import {
} from './admin-read-shared-helpers.js'
import { buildOrderFulfillmentProgress } from './admin-task-read-helpers.js'
import type { AdminOrderItemSummary, AdminOrderListItem } from '../../types/admin/read-models.js'
import type {
AdminOrderFulfillmentMode,
AdminOrderItemSummary,
AdminOrderListItem,
AdminOrderItemFulfillmentMode,
AdminVoucherState,
} from '../../types/admin/read-models.js'
import type {
KuaishouIndustryVoucherRow,
OrderItemRow,
@@ -48,6 +54,7 @@ export function mapAdminOrderListItem(
skuNick: itSkuNick,
matchSku: itMatchSku,
quantity: Math.max(1, Number(orderItem.quantity || 1)),
fulfillmentMode: normalizeOrderItemFulfillmentMode(orderItem.fulfillment_mode),
}
})
resolvedItemTitle = itemsList[0]?.itemTitle || ''
@@ -64,6 +71,7 @@ export function mapAdminOrderListItem(
skuNick: resolvedSkuNick,
matchSku: resolvedMatchSku,
quantity: 1,
fulfillmentMode: 'pending',
},
]
}
@@ -76,15 +84,15 @@ export function mapAdminOrderListItem(
)
const hasResolvedFallback = (resolvedItemTitle || resolvedSkuNick) && rawItemCount === 0
const { fulfillmentType, canClaimH5 } = resolveOrderFulfillmentType({
provider: item.provider || '',
const fulfillment = resolveOrderFulfillmentSummary({
items: normalizedItems,
tasks,
workOrderCount,
vouchers,
})
return {
orderId: item.id,
orderId: Number(item.id),
provider: item.provider || '',
platform: item.platform,
shopId: item.shop_id || '',
@@ -110,8 +118,12 @@ export function mapAdminOrderListItem(
items: itemsList,
taskCount: tasks.length,
workOrderCount,
fulfillmentType,
canClaimH5,
fulfillmentMode: fulfillment.fulfillmentMode,
manualItemCount: fulfillment.manualItemCount,
pendingItemCount: fulfillment.pendingItemCount,
hasVoucher: fulfillment.hasVoucher,
voucherState: fulfillment.voucherState,
canClaimH5: fulfillment.canClaimH5,
resourceStatus: fulfillmentProgress.resourceStatus,
customerStatus: fulfillmentProgress.customerStatus,
preparedTaskCount: fulfillmentProgress.preparedTaskCount,
@@ -122,56 +134,118 @@ export function mapAdminOrderListItem(
}
}
export function resolveOrderFulfillmentType({
provider,
const ORDER_ITEM_FULFILLMENT_MODES = new Set<AdminOrderItemFulfillmentMode>([
'work_order',
'affiliate_dash',
'legacy_platform',
'manual',
'pending',
])
export function normalizeOrderItemFulfillmentMode(value: unknown): AdminOrderItemFulfillmentMode {
const mode = String(value || '')
.trim()
.toLowerCase() as AdminOrderItemFulfillmentMode
return ORDER_ITEM_FULFILLMENT_MODES.has(mode) ? mode : 'pending'
}
export function resolveVoucherSummary(vouchers: KuaishouIndustryVoucherRow[] = []): {
hasVoucher: boolean
voucherState: AdminVoucherState
} {
const statuses = new Set(
vouchers
.map((voucher) =>
String(voucher.status || '')
.trim()
.toLowerCase(),
)
.filter(Boolean),
)
if (statuses.size === 0) {
return { hasVoucher: false, voucherState: 'none' }
}
if (statuses.size === 1) {
const only = Array.from(statuses)[0]
if (only === 'unused' || only === 'consumed' || only === 'destroyed') {
return { hasVoucher: true, voucherState: only }
}
}
return { hasVoucher: true, voucherState: 'mixed' }
}
/**
* 订单级履约摘要:只读 order_items.fulfillment_mode,不再按 provider 或核销码推断。
* 多商品订单出现不同模式时显示 mixed;尚未判定的订单项单独计数。
*/
export function resolveOrderFulfillmentSummary({
items = [],
tasks = [],
workOrderCount = 0,
vouchers = [],
}: {
provider: string
items?: OrderItemRow[]
tasks?: TaskRow[]
workOrderCount?: number
vouchers?: KuaishouIndustryVoucherRow[]
}): {
fulfillmentType: 'auto_claim' | 'worker_dispatch' | 'voucher_only' | 'manual'
fulfillmentMode: AdminOrderFulfillmentMode
manualItemCount: number
pendingItemCount: number
affiliateItemCount: number
hasVoucher: boolean
voucherState: AdminVoucherState
canClaimH5: boolean
} {
const normProvider = String(provider || '')
.trim()
.toLowerCase()
const hasAutoClaimTask =
const normalizedItems = Array.isArray(items) ? items : []
const itemModes = normalizedItems.map((item) =>
normalizeOrderItemFulfillmentMode(item.fulfillment_mode),
)
const knownModes = Array.from(
new Set(
itemModes.filter((mode): mode is Exclude<typeof mode, 'pending'> => mode !== 'pending'),
),
)
let fulfillmentMode: AdminOrderFulfillmentMode
if (knownModes.length === 1) {
fulfillmentMode = knownModes[0]!
} else if (knownModes.length > 1) {
fulfillmentMode = 'mixed'
} else if (normalizedItems.length > 0) {
fulfillmentMode = 'pending'
} else if (workOrderCount > 0) {
// 极少数历史订单没有 order_items,只能用载体表兜底;这不是按 provider 猜。
fulfillmentMode = 'work_order'
} else if (tasks.some((task) => task.executor_key === 'affiliate_dash')) {
fulfillmentMode = 'affiliate_dash'
} else if (
tasks.some(
(t) =>
t.executor_key === 'affiliate_dash' ||
t.executor_key === 'kuaishou_ct_assisted' ||
t.executor_key === 'kuaishou_feifei' ||
Boolean(t.requires_claim),
) || normProvider === 'douyin_mashida'
if (hasAutoClaimTask) {
return {
fulfillmentType: 'auto_claim',
canClaimH5: true,
}
(task) =>
task.executor_key === 'kuaishou_ct_assisted' || task.executor_key === 'kuaishou_feifei',
)
) {
fulfillmentMode = 'legacy_platform'
} else {
fulfillmentMode = 'pending'
}
if (workOrderCount > 0) {
return {
fulfillmentType: 'worker_dispatch',
canClaimH5: false,
}
}
if (vouchers.length > 0 || normProvider === '91kaquan' || normProvider === 'kuaishou_industry') {
return {
fulfillmentType: 'voucher_only',
canClaimH5: false,
}
}
const affiliateItemCount = itemModes.filter((mode) => mode === 'affiliate_dash').length
const voucher = resolveVoucherSummary(vouchers)
const canClaimH5 =
affiliateItemCount > 0 || (normalizedItems.length === 0 && fulfillmentMode === 'affiliate_dash')
return {
fulfillmentType: 'manual',
canClaimH5: false,
fulfillmentMode,
manualItemCount: itemModes.filter((mode) => mode === 'manual').length,
pendingItemCount: itemModes.filter((mode) => mode === 'pending').length,
affiliateItemCount,
hasVoucher: voucher.hasVoucher,
voucherState: voucher.voucherState,
canClaimH5,
}
}
@@ -63,7 +63,8 @@ import {
import { isRetiredFulfillmentExecutor } from '../fulfillment/executors/types.js'
import {
mapAdminOrderListItem,
resolveOrderFulfillmentType,
normalizeOrderItemFulfillmentMode,
resolveOrderFulfillmentSummary,
summarizeOrderItems,
} from './admin-order-read-helpers.js'
import {
@@ -190,8 +191,8 @@ export async function getAdminOrderDetail(
const orderItemTitle = resolveOrderItemTitle(items[0], order, kuaishouIndustryVouchers)
const orderSkuNick = resolveOrderItemSkuNick(items[0], order, kuaishouIndustryVouchers)
const viewerContext = createAdminViewerContext(session)
const { fulfillmentType, canClaimH5 } = resolveOrderFulfillmentType({
provider: order.provider || '',
const fulfillment = resolveOrderFulfillmentSummary({
items,
tasks,
workOrderCount: workOrders.length,
vouchers: kuaishouIndustryVouchers,
@@ -227,18 +228,23 @@ export async function getAdminOrderDetail(
claimStatus: order.claim_status || 'active',
claimStatusReason: order.claim_status_reason || '',
claimStatusUpdatedAt: order.claim_status_updated_at || null,
fulfillmentType,
fulfillmentMode: fulfillment.fulfillmentMode,
manualItemCount: fulfillment.manualItemCount,
pendingItemCount: fulfillment.pendingItemCount,
hasVoucher: fulfillment.hasVoucher,
voucherState: fulfillment.voucherState,
workOrderCount: workOrders.length,
canClaimH5,
canClaimH5: fulfillment.canClaimH5,
},
items: items.map((item) => ({
orderItemId: item.id,
orderItemId: Number(item.id),
skuCode: item.sku_code,
skuName: item.sku_name,
itemTitle: resolveOrderItemTitle(item, order, kuaishouIndustryVouchers),
skuNick: resolveOrderItemSkuNick(item, order, kuaishouIndustryVouchers),
matchSku: resolveOrderItemMatchSku(item, order, kuaishouIndustryVouchers),
quantity: item.quantity,
fulfillmentMode: normalizeOrderItemFulfillmentMode(item.fulfillment_mode),
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
spec:
item.spec_json && typeof item.spec_json === 'object'