fix(fulfillment): 统一渠道规则与标准商品绑定

This commit is contained in:
yml2213
2026-11-05 18:17:12 +08:00
parent 8b9b4fc4ad
commit 89a68b5734
3 changed files with 318 additions and 59 deletions
@@ -0,0 +1,139 @@
-- 136_reconcile_channel_products_and_work_costs.sql
--
-- 渠道商品与成本模型归一:
-- 1. 已启用的第三方卡券规则必须有 active tpv: 标准商品;
-- 2. 接单报酬成本版本不得挂在 man: 人工商品上;无快照引用的历史错误版本退休;
-- 3. 保留有快照引用的历史数据,不删除订单、成本版本或成本快照。
-- 卡券规则已有、但标准商品缺失时补建商品。
INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, status,
attributes_json, created_at, updated_at
)
SELECT
'tpv:' || canonicalize_internal_sku(channel.internal_sku),
BTRIM(channel.internal_sku),
'卡券',
'张',
'active',
'{"source":"voucher_rule","auto_created":true,"reconciled":true}'::jsonb,
NOW(),
NOW()
FROM internal_sku_channels channel
WHERE channel.channel = 'third_party_voucher'
AND channel.enabled = TRUE
AND canonicalize_internal_sku(channel.internal_sku) <> ''
AND char_length('tpv:' || canonicalize_internal_sku(channel.internal_sku)) <= 64
AND NOT EXISTS (
SELECT 1
FROM product_catalog product
WHERE product.status = 'active'
AND product.canonical_sku LIKE 'tpv:%'
AND canonicalize_internal_sku(substring(product.canonical_sku from 5))
= canonicalize_internal_sku(channel.internal_sku)
)
AND NOT EXISTS (
SELECT 1
FROM internal_sku_channels other
WHERE other.enabled = TRUE
AND other.is_fallback = FALSE
AND other.channel <> 'third_party_voucher'
AND canonicalize_internal_sku(other.internal_sku)
= canonicalize_internal_sku(channel.internal_sku)
)
ON CONFLICT (canonical_sku) DO UPDATE SET
status = 'active',
canonical_name = CASE
WHEN BTRIM(product_catalog.canonical_name) = '' THEN EXCLUDED.canonical_name
ELSE product_catalog.canonical_name
END,
updated_at = EXCLUDED.updated_at;
-- 卡券规则统一挂到 active 标准商品,并补齐商品渠道能力。
UPDATE internal_sku_channels channel
SET canonical_product_id = product.id,
priority = 10,
is_fallback = FALSE,
config_json = channel.config_json || jsonb_build_object(
'voucherSku',
canonicalize_internal_sku(channel.internal_sku)
),
updated_at = NOW()
FROM product_catalog product
WHERE channel.channel = 'third_party_voucher'
AND channel.enabled = TRUE
AND product.status = 'active'
AND product.canonical_sku LIKE 'tpv:%'
AND canonicalize_internal_sku(substring(product.canonical_sku from 5))
= canonicalize_internal_sku(channel.internal_sku)
AND (
channel.canonical_product_id IS DISTINCT FROM product.id
OR channel.priority <> 10
OR channel.is_fallback
);
INSERT INTO product_channel_capabilities (
product_id, channel, priority, is_fallback, enabled, source, created_at, updated_at
)
SELECT
channel.canonical_product_id,
'third_party_voucher',
10,
FALSE,
TRUE,
'voucher',
NOW(),
NOW()
FROM internal_sku_channels channel
JOIN product_catalog product
ON product.id = channel.canonical_product_id
AND product.status = 'active'
WHERE channel.channel = 'third_party_voucher'
AND channel.enabled = TRUE
AND channel.canonical_product_id IS NOT NULL
ON CONFLICT (product_id, channel) DO UPDATE SET
enabled = TRUE,
priority = 10,
is_fallback = FALSE,
updated_at = EXCLUDED.updated_at;
-- 无法补齐标准商品的启用卡券规则必须停用,否则会继续把订单路由到无商品渠道。
UPDATE internal_sku_channels channel
SET enabled = FALSE,
is_fallback = FALSE,
note = CASE
WHEN BTRIM(channel.note) = '' THEN '标准商品缺失,迁移 136 自动停用'
ELSE channel.note
END,
updated_at = NOW()
WHERE channel.channel = 'third_party_voucher'
AND channel.enabled = TRUE
AND NOT EXISTS (
SELECT 1
FROM product_catalog product
WHERE product.id = channel.canonical_product_id
AND product.status = 'active'
AND product.canonical_sku LIKE 'tpv:%'
);
-- 早期错误地把“接单报酬”成本挂到了人工商品。无快照引用时直接退休。
UPDATE cost_versions version
SET status = 'retired',
change_reason = CASE
WHEN version.change_reason = '' THEN '清理错误挂到人工商品的接单报酬成本版本'
ELSE version.change_reason
END,
updated_at = NOW()
FROM cost_items item,
product_catalog product
WHERE version.cost_item_id = item.id
AND version.canonical_product_id = product.id
AND version.status = 'active'
AND item.cost_model = 'work_order_reward'
AND product.canonical_sku LIKE 'man:%'
AND NOT EXISTS (
SELECT 1 FROM cost_snapshots snapshot WHERE snapshot.cost_version_id = version.id
);
COMMENT ON COLUMN internal_sku_channels.canonical_product_id IS
'渠道规则关联的标准商品;启用规则必须指向 active 标准商品,历史归档仍保留关联';
@@ -556,13 +556,13 @@ export async function assignOrderItemsAfterCarrierReady(input: {
} else { } else {
await query( await query(
`UPDATE order_items `UPDATE order_items
SET fulfillment_channel = $3, SET fulfillment_channel = NULL,
fulfillment_status = 'pending', fulfillment_status = 'pending',
fulfillment_source = 'operator_assigned', fulfillment_source = 'operator_assigned',
fulfillment_locked = TRUE, fulfillment_locked = TRUE,
fulfillment_pending_reason = 'no_carrier', fulfillment_pending_reason = 'no_carrier',
fulfillment_pending_detail = $2::jsonb, fulfillment_pending_detail = $2::jsonb,
canonical_product_id = COALESCE($4, canonical_product_id), canonical_product_id = COALESCE($3, canonical_product_id),
updated_at = NOW() updated_at = NOW()
WHERE id = $1 AND fulfillment_status <> 'archived'`, WHERE id = $1 AND fulfillment_status <> 'archived'`,
[ [
@@ -573,7 +573,6 @@ export async function assignOrderItemsAfterCarrierReady(input: {
message: carrier.message, message: carrier.message,
carrierIssue: carrier.reason, carrierIssue: carrier.reason,
}), }),
input.channel,
input.canonicalProductId || null, input.canonicalProductId || null,
], ],
) )
@@ -761,6 +760,14 @@ export async function switchChannelForOrderItems(input: {
switchInternalSkus.length === 0 ? 'internal_sku_required' : 'multiple_internal_skus', switchInternalSkus.length === 0 ? 'internal_sku_required' : 'multiple_internal_skus',
}) })
} }
const targetInternalSku = switchInternalSkus[0]!
const draftProductId = await ensureDraftProductForChannel(targetInternalSku, input.channel)
if (!draftProductId) {
throw createHttpError('渠道标准商品创建或启用失败,请先在商品配置中处理', {
statusCode: 409,
errorCode: 'channel_standard_product_required',
})
}
const items = ( const items = (
await query<OrderItemRow>(`SELECT * FROM order_items WHERE id = ANY($1::bigint[])`, [ids]) await query<OrderItemRow>(`SELECT * FROM order_items WHERE id = ANY($1::bigint[])`, [ids])
@@ -909,6 +916,7 @@ export async function switchChannelForOrderItems(input: {
source: 'operator_assigned', source: 'operator_assigned',
lock: true, lock: true,
note: input.note, note: input.note,
canonicalProductId: draftProductId,
}) })
await query( await query(
`UPDATE order_items `UPDATE order_items
@@ -917,12 +925,11 @@ export async function switchChannelForOrderItems(input: {
[readyIds, input.adminUserId], [readyIds, input.adminUserId],
) )
if (assigned.updated > 0) { if (assigned.updated > 0) {
const internalSku = switchInternalSkus[0]! const carrier = await resolveInternalSkuCarrierConfig(targetInternalSku, input.channel)
const carrier = await resolveInternalSkuCarrierConfig(internalSku, input.channel)
await setInternalSkuPrimaryChannel({ await setInternalSkuPrimaryChannel({
internalSku, internalSku: targetInternalSku,
channel: input.channel, channel: input.channel,
canonicalProductId: carrier.canonicalProductId, canonicalProductId: draftProductId,
configJson: carrier.configJson, configJson: carrier.configJson,
source: 'operator', source: 'operator',
note: input.note, note: input.note,
@@ -1580,21 +1587,18 @@ export async function assignChannelByInternalSku(input: {
[internalSku, input.channel], [internalSku, input.channel],
) )
const draftProductId = await ensureDraftProductForChannel(internalSku, input.channel).catch( const draftProductId = await ensureDraftProductForChannel(internalSku, input.channel)
(error) => { if (!draftProductId) {
logWarn('[channel-assign]', '自动创建渠道草稿商品失败', { throw createHttpError('渠道标准商品创建或启用失败,请先在商品配置中处理', {
internalSku, statusCode: 409,
channel: input.channel, errorCode: 'channel_standard_product_required',
error: error instanceof Error ? error.message : String(error), })
}) }
return null
},
)
const carrier = await resolveInternalSkuCarrierConfig(internalSku, input.channel) const carrier = await resolveInternalSkuCarrierConfig(internalSku, input.channel)
await setInternalSkuPrimaryChannel({ await setInternalSkuPrimaryChannel({
internalSku, internalSku,
channel: input.channel, channel: input.channel,
canonicalProductId: carrier.canonicalProductId, canonicalProductId: draftProductId,
source: 'operator', source: 'operator',
configJson: { configJson: {
...carrier.configJson, ...carrier.configJson,
@@ -1818,7 +1822,18 @@ async function ensureDraftProductForChannel(
$5::jsonb, $6, $6 $5::jsonb, $6, $6
) )
ON CONFLICT (canonical_sku) DO UPDATE ON CONFLICT (canonical_sku) DO UPDATE
SET updated_at = product_catalog.updated_at SET status = 'active',
canonical_name = CASE
WHEN BTRIM(product_catalog.canonical_name) = '' THEN EXCLUDED.canonical_name
ELSE product_catalog.canonical_name
END,
attributes_json = (
product_catalog.attributes_json
- 'deletedAt'
- 'deletedBy'
- 'archivedAt'
) || jsonb_build_object('reactivatedAt', EXCLUDED.updated_at),
updated_at = EXCLUDED.updated_at
RETURNING id, status RETURNING id, status
`, `,
[ [
@@ -1891,7 +1906,7 @@ async function resolveInternalSkuCarrierConfig(
FROM product_catalog FROM product_catalog
WHERE status = 'active' WHERE status = 'active'
AND (canonical_sku LIKE $2 OR canonical_sku LIKE $3) AND (canonical_sku LIKE $2 OR canonical_sku LIKE $3)
AND canonicalize_internal_sku(canonical_name) = $1 AND canonicalize_internal_sku(substring(canonical_sku from 5)) = $1
ORDER BY id`, ORDER BY id`,
[key, `${prefixes[0]}%`, `${prefixes[1] || prefixes[0]}%`], [key, `${prefixes[0]}%`, `${prefixes[1] || prefixes[0]}%`],
) )
@@ -1958,19 +1973,28 @@ export async function upsertChannelRule(input: {
}) })
} }
const priority = Math.trunc(Number(input.priority) || 100) const draftProductId = await ensureDraftProductForChannel(matchKey, input.channel)
if (!draftProductId) {
throw createHttpError('渠道标准商品创建或启用失败,请先在商品配置中处理', {
statusCode: 409,
errorCode: 'channel_standard_product_required',
})
}
const priority =
input.channel === 'third_party_voucher' ? 10 : Math.trunc(Number(input.priority) || 100)
const carrier = await resolveInternalSkuCarrierConfig(matchKey, input.channel) const carrier = await resolveInternalSkuCarrierConfig(matchKey, input.channel)
const id = await upsertInternalSkuChannel({ const id = await upsertInternalSkuChannel({
internalSku: matchKey, internalSku: matchKey,
channel: input.channel, channel: input.channel,
priority, priority,
isFallback: priority >= 90, isFallback: input.channel === 'third_party_voucher' ? false : priority >= 90,
enabled: input.enabled !== false, enabled: input.enabled !== false,
source: source === 'operator' ? 'operator' : `rule:${source}`, source: source === 'operator' ? 'operator' : `rule:${source}`,
note: String(input.note || '').slice(0, 300), note: String(input.note || '').slice(0, 300),
adminUserId: input.adminUserId, adminUserId: input.adminUserId,
replacePriority: true, replacePriority: true,
canonicalProductId: carrier.canonicalProductId, canonicalProductId: draftProductId,
configJson: carrier.configJson, configJson: carrier.configJson,
}) })
return { id } return { id }
@@ -2079,15 +2103,18 @@ export async function ensureVoucherRuleForSku(skuCode: string): Promise<boolean>
LIMIT 1`, LIMIT 1`,
[`tpv:${key}`], [`tpv:${key}`],
) )
const productId = Number(product.rows[0]?.id || 0)
if (!productId) return false
await upsertInternalSkuChannel({ await upsertInternalSkuChannel({
internalSku: key, internalSku: key,
channel: 'third_party_voucher', channel: 'third_party_voucher',
priority: 10, priority: 10,
isFallback: false, isFallback: false,
enabled: true, enabled: true,
canonicalProductId: Number(product.rows[0]?.id || 0) || null, canonicalProductId: productId,
configJson: { voucherSku: key }, configJson: { voucherSku: key },
source: 'voucher', source: 'voucher',
replacePriority: true,
}) })
return true return true
} }
@@ -2110,7 +2137,8 @@ export async function ensureVoucherChannelRules(): Promise<{
FROM product_catalog product FROM product_catalog product
WHERE product.status = 'active' WHERE product.status = 'active'
AND product.canonical_sku LIKE 'tpv:%' AND product.canonical_sku LIKE 'tpv:%'
AND canonicalize_internal_sku(channel.internal_sku) = canonicalize_internal_sku(product.canonical_name) AND canonicalize_internal_sku(channel.internal_sku) =
canonicalize_internal_sku(substring(product.canonical_sku from 5))
AND channel.channel <> 'third_party_voucher' AND channel.channel <> 'third_party_voucher'
AND channel.enabled = TRUE AND channel.enabled = TRUE
AND channel.is_fallback = FALSE`, AND channel.is_fallback = FALSE`,
@@ -2122,17 +2150,21 @@ export async function ensureVoucherChannelRules(): Promise<{
canonical_product_id, config_json, source, created_at, updated_at canonical_product_id, config_json, source, created_at, updated_at
) )
SELECT SELECT
canonicalize_internal_sku(p.canonical_name), 'third_party_voucher', 10, FALSE, TRUE, canonicalize_internal_sku(substring(p.canonical_sku from 5)),
'third_party_voucher', 10, FALSE, TRUE,
p.id, jsonb_build_object('voucherSku', substring(p.canonical_sku from 5)), p.id, jsonb_build_object('voucherSku', substring(p.canonical_sku from 5)),
'voucher', NOW(), NOW() 'voucher', NOW(), NOW()
FROM product_catalog p FROM product_catalog p
WHERE p.status = 'active' WHERE p.status = 'active'
AND p.canonical_sku LIKE 'tpv:%' AND p.canonical_sku LIKE 'tpv:%'
AND BTRIM(COALESCE(p.canonical_name, '')) <> '' AND canonicalize_internal_sku(substring(p.canonical_sku from 5)) <> ''
ON CONFLICT (internal_sku, channel) DO UPDATE SET ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = 10,
is_fallback = FALSE,
enabled = TRUE, enabled = TRUE,
canonical_product_id = COALESCE(EXCLUDED.canonical_product_id, internal_sku_channels.canonical_product_id), canonical_product_id = EXCLUDED.canonical_product_id,
config_json = internal_sku_channels.config_json || EXCLUDED.config_json, config_json = internal_sku_channels.config_json || EXCLUDED.config_json,
source = EXCLUDED.source,
updated_at = NOW() updated_at = NOW()
RETURNING id RETURNING id
`, `,
@@ -54,6 +54,8 @@ test(
const retryWorkOrderSku = `集成测试重试接单-${suffix}` const retryWorkOrderSku = `集成测试重试接单-${suffix}`
const manualRemoveSku = `集成测试移除人工-${suffix}` const manualRemoveSku = `集成测试移除人工-${suffix}`
const protectedRuleSku = `集成测试强制删除-${suffix}` const protectedRuleSku = `集成测试强制删除-${suffix}`
const missingVoucherSku = `集成测试无商品卡券-${suffix}`
const reactivatedVoucherSku = `集成测试归档卡券-${suffix}`
const retryWorkOrderRuleKey = `it-retry-${suffix}` const retryWorkOrderRuleKey = `it-retry-${suffix}`
const orderIds: number[] = [] const orderIds: number[] = []
const orderItemIds: number[] = [] const orderItemIds: number[] = []
@@ -69,8 +71,10 @@ test(
retryWorkOrderSku, retryWorkOrderSku,
manualRemoveSku, manualRemoveSku,
protectedRuleSku, protectedRuleSku,
missingVoucherSku,
reactivatedVoucherSku,
] ]
let generatedVoucherProductId: number | null = null const createdVoucherProductIds: number[] = []
let autoManualProductId: number | null = null let autoManualProductId: number | null = null
let retryWorkOrderRuleId: number | null = null let retryWorkOrderRuleId: number | null = null
let fallbackProfileId: number | null = null let fallbackProfileId: number | null = null
@@ -251,8 +255,6 @@ test(
'third_party_voucher', 'third_party_voucher',
) )
assert.equal(switchedItem.rows[0]?.item_snapshot_json.isConfigured, false) assert.equal(switchedItem.rows[0]?.item_snapshot_json.isConfigured, false)
generatedVoucherProductId = Number(switchedItem.rows[0]?.canonical_product_id || 0) || null
const missingAffiliateItemId = await createPendingOrderItem(missingAffiliateSku) const missingAffiliateItemId = await createPendingOrderItem(missingAffiliateSku)
const affiliateFailed = await assignChannelByInternalSku({ const affiliateFailed = await assignChannelByInternalSku({
internalSku: missingAffiliateSku, internalSku: missingAffiliateSku,
@@ -644,13 +646,47 @@ test(
['closed', 'paid'], ['closed', 'paid'],
) )
await ensureVoucherRuleForSku(voucherSku) assert.equal(await ensureVoucherRuleForSku(missingVoucherSku), false)
const voucherChannel = await query<{ channel: string; source: string }>( const missingVoucherRules = await query<{ count: number }>(
`SELECT channel, source FROM internal_sku_channels `SELECT count(*)::int AS count FROM internal_sku_channels
WHERE internal_sku = $1 AND channel = 'third_party_voucher'`,
[missingVoucherSku],
)
assert.equal(missingVoucherRules.rows[0]?.count, 0)
const voucherProduct = await query<{ id: number }>(
`INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, status,
attributes_json, created_at, updated_at
) VALUES ($1, $2, '卡券', '张', 'active', '{}'::jsonb, NOW(), NOW())
RETURNING id`,
[`tpv:${voucherSku}`, `卡券展示名-${suffix}`],
)
createdVoucherProductIds.push(Number(voucherProduct.rows[0]!.id))
assert.equal(await ensureVoucherRuleForSku(voucherSku), true)
const voucherChannel = await query<{
channel: string
source: string
canonical_product_id: string | null
priority: number
is_fallback: boolean
config_json: Record<string, unknown>
}>(
`SELECT channel, source, canonical_product_id, priority, is_fallback, config_json
FROM internal_sku_channels
WHERE internal_sku = $1 ORDER BY channel`, WHERE internal_sku = $1 ORDER BY channel`,
[voucherSku], [voucherSku],
) )
assert.deepEqual(voucherChannel.rows, [{ channel: 'third_party_voucher', source: 'voucher' }]) assert.equal(voucherChannel.rows.length, 1)
assert.equal(voucherChannel.rows[0]?.channel, 'third_party_voucher')
assert.equal(voucherChannel.rows[0]?.source, 'voucher')
assert.equal(
Number(voucherChannel.rows[0]?.canonical_product_id),
createdVoucherProductIds[0],
)
assert.equal(voucherChannel.rows[0]?.priority, 10)
assert.equal(voucherChannel.rows[0]?.is_fallback, false)
assert.equal(voucherChannel.rows[0]?.config_json.voucherSku, voucherSku)
const legacyVoucherRules = await query<{ count: number }>( const legacyVoucherRules = await query<{ count: number }>(
`SELECT count(*)::int AS count FROM sku_channel_rules `SELECT count(*)::int AS count FROM sku_channel_rules
WHERE match_key = $1`, WHERE match_key = $1`,
@@ -692,6 +728,50 @@ test(
const deletedRules = await deleteChannelRules([savedRule.id]) const deletedRules = await deleteChannelRules([savedRule.id])
assert.equal(deletedRules.deletedCount, 1) assert.equal(deletedRules.deletedCount, 1)
assert.deepEqual(deletedRules.deletedIds, [savedRule.id]) assert.deepEqual(deletedRules.deletedIds, [savedRule.id])
const archivedVoucherProduct = await query<{ id: number }>(
`INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, status,
attributes_json, created_at, updated_at
) VALUES (
$1, $2, '卡券', '张', 'archived',
'{"deletedAt":"2026-01-01T00:00:00.000Z","deletedBy":"integration_test"}'::jsonb,
NOW(), NOW()
)
RETURNING id`,
[`tpv:${reactivatedVoucherSku}`, reactivatedVoucherSku],
)
const archivedVoucherProductId = Number(archivedVoucherProduct.rows[0]!.id)
createdVoucherProductIds.push(archivedVoucherProductId)
const reactivatedRule = await upsertChannelRule({
matchKeyType: 'internal_sku',
matchKey: reactivatedVoucherSku,
channel: 'third_party_voucher',
priority: 80,
enabled: true,
note: '集成测试重新激活归档商品',
adminUserId: null,
})
const reactivated = await query<{
status: string
attributes_json: Record<string, unknown>
canonical_product_id: string | null
priority: number
is_fallback: boolean
}>(
`SELECT product.status, product.attributes_json,
channel.canonical_product_id, channel.priority, channel.is_fallback
FROM internal_sku_channels channel
JOIN product_catalog product ON product.id = channel.canonical_product_id
WHERE channel.id = $1`,
[reactivatedRule.id],
)
assert.equal(reactivated.rows[0]?.status, 'active')
assert.equal(Number(reactivated.rows[0]?.canonical_product_id), archivedVoucherProductId)
assert.equal(reactivated.rows[0]?.priority, 10)
assert.equal(reactivated.rows[0]?.is_fallback, false)
assert.equal(reactivated.rows[0]?.attributes_json.deletedAt, undefined)
assert.equal(typeof reactivated.rows[0]?.attributes_json.reactivatedAt, 'string')
} finally { } finally {
if (orderItemIds.length > 0) { if (orderItemIds.length > 0) {
await query(`DELETE FROM cost_exception_items WHERE order_item_id = ANY($1::bigint[])`, [ await query(`DELETE FROM cost_exception_items WHERE order_item_id = ANY($1::bigint[])`, [
@@ -731,32 +811,40 @@ test(
WHERE provider = '91kaquan' AND shop_id = $1 AND match_key = ANY($2::text[])`, WHERE provider = '91kaquan' AND shop_id = $1 AND match_key = ANY($2::text[])`,
[shopId, internalSkus], [shopId, internalSkus],
) )
if (generatedVoucherProductId) { const generatedProducts = await query<{ id: string }>(
await query(`DELETE FROM sales_daily_facts WHERE canonical_product_id = $1`, [ `SELECT id
generatedVoucherProductId, FROM product_catalog
WHERE canonicalize_internal_sku(substring(canonical_sku from 5)) = ANY($1::text[])`,
[internalSkus],
)
const generatedProductIds = generatedProducts.rows.map((row) => Number(row.id))
if (generatedProductIds.length > 0) {
await query(
`DELETE FROM cost_snapshots
WHERE canonical_product_id = ANY($1::bigint[])
OR cost_version_id IN (
SELECT id FROM cost_versions WHERE canonical_product_id = ANY($1::bigint[])
)`,
[generatedProductIds],
)
await query(
`DELETE FROM sales_daily_facts WHERE canonical_product_id = ANY($1::bigint[])`,
[generatedProductIds],
)
await query(
`DELETE FROM channel_product_mappings WHERE canonical_product_id = ANY($1::bigint[])`,
[generatedProductIds],
)
await query(`DELETE FROM cost_versions WHERE canonical_product_id = ANY($1::bigint[])`, [
generatedProductIds,
]) ])
await query(`DELETE FROM cost_snapshots WHERE canonical_product_id = $1`, [ await query(
generatedVoucherProductId, `DELETE FROM product_channel_capabilities WHERE product_id = ANY($1::bigint[])`,
[generatedProductIds],
)
await query(`DELETE FROM product_catalog WHERE id = ANY($1::bigint[])`, [
generatedProductIds,
]) ])
await query(`DELETE FROM channel_product_mappings WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM cost_versions WHERE canonical_product_id = $1`, [
generatedVoucherProductId,
])
await query(`DELETE FROM product_catalog WHERE id = $1 AND canonical_sku = $2`, [
generatedVoucherProductId,
`tpv:${manualSku}`,
])
}
if (autoManualProductId) {
await query(`DELETE FROM sales_daily_facts WHERE canonical_product_id = $1`, [
autoManualProductId,
])
await query(`DELETE FROM cost_versions WHERE canonical_product_id = $1`, [
autoManualProductId,
])
await query(`DELETE FROM product_catalog WHERE id = $1`, [autoManualProductId])
} }
if (retryWorkOrderRuleId) { if (retryWorkOrderRuleId) {
await query(`DELETE FROM work_product_match_logs WHERE rule_id = $1`, [ await query(`DELETE FROM work_product_match_logs WHERE rule_id = $1`, [