优化了自动发货和发货后的消息

This commit is contained in:
yml
2026-04-14 15:54:13 +08:00
parent 962867981e
commit ed45eab26c
13 changed files with 413 additions and 77 deletions
@@ -52,6 +52,7 @@ export async function getAdminAgisoShopConfigs() {
accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: String(config.messageTemplate || '').trim(),
autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
@@ -70,6 +71,7 @@ export async function getAdminAgisoShopConfigs() {
/** @param {AdminAgisoShopConfigSaveInput} [payload] */
export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) {
const rawItems = Array.isArray(payload.shops) ? payload.shops : []
const currentMap = getAgisoShopConfigMap()
const nextMap = {}
for (const item of rawItems) {
@@ -78,22 +80,33 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC
continue
}
const next = {}
const current = currentMap[shopId] || {}
const next = { ...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
@@ -128,6 +141,7 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC
accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: String(config.messageTemplate || '').trim(),
autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
@@ -64,7 +64,7 @@ function normalizeAgisoShopConfigMap(rawValue) {
next.enabled = enabled
}
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) {
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'autoDeliveryMessageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) {
const value = String(config[key] || '').trim()
if (value) {
next[key] = value
@@ -4,6 +4,8 @@ import { runtimeConfig } from '../../../../config/runtime.js'
import { listTasksByOrderId, updateTask } from '../../../../repositories/task-repo.js'
import { createTaskEvent } from '../../../../repositories/task-event-repo.js'
import { getAgisoShopConfig } from '../shop-config-service.js'
import { queryAgisoXianyuOrderDetail } from './order-detail-service.js'
import { ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask } from './message-service.js'
import { parseJsonObject } from '../../../../utils/json.js'
import { logWebhook } from '../../../../utils/logger.js'
import { nowIso } from '../../../../utils/time.js'
@@ -113,7 +115,46 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
if (success) {
const requestId = String(parsed?.RequestId || '').trim()
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功', {
// DummySend 接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~
const confirmResult = await confirmAgisoXianyuAutoDeliveryShipped({
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
requestId,
})
if (!confirmResult.shipped) {
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货接口返回成功,但订单状态未确认进入已发货', {
trigger,
orderId: order.id,
taskId: task.id,
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
responseStatus: response.status,
requestId,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
confirmReason: confirmResult.reason,
}, { level: 'warn' })
return persistAgisoAutoDeliveryResult(task, {
status: 'failed',
trigger,
reason: 'delivery_not_confirmed',
order,
responseStatus: response.status,
response: parsed,
errorMessage: `接口返回成功,但订单状态未确认进入已发货 (orderStatus=${confirmResult.orderStatus}, shipTime=${confirmResult.shipTime})`,
detail: {
requestId,
aldsType: config.aldsType,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
},
})
}
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', {
trigger,
orderId: order.id,
taskId: task.id,
@@ -121,9 +162,11 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
platformOrderId: order.platform_order_id,
responseStatus: response.status,
requestId,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
})
return persistAgisoAutoDeliveryResult(task, {
const result = await persistAgisoAutoDeliveryResult(task, {
status: 'success',
trigger,
reason: '',
@@ -133,8 +176,15 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
detail: {
requestId,
aldsType: config.aldsType,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
},
})
// 发货确认成功后,发送自定义消息通知 meow~
await sendAutoDeliveryMessage({ order, task })
return result
}
const errorMessage = resolveAgisoAutoDeliveryErrorMessage(parsed, rawText, response.status)
@@ -411,3 +461,72 @@ function normalizeBooleanLike(value, fallbackValue) {
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
/**
* DummySend 返回成功后,再查一次 Order/Detail 确认订单是否真的进入已发货状态 meow~
* 只有 ship_time > 0 或 orderStatus >= 3 才算发货确认通过
*/
async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrderId = '', requestId = '' } = {}) {
try {
const detailResult = await queryAgisoXianyuOrderDetail({ shopId, platformOrderId, requestId })
if (detailResult.success && detailResult.shipped) {
return {
shipped: true,
orderStatus: detailResult.orderStatus,
shipTime: detailResult.shipTime,
reason: '',
}
}
return {
shipped: false,
orderStatus: detailResult.orderStatus || 0,
shipTime: detailResult.shipTime || 0,
reason: detailResult.success ? 'order_not_shipped' : detailResult.reason,
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error || '订单发货状态确认失败')
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货确认查询异常', {
requestId,
shopId,
platformOrderId,
errorMessage: message,
}, { level: 'warn' })
return {
shipped: false,
orderStatus: 0,
shipTime: 0,
reason: 'confirm_query_failed',
}
}
}
/**
* 自动发货成功后发送消息通知 meow~
*/
async function sendAutoDeliveryMessage({ order, task } = {}) {
try {
const result = await ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task })
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知结果', {
orderId: order?.id,
taskId: task?.id,
platformOrderId: order?.platform_order_id,
sent: result.sent,
skipped: result.skipped,
reason: result.reason,
})
return result
} catch (error) {
const message = error instanceof Error ? error.message : String(error || '发送自动发货消息通知失败')
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知异常', {
orderId: order?.id,
taskId: task?.id,
platformOrderId: order?.platform_order_id,
errorMessage: message,
}, { level: 'warn' })
return { sent: false, skipped: true, reason: 'message_send_error' }
}
}
@@ -10,9 +10,53 @@ import {
import { nowIso } from '../../../../utils/time.js'
const AGISO_XIANYU_MESSAGE_CHANNEL = 'agiso_im'
const AGISO_XIANYU_AUTO_DELIVERY_MESSAGE_CHANNEL = 'agiso_im_auto_delivery'
export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
if (!order || !task || !claimUrl) {
const config = resolveAgisoXianyuMessagingConfig(order)
const messageContent = renderAgisoClaimMessage({
order,
task,
claimUrl,
expiredAt,
template: config.messageTemplate,
shopName: config.shopName,
})
return deliverAgisoXianyuMessageForTask({
order,
task,
channel: AGISO_XIANYU_MESSAGE_CHANNEL,
messageContent,
claimUrl,
})
}
export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task } = {}) {
const config = resolveAgisoXianyuMessagingConfig(order)
const messageContent = renderAgisoAutoDeliveryMessage({
order,
task,
template: config.autoDeliveryMessageTemplate,
shopName: config.shopName,
})
return deliverAgisoXianyuMessageForTask({
order,
task,
channel: AGISO_XIANYU_AUTO_DELIVERY_MESSAGE_CHANNEL,
messageContent,
})
}
async function deliverAgisoXianyuMessageForTask({
order,
task,
channel,
messageContent,
claimUrl = '',
} = {}) {
if (!order || !task || !messageContent) {
return { sent: false, skipped: true, reason: 'missing_message_context' }
}
@@ -28,19 +72,11 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' }
}
const successful = await findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_XIANYU_MESSAGE_CHANNEL)
const successful = await findLatestSuccessfulMessageDeliveryByTask(task.id, channel)
if (successful) {
return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id }
}
const messageContent = renderAgisoClaimMessage({
order,
task,
claimUrl,
expiredAt,
template: config.messageTemplate,
shopName: config.shopName,
})
const url = buildRequestUrl(endpoint)
const requestBody = buildRequestBody({
tid: String(order.platform_order_id || ''),
@@ -57,7 +93,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
platform: String(order.platform || '').trim() || 'unknown',
shopId: String(order.shop_id || '').trim(),
shopName: String(order.shop_name || '').trim(),
channel: AGISO_XIANYU_MESSAGE_CHANNEL,
channel,
orderId: order.id,
taskId: task.id,
platformOrderId: order.platform_order_id,
@@ -85,11 +121,12 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
const rawText = await response.text()
const parsed = safeParseJson(rawText)
const success = isAgisoSendSuccess(response.status, parsed)
const errorMessage = success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status)
const updated = await updateMessageDelivery(delivery.id, {
status: success ? 'success' : 'failed',
response_status: response.status,
response_json: JSON.stringify(parsed ?? { rawText }),
error_message: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
error_message: errorMessage,
sent_at: success ? nowIso() : null,
updated_at: nowIso(),
})
@@ -100,7 +137,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
deliveryId: updated?.id || delivery.id,
responseStatus: response.status,
response: parsed ?? { rawText },
errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
errorMessage,
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error || '发送消息失败')
@@ -174,15 +211,48 @@ function generateSign(params, appSecret) {
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) {
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
const resolvedShopName = String(shopName || order.shop_name || order.shop_id || '').trim()
return source
.replaceAll('{platformOrderId}', String(order.platform_order_id || ''))
.replaceAll('{taskNo}', String(task.task_no || ''))
return renderAgisoMessageTemplate({
order,
task,
template: source,
shopName,
claimUrl,
expiredAt: String(expiredAt || '尽快'),
})
}
export function renderAgisoAutoDeliveryMessage({ order, task, template, shopName = '' }) {
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已完成自动发货,请注意查收。'
return renderAgisoMessageTemplate({
order,
task,
template: source,
shopName,
resultMessage: '自动发货成功',
})
}
function renderAgisoMessageTemplate({
order,
task,
template,
shopName = '',
claimUrl = '',
expiredAt = '',
resultMessage = '',
} = {}) {
const resolvedShopName = String(shopName || order?.shop_name || order?.shop_id || '').trim()
return String(template || '')
.replaceAll('{platformOrderId}', String(order?.platform_order_id || ''))
.replaceAll('{taskNo}', String(task?.task_no || ''))
.replaceAll('{shopName}', resolvedShopName)
.replaceAll('{shopId}', String(order.shop_id || ''))
.replaceAll('{claimUrl}', claimUrl)
.replaceAll('{expiredAt}', String(expiredAt || '尽快'))
.replaceAll('{shopId}', String(order?.shop_id || ''))
.replaceAll('{claimUrl}', String(claimUrl || ''))
.replaceAll('{expiredAt}', String(expiredAt || ''))
.replaceAll('{resultMessage}', String(resultMessage || ''))
}
function isAgisoSendSuccess(statusCode, payload) {
@@ -14,23 +14,68 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
return { parsed, enriched: false, reason: 'not_needed' }
}
const config = resolveAgisoXianyuTradeDetailConfig(parsed.shopId)
const detailResult = await queryAgisoXianyuOrderDetail({
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
requestId,
})
if (!detailResult.success) {
return {
parsed,
enriched: false,
reason: detailResult.reason,
errorMessage: detailResult.errorMessage,
}
}
const detailPayload = detailResult.detailPayload
if (detailResult.totalAmountFen <= 0) {
return {
parsed: mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount: true }),
enriched: false,
reason: 'amount_still_missing',
}
}
const merged = mergeEnrichedTrade(parsed, detailPayload)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查成功', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
totalAmountFen: merged.totalAmount,
paidAt: merged.paidAt,
})
return {
parsed: merged,
enriched: true,
totalAmountFen: merged.totalAmount,
}
}
export async function queryAgisoXianyuOrderDetail({ shopId = '', platformOrderId = '', requestId = '' } = {}) {
const normalizedShopId = String(shopId || '').trim()
const normalizedPlatformOrderId = String(platformOrderId || '').trim()
const config = resolveAgisoXianyuTradeDetailConfig(normalizedShopId)
if (!config.endpoint || !config.accessToken || !config.appSecret) {
logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
hasEndpoint: Boolean(config.endpoint),
hasAccessToken: Boolean(config.accessToken),
hasAppSecret: Boolean(config.appSecret),
}, { level: 'warn' })
return { parsed, enriched: false, reason: 'missing_config' }
return createFailedDetailResult('missing_config')
}
const requestBody = buildRequestBody({
platformOrderId: parsed.platformOrderId,
platformOrderId: normalizedPlatformOrderId,
appSecret: config.appSecret,
})
const requestHeaders = buildRequestHeaders({
@@ -40,9 +85,9 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
endpoint: config.endpoint,
})
@@ -55,92 +100,103 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
const rawText = await response.text()
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
const detailPayload = extractAgisoDetailPayload(payload)
const detailAmountFen = resolveTotalAmountFen(detailPayload)
const delivery = resolveAgisoXianyuOrderDeliveryState(detailPayload)
const totalAmountFen = resolveTotalAmountFen(detailPayload)
if (response.status < 200 || response.status >= 300) {
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查失败', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status,
errorMessage: message,
}, { level: 'warn' })
return { parsed, enriched: false, reason: 'http_error', errorMessage: message }
return createFailedDetailResult('http_error', {
responseStatus: response.status,
payload,
detailPayload,
errorMessage: message,
totalAmountFen,
delivery,
})
}
if (!isAgisoDetailSuccess(payload)) {
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status,
errorMessage: message,
response: payload,
}, { level: 'warn' })
return { parsed, enriched: false, reason: 'business_error', errorMessage: message }
return createFailedDetailResult('business_error', {
responseStatus: response.status,
payload,
detailPayload,
errorMessage: message,
totalAmountFen,
delivery,
})
}
if (!detailPayload || typeof detailPayload !== 'object' || Object.keys(detailPayload).length === 0) {
if (!isPlainObject(detailPayload) || Object.keys(detailPayload).length === 0) {
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查未返回可用订单体', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status,
}, { level: 'warn' })
return { parsed, enriched: false, reason: 'empty_detail_payload' }
return createFailedDetailResult('empty_detail_payload', {
responseStatus: response.status,
payload,
})
}
if (detailAmountFen <= 0) {
if (totalAmountFen <= 0) {
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情已返回,但仍未解析出金额', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status,
detailKeys: Object.keys(detailPayload),
}, { level: 'warn' })
return {
parsed: mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount: true }),
enriched: false,
reason: 'amount_still_missing',
}
}
const merged = mergeEnrichedTrade(parsed, detailPayload)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查成功', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
totalAmountFen: merged.totalAmount,
paidAt: merged.paidAt,
})
return {
parsed: merged,
enriched: true,
totalAmountFen: merged.totalAmount,
success: true,
reason: '',
errorMessage: '',
responseStatus: response.status,
payload: isPlainObject(payload) ? payload : {},
detailPayload,
totalAmountFen,
shipped: delivery.shipped,
shipTime: delivery.shipTime,
orderStatus: delivery.orderStatus,
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼订单详情补查失败')
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查异常', {
requestId,
platform: parsed.platform,
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
platform: 'xianyu',
shopId: normalizedShopId,
platformOrderId: normalizedPlatformOrderId,
errorMessage: message,
}, { level: 'warn' })
return { parsed, enriched: false, reason: 'request_failed', errorMessage: message }
return createFailedDetailResult('request_failed', {
errorMessage: message,
})
}
}
@@ -210,6 +266,29 @@ function extractAgisoDetailPayload(payload) {
return payload
}
export function resolveAgisoXianyuOrderDeliveryState(payload) {
const shipTime = normalizeTimestampValue(pickFirstNonEmpty([
payload?.ship_time,
payload?.shipTime,
payload?.ShipTime,
payload?.delivery_time,
payload?.deliveryTime,
]))
const orderStatus = normalizeInteger(
pickFirstNonEmpty([
payload?.order_status,
payload?.orderStatus,
payload?.status,
]),
)
return {
shipped: shipTime > 0 || orderStatus >= 3,
shipTime,
orderStatus,
}
}
function resolveTotalAmountFen(payload) {
const fenAmount = normalizeFenInteger(pickFirstNonEmpty([
payload.payment,
@@ -559,6 +638,33 @@ function normalizeFenInteger(value) {
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
}
function normalizeTimestampValue(value) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
}
function createFailedDetailResult(reason, {
responseStatus = 0,
payload = {},
detailPayload = {},
errorMessage = '',
totalAmountFen = 0,
delivery = { shipped: false, shipTime: 0, orderStatus: 0 },
} = {}) {
return {
success: false,
reason: String(reason || '').trim(),
errorMessage: String(errorMessage || '').trim(),
responseStatus: Number(responseStatus || 0),
payload: isPlainObject(payload) ? payload : {},
detailPayload: isPlainObject(detailPayload) ? detailPayload : {},
totalAmountFen: Number(totalAmountFen || 0),
shipped: Boolean(delivery?.shipped),
shipTime: Number(delivery?.shipTime || 0),
orderStatus: Number(delivery?.orderStatus || 0),
}
}
function pickFirstNonEmpty(values) {
for (const value of values) {
const normalized = String(value || '').trim()