抽离后台履约规则校验辅助逻辑
This commit is contained in:
@@ -76,7 +76,6 @@ import {
|
|||||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js'
|
import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js'
|
||||||
import { normalizeProductName } from '../order/product-match-service.js'
|
|
||||||
import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
|
import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
|
||||||
import {
|
import {
|
||||||
hasCloudtentaclesCredentialContextChanged,
|
hasCloudtentaclesCredentialContextChanged,
|
||||||
@@ -90,13 +89,17 @@ import {
|
|||||||
normalizeFulfillmentLookupPayload,
|
normalizeFulfillmentLookupPayload,
|
||||||
resolveFulfillmentLookupDetail,
|
resolveFulfillmentLookupDetail,
|
||||||
} from './admin-platform-config-fulfillment.js'
|
} from './admin-platform-config-fulfillment.js'
|
||||||
|
import {
|
||||||
|
assertAdminFulfillmentBindingsInput,
|
||||||
|
buildAdminFulfillmentBindingUniqueKey,
|
||||||
|
normalizeAdminFulfillmentBindingItem,
|
||||||
|
} from './admin-platform-config-validation.js'
|
||||||
import {
|
import {
|
||||||
applyOptionalStringField,
|
applyOptionalStringField,
|
||||||
mapAdminAgisoMessagingDefaults,
|
mapAdminAgisoMessagingDefaults,
|
||||||
mapAdminAgisoShopConfigItem,
|
mapAdminAgisoShopConfigItem,
|
||||||
mapAdminFulfillmentBindingConfigItem,
|
mapAdminFulfillmentBindingConfigItem,
|
||||||
mapAdminObservedProductItem,
|
mapAdminObservedProductItem,
|
||||||
resolveFulfillmentBindingMatchShopId,
|
|
||||||
} from './admin-platform-config-domain.js'
|
} from './admin-platform-config-domain.js'
|
||||||
import {
|
import {
|
||||||
mapAdminCloudtentaclesSession,
|
mapAdminCloudtentaclesSession,
|
||||||
@@ -845,111 +848,28 @@ export async function updateAdminFulfillmentBindingConfigs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
||||||
if (!Array.isArray(bindings)) {
|
assertAdminFulfillmentBindingsInput(bindings)
|
||||||
throw createHttpError('履约配置格式不正确', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_invalid_payload',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const seenKeys = new Set()
|
const seenKeys = new Set()
|
||||||
const kuaishouEticketSource = getKuaishouEticketSourceConfig()
|
const kuaishouEticketSource = getKuaishouEticketSourceConfig()
|
||||||
const normalizedBindings = []
|
const normalizedBindings = []
|
||||||
|
|
||||||
for (const [index, rawBinding] of bindings.entries()) {
|
for (const [index, rawBinding] of bindings.entries()) {
|
||||||
if (!isPlainObject(rawBinding)) {
|
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, {
|
||||||
throw createHttpError(`第 ${index + 1} 条规则格式不正确`, {
|
index,
|
||||||
statusCode: 400,
|
kuaishouEticketSource,
|
||||||
errorCode: 'admin_fulfillment_bindings_invalid_item',
|
resolveKuaishouEticketShopConfig,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
|
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
|
||||||
const platform = String(rawBinding.platform || '').trim()
|
|
||||||
let shopId = String(rawBinding.shopId || '').trim()
|
|
||||||
let shopName = String(rawBinding.shopName || '').trim()
|
|
||||||
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
|
|
||||||
const skuCode = String(rawBinding.skuCode || '').trim()
|
|
||||||
const skuName = String(rawBinding.skuName || '').trim()
|
|
||||||
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
|
||||||
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
|
|
||||||
const externalItemId = String(match.externalItemId || '').trim()
|
|
||||||
const externalSkuCode = String(match.externalSkuCode || '').trim()
|
|
||||||
const externalSkuName = String(match.externalSkuName || '').trim()
|
|
||||||
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
|
|
||||||
|
|
||||||
if (provider === 'khhao' && platform === 'kuaishou') {
|
|
||||||
if (!khhaoShopId) {
|
|
||||||
throw createHttpError(`第 ${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedShop = resolveKuaishouEticketShopConfig(
|
|
||||||
{
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
},
|
|
||||||
kuaishouEticketSource,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!matchedShop?.shopId || !matchedShop?.kshopName) {
|
|
||||||
throw createHttpError(
|
|
||||||
`第 ${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
|
|
||||||
{
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
shopId = String(matchedShop.shopId || '').trim()
|
|
||||||
shopName = String(matchedShop.kshopName || '').trim()
|
|
||||||
config.kuaishouShop = {
|
|
||||||
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
khhaoShopId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skuCode) {
|
|
||||||
throw createHttpError(`第 ${index + 1} 条规则缺少内部履约 SKU`, {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_missing_sku_code',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!externalItemId && !externalSkuCode && !externalSkuName) {
|
|
||||||
throw createHttpError(`第 ${index + 1} 条规则至少需要一种外部匹配条件`, {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_missing_match_condition',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = await getFulfillmentProfileByKey(profileKey)
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw createHttpError(`第 ${index + 1} 条规则使用了不存在的履约方式: ${profileKey}`, {
|
throw createHttpError(`第 ${index + 1} 条规则使用了不存在的履约方式: ${normalized.profileKey}`, {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'admin_fulfillment_bindings_invalid_profile_key',
|
errorCode: 'admin_fulfillment_bindings_invalid_profile_key',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKey = [
|
const uniqueKey = buildAdminFulfillmentBindingUniqueKey(normalized)
|
||||||
provider,
|
|
||||||
platform,
|
|
||||||
resolveFulfillmentBindingMatchShopId({
|
|
||||||
provider,
|
|
||||||
platform,
|
|
||||||
shopId,
|
|
||||||
khhaoShopId,
|
|
||||||
}),
|
|
||||||
externalItemId,
|
|
||||||
externalSkuCode,
|
|
||||||
normalizeProductName(externalSkuName),
|
|
||||||
skuCode,
|
|
||||||
].join('::')
|
|
||||||
|
|
||||||
if (seenKeys.has(uniqueKey)) {
|
if (seenKeys.has(uniqueKey)) {
|
||||||
throw createHttpError(`第 ${index + 1} 条规则与其它规则重复,请调整匹配条件或内部履约 SKU`, {
|
throw createHttpError(`第 ${index + 1} 条规则与其它规则重复,请调整匹配条件或内部履约 SKU`, {
|
||||||
@@ -959,26 +879,7 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
seenKeys.add(uniqueKey)
|
seenKeys.add(uniqueKey)
|
||||||
|
normalizedBindings.push(normalized)
|
||||||
normalizedBindings.push({
|
|
||||||
provider,
|
|
||||||
platform,
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
khhaoShopId,
|
|
||||||
skuCode,
|
|
||||||
skuName,
|
|
||||||
profileKey,
|
|
||||||
enabled: rawBinding.enabled !== false,
|
|
||||||
priority: rawBinding.priority,
|
|
||||||
config,
|
|
||||||
match: {
|
|
||||||
externalSkuCode,
|
|
||||||
externalItemId,
|
|
||||||
externalSkuName,
|
|
||||||
config: isPlainObject(match.config) ? match.config : {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizedBindings
|
return normalizedBindings
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
|
import { normalizeProductName } from '../order/product-match-service.js'
|
||||||
|
import { isPlainObject } from './admin-platform-config-context.js'
|
||||||
|
import { resolveFulfillmentBindingMatchShopId } from './admin-platform-config-domain.js'
|
||||||
|
|
||||||
|
export function assertAdminFulfillmentBindingsInput(bindings) {
|
||||||
|
if (Array.isArray(bindings)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createHttpError('履约配置格式不正确', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_invalid_payload',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAdminFulfillmentBindingItem(
|
||||||
|
rawBinding,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const index = Number(options.index || 0)
|
||||||
|
const kuaishouEticketSource = /** @type {Record<string, unknown>} */ (options.kuaishouEticketSource || {})
|
||||||
|
const resolveKuaishouEticketShopConfig = /** @type {(shop: Record<string, unknown>, source: Record<string, unknown>) => Record<string, unknown> | null} */ (
|
||||||
|
options.resolveKuaishouEticketShopConfig || (() => null)
|
||||||
|
)
|
||||||
|
if (!isPlainObject(rawBinding)) {
|
||||||
|
throw createHttpError(`第 ${index + 1} 条规则格式不正确`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_invalid_item',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
|
||||||
|
const platform = String(rawBinding.platform || '').trim()
|
||||||
|
let shopId = String(rawBinding.shopId || '').trim()
|
||||||
|
let shopName = String(rawBinding.shopName || '').trim()
|
||||||
|
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
|
||||||
|
const skuCode = String(rawBinding.skuCode || '').trim()
|
||||||
|
const skuName = String(rawBinding.skuName || '').trim()
|
||||||
|
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
||||||
|
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
|
||||||
|
const externalItemId = String(match.externalItemId || '').trim()
|
||||||
|
const externalSkuCode = String(match.externalSkuCode || '').trim()
|
||||||
|
const externalSkuName = String(match.externalSkuName || '').trim()
|
||||||
|
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
|
||||||
|
|
||||||
|
if (provider === 'khhao' && platform === 'kuaishou') {
|
||||||
|
if (!khhaoShopId) {
|
||||||
|
throw createHttpError(`第 ${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedShop = resolveKuaishouEticketShopConfig(
|
||||||
|
{
|
||||||
|
shopId,
|
||||||
|
shopName,
|
||||||
|
},
|
||||||
|
kuaishouEticketSource,
|
||||||
|
)
|
||||||
|
const matchedShopId = String(matchedShop?.shopId || '').trim()
|
||||||
|
const matchedShopName = String(matchedShop?.kshopName || '').trim()
|
||||||
|
|
||||||
|
if (!matchedShopId || !matchedShopName) {
|
||||||
|
throw createHttpError(
|
||||||
|
`第 ${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
|
||||||
|
{
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
shopId = matchedShopId
|
||||||
|
shopName = matchedShopName
|
||||||
|
config.kuaishouShop = {
|
||||||
|
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
|
||||||
|
shopId,
|
||||||
|
shopName,
|
||||||
|
khhaoShopId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skuCode) {
|
||||||
|
throw createHttpError(`第 ${index + 1} 条规则缺少内部履约 SKU`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_missing_sku_code',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!externalItemId && !externalSkuCode && !externalSkuName) {
|
||||||
|
throw createHttpError(`第 ${index + 1} 条规则至少需要一种外部匹配条件`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'admin_fulfillment_bindings_missing_match_condition',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
platform,
|
||||||
|
shopId,
|
||||||
|
shopName,
|
||||||
|
khhaoShopId,
|
||||||
|
skuCode,
|
||||||
|
skuName,
|
||||||
|
profileKey,
|
||||||
|
enabled: rawBinding.enabled !== false,
|
||||||
|
priority: rawBinding.priority,
|
||||||
|
config,
|
||||||
|
match: {
|
||||||
|
externalSkuCode,
|
||||||
|
externalItemId,
|
||||||
|
externalSkuName,
|
||||||
|
config: isPlainObject(match.config) ? match.config : {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAdminFulfillmentBindingUniqueKey(binding) {
|
||||||
|
return [
|
||||||
|
String(binding?.provider || '').trim(),
|
||||||
|
String(binding?.platform || '').trim(),
|
||||||
|
resolveFulfillmentBindingMatchShopId({
|
||||||
|
provider: binding?.provider,
|
||||||
|
platform: binding?.platform,
|
||||||
|
shopId: binding?.shopId,
|
||||||
|
khhaoShopId: binding?.khhaoShopId,
|
||||||
|
}),
|
||||||
|
String(binding?.match?.externalItemId || '').trim(),
|
||||||
|
String(binding?.match?.externalSkuCode || '').trim(),
|
||||||
|
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
|
||||||
|
String(binding?.skuCode || '').trim(),
|
||||||
|
].join('::')
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import {
|
||||||
|
assertAdminFulfillmentBindingsInput,
|
||||||
|
buildAdminFulfillmentBindingUniqueKey,
|
||||||
|
normalizeAdminFulfillmentBindingItem,
|
||||||
|
} from './admin-platform-config-validation.js'
|
||||||
|
|
||||||
|
test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
|
||||||
|
assert.doesNotThrow(() => assertAdminFulfillmentBindingsInput([]))
|
||||||
|
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapping', () => {
|
||||||
|
const result = normalizeAdminFulfillmentBindingItem(
|
||||||
|
{
|
||||||
|
provider: 'khhao',
|
||||||
|
platform: 'kuaishou',
|
||||||
|
shopId: 'legacy-shop',
|
||||||
|
shopName: '旧店铺',
|
||||||
|
khhaoShopId: 'khhao-1',
|
||||||
|
skuCode: 'sku-1',
|
||||||
|
match: {
|
||||||
|
externalSkuCode: 'ext-1',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
kuaishouShop: {
|
||||||
|
note: 'keep',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
kuaishouEticketSource: { source: true },
|
||||||
|
resolveKuaishouEticketShopConfig() {
|
||||||
|
return {
|
||||||
|
shopId: 'ks-100',
|
||||||
|
kshopName: '快手店铺A',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
provider: 'khhao',
|
||||||
|
platform: 'kuaishou',
|
||||||
|
shopId: 'ks-100',
|
||||||
|
shopName: '快手店铺A',
|
||||||
|
khhaoShopId: 'khhao-1',
|
||||||
|
skuCode: 'sku-1',
|
||||||
|
skuName: '',
|
||||||
|
profileKey: 'manual_review',
|
||||||
|
enabled: true,
|
||||||
|
priority: undefined,
|
||||||
|
config: {
|
||||||
|
kuaishouShop: {
|
||||||
|
note: 'keep',
|
||||||
|
shopId: 'ks-100',
|
||||||
|
shopName: '快手店铺A',
|
||||||
|
khhaoShopId: 'khhao-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
match: {
|
||||||
|
externalSkuCode: 'ext-1',
|
||||||
|
externalItemId: '',
|
||||||
|
externalSkuName: '',
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizeAdminFulfillmentBindingItem rejects missing sku and match conditions', () => {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeAdminFulfillmentBindingItem(
|
||||||
|
{
|
||||||
|
provider: 'agiso',
|
||||||
|
platform: 'xianyu',
|
||||||
|
match: {
|
||||||
|
externalSkuCode: 'ext-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ index: 1 },
|
||||||
|
),
|
||||||
|
/第 2 条规则缺少内部履约 SKU/,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeAdminFulfillmentBindingItem(
|
||||||
|
{
|
||||||
|
provider: 'agiso',
|
||||||
|
platform: 'xianyu',
|
||||||
|
skuCode: 'sku-1',
|
||||||
|
},
|
||||||
|
{ index: 2 },
|
||||||
|
),
|
||||||
|
/第 3 条规则至少需要一种外部匹配条件/,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
|
||||||
|
assert.equal(
|
||||||
|
buildAdminFulfillmentBindingUniqueKey({
|
||||||
|
provider: 'khhao',
|
||||||
|
platform: 'kuaishou',
|
||||||
|
shopId: 'shop-a',
|
||||||
|
khhaoShopId: 'khhao-a',
|
||||||
|
skuCode: 'sku-1',
|
||||||
|
match: {
|
||||||
|
externalItemId: '',
|
||||||
|
externalSkuCode: '',
|
||||||
|
externalSkuName: ' 礼包 A ',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'khhao::kuaishou::khhao-a::::::礼包 a::sku-1',
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user