Files
order_site/apps/backend/src/services/cost-center/cost-center-mappers.ts
T

287 lines
10 KiB
TypeScript

/**
* 成本中心共享层 · 行映射与口径判断。
*
* 把数据库行映射成接口 DTO,并提供「成本模型是否适用于履约方式」这类口径函数。
*/
import { GENERIC_COST_MODELS, PRODUCT_SOURCE_PREFIXES } from './cost-center-constants.js'
import type { FulfillmentChannel } from './cost-center-constants.js'
import { parseJsonObject } from './cost-center-normalizers.js'
import type {
CostExceptionDraft,
CostExceptionRow,
CostItemRow,
CostLine,
CostVersionRow,
MappingRow,
MissingCostItem,
ProductRow,
} from './cost-center-types.js'
// ---------------------------------------------------------------------------
// 履约方式 / 商品来源 / 成本模型
// ---------------------------------------------------------------------------
/**
* 由 canonical_sku 前缀推导商品来源。
* 返回空串表示未分类;未分类商品不允许反推履约方式。
*/
export function productSourceOf(canonicalSku: string): string {
const sku = String(canonicalSku || '').trim()
for (const item of PRODUCT_SOURCE_PREFIXES) {
if (sku.startsWith(item.prefix)) return item.source
}
return ''
}
/** 商品来源筛选的 SQL 片段:按 canonical_sku 前缀过滤。 */
export function productSourceFilterSql(
paramIndex: number,
column = 'p.canonical_sku',
nameColumn = 'p.canonical_name',
): string {
return `(
($${paramIndex} = 'affiliate_dash' AND ${column} LIKE 'aff:%')
OR ($${paramIndex} = 'work_order' AND (${column} LIKE 'tpl:%' OR ${column} LIKE 'wo:%'))
OR ($${paramIndex} = 'third_party_voucher' AND ${column} LIKE 'tpv:%')
OR (
$${paramIndex} = 'manual'
AND ${column} LIKE 'man:%'
AND NOT EXISTS (
SELECT 1
FROM internal_sku_channels live_channel
WHERE live_channel.enabled = TRUE
AND live_channel.is_fallback = FALSE
AND live_channel.channel <> 'manual'
AND canonicalize_internal_sku(live_channel.internal_sku)
= canonicalize_internal_sku(${nameColumn})
)
)
)`
}
/**
* 成本模型是否适用于该履约方式。
*
* 专属模型只对对应履约方式生效;通用模型(平台费/支付费/售后损失/其他)
* 对所有履约方式生效。
*/
export function costModelAppliesTo(costModel: string, channel: string) {
if ((GENERIC_COST_MODELS as readonly string[]).includes(costModel)) return true
if (costModel === 'affiliate_purchase') return channel === 'affiliate_dash'
if (costModel === 'work_order_reward') return channel === 'work_order'
if (costModel === 'third_party_voucher_cost') return channel === 'third_party_voucher'
if (costModel === 'manual_actual') return channel === 'manual' || channel === 'on_account'
return false
}
/**
* 成本模型适用范围的 SQL 片段。
* 参数是「履约方式」的 SQL 表达式(可以是占位符 $n,也可以是列引用)。
*/
export function costModelScopeSql(channelExpression: string): string {
return `(
ci.cost_model IN (${GENERIC_COST_MODELS.map((model) => `'${model}'`).join(', ')})
OR (ci.cost_model = 'affiliate_purchase'
AND ${channelExpression} = 'affiliate_dash')
OR (ci.cost_model = 'work_order_reward'
AND ${channelExpression} = 'work_order')
OR (ci.cost_model = 'third_party_voucher_cost'
AND ${channelExpression} = 'third_party_voucher')
OR (ci.cost_model = 'manual_actual'
AND ${channelExpression} IN ('manual', 'on_account'))
)`
}
/** 人工支出分类的默认推断:按成本模型归类。 */
export function inferExpenseCategory(costModel: string | undefined) {
if (costModel === 'affiliate_purchase') return 'goods_purchase'
if (costModel === 'manual_actual') return 'service_fee'
if (costModel === 'other') return 'freight'
if (costModel === 'payment_fee' || costModel === 'platform_fee') return 'service_fee'
return 'other'
}
// ---------------------------------------------------------------------------
// 行映射
// ---------------------------------------------------------------------------
export function mapProduct(row: ProductRow) {
return {
id: Number(row.id),
canonicalSku: row.canonical_sku,
canonicalName: row.canonical_name,
category: row.category || '',
source: productSourceOf(row.canonical_sku),
rootCategoryId: row.root_category_id ? Number(row.root_category_id) : null,
rootCategoryName: row.root_category_name || row.category || '',
subCategoryId: row.sub_category_id ? Number(row.sub_category_id) : null,
subCategoryName: row.sub_category_name || '',
unitName: row.unit_name,
status: row.status,
attributes: parseJsonObject(row.attributes_json),
mappingCount: Number(row.mapping_count || 0),
costVersionCount: Number(row.cost_version_count || 0),
activeCostAmountFen:
row.active_cost_amount_fen !== null && row.active_cost_amount_fen !== undefined
? Number(row.active_cost_amount_fen)
: null,
activeCostEffectiveFrom: row.active_cost_effective_from
? String(row.active_cost_effective_from)
: null,
templatePricingMode: row.template_pricing_mode || null,
templateEnabled:
row.template_enabled === null || row.template_enabled === undefined
? null
: row.template_enabled === true,
templateSharingEnabled:
row.template_sharing_enabled === null || row.template_sharing_enabled === undefined
? null
: row.template_sharing_enabled === true,
activePrimaryChannel: row.active_primary_channel || null,
affiliateTaskCount: Number(row.affiliate_task_count || 0),
affiliateLastFulfilledAt: row.affiliate_last_fulfilled_at || null,
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
export function mapMapping(row: MappingRow) {
return {
id: Number(row.id),
provider: row.provider,
shopId: row.shop_id,
platformSkuCode: row.platform_sku_code,
platformSkuName: row.platform_sku_name,
platformSkuNameKey: row.platform_sku_name_key || '',
matchType: row.match_type || 'sku_code',
canonicalProductId: Number(row.canonical_product_id),
canonicalSku: row.canonical_sku,
canonicalName: row.canonical_name,
quantityMultiplier: Number(row.quantity_multiplier),
confidence: row.confidence,
source: row.source,
enabled: row.enabled !== false,
note: row.note || '',
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
export function mapCostItem(row: CostItemRow) {
return {
id: Number(row.id),
code: row.code,
name: row.name,
costModel: row.cost_model,
pricingMode: row.pricing_mode,
unitName: row.unit_name,
currency: row.currency,
status: row.status,
note: row.note || '',
versionCount: Number(row.version_count || 0),
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
export function mapCostVersion(row: CostVersionRow) {
const revenueItemCount = Number(row.revenue_item_count || 0)
const revenueFen = Number(row.revenue_fen || 0)
const rewardAvgFen = Number(row.reward_avg_fen || 0)
const rewardFen = Number(row.reward_fen || 0)
const ruleRewardAmountFen = Number(row.rule_reward_amount || 0)
const amountKind =
row.cost_item_code === 'sys_work_order_reward' &&
row.scope_type === 'rule' &&
row.work_product_rule_pricing_mode === 'unit'
? 'unit_price'
: 'fixed'
return {
id: Number(row.id),
costItemId: Number(row.cost_item_id),
costItemCode: row.cost_item_code,
costItemName: row.cost_item_name,
costItemCostModel: row.cost_item_cost_model || 'other',
pricingMode: row.pricing_mode,
scopeType: row.scope_type,
provider: row.provider || '',
shopId: row.shop_id || '',
canonicalProductId: row.canonical_product_id ? Number(row.canonical_product_id) : null,
canonicalSku: row.canonical_sku || '',
canonicalName: row.canonical_name || '',
workProductRuleId: row.work_product_rule_id ? Number(row.work_product_rule_id) : null,
workProductRuleName: row.work_product_rule_name || '',
workProductRuleKey: row.work_product_rule_key || '',
ruleRewardAmountFen,
ruleUnitPriceFen: Number(row.rule_unit_price_fen || 0),
revenueItemCount,
revenueFen,
revenueAvgFen: revenueItemCount > 0 ? Math.round(revenueFen / revenueItemCount) : 0,
rewardAvgFen,
rewardFen,
amountKind,
isSystemPlaceholder:
row.cost_item_code === 'sys_work_order_reward' &&
row.scope_type === 'global' &&
Number(row.amount_fen) === 0,
amountFen: Number(row.amount_fen),
rateBps: Number(row.rate_bps),
effectiveFrom: row.effective_from,
effectiveTo: row.effective_to,
source: row.source,
status: row.status,
changeReason: row.change_reason || '',
createdByAdminUserId: row.created_by_admin_user_id
? Number(row.created_by_admin_user_id)
: null,
approvedByAdminUserId: row.approved_by_admin_user_id
? Number(row.approved_by_admin_user_id)
: null,
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
export function mapCostException(row: CostExceptionRow) {
return {
id: Number(row.id),
subjectType: row.subject_type,
subjectId: Number(row.subject_id),
orderItemId: row.order_item_id ? Number(row.order_item_id) : null,
workOrderId: row.work_order_id ? Number(row.work_order_id) : null,
channel: row.channel || '',
reason: row.reason,
status: row.status,
detail: parseJsonObject(row.detail_json),
createdAt: row.created_at,
resolvedAt: row.resolved_at,
resolvedByAdminUserId: row.resolved_by_admin_user_id
? Number(row.resolved_by_admin_user_id)
: null,
resolutionNote: row.resolution_note || '',
}
}
export type CostPreview = {
context: {
orderItemId: number | null
orderId: number | null
provider: string
shopId: string
skuCode: string
skuName: string
channel: FulfillmentChannel
quantity: number
normalizedQuantity: number
revenueFen: number
workOrderId: number | null
workProductRuleId: number | null
}
mapping: ReturnType<typeof mapMapping> | null
product: ReturnType<typeof mapProduct> | null
lines: CostLine[]
missingCostItems: MissingCostItem[]
totalCostFen: number
exceptions: CostExceptionDraft[]
}