// @ts-check import { listTasksByOrderId } from '../../repositories/task-repo.js' import { formatFenToAmount, parseAmountToFen } from '../../utils/money.js' import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js' import { safeParseJson } from './admin-query-utils.js' import { resolveDisplayShopName } from './admin-read-shared-helpers.js' /** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListItem} AdminWebhookEventListItem */ /** @typedef {import('../../types/repository-rows.js').WebhookEventRow} WebhookEventRow */ /** * @param {WebhookEventRow} item * @returns {Promise} */ export async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { const headers = normalizeRecord(safeParseJson(item.headers_json)) const query = normalizeRecord(safeParseJson(item.query_json)) const body = normalizeRecord(safeParseJson(item.body_json)) const payload = extractWebhookPayload(body) const itemSources = extractWebhookItemSources(payload) const rawAmount = pickFirstNonEmpty([ payload.total_fee, payload.totalFee, payload.TotalFee, payload.pay_fee, payload.payFee, payload.Payment, payload.payment, payload.buy_amount, payload.buyAmount, ]) const totalAmountFen = parseAmountToFen(rawAmount) const taskCount = item.related_order_id ? (await listTasksByOrderId(item.related_order_id)).length : 0 const shopId = item.shop_id || pickFirstNonEmpty([ payload.PlatformUserId, payload.platformUserId, payload.platform_user_id, payload.seller_id, payload.sellerId, payload.SellerId, payload.seller_nick, payload.sellerNick, payload.SellerNick, ]) const mapped = { eventId: item.id, provider: item.provider || 'agiso', platform: item.platform, platformRaw: pickFirstNonEmpty([ query.fromPlatform, query.from_platform, body.fromPlatform, body.from_platform, payload.fromPlatform, payload.FromPlatform, payload.platform, payload.Platform, ]), shopId, shopName: resolveDisplayShopName( item.provider, shopId, item.shop_name || pickFirstNonEmpty([ payload.shopName, payload.shop_name, payload.ShopName, payload.seller_nick, payload.sellerNick, payload.SellerNick, ]), ), eventType: item.event_type, eventKey: item.event_key, signatureValid: Boolean(item.signature_valid), processed: Boolean(item.processed), processError: item.process_error, visibilityLevel: resolveWebhookVisibilityLevel(item.process_error), relatedOrderId: item.related_order_id, createdAt: item.created_at, platformOrderId: resolveAgisoTradePlatformOrderId(payload), buyerId: pickFirstNonEmpty([ payload.buyer_id, payload.buyerId, payload.BuyerId, payload.openid, payload.BuyerOpenUid, payload.buyer_open_uid, payload.buyerOpenUid, ]), buyerName: pickFirstNonEmpty([ payload.buyer_name, payload.buyerName, payload.BuyerName, payload.nick, payload.BuyerNick, payload.buyer_nick, payload.buyerNick, ]), totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '', totalAmountFen, currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY', itemCount: itemSources.length, taskCount, messageText: pickFirstNonEmpty([ payload.MsgTypeDes, payload.msgTypeDes, payload.msg_type_desc, payload.message, payload.msg, ]), requestTimestamp: pickFirstNonEmpty([query.timestamp]), requestSign: pickFirstNonEmpty([query.sign]), aopic: pickFirstNonEmpty([query.aopic]), sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]), sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]), userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]), } if (!includeRaw) { return mapped } return { ...mapped, headers, query, body, payload, } } function extractWebhookPayload(body) { return extractAgisoTradePayload(body) } function extractWebhookItemSources(payload) { const normalizedPayload = normalizeRecord(payload) const candidates = [ normalizedPayload.orders, normalizedPayload.Orders, normalizedPayload.items, normalizedPayload.Items, normalizedPayload.order_items, ] for (const candidate of candidates) { if (Array.isArray(candidate) && candidate.length > 0) { return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0) } } if ( pickFirstNonEmpty([ normalizedPayload.item_id, normalizedPayload.itemId, normalizedPayload.ItemId, normalizedPayload.num_iid, normalizedPayload.numIid, normalizedPayload.NumIid, ]) ) { return [normalizedPayload] } return [] } export function resolveWebhookVisibilityLevel(processError) { const normalized = String(processError || '').trim() if (normalized.startsWith('ignored_')) { return 'ignored' } return 'important' } function normalizeRecord(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {} } function pickFirstNonEmpty(values) { for (const value of values) { if (typeof value === 'string' && value.trim()) { return value.trim() } if (typeof value === 'number' && Number.isFinite(value)) { return String(value) } } return '' }