import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js' import { parseTaskContext as parseTaskContextValue, parseTaskState as parseTaskStateValue, } from '../../utils/task-json.js' import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/task-status.js' import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js' import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js' type CloudSourceLabelMap = Map | Record type KuaishouCloudFulfillmentMapOptions = { cloudSourceLabelMap?: CloudSourceLabelMap } type TaskLike = Partial & { state_json?: string | JsonRecord } export type AdminViewerContext = { role: string canViewSensitiveTaskData: boolean canManageTaskLifecycle: boolean canOperateAssistedTask: boolean canOperateKuaishouIndustryVoucher: boolean } type RedeemResolutionAttempt = { attempt: number 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, options: KuaishouCloudFulfillmentMapOptions = {}, ): JsonRecord | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord const ticket = asJsonObject(record.ticket) const binding = asJsonObject(record.binding) const role = asJsonObject(record.role) const purchase = asJsonObject(record.purchase) const dispatch = asJsonObject(record.dispatch) const returnNumber = asJsonObject(record.returnNumber) const consume = asJsonObject(record.consume) const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys) ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) : [] const resolvedSourceKey = String(binding.resolvedSourceKey || '').trim() 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', cloudSourceKeys, cloudSourceLabels: cloudSourceKeys.map((sourceKey: string) => resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap), ), resolvedSourceKey, resolvedSourceLabel: resolvedSourceKey ? resolveCloudSourceLabel(resolvedSourceKey, options.cloudSourceLabelMap) : '', 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(), roleName: String(binding.roleName || role.name || '').trim(), roleId: String(binding.roleId || role.rid || '').trim(), }, role: { status: String(role.status || 'pending').trim() || 'pending', name: String(role.name || binding.roleName || '').trim(), rid: String(role.rid || binding.roleId || '').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(), }, rebind: mapKuaishouCloudRebindContext(record.rebind), notes: String(record.notes || '').trim(), } } export function mapKuaishouCloudTaskStateProjection( task: TaskLike | null | undefined, options: KuaishouCloudFulfillmentMapOptions = {}, ): JsonRecord | null { if (!task || String(task.executor_key || '').trim() !== 'kuaishou_ct_assisted') { return null } const hasProjection = [ task.kuaishou_ticket_status, task.kuaishou_ticket_code_masked, task.kuaishou_bind_url, task.kuaishou_vn_phone, task.kuaishou_role_name, task.kuaishou_role_id, task.kuaishou_dispatch_status, task.kuaishou_consume_status, task.kuaishou_source_key, ].some((value) => String(value || '').trim()) if (!hasProjection) { return null } const sourceKey = String(task.kuaishou_source_key || '').trim() const roleName = String(task.kuaishou_role_name || '').trim() const roleId = String(task.kuaishou_role_id || '').trim() return { flowType: 'kuaishou_cloud_fulfillment', configId: '', internalSkuCode: '', internalSkuName: '', ticket: { code: String(task.kuaishou_ticket_code_masked || '').trim(), capturedAt: null, status: String(task.kuaishou_ticket_status || 'pending').trim() || 'pending', verifiedAt: null, goodsTitle: '', leftCount: 0, }, binding: { prepareStatus: task.kuaishou_bind_url ? 'ready' : 'pending', cloudSourceKeys: sourceKey ? [sourceKey] : [], cloudSourceLabels: sourceKey ? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)] : [], resolvedSourceKey: sourceKey, resolvedSourceLabel: sourceKey ? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap) : '', skuId: 0, skuName: '', vnKey: '', vnId: 0, vnPhone: String(task.kuaishou_vn_phone || '').trim(), bindUrl: String(task.kuaishou_bind_url || '').trim(), bindPreparedAt: null, bindExpiresAt: null, bindProbeAt: null, bindProbeStatus: '', bindProbeMessage: '', }, role: { status: roleName || roleId ? 'ready' : 'pending', name: roleName, rid: roleId, refreshedAt: null, errorMessage: '', rawInfo: null, }, purchase: { autoBuyEnabled: true, minAssetReserve: 0, usedKnapsack: false, purchaseTriggered: false, assetBefore: 0, assetAfter: 0, purchaseAt: null, }, dispatch: { status: String(task.kuaishou_dispatch_status || 'pending').trim() || 'pending', dispatchAt: null, sendType: 0, note: '', }, returnNumber: { status: 'pending', returnedAt: null, autoReturnEnabled: false, }, consume: { status: String(task.kuaishou_consume_status || 'pending').trim() || 'pending', shopId: '', shopName: '', autoConsumeEnabled: false, consumedAt: null, errorMessage: '', }, rebind: { currentAttempt: 0, history: [], }, notes: '', } } function resolveCloudSourceLabel(sourceKey: string, labelMap: CloudSourceLabelMap | undefined) { const key = String(sourceKey || '').trim() if (!key) return '' const label = labelMap instanceof Map ? labelMap.get(key) : labelMap?.[key] return String(label || key).trim() || key } function mapKuaishouCloudRebindContext(value: unknown): JsonRecord { const record = value && typeof value === 'object' ? (value as JsonRecord) : {} const history = Array.isArray(record.history) ? record.history : [] return { currentAttempt: Math.max(0, Number(record.currentAttempt || 0) || 0), history: history.map((item) => mapKuaishouCloudRebindHistoryItem(item)).filter(Boolean), } } function mapKuaishouCloudRebindHistoryItem(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object') { return null } const record = value as JsonRecord const oldBinding = record.oldBinding && typeof record.oldBinding === 'object' ? (record.oldBinding as JsonRecord) : {} const newBinding = record.newBinding && typeof record.newBinding === 'object' ? (record.newBinding as JsonRecord) : null return { attempt: Math.max(1, Number(record.attempt || 1) || 1), source: String(record.source || '').trim(), requestedAt: record.requestedAt || null, requestedBy: record.requestedBy && typeof record.requestedBy === 'object' ? record.requestedBy : null, status: String(record.status || '').trim(), errorMessage: String(record.errorMessage || '').trim(), oldBinding: { vnKey: String(oldBinding.vnKey || '').trim(), vnId: Number(oldBinding.vnId || 0) || 0, vnPhone: String(oldBinding.vnPhone || '').trim(), bindUrl: String(oldBinding.bindUrl || '').trim(), bindPreparedAt: oldBinding.bindPreparedAt || null, bindExpiresAt: oldBinding.bindExpiresAt || null, roleName: String(oldBinding.roleName || '').trim(), roleId: String(oldBinding.roleId || '').trim(), }, newBinding: newBinding ? { vnKey: String(newBinding.vnKey || '').trim(), vnId: Number(newBinding.vnId || 0) || 0, vnPhone: String(newBinding.vnPhone || '').trim(), bindUrl: String(newBinding.bindUrl || '').trim(), bindPreparedAt: newBinding.bindPreparedAt || null, bindExpiresAt: newBinding.bindExpiresAt || null, } : null, } } 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 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) return { role, canViewSensitiveTaskData: role === 'admin' || role === 'operator', canManageTaskLifecycle: role === 'admin' || role === 'operator', canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support', canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support', } } export async function resolveAdminTaskScreenshotUrl( task: TaskLike, viewerContext: AdminViewerContext, ): Promise { if (task.screenshot_path || task.runtime_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 !isTaskFinalStatus(task?.task_status) } 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 canRegenerateClaimLinkForViewer( task: TaskLike, viewerContext: AdminViewerContext, ): boolean { if (isManualDispatchTask(task)) { return false } const baseAllowed = canRegenerateClaimLinkStatus(task.task_status) if (!baseAllowed) { return false } if (viewerContext.canManageTaskLifecycle) { return true } return false } 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)), 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 '' }