/** * 接单模板引导:预览与执行、映射计划构建、模板商品编码。 * * 关联: * - affiliate-product-service.ts 自动发货商品同步与 SKU 回填 * - work-order-product-service.ts 接单平台商品回填与模板发单金额同步 */ import { query, withTransaction } from '../../db/client.js' import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' import { resolveFinanceDateRange } from '../../utils/finance-date-range.js' import { nowIso } from '../../utils/time.js' import type { AdminSession } from '../admin/admin-auth-service.js' import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js' import { candidateKey, mappingKey, mappingMatchTypeLabel } from './cost-center-shared.js' import type { TemplateMappingMatchType } from './cost-center-shared.js' import { backfillAdminNormalizedQuantities, backfillAdminProductMappings, } from './product-catalog-service.js' import { buildTemplateProductSku, inferTemplateUnitName, } from './work-order-template-product-service.js' import { attachFulfillmentConfigurations } from '../fulfillment/fulfillment-state-service.js' export { buildTemplateProductSku, inferTemplateUnitName } export async function previewAdminTemplateProductBootstrap(payload: JsonObject = {}) { const range = resolveBootstrapDateRange(payload) const data = await loadTemplateProductBootstrapData(range) const plan = buildTemplateMappingPlan(data.evidenceRows) const enabledRuleKeys = new Set(data.enabledTemplates.map((row) => row.rule_key)) const candidates = plan.candidates.filter( (row) => enabledRuleKeys.has(row.ruleKey) && !data.existingMappingKeys.has( mappingKey(row.provider, row.shopId, row.skuCode, row.matchType, row.skuNameKey), ), ) const candidatesByMatchType = countMappingCandidates(candidates) const templatesWithoutWorkOrders = data.enabledTemplates.length - data.templatesWithEvidence.length return { dateFrom: range.fromDate, dateTo: range.toDate, totalTemplateCount: data.enabledTemplates.length + data.disabledTemplates.length, templateCount: data.enabledTemplates.length, disabledTemplateCount: data.disabledTemplates.length, evidenceTemplateCount: data.templatesWithEvidence.length, templatesWithoutWorkOrders, productExistingCount: data.enabledTemplates.length - data.productsToCreate.length, productCreateCount: data.productsToCreate.length, mappingCandidateCount: plan.candidates.length, mappingExistingCount: plan.candidates.length - candidates.length, mappingCreateCount: candidates.length, mappingCreateByMatchType: candidatesByMatchType, evidenceItemCount: plan.evidenceItemCount, orderItemCoverCount: Math.max(0, plan.evidenceItemCount - plan.unresolvedItemCount), mappingConflictCount: plan.conflicts.length, mappingConflictItemCount: plan.unresolvedItemCount, multiRuleKeyCount: plan.codeConflictKeyCount, seriesFallbackConflictCount: plan.seriesFallbackConflictCount, samples: candidates.slice(0, 30).map((row) => ({ ruleKey: row.ruleKey, productName: data.enabledTemplates.find((item) => item.rule_key === row.ruleKey)?.product_name || '', provider: row.provider, shopId: row.shopId, skuCode: row.skuCode, skuName: row.skuName, matchType: row.matchType, itemCount: row.itemCount, })), conflicts: plan.conflicts.slice(0, 20).map((row) => ({ provider: row.provider, shopId: row.shopId, skuCode: row.skuCode, skuName: row.skuName, matchType: row.matchType, itemCount: row.itemCount, ruleKeys: row.ruleKeys, })), } } export async function applyAdminTemplateProductBootstrap( payload: JsonObject = {}, _session?: AdminSession, ) { const repair = payload.repair !== false const includeDisabledTemplates = payload.includeDisabledTemplates === true || payload.includeDisabled === true const range = resolveBootstrapDateRange(payload) const resetResult = repair ? await resetImportedTemplateProductBootstrap(range) : { deletedMappingCount: 0, resetOrderItemCount: 0 } const data = await loadTemplateProductBootstrapData(range) // 商品是价格/成本的载体,必须全量同步:新模板还没产生工单也要能定价。 const productTemplates = includeDisabledTemplates ? [...data.enabledTemplates, ...data.disabledTemplates] : data.enabledTemplates if (productTemplates.length === 0) { throw createHttpError('没有可同步的接单模板', { statusCode: 400, errorCode: 'cost_template_bootstrap_empty', }) } const plan = buildTemplateMappingPlan(data.evidenceRows) const now = nowIso() let createdProductCount = 0 let updatedProductCount = 0 let reusedProductCount = 0 let createdMappingCount = 0 let skippedMappingCount = 0 let mappedOrderItemCount = 0 await withTransaction(async (client) => { const productIdByRuleKey = new Map() for (const template of productTemplates) { const canonicalSku = buildTemplateProductSku(template.rule_key) const attributes = { source: 'work_product_rule', ruleId: Number(template.id), ruleKey: template.rule_key, mappingId: template.mapping_id ? Number(template.mapping_id) : null, mappingType: template.mapping_type, skuNick: template.sku_nick, relSkuId: template.rel_sku_id, sellerIds: template.seller_ids_json, pricingMode: template.pricing_mode === 'unit' ? 'unit' : 'fixed', unitPriceFen: Number(template.unit_price_fen || 0), rewardAmountFen: Number(template.reward_amount || 0), templateEnabled: template.enabled, } const existing = data.productByRuleKey.get(template.rule_key) if (existing) { // 模板改名/改价后刷新接单模板自己的商品;其他来源的同名商品只复用,不改写。 const refreshed = await client.query( ` UPDATE product_catalog SET canonical_name = $1, category = $2, unit_name = $3, attributes_json = COALESCE(attributes_json, '{}'::jsonb) || $4::jsonb, updated_at = $5 WHERE id = $6 AND canonical_sku LIKE 'tpl:%' `, [ template.product_name, template.category, inferTemplateUnitName(template.product_name), JSON.stringify(attributes), now, existing, ], ) if (refreshed.rowCount) { updatedProductCount += 1 } else { reusedProductCount += 1 } productIdByRuleKey.set(template.rule_key, existing) continue } const inserted = await client.query<{ id: number }>( ` INSERT INTO product_catalog ( canonical_sku, canonical_name, category, unit_name, status, attributes_json, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $7) ON CONFLICT (canonical_sku) DO NOTHING RETURNING id `, [ canonicalSku, template.product_name, template.category, inferTemplateUnitName(template.product_name), template.enabled ? 'active' : 'archived', JSON.stringify(attributes), now, ], ) let productId = inserted.rows[0]?.id ? Number(inserted.rows[0].id) : 0 if (productId) { createdProductCount += 1 } else { const existingResult = await client.query<{ id: number }>( 'SELECT id FROM product_catalog WHERE canonical_sku = $1 LIMIT 1', [canonicalSku], ) productId = Number(existingResult.rows[0]?.id || 0) if (productId) reusedProductCount += 1 } if (productId) productIdByRuleKey.set(template.rule_key, productId) } for (const row of plan.candidates) { const productId = productIdByRuleKey.get(row.ruleKey) if (!productId) continue const inserted = await client.query<{ id: number }>( ` INSERT INTO channel_product_mappings ( provider, shop_id, platform_sku_code, platform_sku_name, platform_sku_name_key, match_type, canonical_product_id, quantity_multiplier, confidence, source, enabled, note, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, 1, 'rule', 'import', TRUE, $8, $9, $9) ON CONFLICT ( provider, shop_id, platform_sku_code, match_type, platform_sku_name_key ) DO NOTHING RETURNING id `, [ row.provider, row.shopId, row.skuCode, row.skuName, row.skuNameKey, row.matchType, productId, `接单模板 ${row.ruleKey} 自动初始化(${mappingMatchTypeLabel(row.matchType)})`, now, ], ) if (inserted.rows[0]) { createdMappingCount += 1 } else { skippedMappingCount += 1 } } }) // 名称/系列键必须先于映射回填;所有批次都放在事务外,避免语句超时。 await backfillAdminNormalizedQuantities({ limit: 50000, all: true, dateFrom: range.fromDate, dateTo: range.toDate, }) const mappingBackfill = await backfillAdminProductMappings({ limit: 20000, all: true, dateFrom: range.fromDate, dateTo: range.toDate, }) mappedOrderItemCount = mappingBackfill.updatedCount return { dateFrom: range.fromDate, dateTo: range.toDate, templateCount: productTemplates.length, enabledTemplateCount: data.enabledTemplates.length, disabledTemplateCount: includeDisabledTemplates ? data.disabledTemplates.length : 0, evidenceTemplateCount: data.templatesWithEvidence.length, createdProductCount, updatedProductCount, reusedProductCount, createdMappingCount, skippedMappingCount, mappedOrderItemCount, deletedImportedMappingCount: resetResult.deletedMappingCount, resetOrderItemCount: resetResult.resetOrderItemCount, mappingConflictCount: plan.conflicts.length, mappingConflictItemCount: plan.unresolvedItemCount, } } /** * 按自动发货履约证据同步标准商品。 * * 只有真正走 affiliate-dash 执行器的订单项才算自动发货;渠道映射只是 * 平台 SKU 归一,不能作为自动发货依据。SKU 固定为 `aff:`, * 重复同步只更新名称/证据,不会重复建商品。 */ type TemplateBootstrapTemplateRow = { id: number enabled: boolean rule_key: string product_name: string category: string unit_price_fen: number reward_amount: number pricing_mode: 'fixed' | 'unit' mapping_id: number | null mapping_type: string sku_nick: string rel_sku_id: string seller_ids_json: unknown } export type TemplateBootstrapEvidenceRow = { provider: string shop_id: string sku_code: string sku_name: string product_rule_id: number rule_key: string item_count: number } export type TemplateMappingCandidate = { provider: string shopId: string skuCode: string skuName: string skuNameKey: string matchType: TemplateMappingMatchType ruleId: number ruleKey: string itemCount: number } export type TemplateMappingConflict = { provider: string shopId: string skuCode: string skuName: string matchType: 'sku_code_name' itemCount: number ruleIds: number[] ruleKeys: string[] } export type TemplateMappingPlan = { candidates: TemplateMappingCandidate[] conflicts: TemplateMappingConflict[] evidenceItemCount: number unresolvedItemCount: number codeConflictKeyCount: number seriesFallbackConflictCount: number } /** * 将历史工单的直接 rule_id 证据转换为渠道映射候选。 * * 编码唯一时直接按编码映射;编码被复用(91 常见)时,只有 * 编码 + SKU 名称系列 / 编码 + 精确名称唯一才生成映射。 * 多层仍无法唯一归属的组合进入 conflicts,绝不按“最新工单”猜测。 */ export function buildTemplateMappingPlan( rows: TemplateBootstrapEvidenceRow[], ): TemplateMappingPlan { const candidates = new Map() const conflicts = new Map() const evidenceItemCount = sumItemCount(rows) let unresolvedItemCount = 0 let codeConflictKeyCount = 0 let seriesFallbackConflictCount = 0 function addCandidate( input: Omit & { itemCount: number sampleSkuName: string }, ) { const key = candidateKey( input.provider, input.shopId, input.skuCode, input.matchType, input.skuNameKey, ) const current = candidates.get(key) if (current) { current.itemCount += input.itemCount if (input.itemCount > current.bestSampleCount) { current.skuName = input.sampleSkuName current.bestSampleCount = input.itemCount } return } candidates.set(key, { provider: input.provider, shopId: input.shopId, skuCode: input.skuCode, skuName: input.sampleSkuName, skuNameKey: input.skuNameKey, matchType: input.matchType, ruleId: input.ruleId, ruleKey: input.ruleKey, itemCount: input.itemCount, bestSampleCount: input.itemCount, }) } function addConflict(input: { provider: string shopId: string skuCode: string skuName: string nameKey: string itemCount: number ruleIds: number[] ruleKeys: string[] }) { const key = candidateKey( input.provider, input.shopId, input.skuCode, 'sku_code_name', input.nameKey, ) const current = conflicts.get(key) if (current) { current.itemCount += input.itemCount current.ruleIds = Array.from(new Set([...current.ruleIds, ...input.ruleIds])).sort( (left, right) => left - right, ) current.ruleKeys = Array.from(new Set([...current.ruleKeys, ...input.ruleKeys])).sort() return } conflicts.set(key, { provider: input.provider, shopId: input.shopId, skuCode: input.skuCode, skuName: input.skuName, matchType: 'sku_code_name', itemCount: input.itemCount, ruleIds: [...input.ruleIds].sort((left, right) => left - right), ruleKeys: [...input.ruleKeys].sort(), }) } const codeGroups = groupRows(rows, (row) => [row.provider, row.shop_id, row.sku_code]) for (const codeRows of codeGroups.values()) { const codeRuleIds = uniqueRuleIds(codeRows) if (codeRuleIds.length === 1) { const sample = representativeRow(codeRows) addCandidate({ provider: sample.provider, shopId: sample.shop_id, skuCode: sample.sku_code, skuName: sample.sku_name, skuNameKey: '', matchType: 'sku_code', ruleId: Number(sample.product_rule_id), ruleKey: sample.rule_key, itemCount: sumItemCount(codeRows), sampleSkuName: sample.sku_name, }) continue } codeConflictKeyCount += 1 const nameGroups = groupRows(codeRows, (row) => [ normalizeSkuEvidenceName(row.sku_name, row.sku_code), ]) for (const nameRows of nameGroups.values()) { const nameRuleIds = uniqueRuleIds(nameRows) const sample = representativeRow(nameRows) const nameKey = normalizeSkuEvidenceName(sample.sku_name, sample.sku_code) if (nameRuleIds.length === 1) { addCandidate({ provider: sample.provider, shopId: sample.shop_id, skuCode: sample.sku_code, skuName: sample.sku_name, skuNameKey: nameKey, matchType: 'sku_code_name', ruleId: Number(sample.product_rule_id), ruleKey: sample.rule_key, itemCount: sumItemCount(nameRows), sampleSkuName: sample.sku_name, }) } else { const itemCount = sumItemCount(nameRows) unresolvedItemCount += itemCount addConflict({ provider: sample.provider, shopId: sample.shop_id, skuCode: sample.sku_code, skuName: sample.sku_name, nameKey, itemCount, ruleIds: nameRuleIds, ruleKeys: uniqueRuleKeys(nameRows), }) } } const seriesGroups = groupRows(codeRows, (row) => [ normalizeSkuEvidenceSeries(row.sku_name, row.sku_code), ]) for (const seriesRows of seriesGroups.values()) { const seriesRuleIds = uniqueRuleIds(seriesRows) if (seriesRuleIds.length !== 1) { seriesFallbackConflictCount += 1 continue } const sample = representativeRow(seriesRows) addCandidate({ provider: sample.provider, shopId: sample.shop_id, skuCode: sample.sku_code, skuName: sample.sku_name, skuNameKey: normalizeSkuEvidenceSeries(sample.sku_name, sample.sku_code), matchType: 'sku_code_series', ruleId: Number(sample.product_rule_id), ruleKey: sample.rule_key, itemCount: sumItemCount(seriesRows), sampleSkuName: sample.sku_name, }) } } // 商品名系列兜底:仅在同一来源 + 店铺内唯一归属时生成,供后续新编码使用。 const globalSeriesGroups = groupRows(rows, (row) => [ row.provider, row.shop_id, normalizeSkuEvidenceSeries(row.sku_name, row.sku_code), ]) for (const seriesRows of globalSeriesGroups.values()) { const seriesRuleIds = uniqueRuleIds(seriesRows) const sample = representativeRow(seriesRows) const seriesKey = normalizeSkuEvidenceSeries(sample.sku_name, sample.sku_code) if (!seriesKey) continue if (seriesRuleIds.length !== 1) { seriesFallbackConflictCount += 1 continue } addCandidate({ provider: sample.provider, shopId: sample.shop_id, skuCode: '', skuName: sample.sku_name, skuNameKey: seriesKey, matchType: 'sku_name_series', ruleId: Number(sample.product_rule_id), ruleKey: sample.rule_key, itemCount: sumItemCount(seriesRows), sampleSkuName: sample.sku_name, }) } return { candidates: Array.from(candidates.values()) .map(({ bestSampleCount: _bestSampleCount, ...row }) => row) .sort(compareMappingCandidates), conflicts: Array.from(conflicts.values()).sort( (left, right) => right.itemCount - left.itemCount || left.provider.localeCompare(right.provider) || left.shopId.localeCompare(right.shopId) || left.skuCode.localeCompare(right.skuCode), ), evidenceItemCount, unresolvedItemCount, codeConflictKeyCount, seriesFallbackConflictCount, } } function countMappingCandidates(rows: TemplateMappingCandidate[]) { const counts: Record = { sku_code: 0, sku_code_name: 0, sku_code_series: 0, sku_name_series: 0, } for (const row of rows) counts[row.matchType] += 1 return counts } function groupRows(rows: T[], keyOf: (row: T) => Array) { const groups = new Map() for (const row of rows) { const key = keyOf(row).map(String).join('\u0000') const list = groups.get(key) if (list) list.push(row) else groups.set(key, [row]) } return groups } function uniqueRuleIds(rows: TemplateBootstrapEvidenceRow[]) { return Array.from(new Set(rows.map((row) => Number(row.product_rule_id)))).sort( (left, right) => left - right, ) } function uniqueRuleKeys(rows: TemplateBootstrapEvidenceRow[]) { return Array.from(new Set(rows.map((row) => row.rule_key).filter(Boolean))).sort() } function sumItemCount(rows: TemplateBootstrapEvidenceRow[]) { return rows.reduce((sum, row) => sum + Math.max(0, Number(row.item_count) || 0), 0) } function representativeRow(rows: TemplateBootstrapEvidenceRow[]) { return rows.reduce((best, row) => (Number(row.item_count) > Number(best.item_count) ? row : best)) } function normalizeSkuEvidenceName(skuName: string, skuCode: string) { return normalizeMatchText(skuName || skuCode) } function normalizeSkuEvidenceSeries(skuName: string, skuCode: string) { return normalizeSkuSeriesEquivalence(skuName || skuCode) } function compareMappingCandidates(left: TemplateMappingCandidate, right: TemplateMappingCandidate) { const typeOrder: Record = { sku_code: 1, sku_code_name: 2, sku_code_series: 3, sku_name_series: 4, } return ( right.itemCount - left.itemCount || typeOrder[left.matchType] - typeOrder[right.matchType] || left.provider.localeCompare(right.provider) || left.shopId.localeCompare(right.shopId) || left.skuCode.localeCompare(right.skuCode) || left.skuNameKey.localeCompare(right.skuNameKey) ) } async function resetImportedTemplateProductBootstrap(range: { fromDate: string; toDate: string }) { let resetOrderItemCount = 0 for (let batch = 0; batch < 20; batch += 1) { const candidates = await query<{ id: number; expected_product_id: number }>( ` WITH candidates AS ( SELECT oi.id, oi.canonical_product_id AS expected_product_id FROM order_items oi JOIN orders o ON o.id = oi.order_id JOIN channel_product_mappings m ON m.provider = o.provider AND (m.shop_id = '' OR m.shop_id = o.shop_id) AND oi.canonical_product_id = m.canonical_product_id AND m.source = 'import' AND m.note LIKE '接单模板 %自动初始化%' AND ( (m.match_type = 'sku_code' AND m.platform_sku_code = oi.sku_code) OR ( m.match_type = 'sku_code_name' AND m.platform_sku_code = oi.sku_code AND m.platform_sku_name_key = oi.normalized_sku_name ) OR ( m.match_type = 'sku_code_series' AND m.platform_sku_code = oi.sku_code AND m.platform_sku_name_key = oi.normalized_sku_series ) OR ( m.match_type = 'sku_name_series' AND m.platform_sku_name_key = oi.normalized_sku_series ) ) WHERE oi.canonical_product_id IS NOT NULL AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $1::date AND $2::date LIMIT 20000 ) SELECT DISTINCT id, expected_product_id FROM candidates ORDER BY id `, [range.fromDate, range.toDate], ) if (candidates.rows.length === 0) break const reset = await attachFulfillmentConfigurations({ mappings: candidates.rows.map((row) => ({ orderItemId: Number(row.id), canonicalProductId: null, expectedCanonicalProductId: Number(row.expected_product_id), })), }) resetOrderItemCount += reset.length if (candidates.rows.length < 20000) break } const deleted = await query( ` DELETE FROM channel_product_mappings m WHERE m.source = 'import' AND m.note LIKE '接单模板 %自动初始化%' AND EXISTS ( SELECT 1 FROM order_items 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 BETWEEN $1::date AND $2::date ) `, [range.fromDate, range.toDate], ) return { deletedMappingCount: deleted.rowCount || 0, resetOrderItemCount } } async function loadTemplateProductBootstrapData(range: { fromDate: string; toDate: string }) { const [templatesResult, evidenceResult, productsResult, mappingsResult] = await Promise.all([ query( ` SELECT r.id, r.enabled, r.rule_key, r.product_name, COALESCE(c.name, '') AS category, r.unit_price_fen, r.reward_amount, r.pricing_mode, m.id AS mapping_id, COALESCE(m.mapping_type, '') AS mapping_type, COALESCE(m.sku_nick, '') AS sku_nick, COALESCE(m.rel_sku_id, '') AS rel_sku_id, COALESCE(m.seller_ids_json, '[]'::jsonb) AS seller_ids_json FROM work_product_rules r LEFT JOIN work_categories c ON c.id = r.category_id LEFT JOIN LATERAL ( SELECT wm.* FROM work_product_rule_mappings wm WHERE wm.rule_id = r.id AND wm.enabled = TRUE ORDER BY wm.id LIMIT 1 ) m ON TRUE ORDER BY r.id `, ), query( ` SELECT o.provider, o.shop_id, oi.sku_code, oi.sku_name, wo.product_rule_id, r.rule_key, COUNT(DISTINCT oi.id)::int AS item_count FROM order_items 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 WHERE wo.product_rule_id IS NOT NULL AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $1::date AND $2::date GROUP BY o.provider, o.shop_id, oi.sku_code, oi.sku_name, wo.product_rule_id, r.rule_key ORDER BY o.provider, o.shop_id, oi.sku_code, oi.sku_name, wo.product_rule_id `, [range.fromDate, range.toDate], ), query<{ id: number; canonical_sku: string; canonical_name: string }>( 'SELECT id, canonical_sku, canonical_name FROM product_catalog', ), query<{ provider: string shop_id: string platform_sku_code: string platform_sku_name_key: string match_type: string }>( `SELECT provider, shop_id, platform_sku_code, platform_sku_name_key, match_type FROM channel_product_mappings`, ), ]) const usedRuleIds = new Set(evidenceResult.rows.map((row) => Number(row.product_rule_id))) // 商品是「成本/价格」的载体,必须全量同步:新模板还没产生工单时也要能定价。 // 渠道映射仍然只按工单证据建(没有订单就没有平台 SKU 可映射)。 const enabledTemplates = templatesResult.rows.filter((row) => row.enabled) const disabledTemplates = templatesResult.rows.filter((row) => !row.enabled) const templatesWithEvidence = enabledTemplates.filter((row) => usedRuleIds.has(Number(row.id))) const productByCanonicalSku = new Map( productsResult.rows.map((row) => [row.canonical_sku, Number(row.id)]), ) const productByCanonicalName = new Map() for (const row of productsResult.rows) { const key = normalizeProductNameKey(row.canonical_name) if (key && !productByCanonicalName.has(key)) { productByCanonicalName.set(key, Number(row.id)) } } const productByRuleKey = new Map() const productsToCreate: TemplateBootstrapTemplateRow[] = [] for (const template of enabledTemplates) { const existing = productByCanonicalSku.get(buildTemplateProductSku(template.rule_key)) || productByCanonicalName.get(normalizeProductNameKey(template.product_name)) if (existing) { productByRuleKey.set(template.rule_key, existing) } else { productsToCreate.push(template) } } return { enabledTemplates, disabledTemplates, templatesWithEvidence, evidenceRows: evidenceResult.rows, productsToCreate, productByRuleKey, existingMappingKeys: new Set( mappingsResult.rows.map((row) => mappingKey( row.provider, row.shop_id, row.platform_sku_code, row.match_type, row.platform_sku_name_key, ), ), ), } } function resolveBootstrapDateRange(payload: JsonObject) { const range = resolveFinanceDateRange( { dateFrom: payload.dateFrom, dateTo: payload.dateTo }, { defaultDays: 30, maxDays: 400 }, ) return { fromDate: range.fromDate, toDate: range.toDate } } function normalizeProductNameKey(value: unknown) { return String(value || '') .normalize('NFKC') .trim() .toLowerCase() }