feat(worker-platform): 支持客户退款冲正并关闭订单
This commit is contained in:
@@ -124,6 +124,43 @@ export async function cancelDouyinMashidaCouponVerify(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同意用户售后(`afterSale.operate`,type=201)。
|
||||
* update_time 必须使用 refund_RefundCreated 消息中的原值,平台用它做版本校验。
|
||||
*/
|
||||
export async function approveDouyinMashidaAfterSale(
|
||||
input: {
|
||||
afterSaleId: string
|
||||
updateTime: string
|
||||
reason?: string | undefined
|
||||
remark?: string | undefined
|
||||
} & DouyinMashidaCallOptions,
|
||||
): Promise<JsonObject> {
|
||||
const afterSaleId = String(input.afterSaleId || '').trim()
|
||||
const updateTime = String(input.updateTime || '').trim()
|
||||
if (!afterSaleId || !updateTime) {
|
||||
throw new Error('抖音售后审核缺少 aftersale_id 或 update_time')
|
||||
}
|
||||
const response = await douyinMashidaGatewayRequest({
|
||||
method: 'afterSale.operate',
|
||||
params: {
|
||||
type: 201,
|
||||
items: JSON.stringify([
|
||||
{
|
||||
repair_type: 1,
|
||||
aftersale_id: afterSaleId,
|
||||
reason: String(input.reason || '').trim(),
|
||||
remark: String(input.remark || '').trim(),
|
||||
update_time: updateTime,
|
||||
},
|
||||
]),
|
||||
},
|
||||
config: input.config,
|
||||
timeoutMs: input.timeoutMs,
|
||||
})
|
||||
return isPlainObject(response.data) ? response.data : { data: response.data }
|
||||
}
|
||||
|
||||
/** 订单消息(把「去使用链接」发给买家)。 */
|
||||
export async function sendDouyinMashidaOrderMessage(
|
||||
input: {
|
||||
|
||||
@@ -61,7 +61,7 @@ test('data 解析:坏 JSON 不抛错,返回空对象', () => {
|
||||
assert.deepEqual(parseMessageData(''), {})
|
||||
})
|
||||
|
||||
test('幂等键:有订单号时按 topic:pub_time:p_id:s_ids 派生', () => {
|
||||
test('幂等键:有订单号时按 topic:pub_time:p_id:s_ids:aftersale_id 派生', () => {
|
||||
const eventId = buildDouyinMashidaMessageEventId({
|
||||
topic: 'trade_TradePaid',
|
||||
pubTime: '1780000000',
|
||||
@@ -76,6 +76,23 @@ test('幂等键:有订单号时按 topic:pub_time:p_id:s_ids 派生', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('幂等键:同一订单同一秒的两笔售后不会互相去重', () => {
|
||||
const first = buildDouyinMashidaMessageEventId({
|
||||
topic: 'refund_RefundCreated',
|
||||
pubTime: '1780000000',
|
||||
data: '{}',
|
||||
message: { p_id: '4837328765348567000', s_id: '4837328765348567001', aftersale_id: '1' },
|
||||
})
|
||||
const second = buildDouyinMashidaMessageEventId({
|
||||
topic: 'refund_RefundCreated',
|
||||
pubTime: '1780000000',
|
||||
data: '{}',
|
||||
message: { p_id: '4837328765348567000', s_id: '4837328765348567001', aftersale_id: '2' },
|
||||
})
|
||||
|
||||
assert.notEqual(first, second)
|
||||
})
|
||||
|
||||
test('幂等键:拿不到订单号时退回 data 摘要(同一内容稳定、内容变则键变)', () => {
|
||||
const base = { topic: 'refund_RefundSuccess', pubTime: '1780000000' }
|
||||
const first = buildDouyinMashidaMessageEventId({ ...base, data: '{"a":1}', message: {} })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isPlainObject, type JsonObject } from '../../../types/json.js'
|
||||
|
||||
import { insertWebhookEventOnce } from '../../../repositories/webhook-event-repo.js'
|
||||
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
|
||||
import { query } from '../../../db/client.js'
|
||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
||||
import {
|
||||
ensureCertificateForOrderItem,
|
||||
@@ -26,6 +27,11 @@ import {
|
||||
} from './source-config-service.js'
|
||||
import { verifyMessageSignature } from './sign.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js'
|
||||
import {
|
||||
bindPlatformAfterSalesToLocalOrder,
|
||||
markPlatformAfterSaleState,
|
||||
upsertPlatformAfterSale,
|
||||
} from '../platform-after-sales-service.js'
|
||||
|
||||
/**
|
||||
* 抖音码速达消息推送入口(验签 → 幂等 → 分派)。
|
||||
@@ -113,14 +119,96 @@ export async function handleDouyinMashidaMessage(input: {
|
||||
return handlePaidMessage({ message, topic, eventId, config })
|
||||
}
|
||||
|
||||
if (topic === 'refund_RefundCreated') {
|
||||
return handleAfterSaleCreated({ message, topic, eventId, pubTime })
|
||||
}
|
||||
|
||||
if (topic === 'refund_RefundClosed') {
|
||||
return handleAfterSaleClosed({ message, topic, eventId, pubTime })
|
||||
}
|
||||
|
||||
if (topic === 'trade_TradeCanceled' || topic === 'refund_RefundSuccess') {
|
||||
return handleInvalidateMessage({ message, topic, eventId })
|
||||
return handleInvalidateMessage({ message, topic, eventId, pubTime })
|
||||
}
|
||||
|
||||
logIntegration('[douyin-mashida/notify]', 'topic 暂无处理逻辑', { topic, eventId })
|
||||
return { ok: true, duplicate: false, topic, action: 'noop', eventId }
|
||||
}
|
||||
|
||||
async function handleAfterSaleCreated(input: {
|
||||
message: JsonObject
|
||||
topic: string
|
||||
eventId: string
|
||||
pubTime: string
|
||||
}): Promise<DouyinMashidaMessageResult> {
|
||||
const afterSaleId = String(input.message.aftersale_id || '').trim()
|
||||
const parentOrderId = resolveDouyinMashidaParentOrderId(input.message)
|
||||
if (!afterSaleId || !parentOrderId) {
|
||||
throw createHttpError('抖音售后消息缺少 aftersale_id 或 p_id', {
|
||||
statusCode: 400,
|
||||
errorCode: 'douyin_mashida_after_sale_id_missing',
|
||||
context: { topic: input.topic, eventId: input.eventId },
|
||||
})
|
||||
}
|
||||
const order = await findLatestOrderByPlatformOrderId({
|
||||
provider: DOUYIN_MASHIDA_PROVIDER,
|
||||
platform: DOUYIN_MASHIDA_PLATFORM,
|
||||
platformOrderId: parentOrderId,
|
||||
})
|
||||
const orderItems = order
|
||||
? await queryOrderItemsForAfterSale(order.id, String(input.message.s_id || '').trim())
|
||||
: []
|
||||
const afterSale = await upsertPlatformAfterSale({
|
||||
provider: DOUYIN_MASHIDA_PROVIDER,
|
||||
platformOrderId: parentOrderId,
|
||||
subOrderId: String(input.message.s_id || ''),
|
||||
externalAfterSaleId: afterSaleId,
|
||||
localOrderId: order?.id || null,
|
||||
localOrderItemId: orderItems.length === 1 ? orderItems[0]?.id || null : null,
|
||||
amountFen: Number(input.message.refund_amount || 0),
|
||||
afterSaleStatus: numberOrNull(input.message.aftersale_status),
|
||||
afterSaleType: numberOrNull(input.message.aftersale_type),
|
||||
refundStatus: numberOrNull(input.message.refund_status),
|
||||
platformUpdateTime: String(input.message.update_time || ''),
|
||||
state: 'open',
|
||||
raw: input.message,
|
||||
appliedAt: numberToIso(input.message.apply_time),
|
||||
eventAt: numberToIso(input.pubTime),
|
||||
})
|
||||
return {
|
||||
ok: true,
|
||||
duplicate: false,
|
||||
topic: input.topic,
|
||||
action: afterSale ? 'after-sale-opened' : 'after-sale-ignored',
|
||||
eventId: input.eventId,
|
||||
orderId: order?.id || null,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAfterSaleClosed(input: {
|
||||
message: JsonObject
|
||||
topic: string
|
||||
eventId: string
|
||||
pubTime: string
|
||||
}): Promise<DouyinMashidaMessageResult> {
|
||||
const afterSaleId = String(input.message.aftersale_id || '').trim()
|
||||
if (afterSaleId) {
|
||||
await markPlatformAfterSaleState({
|
||||
provider: DOUYIN_MASHIDA_PROVIDER,
|
||||
externalAfterSaleId: afterSaleId,
|
||||
state: 'closed',
|
||||
eventAt: numberToIso(input.pubTime),
|
||||
})
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
duplicate: false,
|
||||
topic: input.topic,
|
||||
action: afterSaleId ? 'after-sale-closed' : 'after-sale-close-id-missing',
|
||||
eventId: input.eventId,
|
||||
}
|
||||
}
|
||||
|
||||
/** `trade_TradePaid`:拉订单详情 → 建单 → 生成券码并同步给平台。 */
|
||||
async function handlePaidMessage(input: {
|
||||
message: JsonObject
|
||||
@@ -171,6 +259,14 @@ async function handlePaidMessage(input: {
|
||||
}
|
||||
}
|
||||
|
||||
const orderItems = result.orderItems || []
|
||||
await bindPlatformAfterSalesToLocalOrder({
|
||||
provider: DOUYIN_MASHIDA_PROVIDER,
|
||||
platformOrderId: parentOrderId,
|
||||
localOrderId: order.id,
|
||||
localOrderItemId: orderItems.length === 1 ? orderItems[0]?.id || null : null,
|
||||
})
|
||||
|
||||
// 只有真正付款的单才发券(详情可能显示已取消/退款)
|
||||
if (String(order.pay_status || '') !== 'paid') {
|
||||
logIntegration('[douyin-mashida/notify]', '订单非已支付状态,跳过发券', {
|
||||
@@ -315,6 +411,7 @@ async function handleInvalidateMessage(input: {
|
||||
message: JsonObject
|
||||
topic: string
|
||||
eventId: string
|
||||
pubTime: string
|
||||
}): Promise<DouyinMashidaMessageResult> {
|
||||
const parentOrderId = resolveDouyinMashidaParentOrderId(input.message)
|
||||
if (!parentOrderId) {
|
||||
@@ -364,6 +461,15 @@ async function handleInvalidateMessage(input: {
|
||||
localOrderId: order.id,
|
||||
reason: input.topic,
|
||||
})
|
||||
const afterSaleId = String(input.message.refund_id || input.message.aftersale_id || '').trim()
|
||||
if (afterSaleId) {
|
||||
await markPlatformAfterSaleState({
|
||||
provider: DOUYIN_MASHIDA_PROVIDER,
|
||||
externalAfterSaleId: afterSaleId,
|
||||
state: input.topic === 'refund_RefundSuccess' ? 'success' : 'closed',
|
||||
eventAt: numberToIso(input.message.success_time || input.pubTime),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -376,6 +482,28 @@ async function handleInvalidateMessage(input: {
|
||||
}
|
||||
}
|
||||
|
||||
async function queryOrderItemsForAfterSale(orderId: number, subOrderId: string) {
|
||||
const result = await query<{ id: number; sku_code: string }>(
|
||||
`SELECT id, sku_code FROM order_items WHERE order_id = $1 ORDER BY id`,
|
||||
[orderId],
|
||||
)
|
||||
if (subOrderId) {
|
||||
const matched = result.rows.find((row) => String(row.sku_code || '').trim() === subOrderId)
|
||||
if (matched) return [matched]
|
||||
}
|
||||
return result.rows.length === 1 ? result.rows : []
|
||||
}
|
||||
|
||||
function numberOrNull(value: unknown) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
function numberToIso(value: unknown) {
|
||||
const seconds = Number(value)
|
||||
return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000).toISOString() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 POST body 取 `data` 原文(必须与平台签名时使用的字符串完全一致)。
|
||||
* 兼容两种发法:body 直接是 JSON 字符串;或 `data=<json>` 的 urlencoded 表单。
|
||||
@@ -474,9 +602,12 @@ export function buildDouyinMashidaMessageEventId(input: {
|
||||
const subIds = Array.isArray(input.message.s_ids)
|
||||
? input.message.s_ids.map((id) => String(id || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const afterSaleId = String(input.message.aftersale_id || input.message.refund_id || '').trim()
|
||||
|
||||
if (parentOrderId) {
|
||||
return [input.topic, input.pubTime, parentOrderId, subIds.join(',')].join(':')
|
||||
return [input.topic, input.pubTime, parentOrderId, subIds.join(','), afterSaleId]
|
||||
.filter(Boolean)
|
||||
.join(':')
|
||||
}
|
||||
|
||||
const digest = createHash('sha256')
|
||||
|
||||
@@ -30,7 +30,7 @@ const DOUYIN_MASHIDA_CONFIG_KEY = APP_CONFIG_KEYS.douyinMashida
|
||||
|
||||
export const DOUYIN_MASHIDA_DEFAULT_BASE_URL = 'https://dy-openplatform-api.wanmahui.com'
|
||||
|
||||
/** 最小接入只处理这几条:付款进单、取消/退款作废券码。 */
|
||||
/** 付款进单、售后自动发现、取消/退款作废券码。 */
|
||||
/** 兑换成功后 order.message 的默认正文(可在后台覆盖)。 */
|
||||
export const DEFAULT_AFTER_VERIFY_MESSAGE = '您的订单已兑换成功'
|
||||
|
||||
@@ -40,6 +40,8 @@ export const DEFAULT_CLAIM_MESSAGE_TEMPLATE = '您的订单已发货,请点击
|
||||
export const DOUYIN_MASHIDA_DEFAULT_HANDLED_TOPICS = [
|
||||
'trade_TradePaid',
|
||||
'trade_TradeCanceled',
|
||||
'refund_RefundCreated',
|
||||
'refund_RefundClosed',
|
||||
'refund_RefundSuccess',
|
||||
] as const
|
||||
|
||||
@@ -165,7 +167,13 @@ function normalizeHandledTopics(value: unknown): string[] {
|
||||
}
|
||||
|
||||
const topics = [...new Set(value.map((item) => String(item || '').trim()).filter(Boolean))]
|
||||
return topics.length > 0 ? topics : [...DOUYIN_MASHIDA_DEFAULT_HANDLED_TOPICS]
|
||||
return [
|
||||
...new Set([
|
||||
...(topics.length > 0 ? topics : DOUYIN_MASHIDA_DEFAULT_HANDLED_TOPICS),
|
||||
'refund_RefundCreated',
|
||||
'refund_RefundClosed',
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value: unknown, fallback: number) {
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { query } from '../../db/client.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
export type PlatformAfterSaleState = 'open' | 'agreed' | 'refused' | 'closed' | 'success' | 'failed'
|
||||
|
||||
export type PlatformAfterSaleInput = {
|
||||
provider: string
|
||||
platformOrderId: string
|
||||
subOrderId?: string
|
||||
externalAfterSaleId: string
|
||||
localOrderId?: number | null
|
||||
localOrderItemId?: number | null
|
||||
amountFen?: number
|
||||
afterSaleStatus?: number | null
|
||||
afterSaleType?: number | null
|
||||
refundStatus?: number | null
|
||||
platformUpdateTime?: string
|
||||
state?: PlatformAfterSaleState
|
||||
raw?: JsonObject
|
||||
appliedAt?: string | null
|
||||
eventAt?: string | null
|
||||
}
|
||||
|
||||
export type PlatformAfterSale = {
|
||||
id: number
|
||||
provider: string
|
||||
platformOrderId: string
|
||||
subOrderId: string
|
||||
externalAfterSaleId: string
|
||||
localOrderId: number | null
|
||||
localOrderItemId: number | null
|
||||
amountFen: number
|
||||
afterSaleStatus: number | null
|
||||
afterSaleType: number | null
|
||||
refundStatus: number | null
|
||||
platformUpdateTime: string
|
||||
state: PlatformAfterSaleState
|
||||
raw: JsonObject
|
||||
appliedAt: string | null
|
||||
lastEventAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type PlatformAfterSaleRow = {
|
||||
id: number
|
||||
provider: string
|
||||
platform_order_id: string
|
||||
sub_order_id: string
|
||||
external_after_sale_id: string
|
||||
local_order_id: number | null
|
||||
local_order_item_id: number | null
|
||||
amount_fen: number | string
|
||||
after_sale_status: number | null
|
||||
after_sale_type: number | null
|
||||
refund_status: number | null
|
||||
platform_update_time: string
|
||||
state: PlatformAfterSaleState
|
||||
raw_json: unknown
|
||||
applied_at: string | null
|
||||
last_event_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export async function upsertPlatformAfterSale(
|
||||
input: PlatformAfterSaleInput,
|
||||
): Promise<PlatformAfterSale | null> {
|
||||
const provider = normalizeText(input.provider, 80)
|
||||
const platformOrderId = normalizeText(input.platformOrderId, 120)
|
||||
const externalAfterSaleId = normalizeText(input.externalAfterSaleId, 120)
|
||||
if (!provider || !platformOrderId || !externalAfterSaleId) return null
|
||||
const now = nowIso()
|
||||
const result = await query<PlatformAfterSaleRow>(
|
||||
`
|
||||
INSERT INTO platform_after_sales (
|
||||
provider, platform_order_id, sub_order_id, external_after_sale_id,
|
||||
local_order_id, local_order_item_id, amount_fen,
|
||||
after_sale_status, after_sale_type, refund_status, platform_update_time,
|
||||
state, raw_json, applied_at, last_event_at, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7,
|
||||
$8, $9, $10, $11,
|
||||
$12, $13::jsonb, $14, $15, $16, $16
|
||||
)
|
||||
ON CONFLICT (provider, external_after_sale_id)
|
||||
DO UPDATE SET
|
||||
platform_order_id = EXCLUDED.platform_order_id,
|
||||
sub_order_id = EXCLUDED.sub_order_id,
|
||||
local_order_id = COALESCE(EXCLUDED.local_order_id, platform_after_sales.local_order_id),
|
||||
local_order_item_id = COALESCE(
|
||||
EXCLUDED.local_order_item_id,
|
||||
platform_after_sales.local_order_item_id
|
||||
),
|
||||
amount_fen = CASE
|
||||
WHEN EXCLUDED.amount_fen > 0 THEN EXCLUDED.amount_fen
|
||||
ELSE platform_after_sales.amount_fen
|
||||
END,
|
||||
after_sale_status = COALESCE(
|
||||
EXCLUDED.after_sale_status,
|
||||
platform_after_sales.after_sale_status
|
||||
),
|
||||
after_sale_type = COALESCE(EXCLUDED.after_sale_type, platform_after_sales.after_sale_type),
|
||||
refund_status = COALESCE(EXCLUDED.refund_status, platform_after_sales.refund_status),
|
||||
platform_update_time = CASE
|
||||
WHEN EXCLUDED.platform_update_time <> '' THEN EXCLUDED.platform_update_time
|
||||
ELSE platform_after_sales.platform_update_time
|
||||
END,
|
||||
state = CASE
|
||||
WHEN platform_after_sales.state IN ('agreed', 'success', 'closed', 'refused')
|
||||
AND EXCLUDED.state = 'open'
|
||||
THEN platform_after_sales.state
|
||||
ELSE EXCLUDED.state
|
||||
END,
|
||||
raw_json = EXCLUDED.raw_json,
|
||||
applied_at = COALESCE(EXCLUDED.applied_at, platform_after_sales.applied_at),
|
||||
last_event_at = COALESCE(EXCLUDED.last_event_at, platform_after_sales.last_event_at),
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
provider,
|
||||
platformOrderId,
|
||||
normalizeText(input.subOrderId, 120),
|
||||
externalAfterSaleId,
|
||||
normalizeOptionalId(input.localOrderId),
|
||||
normalizeOptionalId(input.localOrderItemId),
|
||||
normalizeAmountFen(input.amountFen),
|
||||
normalizeOptionalInteger(input.afterSaleStatus),
|
||||
normalizeOptionalInteger(input.afterSaleType),
|
||||
normalizeOptionalInteger(input.refundStatus),
|
||||
normalizeText(input.platformUpdateTime, 80),
|
||||
input.state || 'open',
|
||||
JSON.stringify(input.raw || {}),
|
||||
input.appliedAt || null,
|
||||
input.eventAt || now,
|
||||
now,
|
||||
],
|
||||
)
|
||||
return result.rows[0] ? mapPlatformAfterSale(result.rows[0]) : null
|
||||
}
|
||||
|
||||
export async function findOpenPlatformAfterSale(input: {
|
||||
provider: string
|
||||
localOrderId: number
|
||||
localOrderItemId?: number | null
|
||||
}): Promise<PlatformAfterSale | null> {
|
||||
const params: unknown[] = [input.provider, input.localOrderId]
|
||||
let itemFilter = ''
|
||||
if (input.localOrderItemId) {
|
||||
params.push(input.localOrderItemId)
|
||||
itemFilter = `AND local_order_item_id = $${params.length}`
|
||||
}
|
||||
const result = await query<PlatformAfterSaleRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM platform_after_sales
|
||||
WHERE provider = $1
|
||||
AND local_order_id = $2
|
||||
AND state = 'open'
|
||||
${itemFilter}
|
||||
ORDER BY last_event_at DESC NULLS LAST, id DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return result.rows[0] ? mapPlatformAfterSale(result.rows[0]) : null
|
||||
}
|
||||
|
||||
export async function findPlatformAfterSaleByExternalId(input: {
|
||||
provider: string
|
||||
externalAfterSaleId: string
|
||||
}): Promise<PlatformAfterSale | null> {
|
||||
const result = await query<PlatformAfterSaleRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM platform_after_sales
|
||||
WHERE provider = $1 AND external_after_sale_id = $2
|
||||
LIMIT 1
|
||||
`,
|
||||
[input.provider, input.externalAfterSaleId],
|
||||
)
|
||||
return result.rows[0] ? mapPlatformAfterSale(result.rows[0]) : null
|
||||
}
|
||||
|
||||
export async function bindPlatformAfterSalesToLocalOrder(input: {
|
||||
provider: string
|
||||
platformOrderId: string
|
||||
localOrderId: number
|
||||
localOrderItemId?: number | null
|
||||
}): Promise<number> {
|
||||
const result = await query(
|
||||
`
|
||||
UPDATE platform_after_sales
|
||||
SET local_order_id = $3,
|
||||
local_order_item_id = COALESCE(local_order_item_id, $4),
|
||||
updated_at = $5
|
||||
WHERE provider = $1
|
||||
AND platform_order_id = $2
|
||||
AND local_order_id IS NULL
|
||||
`,
|
||||
[
|
||||
input.provider,
|
||||
input.platformOrderId,
|
||||
input.localOrderId,
|
||||
normalizeOptionalId(input.localOrderItemId),
|
||||
nowIso(),
|
||||
],
|
||||
)
|
||||
return result.rowCount || 0
|
||||
}
|
||||
|
||||
export async function markPlatformAfterSaleState(input: {
|
||||
provider: string
|
||||
externalAfterSaleId: string
|
||||
state: PlatformAfterSaleState
|
||||
eventAt?: string | null
|
||||
}): Promise<void> {
|
||||
await query(
|
||||
`
|
||||
UPDATE platform_after_sales
|
||||
SET state = CASE
|
||||
WHEN state = 'success' THEN state
|
||||
WHEN state IN ('agreed', 'closed', 'refused') AND $3 = 'open' THEN state
|
||||
ELSE $3
|
||||
END,
|
||||
last_event_at = $4,
|
||||
updated_at = $4
|
||||
WHERE provider = $1 AND external_after_sale_id = $2
|
||||
`,
|
||||
[input.provider, input.externalAfterSaleId, input.state, input.eventAt || nowIso()],
|
||||
)
|
||||
}
|
||||
|
||||
function mapPlatformAfterSale(row: PlatformAfterSaleRow): PlatformAfterSale {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
provider: row.provider,
|
||||
platformOrderId: row.platform_order_id,
|
||||
subOrderId: row.sub_order_id || '',
|
||||
externalAfterSaleId: row.external_after_sale_id,
|
||||
localOrderId: row.local_order_id ? Number(row.local_order_id) : null,
|
||||
localOrderItemId: row.local_order_item_id ? Number(row.local_order_item_id) : null,
|
||||
amountFen: Number(row.amount_fen || 0),
|
||||
afterSaleStatus: row.after_sale_status,
|
||||
afterSaleType: row.after_sale_type,
|
||||
refundStatus: row.refund_status,
|
||||
platformUpdateTime: row.platform_update_time || '',
|
||||
state: row.state,
|
||||
raw:
|
||||
row.raw_json && typeof row.raw_json === 'object' && !Array.isArray(row.raw_json)
|
||||
? (row.raw_json as JsonObject)
|
||||
: {},
|
||||
appliedAt: row.applied_at,
|
||||
lastEventAt: row.last_event_at,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown, maxLength: number) {
|
||||
return String(value ?? '')
|
||||
.trim()
|
||||
.slice(0, maxLength)
|
||||
}
|
||||
|
||||
function normalizeOptionalId(value: unknown): number | null {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null
|
||||
}
|
||||
|
||||
function normalizeAmountFen(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
|
||||
}
|
||||
|
||||
function normalizeOptionalInteger(value: unknown): number | null {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : null
|
||||
}
|
||||
Reference in New Issue
Block a user