// @ts-check import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js' import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' export function mapManualDispatchContext(value, viewerContext = createAdminViewerContext()) { if (!value || typeof value !== 'object') { return null } return { outcome: String(value.outcome || '').trim(), deliveryReference: String(value.deliveryReference || '').trim(), deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(value.deliveredCredential || '').trim() : '', resultMessage: String(value.resultMessage || '').trim(), completedAt: value.completedAt || null, completedBy: value.completedBy && typeof value.completedBy === 'object' ? { userId: Number(value.completedBy.userId || 0) || 0, username: String(value.completedBy.username || '').trim(), role: String(value.completedBy.role || '').trim(), } : null, } } export function mapKuaishouCloudFulfillmentContext(value) { if (!value || typeof value !== 'object') { return null } const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {} const binding = value.binding && typeof value.binding === 'object' ? value.binding : {} const role = value.role && typeof value.role === 'object' ? value.role : {} const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {} const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {} const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {} const consume = value.consume && typeof value.consume === 'object' ? value.consume : {} return { flowType: String(value.flowType || '').trim(), configId: String(value.configId || '').trim(), internalSkuCode: String(value.internalSkuCode || '').trim(), internalSkuName: String(value.internalSkuName || '').trim(), ticket: { code: String(ticket.code || '').trim(), capturedAt: ticket.capturedAt || null, status: String(ticket.status || 'pending').trim() || 'pending', verifiedAt: ticket.verifiedAt || null, goodsTitle: String(ticket.goodsTitle || '').trim(), leftCount: Number(ticket.leftCount || 0) || 0, }, binding: { prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', cloudSourceKey: String(binding.cloudSourceKey || '').trim(), skuId: Number(binding.skuId || 0) || 0, skuName: String(binding.skuName || '').trim(), vnKey: String(binding.vnKey || '').trim(), vnId: Number(binding.vnId || 0) || 0, vnPhone: String(binding.vnPhone || '').trim(), bindUrl: String(binding.bindUrl || '').trim(), bindPreparedAt: binding.bindPreparedAt || null, }, role: { status: String(role.status || 'pending').trim() || 'pending', name: String(role.name || '').trim(), rid: String(role.rid || '').trim(), refreshedAt: role.refreshedAt || null, errorMessage: String(role.errorMessage || '').trim(), rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null, }, purchase: { autoBuyEnabled: purchase.autoBuyEnabled !== false, minAssetReserve: Number(purchase.minAssetReserve || 0) || 0, usedKnapsack: purchase.usedKnapsack === true, purchaseTriggered: purchase.purchaseTriggered === true, assetBefore: Number(purchase.assetBefore || 0) || 0, assetAfter: Number(purchase.assetAfter || 0) || 0, purchaseAt: purchase.purchaseAt || null, }, dispatch: { status: String(dispatch.status || 'pending').trim() || 'pending', dispatchAt: dispatch.dispatchAt || null, sendType: Number(dispatch.sendType || 0) || 0, note: String(dispatch.note || '').trim(), }, returnNumber: { status: String(returnNumber.status || 'pending').trim() || 'pending', returnedAt: returnNumber.returnedAt || null, autoReturnEnabled: returnNumber.autoReturnEnabled === true, }, consume: { status: String(consume.status || 'pending').trim() || 'pending', shopId: String(consume.shopId || '').trim(), shopName: String(consume.shopName || '').trim(), autoConsumeEnabled: consume.autoConsumeEnabled === true, consumedAt: consume.consumedAt || null, errorMessage: String(consume.errorMessage || '').trim(), }, notes: String(value.notes || '').trim(), } } export function mapRedeemResolutionContext(value) { if (!value || typeof value !== 'object') { return null } const attempts = Array.isArray(value.attempts) ? value.attempts .map((attempt) => mapRedeemResolutionAttempt(attempt)) .filter(Boolean) : [] return { status: String(value.status || '').trim(), taskStatus: String(value.taskStatus || '').trim(), replacementCount: Math.max(0, Number(value.replacementCount || 0)), finishedAt: value.finishedAt || null, attempts, } } export function getTaskPrimaryInventoryItemId(task) { const value = Number(task?.primary_inventory_item_id || 0) return Number.isFinite(value) && value > 0 ? value : null } export function getTaskPrimaryClaimTokenId(task) { const value = Number(task?.primary_claim_token_id || 0) return Number.isFinite(value) && value > 0 ? value : null } export function createAdminViewerContext(session = null) { const role = normalizeAdminRole(session?.role) const allowedInventoryGroupCodes = role === 'support' ? normalizeInventoryGroupCodes(session?.allowedInventoryGroups) : null return { role, allowedInventoryGroupCodes, restrictInventoryGroups: role === 'support', canViewSensitiveTaskData: role === 'admin' || role === 'operator', canManageSensitiveInventory: role === 'admin', canManageTaskLifecycle: role === 'admin' || role === 'operator', canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support', } } export async function resolveAdminTaskScreenshotUrl(task, viewerContext) { if (task.screenshot_path || task.browser_session_id) { return `/api/v1/admin/tasks/${task.id}/screenshot` } return '' } export function canViewerCloseTask(task, viewerContext) { if (!['admin', 'operator', 'support'].includes(viewerContext.role)) { return false } return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim()) } export function resolveDisplayShopName(provider, shopId, shopName) { const normalizedShopName = String(shopName || '').trim() if (normalizedShopName) { return normalizedShopName } const normalizedShopId = String(shopId || '').trim() if (!normalizedShopId) { return '' } if (String(provider || '').trim().toLowerCase() === 'agiso') { const configuredName = String(getAgisoShopConfig(normalizedShopId)?.shopName || '').trim() if (configuredName) { return configuredName } } return normalizedShopId } export function parseTaskContext(task) { const value = task?.context_json if (!value) { return {} } if (typeof value === 'object') { return value } try { return JSON.parse(String(value || '{}')) } catch { return {} } } export function parseTaskState(task) { const value = task?.state_json if (!value) { return {} } if (typeof value === 'object') { return value } try { return JSON.parse(String(value || '{}')) } catch { return {} } } export function resolveOrderItemTitle(item) { if (!item) { return '' } const spec = safeParseJson(item.spec_json) return pickFirstNonEmpty([ spec.title, spec.Title, spec.itemTitle, spec.item_title, spec.goods_name, spec.goodsName, item.sku_name, item.sku_code, ]) } export function resolveOrderItemDeliveryMode(tasks, orderItemId) { const task = (Array.isArray(tasks) ? tasks : []).find((item) => item.order_item_id === orderItemId) if (!task) { return '' } if (String(task.executor_key || '').trim() === 'manual_dispatch') { return 'manual_dispatch' } if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') { return 'kuaishou_cloud' } if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) { return 'claim_link' } return String(task.executor_key || '').trim() } export function isManualDispatchTask(task) { return String(task?.executor_key || '').trim() === 'manual_dispatch' } export function isAssistedClaimTask(task) { return String(task?.executor_key || '').trim() === 'tencent_claim_assisted' } export function canRegenerateClaimLinkForViewer(task, viewerContext) { if (isManualDispatchTask(task)) { return false } const baseAllowed = ['link_generated', 'claimed', 'role_confirmed', 'retry_pending', 'manual_review'].includes(task.task_status) if (!baseAllowed) { return false } if (viewerContext.canManageTaskLifecycle) { return true } return viewerContext.canOperateAssistedTask && isAssistedClaimTask(task) && canViewerAccessTaskInventoryGroup(task, viewerContext) } export function canViewerConfirmAssistedRole(task, viewerContext) { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } return String(task?.task_status || '').trim() === 'claimed' } export function canViewerRedeemAssistedTask(task, viewerContext) { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim()) } export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAdminViewerContext()) { if (!viewerContext.restrictInventoryGroups) { return true } const allowedInventoryGroupCodes = Array.isArray(viewerContext.allowedInventoryGroupCodes) ? viewerContext.allowedInventoryGroupCodes : [] if (allowedInventoryGroupCodes.length === 0) { return false } const taskContext = parseTaskContext(task) const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim() if (!inventoryGroupCode) { return true } return allowedInventoryGroupCodes.includes(inventoryGroupCode) } export function normalizeInventoryGroupCodes(values) { return Array.from(new Set((Array.isArray(values) ? values : []) .map((value) => String(value || '').trim()) .filter(Boolean))) } function mapRedeemResolutionAttempt(value) { if (!value || typeof value !== 'object') { return null } return { attempt: Math.max(1, Number(value.attempt || 1)), inventoryItemId: Number(value.inventoryItemId || 0) || null, codeMasked: String(value.codeMasked || '').trim(), credentialType: String(value.credentialType || '').trim(), outcome: String(value.outcome || '').trim(), resultCode: String(value.resultCode || '').trim(), resultMessage: String(value.resultMessage || '').trim(), } } 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 '' }