diff --git a/apps/backend/src/services/admin/admin-platform-config-fulfillment.js b/apps/backend/src/services/admin/admin-platform-config-fulfillment.js new file mode 100644 index 00000000..b044f945 --- /dev/null +++ b/apps/backend/src/services/admin/admin-platform-config-fulfillment.js @@ -0,0 +1,128 @@ +// @ts-check + +import { createHttpError } from '../../utils/http.js' +import { formatFenToAmount } from '../../utils/money.js' +import { isPlainObject, pickFirstNonEmpty } from './admin-platform-config-context.js' +import { mapAdminObservedProductItem } from './admin-platform-config-domain.js' + +export function normalizeFulfillmentLookupPayload(payload = {}) { + 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, platformOrderId) { + 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 = /** @type {Record} */ ({}), + detailResult = /** @type {Record} */ ({}), + bindings = [], + fallbackShopName = '', +} = {}) { + 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), + } + }), + } +} diff --git a/apps/backend/src/services/admin/admin-platform-config-fulfillment.test.js b/apps/backend/src/services/admin/admin-platform-config-fulfillment.test.js new file mode 100644 index 00000000..8ee679b2 --- /dev/null +++ b/apps/backend/src/services/admin/admin-platform-config-fulfillment.test.js @@ -0,0 +1,124 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildFulfillmentLookupResult, + normalizeFulfillmentLookupPayload, + resolveFulfillmentLookupDetail, +} from './admin-platform-config-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', + }, + }, + ], + }) +}) diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index c9170748..5db70109 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -75,7 +75,6 @@ import { } from '../order/kuaishou-cloud-fulfillment-config-service.js' import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js' import { createHttpError } from '../../utils/http.js' -import { formatFenToAmount } from '../../utils/money.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' @@ -86,6 +85,11 @@ import { resolveCloudtentaclesAdminContext, resolveKuaishouEticketAdminContext, } from './admin-platform-config-context.js' +import { + buildFulfillmentLookupResult, + normalizeFulfillmentLookupPayload, + resolveFulfillmentLookupDetail, +} from './admin-platform-config-fulfillment.js' import { applyOptionalStringField, mapAdminAgisoMessagingDefaults, @@ -789,31 +793,7 @@ export async function updateAdminKuaishouCloudFulfillmentConfig( /** @param {AdminFulfillmentBindingLookupInput} [payload] */ export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) { - 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', - }) - } + const { provider, platform, shopId, platformOrderId } = normalizeFulfillmentLookupPayload(payload) const detailResult = await enrichAgisoXianyuTradeOrder({ provider, @@ -835,80 +815,18 @@ export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {Ad requestId: `admin-fulfillment-lookup:${shopId}:${platformOrderId}`, }) - const detail = isPlainObject(detailResult?.parsed) ? detailResult.parsed : {} - const items = Array.isArray(detail.items) ? detail.items : [] - - if (items.length === 0) { - 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', - }) - } - + const { detail } = resolveFulfillmentLookupDetail(detailResult, platformOrderId) const bindings = getOrderFulfillmentBindingConfigs() - const resolvedShopName = pickFirstNonEmpty([ - detail.shopName, - getAgisoShopConfig(shopId)?.shopName, - ]) - - 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), - } - }), - } + return buildFulfillmentLookupResult({ + provider, + platform, + shopId, + platformOrderId, + detail, + detailResult, + bindings, + fallbackShopName: getAgisoShopConfig(shopId)?.shopName, + }) } /** @param {AdminFulfillmentBindingConfigSaveInput} [payload] */