Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/integrations/taskAudit.server.ts
T

88 lines
3.1 KiB
TypeScript

// 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<TasksRouteContext, "tenantId" | "userId">;
action: TaskAuditAction;
taskId: string;
metadata?: Record<string, JsonValue>;
}): Promise<void> {
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<Record<TaskEntityType, ActivitySubjectType>> = {
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<TasksRouteContext, "tenantId" | "userId">;
taskId: string;
activityType: Extract<ActivityType, "task_created" | "task_completed">;
summaryText: string;
linkedEntityType?: TaskEntityType | null;
linkedEntityId?: string | null;
}): Promise<void> {
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.
}
}