import type { OutboxPublisherTransaction } from "@hivegrid/db/outbox"; import type { AuditEventWriterClient } from "@hivegrid/db/audit-events.server"; import { CatalogError, foundationErrors, requirePermission } from "@hivegrid/foundation"; import { publishChatPersistentRealtimeWakeup } from "@hivegrid/chat/realtime-wakeup.server"; import { withWritableChatMessageTaskSource } from "@hivegrid/chat/repository.server"; import { tenantHasTasksModuleWithPermissionsTx } from "@hivegrid/chat/module-access.server"; import { writeChatSystemMessageWithTx } from "@hivegrid/chat/system-message.server"; import type { ChatRouteContext } from "../routes/chatAccess.server"; import { linkEntitiesWithTx } from "~/utils/integrations/entityLinkService.server"; import type { TasksRouteContext } from "~/utils/integrations/taskAccess.server"; import { auditTaskEvent, logTaskActivity } from "~/utils/integrations/taskAudit.server"; import { tasksDetailPath } from "~/utils/integrations/taskRoutePaths"; import { createIdempotentTaskFromSourceWithTx } from "~/utils/integrations/taskService.server"; export async function markChatMessageAsTask(input: { readonly chatContext: ChatRouteContext; readonly taskContext: TasksRouteContext; readonly chatId: string; readonly messageId: string; readonly requestId: string; readonly tenantSlug: string; readonly fallbackTitle: string; }): Promise<{ readonly taskId: string; readonly replayed: boolean }> { if (input.chatContext.tenantId !== input.taskContext.tenantId || input.chatContext.userId !== input.taskContext.userId) { throw new Error("Chat and task contexts must belong to the same tenant user"); } const tenantSlug = input.tenantSlug.trim(); if (!tenantSlug) throw new CatalogError(foundationErrors.notFound); return withWritableChatMessageTaskSource( { tenantId: input.chatContext.tenantId, userId: input.chatContext.userId, permissions: input.chatContext.permissions, chatId: input.chatId, messageId: input.messageId, }, async (tx, source, currentPermissions) => { if (!currentPermissions.includes("tasks.write")) { throw new CatalogError(foundationErrors.notFound, { permission: "tasks.write" }); } if (!(await tenantHasTasksModuleWithPermissionsTx(tx, input.chatContext.tenantId, currentPermissions))) { throw new CatalogError(foundationErrors.notFound, { module: "tasks" }); } requirePermission({ tenantId: input.taskContext.tenantId, userId: input.taskContext.userId, permissions: currentPermissions }, "tasks.write"); const currentTaskContext = { ...input.taskContext, permissions: currentPermissions }; const firstLine = source.body .split(/\r?\n/u) .map((line) => line.trim()) .find(Boolean); const title = (firstLine || input.fallbackTitle).slice(0, 255); const result = await createIdempotentTaskFromSourceWithTx(tx, { tenantId: currentTaskContext.tenantId, ctx: currentTaskContext, title, description: source.body.trim() || null, linkedEntityType: source.projectId ? "project" : null, linkedEntityId: source.projectId, source: "chat", sourceRef: `chat-message:${source.messageId}`, }); await linkEntitiesWithTx(tx, { tenantId: currentTaskContext.tenantId, userId: currentTaskContext.userId, sourceType: "chat_message", sourceId: source.messageId, targetType: "task", targetId: result.task.id, linkRole: "created-from", }); if (!result.replayed) { await auditTaskEvent( { ctx: currentTaskContext, action: "task.created", taskId: result.task.id, metadata: { source: "chat", chatId: source.chatId, messageId: source.messageId }, }, { client: tx as unknown as AuditEventWriterClient, strict: true } ); await logTaskActivity( { ctx: currentTaskContext, taskId: result.task.id, activityType: "task_created", summaryText: title, linkedEntityType: result.task.linkedEntityType, linkedEntityId: result.task.linkedEntityId, }, { client: tx, strict: true } ); } const taskHref = tasksDetailPath(tenantSlug, result.task.id); const systemMessage = await writeChatSystemMessageWithTx(tx, { tenantId: currentTaskContext.tenantId, chatId: source.chatId, systemEventKey: `task-created:${source.messageId}`, requestId: `${input.requestId}:task-created`, kind: "task_created", metadata: { sourceMessageId: source.messageId, taskId: result.task.id, taskTitle: title, taskHref }, body: `${input.fallbackTitle}: ${title}`, actorUserId: currentTaskContext.userId, }); if (!systemMessage.replayed) { await publishChatPersistentRealtimeWakeup(tx as unknown as OutboxPublisherTransaction, { event: systemMessage.event }); } return { taskId: result.task.id, replayed: result.replayed }; } ); }