重构:统一商品渠道投影并收敛权威文档

This commit is contained in:
yml2213
2026-11-08 19:44:27 +08:00
parent 0a595acfa9
commit 1634502363
67 changed files with 2147 additions and 444 deletions
@@ -21,6 +21,18 @@ import type {
// 履约方式 / 商品来源 / 成本模型
// ---------------------------------------------------------------------------
/** 标准商品目录允许使用的当前渠道筛选,包含“未路由”这个投影状态。 */
export const PRODUCT_CURRENT_CHANNEL_FILTERS = [
'affiliate_dash',
'work_order',
'third_party_voucher',
'manual',
'on_account',
'unrouted',
] as const
export type ProductCurrentChannelFilter = (typeof PRODUCT_CURRENT_CHANNEL_FILTERS)[number]
/**
* 由 canonical_sku 前缀推导商品来源。
* 返回空串表示未分类;未分类商品不允许反推履约方式。
@@ -43,6 +55,20 @@ export function productSourceFilterSql(paramIndex: number, column = 'p.canonical
)`
}
/** 当前渠道筛选:标准商品页签的唯一筛选口径。 */
export function productChannelFilterSql(
paramIndex: number,
column = 'p.active_primary_channel',
): string {
return `(
($${paramIndex} = 'unrouted' AND ${column} IS NULL)
OR (
$${paramIndex} <> 'unrouted'
AND ${column} = $${paramIndex}
)
)`
}
/**
* 成本模型是否适用于该履约方式。
*
@@ -95,7 +121,8 @@ export function mapProduct(row: ProductRow) {
canonicalSku: row.canonical_sku,
canonicalName: row.canonical_name,
category: row.category || '',
source: productSourceOf(row.canonical_sku),
source: String(row.source_kind || productSourceOf(row.canonical_sku)),
sourceKind: String(row.source_kind || 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,
@@ -122,6 +149,7 @@ export function mapProduct(row: ProductRow) {
? null
: row.template_sharing_enabled === true,
activePrimaryChannel: row.active_primary_channel || null,
fallbackChannels: Array.isArray(row.fallback_channels) ? row.fallback_channels : [],
affiliateTaskCount: Number(row.affiliate_task_count || 0),
affiliateLastFulfilledAt: row.affiliate_last_fulfilled_at || null,
createdAt: row.created_at,
@@ -2,20 +2,26 @@
* 成本中心总览与选项。
*
* 口径(2026-10-23 重写):
* 1. 商品来源统计按 canonical_sku 前缀分组,不再读 product_catalog.carrier_type;
* 1. 商品统计按 v_product_current.active_primary_channel 分组;
* 2. 成本项统计按 cost_model 分组,不再读 cost_items.carrier_type。
*/
import { query } from '../../db/client.js'
import { productSourceFilterSql } from './cost-center-mappers.js'
import { PROVIDERS, productSourceOf, toNumber } from './cost-center-shared.js'
import { productChannelFilterSql } from './cost-center-mappers.js'
import { PROVIDERS, toNumber } from './cost-center-shared.js'
export type CostCenterScope = 'affiliate_dash' | 'work_order' | 'manual'
export type CostCenterScope =
| 'affiliate_dash'
| 'work_order'
| 'third_party_voucher'
| 'manual'
| 'on_account'
| 'unrouted'
/**
* 成本中心总览。
*
* scope 为空表示「全部来源」;传入具体来源时,商品/映射/成本项/版本/异常/未映射
* 六项统计都按该来源收敛,页面顶部数字才会和下面的列表对得上。
* scope 为空表示「全部渠道」;传入具体渠道时,商品/映射/成本项/版本/异常/未映射
* 六项统计都按该渠道收敛,页面顶部数字才会和下面的列表对得上。
*
* 来源与成本模型的对应关系:
* affiliate_dash → affiliate_purchase
@@ -36,9 +42,9 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {}
`
WITH scoped_products AS (
SELECT p.id
FROM product_catalog p
FROM v_product_current p
WHERE p.status = 'active'
AND ($1::text IS NULL OR ${productSourceFilterSql(1)})
AND ($1::text IS NULL OR ${productChannelFilterSql(1)})
),
scoped_cost_models AS (
SELECT CASE $1
@@ -46,6 +52,7 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {}
WHEN 'work_order' THEN 'work_order_reward'
WHEN 'third_party_voucher' THEN 'third_party_voucher_cost'
WHEN 'manual' THEN 'manual_actual'
WHEN 'on_account' THEN 'manual_actual'
END AS cost_model
)
SELECT
@@ -93,22 +100,12 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {}
)
const row = result.rows[0]
const [productSourceResult, costModelResult] = await Promise.all([
query<{ canonical_sku: string; active_primary_channel: string | null; total: number }>(
`SELECT p.canonical_sku,
(
SELECT c.channel
FROM internal_sku_channels c
WHERE c.enabled = TRUE
AND c.is_fallback = FALSE
AND canonicalize_internal_sku(c.internal_sku)
= canonicalize_internal_sku(p.canonical_name)
ORDER BY c.priority, c.channel
LIMIT 1
) AS active_primary_channel,
query<{ channel: string | null; total: number }>(
`SELECT p.active_primary_channel AS channel,
COUNT(*)::int AS total
FROM product_catalog p
FROM v_product_current p
WHERE p.status = 'active'
GROUP BY p.canonical_sku, p.canonical_name`,
GROUP BY p.active_primary_channel`,
),
query<{ cost_model: string; total: number }>(
`SELECT cost_model, COUNT(*)::int AS total FROM cost_items WHERE status = 'active' GROUP BY cost_model`,
@@ -134,6 +131,8 @@ const COST_CENTER_SCOPES = new Set([
'work_order',
'third_party_voucher',
'manual',
'on_account',
'unrouted',
])
function normalizeScope(raw: unknown): CostCenterScope | null {
@@ -141,27 +140,18 @@ function normalizeScope(raw: unknown): CostCenterScope | null {
return COST_CENTER_SCOPES.has(value) ? (value as CostCenterScope) : null
}
/** 商品来源计数:按 canonical_sku 前缀归类,未分类单独成桶。 */
function productSourceCountsOf(
rows: Array<{ canonical_sku: string; active_primary_channel: string | null; total: number }>,
) {
/** 商品当前渠道计数:总览和列表共用 v_product_current。 */
function productSourceCountsOf(rows: Array<{ channel: string | null; total: number }>) {
const counts: Record<string, number> = {
affiliate_dash: 0,
work_order: 0,
third_party_voucher: 0,
manual: 0,
uncategorized: 0,
on_account: 0,
unrouted: 0,
}
for (const row of rows) {
const source = productSourceOf(row.canonical_sku)
// man: 只是旧来源;已有其他主渠道的商品不再计入当前人工客服。
if (
source === 'manual' &&
row.active_primary_channel &&
row.active_primary_channel !== 'manual'
) {
continue
}
const key = source || 'uncategorized'
const key = row.channel || 'unrouted'
counts[key] = (counts[key] || 0) + toNumber(row.total)
}
return counts
@@ -183,10 +173,12 @@ export async function getAdminCostCenterOptions() {
canonical_sku: string
canonical_name: string
unit_name: string
source_kind: string
active_primary_channel: string | null
}>(
`
SELECT id, canonical_sku, canonical_name, unit_name
FROM product_catalog
SELECT id, canonical_sku, canonical_name, unit_name, source_kind, active_primary_channel
FROM v_product_current
WHERE status = 'active'
ORDER BY canonical_name, id
LIMIT 5000
@@ -225,7 +217,8 @@ export async function getAdminCostCenterOptions() {
canonicalSku: row.canonical_sku,
canonicalName: row.canonical_name,
unitName: row.unit_name,
source: productSourceOf(row.canonical_sku),
source: row.source_kind,
activePrimaryChannel: row.active_primary_channel,
})),
costItems: costItemsResult.rows.map((row) => ({
id: Number(row.id),
@@ -29,7 +29,10 @@ export type ProductRow = {
template_pricing_mode?: string | null
template_enabled?: boolean | null
template_sharing_enabled?: boolean | null
source_kind?: string | null
active_primary_channel?: string | null
fallback_channels?: string[] | null
manual_config_id?: number | null
affiliate_task_count?: number
affiliate_last_fulfilled_at?: string | null
}
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { costModelAppliesTo } from './cost-center-mappers.js'
@@ -9,7 +10,7 @@ import {
} from './cost-center-shared.js'
import { calculateCostTotal, validateCostVersionScope } from './cost-resolution-service.js'
import { createAdminProduct, resolveMappingProductId } from './product-catalog-service.js'
import { productSourceFilterSql } from './cost-center-mappers.js'
import { productChannelFilterSql, productSourceFilterSql } from './cost-center-mappers.js'
import {
buildTemplateMappingPlan,
buildTemplateProductSku,
@@ -289,6 +290,10 @@ test('成本模型与商品来源必须覆盖第三方卡券渠道', () => {
assert.equal(costModelAppliesTo('third_party_voucher_cost', 'manual'), false)
assert.ok(productSourceFilterSql(1).includes("'third_party_voucher'"))
assert.ok(productSourceFilterSql(1).includes("'tpv:%'"))
const channelSql = productChannelFilterSql(1)
assert.ok(channelSql.includes('active_primary_channel'))
assert.ok(channelSql.includes("$1 = 'unrouted'"))
assert.ok(!channelSql.includes('canonical_sku'))
})
test('第三方卡券删除:无历史引用硬删除,有历史引用自动归档并停用规则', async () => {
@@ -310,11 +315,15 @@ test('第三方卡券删除:无历史引用硬删除,有历史引用自动
assert.ok(routeSource.includes("'/cost-center/voucher-products/:productId'"))
})
test('人工客服筛选保留所有可维护的 man 商品和显式兜底配置', () => {
const sql = productSourceFilterSql(1)
assert.ok(sql.includes(`$1 = 'manual'`))
assert.ok(sql.includes("LIKE 'man:%'"))
assert.ok(!sql.includes('live_channel'))
test('人工客服页签只按当前主渠道筛选,不能再按 man 前缀放回人工页', async () => {
const source = await readFile(new URL('./product-catalog-service.ts', import.meta.url), 'utf8')
const sql = productChannelFilterSql(1)
assert.ok(sql.includes('active_primary_channel'))
assert.ok(sql.includes("$1 <> 'unrouted'"))
assert.ok(sql.includes('active_primary_channel = $1'))
assert.ok(!sql.includes('canonical_sku'))
assert.ok(source.includes('FROM v_product_current p'))
assert.ok(source.includes('productChannelFilterSql'))
})
test('成本中心总览异常统计必须走异常表关联,不能全表扫描订单项子查询', async () => {
@@ -325,7 +334,21 @@ test('成本中心总览异常统计必须走异常表关联,不能全表扫
)
assert.ok(source.includes('JOIN order_items oi ON oi.id = ce.order_item_id'))
assert.ok(!source.includes('ce.order_item_id IN ('))
assert.ok(source.includes('productSourceFilterSql(1)'))
assert.ok(source.includes('productChannelFilterSql(1)'))
})
test('运行期停止写入已废弃的 product_channel_capabilities', async () => {
const files = await Promise.all(
[
'./voucher-product-service.ts',
'./product-normalize-service.ts',
'./product-catalog-service.ts',
'../fulfillment/channel-assign-service.ts',
].map((path) => readFile(new URL(path, import.meta.url), 'utf8')),
)
for (const source of files) {
assert.ok(!source.includes('product_channel_capabilities'))
}
})
test('tpv: 商品来源归为第三方卡券', async () => {
@@ -27,7 +27,6 @@ import { orderItemBasisDates, refreshFactsForDates } from '../finance/refresh-fa
import { syncAdminWorkProductRulePrice } from '../worker-platform/admin-worker-catalog-service.js'
import {
AFFILIATE_TASK_AGG_JOIN,
FULFILLMENT_CHANNELS,
MAPPING_CONFIDENCES,
MAPPING_SOURCES,
PRODUCT_CATEGORY_JOINS,
@@ -52,7 +51,8 @@ import {
normalizeRequiredText,
normalizeStatus,
parseJsonObject,
productSourceFilterSql,
productChannelFilterSql,
PRODUCT_CURRENT_CHANNEL_FILTERS,
requiredRow,
resolveMappingNameKey,
text,
@@ -64,13 +64,8 @@ import {
type WorkCategoryRow,
} from './cost-center-shared.js'
/** 商品来源的筛选取值:与 canonical_sku 前缀一一对应。 */
const PRODUCT_SOURCE_FILTERS = new Set([
'affiliate_dash',
'work_order',
'third_party_voucher',
'manual',
])
/** 标准商品页签的当前渠道筛选取值。 */
const PRODUCT_CHANNEL_FILTERS = new Set<string>(PRODUCT_CURRENT_CHANNEL_FILTERS)
/**
* 校验新建商品的 SKU 前缀。
@@ -108,17 +103,19 @@ export async function listAdminProducts(payload: JsonObject = {}) {
assertOneOf(status, PRODUCT_STATUSES, '商品状态无效')
baseParams.push(status)
baseClauses.push(`p.status = $${baseParams.length}`)
// carrierType 参数保留,语义改为「按 canonical_sku 前缀筛选商品来源」。
const source = text(payload.carrierType ?? payload.carrier_type ?? payload.source)
if (source) {
assertOneOf(source, PRODUCT_SOURCE_FILTERS, '商品来源无效')
baseParams.push(source)
baseClauses.push(productSourceFilterSql(baseParams.length))
// carrierType 参数仅为兼容旧前端,语义统一为「按当前主渠道筛选」。
const channel = text(
payload.channel ?? payload.currentChannel ?? payload.carrierType ?? payload.carrier_type,
)
if (channel) {
assertOneOf(channel, PRODUCT_CHANNEL_FILTERS, '商品渠道无效')
baseParams.push(channel)
baseClauses.push(productChannelFilterSql(baseParams.length))
}
// 人工/接单平台商品不需要 affiliate 任务统计:跳过 30 天 fulfillment_tasks 聚合。
// 只有「全部」和「自动发货」Tab 才需要这条聚合,避免小库 IO 被无关数据拖慢。
const needsAffiliateTaskStats = !source || source === 'affiliate_dash'
const needsAffiliateTaskStats = !channel || channel === 'affiliate_dash'
const affiliateTaskJoinSql = needsAffiliateTaskStats ? AFFILIATE_TASK_AGG_JOIN : ''
const affiliateColumnsSql = needsAffiliateTaskStats
? 'COALESCE(aff.task_count, 0) AS affiliate_task_count, aff.last_fulfilled_at AS affiliate_last_fulfilled_at'
@@ -146,7 +143,7 @@ export async function listAdminProducts(payload: JsonObject = {}) {
const [countResult, result, categoryStatResult, categoryResult] = await Promise.all([
query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM product_catalog p ${PRODUCT_CATEGORY_JOINS} ${where}`,
`SELECT COUNT(*)::int AS total FROM v_product_current p ${PRODUCT_CATEGORY_JOINS} ${where}`,
listParams,
),
query<ProductRow>(
@@ -156,16 +153,6 @@ export async function listAdminProducts(payload: JsonObject = {}) {
tpl_rule.pricing_mode AS template_pricing_mode,
tpl_rule.enabled AS template_enabled,
tpl_rule.sharing_enabled AS template_sharing_enabled,
(
SELECT c.channel
FROM internal_sku_channels c
WHERE c.enabled = TRUE
AND c.is_fallback = FALSE
AND canonicalize_internal_sku(c.internal_sku)
= canonicalize_internal_sku(p.canonical_name)
ORDER BY c.priority, c.channel
LIMIT 1
) AS active_primary_channel,
CASE WHEN c.parent_id IS NOT NULL THEN c.id END AS sub_category_id,
CASE WHEN c.parent_id IS NOT NULL THEN c.name ELSE '' END AS sub_category_name,
root.id AS root_category_id,
@@ -228,7 +215,7 @@ export async function listAdminProducts(payload: JsonObject = {}) {
)
) AS active_cost_effective_from,
${affiliateColumnsSql}
FROM product_catalog p
FROM v_product_current p
${PRODUCT_CATEGORY_JOINS}
LEFT JOIN work_product_rules tpl_rule
ON p.canonical_sku LIKE 'tpl:%'
@@ -252,7 +239,7 @@ export async function listAdminProducts(payload: JsonObject = {}) {
query<ProductCategoryStatRow>(
`
SELECT c.id AS category_id, c.name, c.parent_id, COUNT(*)::int AS total
FROM product_catalog p
FROM v_product_current p
${PRODUCT_CATEGORY_JOINS}
${baseWhere}
GROUP BY c.id, c.name, c.parent_id
@@ -512,16 +499,6 @@ export async function deleteAdminVoucherProduct(productId: number | string) {
`,
[id, internalSku, nowIso()],
)
await client.query(
`
UPDATE product_channel_capabilities
SET enabled = FALSE,
updated_at = $2
WHERE product_id = $1 AND channel = 'third_party_voucher'
`,
[id, nowIso()],
)
if (mappingCount > 0 || snapshotCount > 0 || orderItemCount > 0) {
await client.query(
`
@@ -566,7 +543,6 @@ export async function deleteAdminVoucherProduct(productId: number | string) {
`DELETE FROM cost_versions WHERE canonical_product_id = $1`,
[id],
)
await client.query(`DELETE FROM product_channel_capabilities WHERE product_id = $1`, [id])
await client.query(
`
DELETE FROM internal_sku_channels
@@ -627,12 +603,14 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
params.push(canonicalProductId)
clauses.push(`m.canonical_product_id = $${params.length}`)
}
// carrierType 参数保留,语义改为「按 canonical_sku 前缀筛选商品来源」。
const source = text(payload.carrierType ?? payload.carrier_type ?? payload.source)
if (source) {
assertOneOf(source, PRODUCT_SOURCE_FILTERS, '商品来源无效')
params.push(source)
clauses.push(productSourceFilterSql(params.length))
// carrierType 仅为旧客户端兼容,筛选口径统一是商品当前主渠道。
const channel = text(
payload.channel ?? payload.currentChannel ?? payload.carrierType ?? payload.carrier_type,
)
if (channel) {
assertOneOf(channel, PRODUCT_CHANNEL_FILTERS, '商品渠道无效')
params.push(channel)
clauses.push(productChannelFilterSql(params.length))
}
if (payload.enabled !== undefined && payload.enabled !== '') {
params.push(normalizeBoolean(payload.enabled))
@@ -652,7 +630,7 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
p.unit_name,
p.status
FROM channel_product_mappings m
JOIN product_catalog p ON p.id = m.canonical_product_id
JOIN v_product_current p ON p.id = m.canonical_product_id
${where}
GROUP BY
p.id,
@@ -672,7 +650,7 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
COUNT(DISTINCT p.id)::int AS total_count,
COUNT(*)::int AS mapping_total
FROM channel_product_mappings m
JOIN product_catalog p ON p.id = m.canonical_product_id
JOIN v_product_current p ON p.id = m.canonical_product_id
${where}
`,
params,
@@ -698,7 +676,7 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
p.canonical_sku,
p.canonical_name
FROM channel_product_mappings m
JOIN product_catalog p ON p.id = m.canonical_product_id
JOIN v_product_current p ON p.id = m.canonical_product_id
${where}
${where ? 'AND' : 'WHERE'} m.canonical_product_id = ANY($${mappingParams.length}::bigint[])
ORDER BY
@@ -748,16 +726,16 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
* 「全部」时并行跑四种模式再求和,避免一次全表 hash join。
*/
export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {}) {
const source = text(payload.carrierType ?? payload.carrier_type ?? payload.source)
if (source) assertOneOf(source, PRODUCT_SOURCE_FILTERS, '商品来源无效')
const channel = text(payload.channel ?? payload.channel)
if (channel) assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效')
const channel = text(
payload.channel ?? payload.currentChannel ?? payload.carrierType ?? payload.carrier_type,
)
if (channel) assertOneOf(channel, PRODUCT_CHANNEL_FILTERS, '商品渠道无效')
const productParams: unknown[] = []
let sourceSql = 'TRUE'
if (source) {
productParams.push(source)
sourceSql = productSourceFilterSql(productParams.length)
let productChannelSql = 'TRUE'
if (channel) {
productParams.push(channel)
productChannelSql = productChannelFilterSql(productParams.length)
}
const counts = await query<{
product_total: number
@@ -767,20 +745,20 @@ export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {}
`
SELECT
(
SELECT COUNT(*)::int FROM product_catalog p
WHERE p.status = 'active' AND ${sourceSql}
SELECT COUNT(*)::int FROM v_product_current p
WHERE p.status = 'active' AND ${productChannelSql}
) AS product_total,
(
SELECT COUNT(*)::int
FROM channel_product_mappings m
JOIN product_catalog p ON p.id = m.canonical_product_id
WHERE m.enabled = TRUE AND ${sourceSql}
JOIN v_product_current p ON p.id = m.canonical_product_id
WHERE m.enabled = TRUE AND ${productChannelSql}
) AS mapping_total,
(
SELECT COUNT(DISTINCT m.canonical_product_id)::int
FROM channel_product_mappings m
JOIN product_catalog p ON p.id = m.canonical_product_id
WHERE m.enabled = TRUE AND ${sourceSql}
JOIN v_product_current p ON p.id = m.canonical_product_id
WHERE m.enabled = TRUE AND ${productChannelSql}
) AS mapping_product_total
`,
productParams,
@@ -816,9 +794,12 @@ export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {}
}
}
const modes = channel
? [channel]
: ['work_order', 'affiliate_dash', 'third_party_voucher', 'manual', 'on_account']
const modes =
channel === 'unrouted'
? []
: channel
? [channel]
: ['work_order', 'affiliate_dash', 'third_party_voucher', 'manual', 'on_account']
// 顺序跑:并行会同时扫 cost_exceptions,实测 4 路并发比串行更慢(I/O 争抢)。
const aggregates: Array<{ groupTotal: number; itemTotal: number }> = []
for (const mode of modes) {
@@ -849,8 +830,14 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) {
const params: unknown[] = []
const clauses = [`ce.reason = 'product_mapping_missing'`, `ce.status = 'open'`]
const channel = text(payload.channel ?? payload.channel)
if (channel) assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效')
const channel = text(payload.channel ?? payload.currentChannel)
if (channel) assertOneOf(channel, PRODUCT_CHANNEL_FILTERS, '商品渠道无效')
if (channel === 'unrouted') {
return {
items: [],
pagination: { page, pageSize, total: 0, itemTotal: 0 },
}
}
const provider = text(payload.provider)
if (provider) {
params.push(normalizeProviderKey(provider))
@@ -210,15 +210,6 @@ async function ensureVoucherProductForItem(item: ItemRow): Promise<number | null
onlyWhenCanonicalProductDifferent: true,
})
await query(
`
INSERT INTO product_channel_capabilities (product_id, channel, priority, is_fallback, enabled, source)
VALUES ($1, 'third_party_voucher', 10, FALSE, TRUE, 'auto')
ON CONFLICT (product_id, channel) DO NOTHING
`,
[productId],
)
await upsertInternalSkuChannel({
internalSku,
channel: 'third_party_voucher',
@@ -89,21 +89,6 @@ export async function upsertAdminVoucherProduct(payload: JsonObject, session: Ad
throw createHttpError('卡券标准商品保存失败', { statusCode: 500 })
}
await client.query(
`
INSERT INTO product_channel_capabilities (
product_id, channel, priority, is_fallback, enabled, source, created_at, updated_at
) VALUES ($1, 'third_party_voucher', 10, FALSE, TRUE, 'voucher', $2, $2)
ON CONFLICT (product_id, channel) DO UPDATE SET
priority = 10,
is_fallback = FALSE,
enabled = TRUE,
source = 'voucher',
updated_at = EXCLUDED.updated_at
`,
[productId, now],
)
await upsertInternalSkuChannelWithClient(client, {
internalSku: skuKey,
channel: 'third_party_voucher',