// P4 Tasks (AK-P4b) §12 — audit + activity wiring for the Tasks module. // // Audit (DoD 13): every write action records a tamper-evident AuditEvent (M001) // via createAuditEvent with the shared db client. // // Activity (DoD 14): user-facing timeline entries are logged against the most // relevant entity. When the task is linked to a CRM entity that maps to an // Activity subject (contact/company/lead/deal/project) the entry hangs off that // entity's timeline with the task referenced; otherwise it hangs off the task's // own timeline (`task` is itself a valid Activity subject). import type { JsonValue } from "@toolctrl/foundation"; import { createAuditEvent } from "@toolctrl/db/audit-events.server"; import { logActivity, type ActivitySubjectType, type ActivityType } from "@hivegrid/activity"; import type { TasksRouteContext } from "./taskAccess.server"; import type { TaskEntityType } from "./taskService.server"; export type TaskAuditAction = | "task.created" | "task.updated" | "task.reassigned" | "task.completed" | "task.archived" | "task.reactivated" | "task.deleted" | "task.watcher_added" | "task.watcher_removed"; export async function auditTaskEvent(input: { ctx: Pick; action: TaskAuditAction; taskId: string; metadata?: Record; }): Promise { try { await createAuditEvent({ tenantId: input.ctx.tenantId, actorId: input.ctx.userId, actorType: "user", action: input.action, targetType: "task", targetId: input.taskId, metadata: input.metadata ?? null, }); } catch { // Audit must never break the user action; the failure is logged upstream. } } const ENTITY_TO_ACTIVITY_SUBJECT: Partial> = { contact: "contact", company: "company", lead: "lead", deal: "deal", project: "project", }; /** * Log a task activity. When the task links to a CRM entity with an Activity * subject, the entry hangs off that entity (with the task as a related ref); * otherwise it hangs off the task's own timeline. Best-effort. */ export async function logTaskActivity(input: { ctx: Pick; taskId: string; activityType: Extract; summaryText: string; linkedEntityType?: TaskEntityType | null; linkedEntityId?: string | null; }): Promise { const linkedSubject = input.linkedEntityType ? ENTITY_TO_ACTIVITY_SUBJECT[input.linkedEntityType] : undefined; const useLinked = Boolean(linkedSubject && input.linkedEntityId); try { await logActivity({ tenantId: input.ctx.tenantId, actorUserId: input.ctx.userId, actorKind: "user", activityType: input.activityType, subjectType: useLinked ? (linkedSubject as ActivitySubjectType) : "task", subjectId: useLinked ? (input.linkedEntityId as string) : input.taskId, summaryText: input.summaryText, relatedEntityRefs: useLinked ? [{ type: "task", id: input.taskId }] : undefined, }); } catch { // Activity logging is best-effort; never break the user action. } }