清理旧履约模型残留
This commit is contained in:
@@ -2,7 +2,7 @@ import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
||||
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js'
|
||||
import { buildOrderBindingSummary } from './admin-task-read-helpers.js'
|
||||
import { buildOrderFulfillmentProgress } from './admin-task-read-helpers.js'
|
||||
|
||||
import type { AdminOrderListItem } from '../../types/admin/read-models.js'
|
||||
import type { OrderItemRow, OrderListRow } from '../../types/repository/rows.js'
|
||||
@@ -12,7 +12,7 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
|
||||
listTasksByOrderId(item.id),
|
||||
listOrderItemsByOrderId(item.id),
|
||||
])
|
||||
const bindingSummary = buildOrderBindingSummary(tasks)
|
||||
const fulfillmentProgress = buildOrderFulfillmentProgress(tasks)
|
||||
const itemSummary = summarizeOrderItems(orderItems)
|
||||
|
||||
return {
|
||||
@@ -37,14 +37,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
|
||||
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
|
||||
itemSummary,
|
||||
taskCount: Number(item.task_count || tasks.length || 0),
|
||||
systemBindingStatus: bindingSummary.systemBindingStatus,
|
||||
userBindingStatus: bindingSummary.userBindingStatus,
|
||||
systemBoundTaskCount: bindingSummary.systemBoundTaskCount,
|
||||
completedBindingTaskCount: bindingSummary.completedBindingTaskCount,
|
||||
totalBindingCount: bindingSummary.totalBindingCount,
|
||||
reservedBindingCount: bindingSummary.reservedBindingCount,
|
||||
consumedBindingCount: bindingSummary.consumedBindingCount,
|
||||
releasedBindingCount: bindingSummary.releasedBindingCount,
|
||||
resourceStatus: fulfillmentProgress.resourceStatus,
|
||||
customerStatus: fulfillmentProgress.customerStatus,
|
||||
preparedTaskCount: fulfillmentProgress.preparedTaskCount,
|
||||
completedTaskCount: fulfillmentProgress.completedTaskCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from './admin-read-shared-helpers.js'
|
||||
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
|
||||
import {
|
||||
buildOrderBindingSummary,
|
||||
buildOrderFulfillmentProgress,
|
||||
getRequiredTask,
|
||||
mapAdminTaskEvent,
|
||||
mapAdminTaskListItem,
|
||||
@@ -104,7 +104,7 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
|
||||
rawPayload: order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
||||
? order.raw_payload_json
|
||||
: JSON.parse(String(order.raw_payload_json || '{}')),
|
||||
bindingSummary: buildOrderBindingSummary(tasks),
|
||||
fulfillmentProgress: buildOrderFulfillmentProgress(tasks),
|
||||
},
|
||||
items: items.map((item) => ({
|
||||
orderItemId: item.id,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from './admin-read-shared-helpers.js'
|
||||
|
||||
import type {
|
||||
AdminTaskBindingSummary,
|
||||
AdminOrderFulfillmentProgress,
|
||||
AdminTaskListItem,
|
||||
} from '../../types/admin/read-models.js'
|
||||
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
|
||||
@@ -19,36 +19,23 @@ import type { AdminViewerContext } from './admin-read-shared-helpers.js'
|
||||
|
||||
type JsonRecord = Record<string, any>
|
||||
|
||||
type TaskBindingState = {
|
||||
systemBindingStatus: string
|
||||
userBindingStatus: string
|
||||
}
|
||||
|
||||
type OrderBindingSummary = {
|
||||
totalTaskCount: number
|
||||
systemBoundTaskCount: number
|
||||
completedBindingTaskCount: number
|
||||
totalBindingCount: number
|
||||
reservedBindingCount: number
|
||||
consumedBindingCount: number
|
||||
releasedBindingCount: number
|
||||
systemBindingStatus: string
|
||||
userBindingStatus: string
|
||||
type TaskFulfillmentState = {
|
||||
resourceStatus: string
|
||||
customerStatus: string
|
||||
}
|
||||
|
||||
export function mapAdminTaskSummary(
|
||||
task: TaskRow,
|
||||
bindingSummary: AdminTaskBindingSummary = createEmptyTaskBindingSummary(),
|
||||
): JsonRecord {
|
||||
const binding = buildTaskBindingState(task)
|
||||
const fulfillment = buildTaskFulfillmentState(task)
|
||||
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
deliveryStatus: task.delivery_status || '',
|
||||
status: task.task_status,
|
||||
systemBindingStatus: binding.systemBindingStatus,
|
||||
userBindingStatus: binding.userBindingStatus,
|
||||
resourceStatus: fulfillment.resourceStatus,
|
||||
customerStatus: fulfillment.customerStatus,
|
||||
loginType: task.login_type,
|
||||
runtimeSessionId: task.runtime_session_id,
|
||||
claimedAt: task.claimed_at,
|
||||
@@ -56,7 +43,6 @@ export function mapAdminTaskSummary(
|
||||
redeemedAt: task.redeemed_at,
|
||||
lastError: task.last_error,
|
||||
retryCount: getTaskRetryCount(task),
|
||||
bindingSummary,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
}
|
||||
@@ -95,8 +81,7 @@ export function mapAdminTaskListItem(
|
||||
task: TaskRow,
|
||||
viewerContext: AdminViewerContext = createAdminViewerContext(),
|
||||
): AdminTaskListItem {
|
||||
const binding = buildTaskBindingState(task)
|
||||
const bindingSummary = createEmptyTaskBindingSummary()
|
||||
const fulfillment = buildTaskFulfillmentState(task)
|
||||
|
||||
return {
|
||||
taskId: task.id,
|
||||
@@ -109,8 +94,8 @@ export function mapAdminTaskListItem(
|
||||
deliveryStatus: task.delivery_status || '',
|
||||
resultCode: task.result_code || '',
|
||||
resultMessage: task.result_message || '',
|
||||
systemBindingStatus: binding.systemBindingStatus,
|
||||
userBindingStatus: binding.userBindingStatus,
|
||||
resourceStatus: fulfillment.resourceStatus,
|
||||
customerStatus: fulfillment.customerStatus,
|
||||
loginType: task.login_type,
|
||||
roleName: task.role_name,
|
||||
roleId: task.role_id,
|
||||
@@ -119,7 +104,6 @@ export function mapAdminTaskListItem(
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
retryCount: getTaskRetryCount(task),
|
||||
bindingSummary,
|
||||
lastError: task.last_error,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
@@ -139,158 +123,140 @@ export function mapAdminTaskEvent(event: TaskEventRow): JsonRecord {
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyTaskBindingSummary(): AdminTaskBindingSummary {
|
||||
return {
|
||||
totalBindingCount: 0,
|
||||
reservedBindingCount: 0,
|
||||
consumedBindingCount: 0,
|
||||
releasedBindingCount: 0,
|
||||
roleKeys: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function buildOrderBindingSummary(
|
||||
export function buildOrderFulfillmentProgress(
|
||||
tasks: TaskRow[] | null | undefined,
|
||||
): OrderBindingSummary {
|
||||
): AdminOrderFulfillmentProgress {
|
||||
const normalizedTasks = Array.isArray(tasks) ? tasks : []
|
||||
const totalTaskCount = normalizedTasks.length
|
||||
const taskBindings = normalizedTasks.map((task) => buildTaskBindingState(task))
|
||||
const systemBoundTaskCount = normalizedTasks.filter((task) => isTaskSystemBound(task)).length
|
||||
const completedBindingTaskCount = normalizedTasks.filter((task) => String(task?.task_status || '') === 'redeemed').length
|
||||
const totalBindingCount = 0
|
||||
const reservedBindingCount = 0
|
||||
const consumedBindingCount = 0
|
||||
const releasedBindingCount = 0
|
||||
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
|
||||
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
|
||||
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length
|
||||
|
||||
let systemBindingStatus = 'pending_binding'
|
||||
let userBindingStatus = 'not_started'
|
||||
let resourceStatus = 'pending_prepare'
|
||||
let customerStatus = 'not_started'
|
||||
|
||||
if (totalTaskCount === 0) {
|
||||
return {
|
||||
totalTaskCount,
|
||||
systemBoundTaskCount,
|
||||
completedBindingTaskCount,
|
||||
totalBindingCount,
|
||||
reservedBindingCount,
|
||||
consumedBindingCount,
|
||||
releasedBindingCount,
|
||||
systemBindingStatus,
|
||||
userBindingStatus,
|
||||
preparedTaskCount,
|
||||
completedTaskCount,
|
||||
resourceStatus,
|
||||
customerStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedTasks.every((task) => String(task.task_status || '') === 'redeemed')) {
|
||||
systemBindingStatus = 'system_bound'
|
||||
userBindingStatus = 'binding_completed'
|
||||
} else if (taskBindings.some((item) => ['binding_in_progress', 'binding_confirmed', 'link_opened'].includes(item.userBindingStatus))) {
|
||||
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
||||
userBindingStatus = 'user_binding'
|
||||
} else if (taskBindings.some((item) => item.userBindingStatus === 'waiting_user_claim')) {
|
||||
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
||||
userBindingStatus = 'waiting_user_claim'
|
||||
} else if (taskBindings.some((item) => item.userBindingStatus === 'binding_exception')) {
|
||||
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
||||
userBindingStatus = 'binding_exception'
|
||||
} else if (systemBoundTaskCount > 0) {
|
||||
systemBindingStatus = 'system_bound'
|
||||
} else if (taskBindings.some((item) => ['manual_review', 'retry_pending'].includes(item.systemBindingStatus))) {
|
||||
systemBindingStatus = 'binding_exception'
|
||||
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
|
||||
resourceStatus = 'resource_ready'
|
||||
customerStatus = 'customer_completed'
|
||||
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) {
|
||||
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
||||
customerStatus = 'customer_processing'
|
||||
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
|
||||
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
||||
customerStatus = 'waiting_customer'
|
||||
} else if (taskFulfillments.some((item) => item.customerStatus === 'customer_exception')) {
|
||||
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
||||
customerStatus = 'customer_exception'
|
||||
} else if (preparedTaskCount > 0) {
|
||||
resourceStatus = 'resource_ready'
|
||||
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) {
|
||||
resourceStatus = 'resource_exception'
|
||||
}
|
||||
|
||||
return {
|
||||
totalTaskCount,
|
||||
systemBoundTaskCount,
|
||||
completedBindingTaskCount,
|
||||
totalBindingCount,
|
||||
reservedBindingCount,
|
||||
consumedBindingCount,
|
||||
releasedBindingCount,
|
||||
systemBindingStatus,
|
||||
userBindingStatus,
|
||||
preparedTaskCount,
|
||||
completedTaskCount,
|
||||
resourceStatus,
|
||||
customerStatus,
|
||||
}
|
||||
}
|
||||
|
||||
function buildTaskBindingState(task: TaskRow | null | undefined): TaskBindingState {
|
||||
function buildTaskFulfillmentState(task: TaskRow | null | undefined): TaskFulfillmentState {
|
||||
const normalizedStatus = String(task?.task_status || '').trim()
|
||||
|
||||
if (!normalizedStatus) {
|
||||
return {
|
||||
systemBindingStatus: 'pending_binding',
|
||||
userBindingStatus: 'not_started',
|
||||
resourceStatus: 'pending_prepare',
|
||||
customerStatus: 'not_started',
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'pending_payment') {
|
||||
return { systemBindingStatus: 'pending_payment', userBindingStatus: 'not_started' }
|
||||
return { resourceStatus: 'pending_payment', customerStatus: 'not_started' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'paid') {
|
||||
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
||||
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'pending_binding_prepare') {
|
||||
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
||||
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'waiting_binding') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'dispatched_pending_return') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'completed') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'manual_review') {
|
||||
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
|
||||
return { resourceStatus: 'manual_review', customerStatus: 'customer_exception' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'retry_pending') {
|
||||
return { systemBindingStatus: 'retry_pending', userBindingStatus: 'binding_exception' }
|
||||
return { resourceStatus: 'retry_pending', customerStatus: 'customer_exception' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'closed') {
|
||||
return { systemBindingStatus: 'closed', userBindingStatus: 'closed' }
|
||||
return { resourceStatus: 'closed', customerStatus: 'closed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'expired') {
|
||||
return { systemBindingStatus: 'expired', userBindingStatus: 'expired' }
|
||||
return { resourceStatus: 'expired', customerStatus: 'expired' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'link_generated') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'claimed') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'link_opened' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'link_opened' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'role_confirmed') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_confirmed' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'customer_confirmed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'redeeming') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_in_progress' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'customer_processing' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'redeemed') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
||||
}
|
||||
|
||||
if (isTaskSystemBound(task)) {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
||||
if (isTaskResourcePrepared(task)) {
|
||||
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
||||
}
|
||||
|
||||
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
||||
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
||||
}
|
||||
|
||||
function isTaskSystemBound(task: TaskRow | null | undefined): boolean {
|
||||
function isTaskResourcePrepared(task: TaskRow | null | undefined): boolean {
|
||||
return Boolean(task && getTaskPrimaryClaimTokenId(task))
|
||||
}
|
||||
|
||||
function isTaskFulfillmentCompleted(task: TaskRow | null | undefined): boolean {
|
||||
return ['redeemed', 'completed', 'dispatched_pending_return'].includes(String(task?.task_status || '').trim())
|
||||
}
|
||||
|
||||
function normalizeRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
findMatchingObservedBinding,
|
||||
mapAdminObservedProductItem,
|
||||
matchesObservedProduct,
|
||||
} from './domain.js'
|
||||
|
||||
test('matchesObservedProduct honors provider platform shop and external fields', () => {
|
||||
const binding = {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
match: {
|
||||
externalSkuCode: 'sku-ext-1',
|
||||
externalItemId: '',
|
||||
externalSkuName: '',
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
matchesObservedProduct(binding, {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
externalSkuCode: 'sku-ext-1',
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
matchesObservedProduct(binding, {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-2',
|
||||
externalSkuCode: 'sku-ext-1',
|
||||
}),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('mapAdminObservedProductItem attaches matched binding summary', () => {
|
||||
const bindings = [
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
skuCode: 'sku-1',
|
||||
skuName: 'SKU 1',
|
||||
profileKey: 'profile-1',
|
||||
match: {
|
||||
externalSkuCode: 'ext-1',
|
||||
},
|
||||
},
|
||||
]
|
||||
const observed = {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
shopName: '店铺1',
|
||||
externalItemId: '',
|
||||
externalSkuCode: 'ext-1',
|
||||
externalSkuName: '礼包1',
|
||||
latestSeenAt: '2026-05-04T10:00:00.000Z',
|
||||
orderItemCount: 2,
|
||||
}
|
||||
|
||||
assert.deepEqual(findMatchingObservedBinding(bindings, observed), bindings[0])
|
||||
assert.deepEqual(mapAdminObservedProductItem(observed, bindings), {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
shopName: '店铺1',
|
||||
externalItemId: '',
|
||||
externalSkuCode: 'ext-1',
|
||||
externalSkuName: '礼包1',
|
||||
latestSeenAt: '2026-05-04T10:00:00.000Z',
|
||||
orderItemCount: 2,
|
||||
configured: true,
|
||||
matchedBinding: {
|
||||
skuCode: 'sku-1',
|
||||
skuName: 'SKU 1',
|
||||
profileKey: 'profile-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export function matchesObservedProduct(binding: JsonObject, observed: JsonObject) {
|
||||
const provider = String(binding?.provider || '').trim()
|
||||
const platform = String(binding?.platform || '').trim()
|
||||
const shopId = String(binding?.shopId || '').trim()
|
||||
const match = binding?.match || {}
|
||||
const externalSkuCode = String(match.externalSkuCode || '').trim()
|
||||
const externalItemId = String(match.externalItemId || '').trim()
|
||||
const externalSkuName = String(match.externalSkuName || '').trim()
|
||||
|
||||
if (provider && provider !== String(observed?.provider || '').trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (platform && platform !== String(observed?.platform || '').trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (shopId && shopId !== String(observed?.shopId || '').trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
(externalSkuCode && externalSkuCode === String(observed?.externalSkuCode || '').trim())
|
||||
|| (externalItemId && externalItemId === String(observed?.externalItemId || '').trim())
|
||||
|| (externalSkuName && externalSkuName === String(observed?.externalSkuName || '').trim())
|
||||
)
|
||||
}
|
||||
|
||||
export function findMatchingObservedBinding(bindings: JsonObject[], observed: JsonObject) {
|
||||
return (Array.isArray(bindings) ? bindings : []).find((binding) => matchesObservedProduct(binding, observed)) || null
|
||||
}
|
||||
|
||||
export function mapAdminObservedProductItem(item: JsonObject, bindings: JsonObject[] = []) {
|
||||
const matchedBinding = findMatchingObservedBinding(bindings, item)
|
||||
|
||||
return {
|
||||
provider: String(item?.provider || '').trim(),
|
||||
platform: String(item?.platform || '').trim(),
|
||||
shopId: String(item?.shopId || '').trim(),
|
||||
shopName: String(item?.shopName || '').trim(),
|
||||
externalItemId: String(item?.externalItemId || '').trim(),
|
||||
externalSkuCode: String(item?.externalSkuCode || '').trim(),
|
||||
externalSkuName: String(item?.externalSkuName || '').trim(),
|
||||
latestSeenAt: item?.latestSeenAt || null,
|
||||
orderItemCount: Number(item?.orderItemCount || 0),
|
||||
configured: Boolean(matchedBinding),
|
||||
matchedBinding: matchedBinding
|
||||
? {
|
||||
skuCode: String(matchedBinding.skuCode || '').trim(),
|
||||
skuName: String(matchedBinding.skuName || '').trim(),
|
||||
profileKey: String(matchedBinding.profileKey || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
ADMIN_OBSERVED_PRODUCTS_QUERY,
|
||||
listAdminObservedProducts,
|
||||
mapAdminObservedProductRow,
|
||||
} from './observed-products.js'
|
||||
|
||||
test('mapAdminObservedProductRow normalizes query row fields', () => {
|
||||
assert.deepEqual(
|
||||
mapAdminObservedProductRow({
|
||||
provider: ' 91kaquan ',
|
||||
platform: ' kuaishou ',
|
||||
shop_id: ' shop-1 ',
|
||||
shop_name: ' 店铺A ',
|
||||
external_item_id: ' item-1 ',
|
||||
external_sku_code: ' sku-1 ',
|
||||
external_sku_name: ' 商品A ',
|
||||
latest_seen_at: '2026-05-04T12:00:00.000Z',
|
||||
order_item_count: '3',
|
||||
}),
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
shopName: '店铺A',
|
||||
externalItemId: 'item-1',
|
||||
externalSkuCode: 'sku-1',
|
||||
externalSkuName: '商品A',
|
||||
latestSeenAt: '2026-05-04T12:00:00.000Z',
|
||||
orderItemCount: 3,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('listAdminObservedProducts queries rows and maps matched binding summary', async () => {
|
||||
/** @type {string[]} */
|
||||
const sqlCalls = []
|
||||
const result = await listAdminObservedProducts(
|
||||
[
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
skuCode: 'inner-1',
|
||||
skuName: '内部商品A',
|
||||
profileKey: 'manual_review',
|
||||
match: {
|
||||
externalSkuCode: 'sku-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
async query(sql) {
|
||||
sqlCalls.push(sql)
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shop_id: 'shop-1',
|
||||
shop_name: '店铺A',
|
||||
external_item_id: '',
|
||||
external_sku_code: 'sku-1',
|
||||
external_sku_name: '商品A',
|
||||
latest_seen_at: '2026-05-04T12:00:00.000Z',
|
||||
order_item_count: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(sqlCalls.length, 1)
|
||||
assert.equal(sqlCalls[0], ADMIN_OBSERVED_PRODUCTS_QUERY)
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-1',
|
||||
shopName: '店铺A',
|
||||
externalItemId: '',
|
||||
externalSkuCode: 'sku-1',
|
||||
externalSkuName: '商品A',
|
||||
latestSeenAt: '2026-05-04T12:00:00.000Z',
|
||||
orderItemCount: 2,
|
||||
configured: true,
|
||||
matchedBinding: {
|
||||
skuCode: 'inner-1',
|
||||
skuName: '内部商品A',
|
||||
profileKey: 'manual_review',
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { query } from '../../../../db/client.js'
|
||||
import { mapAdminObservedProductItem } from './domain.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export const ADMIN_OBSERVED_PRODUCTS_QUERY = `
|
||||
SELECT
|
||||
o.provider,
|
||||
o.platform,
|
||||
o.shop_id,
|
||||
MAX(CASE WHEN trim(o.shop_name) != '' THEN o.shop_name ELSE '' END) AS shop_name,
|
||||
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalItemId', ''), '') AS external_item_id,
|
||||
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuCode', ''), '') AS external_sku_code,
|
||||
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuName', ''), '') AS external_sku_name,
|
||||
MAX(oi.created_at) AS latest_seen_at,
|
||||
COUNT(*)::int AS order_item_count
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE
|
||||
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalItemId', ''), '') != ''
|
||||
OR COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuCode', ''), '') != ''
|
||||
OR COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuName', ''), '') != ''
|
||||
GROUP BY
|
||||
o.provider,
|
||||
o.platform,
|
||||
o.shop_id,
|
||||
external_item_id,
|
||||
external_sku_code,
|
||||
external_sku_name
|
||||
ORDER BY latest_seen_at DESC, o.platform ASC, o.shop_id ASC, external_sku_code ASC
|
||||
LIMIT 200
|
||||
`
|
||||
|
||||
export function mapAdminObservedProductRow(row: JsonObject = {}) {
|
||||
return {
|
||||
provider: String(row.provider || '').trim(),
|
||||
platform: String(row.platform || '').trim(),
|
||||
shopId: String(row.shop_id || '').trim(),
|
||||
shopName: String(row.shop_name || '').trim(),
|
||||
externalItemId: String(row.external_item_id || '').trim(),
|
||||
externalSkuCode: String(row.external_sku_code || '').trim(),
|
||||
externalSkuName: String(row.external_sku_name || '').trim(),
|
||||
latestSeenAt: row.latest_seen_at || null,
|
||||
orderItemCount: Number(row.order_item_count || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAdminObservedProducts(bindings: JsonObject[] = [], options: JsonObject = {}) {
|
||||
const queryImpl = (options.query || query) as (sql: string) => Promise<{ rows: JsonObject[] }>
|
||||
const rowsResult = await queryImpl(ADMIN_OBSERVED_PRODUCTS_QUERY)
|
||||
return rowsResult.rows.map((row) => mapAdminObservedProductItem(mapAdminObservedProductRow(row), bindings))
|
||||
}
|
||||
Reference in New Issue
Block a user