删除咸鱼旧链路

This commit is contained in:
yml
2026-05-25 21:46:39 +08:00
parent 9aafddd624
commit e7aa194dde
142 changed files with 539 additions and 17373 deletions
@@ -1,65 +0,0 @@
import { query } from '../../../db/client.js'
import {
getAgisoMessagingDefaults,
getAgisoShopConfigMap,
getAgisoShopsFilePath,
saveAgisoMessagingConfig,
} from '../../platforms/agiso/shop-config-service.js'
import { resolveDisplayShopName } from '../admin-read-shared-helpers.js'
import {
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
} from './domain.js'
import { buildAdminAgisoMessagingSavePayload } from './writes.js'
type JsonObject = Record<string, any>
export async function getAdminAgisoShopConfigs() {
const configMap = getAgisoShopConfigMap()
const defaults = getAgisoMessagingDefaults()
const rowsResult = await query(
`
SELECT
shop_id,
MAX(CASE WHEN trim(shop_name) != '' THEN shop_name ELSE '' END) AS detected_shop_name,
MAX(created_at) AS latest_seen_at,
COUNT(*)::int AS webhook_event_count
FROM webhook_events
WHERE provider = 'agiso' AND trim(shop_id) != ''
GROUP BY shop_id
ORDER BY latest_seen_at DESC, shop_id DESC
`,
)
const rows = rowsResult.rows
return {
filePath: getAgisoShopsFilePath(),
defaults: mapAdminAgisoMessagingDefaults(defaults),
shops: Object.entries(configMap)
.sort(([left], [right]) => left.localeCompare(right))
.map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)),
observedShops: rows.map((row) => ({
shopId: String(row.shop_id || '').trim(),
detectedShopName: String(row.detected_shop_name || '').trim(),
displayShopName: resolveDisplayShopName('agiso', row.shop_id, row.detected_shop_name),
latestSeenAt: row.latest_seen_at || null,
webhookEventCount: Number(row.webhook_event_count || 0),
configured: Boolean(configMap[String(row.shop_id || '').trim()]),
})),
}
}
export function updateAdminAgisoShopConfigs(payload: JsonObject = {}) {
const saved = saveAgisoMessagingConfig(buildAdminAgisoMessagingSavePayload(payload, {
currentDefaults: getAgisoMessagingDefaults(),
currentMap: getAgisoShopConfigMap(),
}))
return {
filePath: getAgisoShopsFilePath(),
defaults: mapAdminAgisoMessagingDefaults(saved.defaults),
shops: Object.entries(saved.shops)
.sort(([left], [right]) => left.localeCompare(right))
.map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)),
}
}
@@ -1,6 +0,0 @@
export function normalizeAgisoMessageTemplate(value: unknown): string {
return String(value || "")
.replaceAll("\\r\\n", "\n")
.replaceAll("\\n", "\n")
.replaceAll("\r\n", "\n");
}
@@ -2,101 +2,15 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import {
applyOptionalStringField,
findMatchingObservedBinding,
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
mapAdminFulfillmentBindingConfigItem,
mapAdminObservedProductItem,
matchesObservedProduct,
} from './domain.js'
test('mapAdminAgisoMessagingDefaults normalizes escaped newlines', () => {
assert.deepEqual(
mapAdminAgisoMessagingDefaults({
messageTemplate: ' hello\\nworld ',
autoDeliveryMessageTemplate: ' done\\nnow ',
}),
{
messageTemplate: 'hello\nworld',
autoDeliveryMessageTemplate: 'done\nnow',
},
)
})
test('mapAdminAgisoShopConfigItem masks secrets and reports configured flags', () => {
assert.deepEqual(
mapAdminAgisoShopConfigItem('1001', {
shopName: ' 店铺A ',
accessToken: 'abcdef1234567890',
enabled: true,
messageTemplate: ' hi\\nall ',
autoDeliveryMessageTemplate: ' ok ',
appSecret: 'secret-1',
apiVersion: ' v2 ',
sendMessageEndpoint: ' /send ',
}),
{
shopId: '1001',
shopName: '店铺A',
accessToken: 'abcdef1234567890',
accessTokenMasked: 'abcdef****567890',
enabled: true,
messageTemplate: 'hi\nall',
autoDeliveryMessageTemplate: 'ok',
appSecretConfigured: true,
apiVersion: 'v2',
sendMessageEndpoint: '/send',
},
)
})
test('applyOptionalStringField updates and clears normalized template fields', () => {
const target = { messageTemplate: 'old' }
applyOptionalStringField(target, 'messageTemplate', { messageTemplate: ' next\\nline ' })
assert.deepEqual(target, { messageTemplate: 'next\nline' })
applyOptionalStringField(target, 'messageTemplate', { messageTemplate: ' ' })
assert.deepEqual(target, {})
})
test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
assert.deepEqual(
mapAdminFulfillmentBindingConfigItem({
provider: ' agiso ',
platform: ' xianyu ',
shopId: ' shop-1 ',
skuCode: ' sku-1 ',
priority: '80',
match: {
externalSkuCode: ' ext-1 ',
},
}),
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '',
skuCode: 'sku-1',
skuName: '',
profileKey: '',
enabled: true,
priority: 80,
config: {},
match: {
externalSkuCode: 'ext-1',
externalItemId: '',
externalSkuName: '',
config: {},
},
},
)
})
test('matchesObservedProduct honors provider platform shop and external fields', () => {
const binding = {
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-1',
match: {
externalSkuCode: 'sku-ext-1',
@@ -107,8 +21,8 @@ test('matchesObservedProduct honors provider platform shop and external fields',
assert.equal(
matchesObservedProduct(binding, {
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-1',
externalSkuCode: 'sku-ext-1',
}),
@@ -117,8 +31,8 @@ test('matchesObservedProduct honors provider platform shop and external fields',
assert.equal(
matchesObservedProduct(binding, {
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-2',
externalSkuCode: 'sku-ext-1',
}),
@@ -129,8 +43,8 @@ test('matchesObservedProduct honors provider platform shop and external fields',
test('mapAdminObservedProductItem attaches matched binding summary', () => {
const bindings = [
{
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-1',
skuCode: 'sku-1',
skuName: 'SKU 1',
@@ -141,8 +55,8 @@ test('mapAdminObservedProductItem attaches matched binding summary', () => {
},
]
const observed = {
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-1',
shopName: '店铺1',
externalItemId: '',
@@ -154,8 +68,8 @@ test('mapAdminObservedProductItem attaches matched binding summary', () => {
assert.deepEqual(findMatchingObservedBinding(bindings, observed), bindings[0])
assert.deepEqual(mapAdminObservedProductItem(observed, bindings), {
provider: 'agiso',
platform: 'xianyu',
provider: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-1',
shopName: '店铺1',
externalItemId: '',
@@ -1,70 +1,5 @@
import { normalizeAgisoMessageTemplate } from './agiso-template.js'
import { maskSecret } from './mappers.js'
type JsonObject = Record<string, any>
export function mapAdminAgisoMessagingDefaults(defaults: JsonObject = {}) {
return {
messageTemplate: normalizeAgisoMessageTemplate(String(defaults.messageTemplate || '').trim()),
autoDeliveryMessageTemplate: normalizeAgisoMessageTemplate(
String(defaults.autoDeliveryMessageTemplate || '').trim(),
),
}
}
export function mapAdminAgisoShopConfigItem(shopId: unknown, config: JsonObject = {}) {
return {
shopId,
shopName: String(config.shopName || '').trim(),
accessToken: String(config.accessToken || '').trim(),
accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: normalizeAgisoMessageTemplate(String(config.messageTemplate || '').trim()),
autoDeliveryMessageTemplate: normalizeAgisoMessageTemplate(
String(config.autoDeliveryMessageTemplate || '').trim(),
),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
}
}
export function applyOptionalStringField(target: JsonObject, key: string, source: JsonObject) {
if (!source || typeof source[key] !== 'string') {
return
}
const value = String(source[key] || '').trim()
if (value) {
target[key] = normalizeAgisoMessageTemplate(value)
return
}
delete target[key]
}
export function mapAdminFulfillmentBindingConfigItem(item: JsonObject) {
const match = item?.match || {}
return {
provider: String(item?.provider || '').trim(),
platform: String(item?.platform || '').trim(),
shopId: String(item?.shopId || '').trim(),
shopName: String(item?.shopName || '').trim(),
skuCode: String(item?.skuCode || '').trim(),
skuName: String(item?.skuName || '').trim(),
profileKey: String(item?.profileKey || '').trim(),
enabled: item?.enabled !== false,
priority: Number(item?.priority || 100),
config: item?.config || {},
match: {
externalSkuCode: String(match.externalSkuCode || '').trim(),
externalItemId: String(match.externalItemId || '').trim(),
externalSkuName: String(match.externalSkuName || '').trim(),
config: match.config || {},
},
}
}
export function matchesObservedProduct(binding: JsonObject, observed: JsonObject) {
const provider = String(binding?.provider || '').trim()
const platform = String(binding?.platform || '').trim()
@@ -1,103 +0,0 @@
import { getAgisoShopConfig } from '../../platforms/agiso/shop-config-service.js'
import {
filterLegacyOrderFulfillmentBindings,
getOrderFulfillmentBindingsFilePath,
getLegacyOrderFulfillmentBindingConfigs,
saveOrderFulfillmentBindingConfigs,
} from '../../order/fulfillment-binding-config-service.js'
import { enrichAgisoXianyuTradeOrder } from '../../platforms/agiso/xianyu/order-detail-service.js'
import { syncConfiguredFulfillmentBindings } from '../../bootstrap/fulfillment-bootstrap-service.js'
import { getFulfillmentProfileByKey } from '../../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../../utils/http.js'
import {
buildFulfillmentLookupResult,
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './fulfillment.js'
import { listAdminObservedProducts } from './observed-products.js'
import { validateAdminFulfillmentBindingConfigs } from './validation.js'
import { mapAdminFulfillmentBindingConfigItem } from './domain.js'
import type {
AdminFulfillmentBindingConfigSaveInput,
AdminFulfillmentBindingLookupInput,
} from '../../../types/admin-write-inputs.js'
type JsonObject = Record<string, any>
export async function getAdminFulfillmentBindingConfigs() {
const bindings = getLegacyOrderFulfillmentBindingConfigs()
return {
filePath: getOrderFulfillmentBindingsFilePath(),
bindings: bindings.map(mapAdminFulfillmentBindingConfigItem),
observedProducts: await listAdminObservedProducts(bindings),
}
}
export async function lookupAdminFulfillmentBindingOrder(
payload: AdminFulfillmentBindingLookupInput = {},
) {
const { provider, platform, shopId, platformOrderId } = normalizeFulfillmentLookupPayload(payload)
const detailResult = await enrichAgisoXianyuTradeOrder({
provider,
platform,
shopId,
shopName: '',
platformOrderId,
orderStatus: 'created',
payStatus: 'unpaid',
buyerId: '',
buyerName: '',
receiverContact: '',
totalAmount: 0,
currency: 'CNY',
paidAt: null,
rawPayload: {},
items: [],
}, {
requestId: `admin-fulfillment-lookup:${shopId}:${platformOrderId}`,
})
const { detail } = resolveFulfillmentLookupDetail(detailResult, platformOrderId)
const bindings = getLegacyOrderFulfillmentBindingConfigs()
return buildFulfillmentLookupResult({
provider,
platform,
shopId,
platformOrderId,
detail,
detailResult,
bindings,
fallbackShopName: getAgisoShopConfig(shopId)?.shopName,
})
}
export async function updateAdminFulfillmentBindingConfigs(
payload: AdminFulfillmentBindingConfigSaveInput = {},
) {
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput, {
getFulfillmentProfileByKey,
})
assertLegacyBindingConfigsOnly(normalizedBindings)
const saved = saveOrderFulfillmentBindingConfigs(normalizedBindings)
await syncConfiguredFulfillmentBindings()
return {
filePath: getOrderFulfillmentBindingsFilePath(),
bindings: saved.map(mapAdminFulfillmentBindingConfigItem),
}
}
function assertLegacyBindingConfigsOnly(bindings: JsonObject[] = []) {
if (filterLegacyOrderFulfillmentBindings(bindings).length === bindings.length) {
return
}
throw createHttpError('快手履约规则请改到“快手 Cloud 新履约”页维护,旧履约规则仅保留给 Agiso 等历史场景', {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_kuaishou_cloud_moved',
})
}
@@ -1,124 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildFulfillmentLookupResult,
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './fulfillment.js'
test('normalizeFulfillmentLookupPayload validates required fields and defaults provider/platform', () => {
assert.deepEqual(
normalizeFulfillmentLookupPayload({
shopId: ' shop-1 ',
platformOrderId: ' order-1 ',
}),
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
platformOrderId: 'order-1',
},
)
assert.throws(
() => normalizeFulfillmentLookupPayload({ platformOrderId: 'order-1' }),
/请先填写店铺 ID/,
)
})
test('resolveFulfillmentLookupDetail maps missing config into readable error', () => {
assert.throws(
() =>
resolveFulfillmentLookupDetail(
{
reason: 'missing_config',
parsed: { items: [] },
},
'order-1',
),
/当前店铺缺少订单详情查询配置/,
)
})
test('buildFulfillmentLookupResult assembles order and item matching payload', () => {
const result = buildFulfillmentLookupResult({
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
platformOrderId: 'order-1',
detail: {
shopName: '店铺A',
buyerName: '买家A',
totalAmount: 12345,
paidAt: '2026-05-04T10:00:00.000Z',
items: [
{
externalItemId: 'item-1',
externalSkuCode: 'sku-1',
externalSkuName: '礼包A',
skuName: '礼包A',
quantity: 2,
},
],
},
detailResult: {
enriched: true,
reason: 'ok',
errorMessage: '',
},
bindings: [
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'internal-1',
skuName: '内部商品',
profileKey: 'profile-1',
match: {
externalSkuCode: 'sku-1',
},
},
],
fallbackShopName: '备用店铺',
})
assert.deepEqual(result, {
order: {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
platformOrderId: 'order-1',
buyerName: '买家A',
totalAmountFen: 12345,
totalAmount: '123.45',
paidAt: '2026-05-04T10:00:00.000Z',
enriched: true,
enrichReason: 'ok',
errorMessage: '',
},
items: [
{
lineId: 'order-1:1:sku-1:item-1',
itemTitle: '礼包A',
quantity: 2,
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
externalItemId: 'item-1',
externalSkuCode: 'sku-1',
externalSkuName: '礼包A',
latestSeenAt: null,
orderItemCount: 2,
configured: true,
matchedBinding: {
skuCode: 'internal-1',
skuName: '内部商品',
profileKey: 'profile-1',
},
},
],
})
})
@@ -1,137 +0,0 @@
import { createHttpError } from '../../../utils/http.js'
import { formatFenToAmount } from '../../../utils/money.js'
import { isPlainObject, pickFirstNonEmpty } from './context.js'
import { mapAdminObservedProductItem } from './domain.js'
type JsonObject = Record<string, any>
export function normalizeFulfillmentLookupPayload(payload: JsonObject = {}) {
const provider = String(payload.provider || 'agiso').trim() || 'agiso'
const platform = String(payload.platform || 'xianyu').trim() || 'xianyu'
const shopId = String(payload.shopId || '').trim()
const platformOrderId = String(payload.platformOrderId || '').trim()
if (!shopId) {
throw createHttpError('请先填写店铺 ID', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_missing_shop_id',
})
}
if (!platformOrderId) {
throw createHttpError('请先填写平台订单号', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_missing_platform_order_id',
})
}
if (provider !== 'agiso' || platform !== 'xianyu') {
throw createHttpError('目前仅支持 Agiso 咸鱼订单手动查询', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_platform_not_supported',
})
}
return { provider, platform, shopId, platformOrderId }
}
export function resolveFulfillmentLookupDetail(detailResult: JsonObject, platformOrderId: unknown) {
const detail = isPlainObject(detailResult?.parsed) ? detailResult.parsed : {}
const items = Array.isArray(detail.items) ? detail.items : []
if (items.length > 0) {
return { detail, items }
}
const detailReason = String(detailResult?.reason || '').trim()
const detailMessage = String(detailResult?.errorMessage || '').trim()
let message = detailMessage
if (!message && detailReason === 'missing_config') {
message = '当前店铺缺少订单详情查询配置,请先检查 accessToken、appSecret 和详情接口地址'
}
if (!message) {
message = `未查询到订单 ${platformOrderId} 的商品明细`
}
throw createHttpError(message, {
statusCode: 404,
errorCode: 'admin_fulfillment_lookup_order_items_not_found',
})
}
export function buildFulfillmentLookupResult({
provider = '',
platform = '',
shopId = '',
platformOrderId = '',
detail = {},
detailResult = {},
bindings = [],
fallbackShopName = '',
}: {
provider?: string
platform?: string
shopId?: string
platformOrderId?: string
detail?: JsonObject
detailResult?: JsonObject
bindings?: JsonObject[]
fallbackShopName?: string
} = {}) {
const resolvedShopName = pickFirstNonEmpty([detail.shopName, fallbackShopName])
const items = Array.isArray(detail.items) ? detail.items : []
return {
order: {
provider,
platform,
shopId,
shopName: resolvedShopName,
platformOrderId,
buyerName: String(detail.buyerName || '').trim(),
totalAmountFen: Number(detail.totalAmount || 0),
totalAmount: formatFenToAmount(detail.totalAmount),
paidAt: detail.paidAt || null,
enriched: Boolean(detailResult?.enriched),
enrichReason: String(detailResult?.reason || '').trim(),
errorMessage: String(detailResult?.errorMessage || '').trim(),
},
items: items.map((item, index) => {
const observed = {
provider,
platform,
shopId,
shopName: resolvedShopName,
externalItemId: pickFirstNonEmpty([item?.externalItemId, item?.itemId]),
externalSkuCode: pickFirstNonEmpty([
item?.externalSkuCode,
item?.skuCode,
item?.externalItemId,
item?.itemId,
]),
externalSkuName: pickFirstNonEmpty([item?.externalSkuName, item?.skuName]),
latestSeenAt: null,
orderItemCount: Math.max(1, Number(item?.quantity || 0) || 1),
}
return {
lineId: [
platformOrderId,
index + 1,
observed.externalSkuCode || 'na',
observed.externalItemId || 'na',
].join(':'),
itemTitle: pickFirstNonEmpty([
item?.skuName,
item?.externalSkuName,
item?.externalSkuCode,
item?.externalItemId,
]),
quantity: observed.orderItemCount,
...mapAdminObservedProductItem(observed, bindings),
}
}),
}
}
@@ -1,61 +0,0 @@
export {
getAdminAgisoShopConfigs,
updateAdminAgisoShopConfigs,
} from "./agiso-service.js";
export {
getAdminNinetyoneOrders,
retryAdminNinetyoneOrder,
failAdminNinetyoneOrder,
} from "./ninetyone-service.js";
export {
getAdminKuaishouEticketSourceConfig,
updateAdminKuaishouEticketSourceConfig,
queryAdminKuaishouEticketDetail,
queryAdminKuaishouEticketShopInfo,
consumeAdminKuaishouEticket,
} from "./kuaishou-eticket-service.js";
export {
listAdminCloudtentaclesSources,
deleteAdminCloudtentaclesSource,
updateAdminCloudtentaclesSourceConfig,
sendAdminCloudtentaclesSmsCode,
testAdminCloudtentaclesLogin,
validateAdminCloudtentaclesSession,
getAdminCloudtentaclesAsset,
getAdminCloudtentaclesCategories,
getAdminCloudtentaclesSkuList,
buyAdminCloudtentaclesSku,
useAdminCloudtentaclesSku,
getAdminCloudtentaclesKnapsack,
listAdminCloudtentaclesVirtualNumbers,
appointAdminCloudtentaclesVirtualNumber,
generateAdminCloudtentaclesLoginCode,
fetchAdminCloudtentaclesVirtualNumberCode,
verifyAdminCloudtentaclesLoginCode,
getAdminCloudtentaclesBindUrl,
backAdminCloudtentaclesVirtualNumber,
runAdminCloudtentaclesFullFlow,
} from "./cloudtentacles-service.js";
export {
getAdminFulfillmentBindingConfigs,
lookupAdminFulfillmentBindingOrder,
updateAdminFulfillmentBindingConfigs,
} from "./fulfillment-bindings-service.js";
export {
getAdminKuaishouCloudFulfillmentConfig,
updateAdminKuaishouCloudFulfillmentConfig,
} from "./kuaishou-cloud-fulfillment-service.js";
export {
getAdminNotificationConfig,
updateAdminNotificationConfig,
testAdminNotification,
getAdminScheduledJobsConfig,
updateAdminScheduledJobsConfig,
runAdminScheduledJobNow,
} from "./notification-service.js";
@@ -1,175 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
validateAdminFulfillmentBindingConfigs,
} from './validation.js'
test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
assert.doesNotThrow(() => assertAdminFulfillmentBindingsInput([]))
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
})
test('normalizeAdminFulfillmentBindingItem normalizes generic binding fields', () => {
const result = normalizeAdminFulfillmentBindingItem(
{
provider: ' agiso ',
platform: ' xianyu ',
shopId: ' shop-1 ',
shopName: ' 店铺 ',
skuCode: 'sku-1',
match: {
externalSkuCode: 'ext-1',
},
config: {
kuaishouShop: {
note: 'keep',
},
},
},
{ index: 0 },
)
assert.deepEqual(result, {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺',
skuCode: 'sku-1',
skuName: '',
profileKey: 'manual_review',
enabled: true,
priority: undefined,
config: {
kuaishouShop: {
note: 'keep',
},
},
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: '91kaquan',
platform: 'kuaishou',
shopId: 'shop-a',
skuCode: 'sku-1',
match: {
externalItemId: '',
externalSkuCode: '',
externalSkuName: ' 礼包 A ',
},
}),
'91kaquan::kuaishou::shop-a::::::礼包 a::sku-1',
)
})
test('validateAdminFulfillmentBindingConfigs rejects missing fulfillment profile', async () => {
await assert.rejects(
() =>
validateAdminFulfillmentBindingConfigs(
[
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'sku-1',
profileKey: 'missing-profile',
match: {
externalSkuCode: 'ext-1',
},
},
],
{
kuaishouEticketSource: {},
resolveKuaishouEticketShopConfig() {
return null
},
async getFulfillmentProfileByKey() {
return null
},
},
),
/第 1 条规则使用了不存在的履约方式: missing-profile/,
)
})
test('validateAdminFulfillmentBindingConfigs rejects duplicate rules after normalization', async () => {
await assert.rejects(
() =>
validateAdminFulfillmentBindingConfigs(
[
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'sku-1',
profileKey: 'manual_review',
match: {
externalSkuCode: ' ext-1 ',
},
},
{
provider: 'agiso',
platform: 'xianyu',
shopId: ' shop-1 ',
skuCode: 'sku-1',
profileKey: 'manual_review',
match: {
externalSkuCode: 'ext-1',
},
},
],
{
kuaishouEticketSource: {},
resolveKuaishouEticketShopConfig() {
return null
},
async getFulfillmentProfileByKey() {
return { key: 'manual_review' }
},
},
),
/第 2 条规则与其它规则重复/,
)
})
@@ -1,125 +0,0 @@
import { createHttpError } from '../../../utils/http.js'
import { normalizeProductName } from '../../order/product-match-service.js'
import { isPlainObject } from './context.js'
type JsonObject = Record<string, any>
export function assertAdminFulfillmentBindingsInput(bindings: unknown) {
if (Array.isArray(bindings)) {
return
}
throw createHttpError('履约配置格式不正确', {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_payload',
})
}
export function normalizeAdminFulfillmentBindingItem(
rawBinding: unknown,
options: JsonObject = {},
) {
const index = Number(options.index || 0)
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()
const shopId = String(rawBinding.shopId || '').trim()
const shopName = String(rawBinding.shopName || '').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 (!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,
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(),
String(binding?.shopId || '').trim(),
String(binding?.match?.externalItemId || '').trim(),
String(binding?.match?.externalSkuCode || '').trim(),
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
String(binding?.skuCode || '').trim(),
].join('::')
}
export async function validateAdminFulfillmentBindingConfigs(
bindings: JsonObject[],
options: JsonObject = {},
) {
assertAdminFulfillmentBindingsInput(bindings)
const getFulfillmentProfileByKey = (
options.getFulfillmentProfileByKey || (async () => null)
) as (profileKey: string) => Promise<unknown>
const seenKeys = new Set()
const normalizedBindings: JsonObject[] = []
for (const [index, rawBinding] of bindings.entries()) {
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, { index })
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
if (!profile) {
throw createHttpError(`${index + 1} 条规则使用了不存在的履约方式: ${normalized.profileKey}`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_profile_key',
})
}
const uniqueKey = buildAdminFulfillmentBindingUniqueKey(normalized)
if (seenKeys.has(uniqueKey)) {
throw createHttpError(`${index + 1} 条规则与其它规则重复,请调整匹配条件或内部履约 SKU`, {
statusCode: 409,
errorCode: 'admin_fulfillment_bindings_duplicate_rule',
})
}
seenKeys.add(uniqueKey)
normalizedBindings.push(normalized)
}
return normalizedBindings
}
@@ -1,127 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildAdminAgisoMessagingSavePayload,
buildAdminAgisoShopConfigUpdate,
normalizeAdminCloudtentaclesSourceConfigPayload,
} from './writes.js'
test('buildAdminAgisoShopConfigUpdate merges item fields and drops explicit empty templates', () => {
assert.deepEqual(
buildAdminAgisoShopConfigUpdate(
{
shopId: ' shop-1 ',
shopName: ' ',
accessToken: ' new-token ',
messageTemplate: ' ',
autoDeliveryMessageTemplate: ' done ',
enabled: false,
},
{
shopName: '旧店铺',
accessToken: 'old-token',
messageTemplate: 'old-template',
autoDeliveryMessageTemplate: 'old-auto',
appSecret: 'keep-secret',
},
),
{
shopId: 'shop-1',
config: {
accessToken: 'new-token',
autoDeliveryMessageTemplate: 'done',
appSecret: 'keep-secret',
enabled: false,
},
},
)
})
test('buildAdminAgisoMessagingSavePayload updates defaults and keeps only shops with resulting access token', () => {
assert.deepEqual(
buildAdminAgisoMessagingSavePayload(
{
defaults: {
messageTemplate: ' next\\nline ',
autoDeliveryMessageTemplate: ' ',
},
shops: [
{
shopId: ' shop-1 ',
shopName: '店铺一',
accessToken: '',
enabled: true,
},
{
shopId: 'shop-2',
shopName: ' 店铺二 ',
accessToken: ' token-2 ',
messageTemplate: ' hi ',
apiVersion: ' v2 ',
},
{
shopId: '',
accessToken: 'skip',
},
],
},
{
currentDefaults: {
messageTemplate: 'old',
autoDeliveryMessageTemplate: 'old-auto',
},
currentMap: {
'shop-1': {
accessToken: 'token-1',
autoDeliveryMessageTemplate: 'keep-auto',
},
},
},
),
{
defaults: {
messageTemplate: 'next\nline',
},
shops: {
'shop-1': {
accessToken: 'token-1',
autoDeliveryMessageTemplate: 'keep-auto',
enabled: true,
shopName: '店铺一',
},
'shop-2': {
accessToken: 'token-2',
apiVersion: 'v2',
messageTemplate: 'hi',
shopName: '店铺二',
},
},
},
)
})
test('normalizeAdminCloudtentaclesSourceConfigPayload applies defaults', () => {
assert.deepEqual(
normalizeAdminCloudtentaclesSourceConfigPayload({
enabled: false,
baseUrl: ' ',
username: ' user ',
password: ' pass ',
phone: ' 13800138000 ',
deviceId: ' ',
deviceType: '2',
}),
{
key: 'default',
label: '',
enabled: false,
baseUrl: 'https://123.207.217.176',
username: 'user',
password: 'pass',
phone: '13800138000',
deviceId: '-',
deviceType: 2,
},
)
})
@@ -1,100 +0,0 @@
import { applyOptionalStringField } from "./domain.js";
type JsonObject = Record<string, any>
export function buildAdminAgisoShopConfigUpdate(item: JsonObject, current: JsonObject = {}) {
const shopId = String(item?.shopId || "").trim();
if (!shopId) {
return null;
}
const next: JsonObject = { ...current };
const shopName = String(item?.shopName || "").trim();
const accessToken = String(item?.accessToken || "").trim();
const messageTemplate = String(item?.messageTemplate || "").trim();
const autoDeliveryMessageTemplate = String(
item?.autoDeliveryMessageTemplate || ""
).trim();
const appSecret = String(item?.appSecret || "").trim();
const apiVersion = String(item?.apiVersion || "").trim();
const sendMessageEndpoint = String(item?.sendMessageEndpoint || "").trim();
if (shopName) {
next.shopName = shopName;
} else {
delete next.shopName;
}
if (accessToken) {
next.accessToken = accessToken;
}
if (messageTemplate) {
next.messageTemplate = messageTemplate;
} else if (typeof item?.messageTemplate === "string") {
delete next.messageTemplate;
}
if (autoDeliveryMessageTemplate) {
next.autoDeliveryMessageTemplate = autoDeliveryMessageTemplate;
} else if (typeof item?.autoDeliveryMessageTemplate === "string") {
delete next.autoDeliveryMessageTemplate;
}
if (appSecret) {
next.appSecret = appSecret;
}
if (apiVersion) {
next.apiVersion = apiVersion;
}
if (sendMessageEndpoint) {
next.sendMessageEndpoint = sendMessageEndpoint;
}
if (typeof item?.enabled === "boolean") {
next.enabled = item.enabled;
}
if (!next.accessToken) {
return null;
}
return { shopId, config: next };
}
export function buildAdminAgisoMessagingSavePayload(
payload: JsonObject = {},
options: JsonObject = {}
) {
const rawItems = Array.isArray(payload.shops) ? payload.shops : [];
const currentDefaults: JsonObject = options.currentDefaults || {};
const currentMap: Record<string, JsonObject> = options.currentMap || {};
const nextDefaults: JsonObject = {
...currentDefaults,
};
const nextMap: Record<string, JsonObject> = {};
applyOptionalStringField(nextDefaults, "messageTemplate", payload.defaults);
applyOptionalStringField(
nextDefaults,
"autoDeliveryMessageTemplate",
payload.defaults
);
for (const item of rawItems) {
const shopId = String(item?.shopId || "").trim();
if (!shopId) {
continue;
}
const updated = buildAdminAgisoShopConfigUpdate(
item,
currentMap[shopId] || {}
);
if (!updated) {
continue;
}
nextMap[updated.shopId] = updated.config;
}
return {
defaults: nextDefaults,
shops: nextMap,
};
}