重构:硬切履约状态唯一真源
This commit is contained in:
@@ -23,7 +23,7 @@ export async function cleanupDirtyAffiliateDashProducts() {
|
||||
oi.id AS order_item_id,
|
||||
clean.id AS clean_product_id,
|
||||
dirty.id AS expected_product_id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN product_catalog dirty ON dirty.id = oi.canonical_product_id
|
||||
JOIN fulfillment_tasks ft ON ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
|
||||
JOIN product_catalog clean ON clean.canonical_sku = 'aff:' || COALESCE(
|
||||
@@ -58,7 +58,7 @@ export async function cleanupDirtyAffiliateDashProducts() {
|
||||
await query(`
|
||||
DELETE FROM product_catalog
|
||||
WHERE canonical_sku ~ '^aff:[^a-z0-9_]'
|
||||
AND NOT EXISTS (SELECT 1 FROM order_items WHERE canonical_product_id = product_catalog.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM fulfillments WHERE canonical_product_id = product_catalog.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM cost_versions WHERE canonical_product_id = product_catalog.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM cost_snapshots WHERE canonical_product_id = product_catalog.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM sales_daily_facts WHERE canonical_product_id = product_catalog.id)
|
||||
@@ -207,7 +207,7 @@ export async function backfillAdminAffiliateDashSkus() {
|
||||
FROM affiliate_item ai
|
||||
JOIN product_catalog p
|
||||
ON p.canonical_sku = 'aff:' || ai.sku_code
|
||||
JOIN order_items oi ON oi.id = ai.id
|
||||
JOIN v_order_item_current oi ON oi.id = ai.id
|
||||
)
|
||||
SELECT id, canonical_product_id, expected_product_id
|
||||
FROM target
|
||||
@@ -240,7 +240,7 @@ export async function backfillAdminAffiliateDashSkus() {
|
||||
'YYYY-MM-DD'
|
||||
)
|
||||
FROM affiliate_item
|
||||
JOIN order_items oi ON oi.id = affiliate_item.id
|
||||
JOIN v_order_item_current oi ON oi.id = affiliate_item.id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
) AS from_date,
|
||||
(
|
||||
@@ -249,7 +249,7 @@ export async function backfillAdminAffiliateDashSkus() {
|
||||
'YYYY-MM-DD'
|
||||
)
|
||||
FROM affiliate_item
|
||||
JOIN order_items oi ON oi.id = affiliate_item.id
|
||||
JOIN v_order_item_current oi ON oi.id = affiliate_item.id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
) AS to_date
|
||||
`,
|
||||
@@ -275,7 +275,7 @@ export async function backfillAdminAffiliateDashSkus() {
|
||||
const remainingResult = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fulfillment_tasks ft
|
||||
|
||||
@@ -58,7 +58,7 @@ export const COST_MODEL_CODE_BASE: Record<string, string> = {
|
||||
other: 'other_cost',
|
||||
}
|
||||
|
||||
/** 履约渠道:唯一真源是 order_items.fulfillment_channel + fulfillment_status。 */
|
||||
/** 履约渠道:唯一真源是 fulfillments.channel + status。 */
|
||||
export type FulfillmentChannel =
|
||||
| 'work_order'
|
||||
| 'affiliate_dash'
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {}
|
||||
(
|
||||
SELECT COUNT(*)::int
|
||||
FROM cost_exceptions ce
|
||||
JOIN order_items oi ON oi.id = ce.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = ce.order_item_id
|
||||
WHERE ce.status = 'open'
|
||||
AND (
|
||||
$1::text IS NULL
|
||||
@@ -89,7 +89,7 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {}
|
||||
) AS open_exception_count,
|
||||
(
|
||||
SELECT COUNT(*)::int
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
-- 只统计已定渠道的单:待分类单还没有渠道,不能算「未映射」。
|
||||
AND oi.fulfillment_status = 'assigned'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 成本中心共享层入口。
|
||||
*
|
||||
* 口径(2026-10-23 重写):
|
||||
* 1. 履约渠道只有 order_items.fulfillment_channel/status 一个真源;
|
||||
* 1. 履约渠道只有 fulfillments.channel/status 一个真源;
|
||||
* 2. 成本项用 cost_model 表达「成本怎么算」,与履约方式正交;
|
||||
* 3. 商品来源由 canonical_sku 前缀推导,商品表不再存履约载体;
|
||||
* 4. 快照与异常表都不再存履约载体,join order_items 即可拿到。
|
||||
|
||||
@@ -332,7 +332,7 @@ test('成本中心总览异常统计必须走异常表关联,不能全表扫
|
||||
new URL('./cost-center-overview-service.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(source.includes('JOIN order_items oi ON oi.id = ce.order_item_id'))
|
||||
assert.ok(source.includes('JOIN v_order_item_current oi ON oi.id = ce.order_item_id'))
|
||||
assert.ok(!source.includes('ce.order_item_id IN ('))
|
||||
assert.ok(source.includes('productChannelFilterSql(1)'))
|
||||
})
|
||||
|
||||
@@ -343,7 +343,7 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
ROUND(AVG(wo.reward_amount))::bigint AS reward_avg_fen,
|
||||
SUM(wo.reward_amount)::bigint AS reward_fen
|
||||
FROM work_orders wo
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE o.pay_status IN ('paid', 'refunded')
|
||||
GROUP BY wo.product_rule_id
|
||||
@@ -583,7 +583,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) {
|
||||
params.push(channel)
|
||||
clauses.push(
|
||||
`ce.order_item_id IN (
|
||||
SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length}
|
||||
SELECT oi.id FROM v_order_item_current oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length}
|
||||
)`,
|
||||
)
|
||||
}
|
||||
@@ -600,7 +600,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) {
|
||||
ce.*,
|
||||
COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel
|
||||
FROM cost_exceptions ce
|
||||
LEFT JOIN order_items oi ON oi.id = ce.order_item_id
|
||||
LEFT JOIN v_order_item_current oi ON oi.id = ce.order_item_id
|
||||
${where}
|
||||
ORDER BY ce.status ASC, ce.created_at DESC, ce.id DESC
|
||||
LIMIT $${listParams.length - 1} OFFSET $${listParams.length}
|
||||
@@ -642,7 +642,7 @@ export async function summarizeAdminCostExceptions(
|
||||
params.push(channel)
|
||||
clauses.push(
|
||||
`ce.order_item_id IN (
|
||||
SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length}
|
||||
SELECT oi.id FROM v_order_item_current oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length}
|
||||
)`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ async function resolveManualCostHint(orderItemId: number | null) {
|
||||
cfg.credit_quantity,
|
||||
cfg.cost_mode,
|
||||
COALESCE((fp.config_json->>'rechargeUnitPriceFen')::bigint, 1) AS unit_price_fen
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN manual_fulfillment_configs cfg ON cfg.id = oi.fulfillment_config_id
|
||||
LEFT JOIN fulfillment_profiles fp ON fp.profile_key = 'manual_' || cfg.platform_key
|
||||
WHERE oi.id = $1
|
||||
@@ -317,7 +317,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
wo.id AS work_order_id,
|
||||
wo.product_rule_id AS work_product_rule_id,
|
||||
COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id, product_rule_id
|
||||
@@ -394,7 +394,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
|
||||
}
|
||||
|
||||
/**
|
||||
* 履约渠道只读 order_items.fulfillment_channel / fulfillment_status。
|
||||
* 履约渠道只读 fulfillments.channel / status。
|
||||
* 订单项还没判定时按人工处理,不允许用 provider 或核销码兜底。
|
||||
*/
|
||||
function resolveFulfillmentMode(value: unknown): FulfillmentChannel {
|
||||
|
||||
@@ -317,7 +317,7 @@ const CANDIDATE_SQL = `
|
||||
wpr.pricing_mode AS work_product_rule_pricing_mode,
|
||||
wo.status AS work_order_status,
|
||||
wo.reward_amount
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id AND wo.status <> 'cancelled'
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id
|
||||
|
||||
@@ -156,7 +156,7 @@ export async function scanManualSkus(options: ScanOptions) {
|
||||
COALESCE(oi.revenue_amount_fen, 0)::bigint AS revenue_fen,
|
||||
(COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date AS stat_date,
|
||||
resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) AS sku_key
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel = 'manual'
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
@@ -273,7 +273,7 @@ export async function listManualSkus(input: Record<string, unknown> = {}) {
|
||||
if (provider) {
|
||||
params.push(provider)
|
||||
conditions.push(`EXISTS (
|
||||
SELECT 1 FROM order_items oi
|
||||
SELECT 1 FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel = 'manual'
|
||||
AND o.provider = $${params.length}
|
||||
@@ -314,7 +314,7 @@ export async function listManualSkus(input: Record<string, unknown> = {}) {
|
||||
AS first_seen_date,
|
||||
MAX((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date)
|
||||
AS last_seen_date
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel = 'manual'
|
||||
@@ -381,7 +381,7 @@ export async function getManualSkuOverview() {
|
||||
COUNT(*)::int AS item_count,
|
||||
MAX((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date)
|
||||
AS last_seen_date
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel = 'manual'
|
||||
@@ -602,7 +602,7 @@ async function retryPendingManualItems(skuKey: string): Promise<number> {
|
||||
if (!key) return 0
|
||||
const candidates = await query<{ id: number }>(
|
||||
`SELECT oi.id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
WHERE oi.fulfillment_status = 'pending'
|
||||
AND resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) = $1
|
||||
ORDER BY oi.id
|
||||
@@ -688,7 +688,7 @@ export async function deleteManualSkuClassification(
|
||||
oi.id,
|
||||
(COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date::text
|
||||
AS basis_date
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel = 'manual'
|
||||
@@ -867,7 +867,7 @@ export async function rematchManualFulfillments(input: Record<string, unknown> =
|
||||
oi.id AS order_item_id,
|
||||
cfg.id AS config_id,
|
||||
cfg.canonical_product_id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN manual_fulfillment_configs cfg
|
||||
ON canonicalize_internal_sku(cfg.sku_key) = resolve_internal_sku(
|
||||
|
||||
@@ -410,7 +410,7 @@ export async function deleteAdminProduct(productId: number | string) {
|
||||
(SELECT COUNT(*)::int FROM channel_product_mappings WHERE canonical_product_id = $1) AS mapping_count,
|
||||
(SELECT COUNT(*)::int FROM cost_versions WHERE canonical_product_id = $1) AS version_count,
|
||||
(SELECT COUNT(*)::int FROM cost_snapshots WHERE canonical_product_id = $1) AS snapshot_count,
|
||||
(SELECT COUNT(*)::int FROM order_items WHERE canonical_product_id = $1) AS order_item_count
|
||||
(SELECT COUNT(*)::int FROM fulfillments WHERE canonical_product_id = $1) AS order_item_count
|
||||
`,
|
||||
[id],
|
||||
)
|
||||
@@ -474,7 +474,7 @@ export async function deleteAdminVoucherProduct(productId: number | string) {
|
||||
)
|
||||
)
|
||||
AS snapshot_count,
|
||||
(SELECT COUNT(*)::int FROM order_items WHERE canonical_product_id = $1)
|
||||
(SELECT COUNT(*)::int FROM fulfillments WHERE canonical_product_id = $1)
|
||||
AS order_item_count
|
||||
`,
|
||||
[id],
|
||||
@@ -779,7 +779,7 @@ export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {}
|
||||
COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel,
|
||||
COUNT(*)::int AS item_count
|
||||
FROM cost_exceptions ce
|
||||
JOIN order_items oi ON oi.id = ce.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = ce.order_item_id
|
||||
WHERE ce.reason = 'product_mapping_missing'
|
||||
AND ce.status = 'open'
|
||||
AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $1
|
||||
@@ -874,7 +874,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) {
|
||||
COALESCE(ce.detail_json->>'skuCode', '') AS sku_code,
|
||||
COUNT(*)::int AS item_count
|
||||
FROM cost_exceptions ce
|
||||
JOIN order_items oi ON oi.id = ce.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = ce.order_item_id
|
||||
WHERE ${clauses.join(' AND ')}
|
||||
AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${totalParams.length}
|
||||
GROUP BY 1, 2, 3
|
||||
@@ -927,7 +927,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) {
|
||||
MAX(NULLIF(ce.detail_json->>'basisAt', '')) AS last_seen_at,
|
||||
COUNT(*)::int AS item_count
|
||||
FROM cost_exceptions ce
|
||||
JOIN order_items oi ON oi.id = ce.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = ce.order_item_id
|
||||
WHERE ${clauses.join(' AND ')}
|
||||
${modeClause}
|
||||
GROUP BY 1, 2, 3, 4
|
||||
@@ -1192,7 +1192,7 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
oi.sku_code,
|
||||
oi.normalized_sku_name,
|
||||
oi.normalized_sku_series
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND oi.id > $1
|
||||
@@ -1241,7 +1241,7 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
AND ce.subject_type = 'order_item'
|
||||
AND ce.reason = 'product_mapping_missing'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM order_items oi
|
||||
SELECT 1 FROM v_order_item_current oi
|
||||
WHERE oi.id = ce.subject_id AND oi.canonical_product_id IS NOT NULL
|
||||
)
|
||||
`,
|
||||
@@ -1252,7 +1252,7 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
const remainingResult = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND ($1 = '' OR o.provider = $1)
|
||||
@@ -1354,7 +1354,7 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
`
|
||||
SELECT oi.id, oi.sku_name, oi.sku_code, oi.normalized_sku_name,
|
||||
oi.normalized_sku_series, oi.normalized_quantity
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
${range ? 'JOIN orders o ON o.id = oi.order_id' : ''}
|
||||
WHERE (oi.normalized_quantity IS NULL
|
||||
OR oi.normalized_sku_name IS NULL
|
||||
@@ -1404,7 +1404,7 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
|
||||
const remainingResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
${range ? 'JOIN orders o ON o.id = oi.order_id' : ''}
|
||||
WHERE (oi.normalized_quantity IS NULL
|
||||
OR oi.normalized_sku_name IS NULL
|
||||
@@ -1758,7 +1758,7 @@ export async function setAdminProductPrice(
|
||||
const earliest = await query<{ basis_date: string | null }>(
|
||||
`
|
||||
SELECT MIN((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date)::text AS basis_date
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id = $1
|
||||
AND oi.fulfillment_status = 'assigned'
|
||||
@@ -1840,7 +1840,7 @@ export async function setAdminProductPrice(
|
||||
const missingItems = await query<{ id: number }>(
|
||||
`
|
||||
SELECT oi.id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
WHERE oi.canonical_product_id = $1
|
||||
AND oi.fulfillment_status = 'assigned'
|
||||
AND NOT EXISTS (
|
||||
@@ -1866,7 +1866,7 @@ export async function setAdminProductPrice(
|
||||
const coveredItems = await query<{ id: number }>(
|
||||
`
|
||||
SELECT oi.id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
WHERE oi.canonical_product_id = $1
|
||||
AND oi.fulfillment_status = 'assigned'
|
||||
AND EXISTS (
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
/**
|
||||
* 管道 B:商品归一。
|
||||
*
|
||||
* 只写 `order_items.canonical_product_id`,不改渠道、不碰成本;
|
||||
* 只写 `fulfillments.canonical_product_id`,不改渠道、不碰成本;
|
||||
* 例外:三方卡券缺商品时按内部中文 SKU 自动建 `tpv:` 商品并补渠道规则。
|
||||
*
|
||||
* 解析顺序:
|
||||
@@ -339,7 +339,7 @@ export async function normalizeOrderItemProducts(
|
||||
wpr.rule_key AS work_rule_key,
|
||||
cfg.canonical_product_id AS manual_config_product_id,
|
||||
cfg_product.canonical_sku AS manual_config_product_sku
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id AND wo.status <> 'cancelled'
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id
|
||||
@@ -529,7 +529,7 @@ export async function normalizeProductsForItems(itemIds: number[]): Promise<{
|
||||
wpr.rule_key AS work_rule_key,
|
||||
cfg.canonical_product_id AS manual_config_product_id,
|
||||
cfg_product.canonical_sku AS manual_config_product_sku
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id AND wo.status <> 'cancelled'
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id
|
||||
|
||||
@@ -626,7 +626,7 @@ async function resetImportedTemplateProductBootstrap(range: { fromDate: string;
|
||||
`
|
||||
WITH candidates AS (
|
||||
SELECT oi.id, oi.canonical_product_id AS expected_product_id
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN channel_product_mappings m
|
||||
ON m.provider = o.provider
|
||||
@@ -680,7 +680,7 @@ async function resetImportedTemplateProductBootstrap(range: { fromDate: string;
|
||||
AND m.note LIKE '接单模板 %自动初始化%'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id = m.canonical_product_id
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
@@ -732,7 +732,7 @@ async function loadTemplateProductBootstrapData(range: { fromDate: string; toDat
|
||||
wo.product_rule_id,
|
||||
r.rule_key,
|
||||
COUNT(DISTINCT oi.id)::int AS item_count
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN work_orders wo ON wo.order_item_id = oi.id AND wo.status <> 'cancelled'
|
||||
JOIN work_product_rules r ON r.id = wo.product_rule_id AND r.enabled = TRUE
|
||||
|
||||
@@ -8,7 +8,8 @@ test('卡券 SKU 一个入口完成商品、规则、成本版本和订单回填
|
||||
assert.ok(service.includes('withTransaction('))
|
||||
assert.ok(service.includes("channel: 'third_party_voucher'"))
|
||||
assert.ok(service.includes('sys_third_party_voucher_cost'))
|
||||
assert.ok(service.includes('UPDATE order_items oi'))
|
||||
assert.ok(service.includes('attachFulfillmentConfigurationsWithExecutor'))
|
||||
assert.ok(!service.includes('UPDATE order_items oi'))
|
||||
assert.ok(service.includes('syncCostSnapshotsForItems'))
|
||||
assert.ok(service.includes('refreshFactsForDates'))
|
||||
assert.ok(service.includes("resolveExceptionsForItemsByReason(result.itemIds, 'cost_missing')"))
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AdminSession } from '../admin/admin-auth-service.js'
|
||||
import { orderItemBasisDates, refreshFactsForDates } from '../finance/refresh-facts.js'
|
||||
import { upsertInternalSkuChannelWithClient } from '../fulfillment/internal-sku-channel-service.js'
|
||||
import { canonicalizeInternalSku, containsCjk } from '../fulfillment/internal-sku-resolver.js'
|
||||
import { attachFulfillmentConfigurationsWithExecutor } from '../fulfillment/fulfillment-state-service.js'
|
||||
import { resolvePriceEffectiveFrom } from './cost-center-normalizers.js'
|
||||
import {
|
||||
resolveExceptionsForItemsByReason,
|
||||
@@ -103,24 +104,31 @@ export async function upsertAdminVoucherProduct(payload: JsonObject, session: Ad
|
||||
replacePriority: true,
|
||||
})
|
||||
|
||||
const repairedItems = await client.query<VoucherProductItemRow>(
|
||||
const repairCandidates = await client.query<VoucherProductItemRow>(
|
||||
`
|
||||
UPDATE order_items oi
|
||||
SET canonical_product_id = $2,
|
||||
updated_at = $3
|
||||
FROM orders o
|
||||
WHERE o.id = oi.order_id
|
||||
AND oi.fulfillment_channel = 'third_party_voucher'
|
||||
AND resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) = $1
|
||||
AND oi.canonical_product_id IS DISTINCT FROM $2
|
||||
RETURNING
|
||||
SELECT
|
||||
oi.id AS order_item_id,
|
||||
(COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date::text
|
||||
AS basis_date
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE TRUE
|
||||
AND oi.fulfillment_channel = 'third_party_voucher'
|
||||
AND resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) = $1
|
||||
AND oi.canonical_product_id IS DISTINCT FROM $2
|
||||
`,
|
||||
[skuKey, productId, now],
|
||||
[skuKey, productId],
|
||||
)
|
||||
const itemIds = repairedItems.rows.map((row) => Number(row.order_item_id))
|
||||
const itemIds = repairCandidates.rows.map((row) => Number(row.order_item_id))
|
||||
if (itemIds.length > 0) {
|
||||
await attachFulfillmentConfigurationsWithExecutor(client, {
|
||||
mappings: itemIds.map((orderItemId) => ({
|
||||
orderItemId,
|
||||
canonicalProductId: productId,
|
||||
})),
|
||||
onlyWhenCanonicalProductDifferent: true,
|
||||
})
|
||||
}
|
||||
if (itemIds.length > 0) {
|
||||
await client.query(
|
||||
`UPDATE cost_snapshots
|
||||
@@ -145,13 +153,13 @@ export async function upsertAdminVoucherProduct(payload: JsonObject, session: Ad
|
||||
}
|
||||
|
||||
let effectiveFrom = initialEffectiveFrom
|
||||
if (!requestedEffectiveFrom && repairedItems.rows.length > 0) {
|
||||
if (!requestedEffectiveFrom && repairCandidates.rows.length > 0) {
|
||||
const missingDates = await client.query<{ basis_date: string }>(
|
||||
`
|
||||
SELECT MIN(
|
||||
(COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
)::text AS basis_date
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.id = ANY($1::bigint[])
|
||||
AND NOT EXISTS (
|
||||
@@ -272,7 +280,7 @@ export async function upsertAdminVoucherProduct(payload: JsonObject, session: Ad
|
||||
return {
|
||||
product: mapProduct(product),
|
||||
itemIds,
|
||||
dates: [...new Set(repairedItems.rows.map((row) => row.basis_date).filter(Boolean))],
|
||||
dates: [...new Set(repairCandidates.rows.map((row) => row.basis_date).filter(Boolean))],
|
||||
costVersionId,
|
||||
effectiveFrom,
|
||||
amountFen,
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
'YYYY-MM-DD'
|
||||
) AS to_date
|
||||
FROM work_orders wo
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE wo.order_item_id IS NOT NULL
|
||||
AND wo.status <> 'cancelled'
|
||||
@@ -77,7 +77,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
wo.product_rule_id,
|
||||
oi.canonical_product_id AS expected_product_id
|
||||
FROM work_orders wo
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN v_order_item_current oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE wo.order_item_id IS NOT NULL
|
||||
AND wo.status <> 'cancelled'
|
||||
@@ -120,7 +120,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
SELECT
|
||||
oi.normalized_sku_name AS name,
|
||||
MIN(oi.sku_name) AS sample_name
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
@@ -174,7 +174,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
oi.sku_code,
|
||||
MIN(oi.sku_name) AS sku_name,
|
||||
oi.normalized_sku_name
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
@@ -219,7 +219,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
`
|
||||
WITH tail AS (
|
||||
SELECT oi.id, oi.normalized_sku_name
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
@@ -267,7 +267,7 @@ export async function backfillAdminWorkOrderProductSkus(payload: Record<string,
|
||||
const remainingResult = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM order_items oi
|
||||
FROM v_order_item_current oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
|
||||
Reference in New Issue
Block a user