feat(finance): 统一履约事实并改造人工账单分类
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user