import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' import { parseTaskContext as parseTaskContextValue, parseTaskState as parseTaskStateValue, } from '../../utils/task-json.js' import type { AdminViewerSessionInput } from '../../types/admin-read-inputs.js' import type { OrderItemRow, TaskRow } from '../../types/repository-rows.js' type JsonRecord = Record type TaskLike = Partial & { state_json?: string | JsonRecord } export type AdminViewerContext = { role: string allowedInventoryGroupCodes: string[] | null restrictInventoryGroups: boolean canViewSensitiveTaskData: boolean canManageSensitiveInventory: boolean canManageTaskLifecycle: boolean canOperateAssistedTask: boolean } type RedeemResolutionAttempt = { attempt: number inventoryItemId: number | null codeMasked: string credentialType: string outcome: string resultCode: string resultMessage: string } export function mapManualDispatchContext( value: unknown, viewerContext: AdminViewerContext = createAdminViewerContext(), ): JsonRecord | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord return { outcome: String(record.outcome || '').trim(), deliveryReference: String(record.deliveryReference || '').trim(), deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(record.deliveredCredential || '').trim() : '', resultMessage: String(record.resultMessage || '').trim(), completedAt: record.completedAt || null, completedBy: record.completedBy && typeof record.completedBy === 'object' ? { userId: Number(record.completedBy.userId || 0) || 0, username: String(record.completedBy.username || '').trim(), role: String(record.completedBy.role || '').trim(), } : null, } } export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord const ticket = record.ticket && typeof record.ticket === 'object' ? record.ticket : {} const binding = record.binding && typeof record.binding === 'object' ? record.binding : {} const role = record.role && typeof record.role === 'object' ? record.role : {} const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {} const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {} const returnNumber = record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {} const consume = record.consume && typeof record.consume === 'object' ? record.consume : {} return { flowType: String(record.flowType || '').trim(), configId: String(record.configId || '').trim(), internalSkuCode: String(record.internalSkuCode || '').trim(), internalSkuName: String(record.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(), cloudSourceKeyFallbacks: Array.isArray(binding.cloudSourceKeyFallbacks) ? binding.cloudSourceKeyFallbacks.map((value) => String(value || '').trim()).filter(Boolean) : [], resolvedSourceKey: String(binding.resolvedSourceKey || '').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, bindExpiresAt: binding.bindExpiresAt || null, bindProbeAt: binding.bindProbeAt || null, bindProbeStatus: String(binding.bindProbeStatus || '').trim(), bindProbeMessage: String(binding.bindProbeMessage || '').trim(), }, 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(record.notes || '').trim(), } } export function mapRedeemResolutionContext(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord const attempts = Array.isArray(record.attempts) ? record.attempts .map((attempt) => mapRedeemResolutionAttempt(attempt)) .filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt)) : [] return { status: String(record.status || '').trim(), taskStatus: String(record.taskStatus || '').trim(), replacementCount: Math.max(0, Number(record.replacementCount || 0)), finishedAt: record.finishedAt || null, attempts, } } export function getTaskPrimaryInventoryItemId(task: TaskLike | null | undefined): number | null { const value = Number(task?.primary_inventory_item_id || 0) return Number.isFinite(value) && value > 0 ? value : null } export function getTaskPrimaryClaimTokenId(task: TaskLike | null | undefined): number | null { const value = Number(task?.primary_claim_token_id || 0) return Number.isFinite(value) && value > 0 ? value : null } export function createAdminViewerContext(session: AdminViewerSessionInput | null = null): AdminViewerContext { 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: TaskLike, viewerContext: AdminViewerContext, ): Promise { if (task.screenshot_path || task.browser_session_id) { return `/api/v1/admin/tasks/${task.id}/screenshot` } return '' } export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (!['admin', 'operator', 'support'].includes(viewerContext.role)) { return false } return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim()) } export function resolveDisplayShopName(provider: unknown, shopId: unknown, shopName: unknown): string { const normalizedShopName = String(shopName || '').trim() if (normalizedShopName) { return normalizedShopName } const normalizedShopId = String(shopId || '').trim() if (!normalizedShopId) { return '' } return normalizedShopId } export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord { return parseTaskContextValue(task) } export function parseTaskState(task: TaskLike | null | undefined): JsonRecord { return parseTaskStateValue(task) } export function resolveOrderItemTitle(item: Partial | null | undefined): string { 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: TaskLike[] | null | undefined, orderItemId: number | string): string { 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: TaskLike | null | undefined): boolean { return String(task?.executor_key || '').trim() === 'manual_dispatch' } export function isAssistedClaimTask(task: TaskLike | null | undefined): boolean { return String(task?.executor_key || '').trim() === 'tencent_claim_assisted' } export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean { 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: TaskLike, viewerContext: AdminViewerContext): boolean { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } return String(task?.task_status || '').trim() === 'claimed' } export function canViewerRedeemAssistedTask(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim()) } export function canViewerAccessTaskInventoryGroup( task: TaskLike, viewerContext: AdminViewerContext = createAdminViewerContext(), ): boolean { 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: unknown): string[] { return Array.from(new Set((Array.isArray(values) ? values : []) .map((value) => String(value || '').trim()) .filter(Boolean))) } function mapRedeemResolutionAttempt(value: unknown): RedeemResolutionAttempt | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord return { attempt: Math.max(1, Number(record.attempt || 1)), inventoryItemId: Number(record.inventoryItemId || 0) || null, codeMasked: String(record.codeMasked || '').trim(), credentialType: String(record.credentialType || '').trim(), outcome: String(record.outcome || '').trim(), resultCode: String(record.resultCode || '').trim(), resultMessage: String(record.resultMessage || '').trim(), } } function pickFirstNonEmpty(values: unknown[]): string { 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 '' }