import { writeAuditLog } from "@/server/audit"; import { assertCan, ServiceError, type ServiceCtx } from "@/server/services/context"; import { requireVisibleWorkOrder } from "@/server/services/work-orders/visibility"; import { FIELD_EDITABLE, type WorkOrderStatus } from "@/lib/work-orders/status"; /** Shared helpers of the field services (lane L4). */ export type FieldOrder = { id: string; number: string; status: WorkOrderStatus; version: number; siteId: string | null; customerId: string; assignedTeamId: string | null; }; const FIELD_ORDER_SELECT = { id: true, number: true, status: true, version: true, siteId: true, customerId: true, assignedTeamId: true, } as const; /** * Loads a work order for a field mutation: requires `field:execute`, the order must be in the * user's visibility scope (otherwise not_found — existence is never revealed) and, when * `editable`, in a status that allows field documentation. */ export async function requireFieldOrder(ctx: ServiceCtx, workOrderId: string, opts: { editable?: boolean } = {}): Promise { assertCan(ctx, "field:execute"); const wo = (await requireVisibleWorkOrder(ctx, workOrderId, FIELD_ORDER_SELECT)) as FieldOrder; if (opts.editable && !FIELD_EDITABLE.includes(wo.status)) { throw new ServiceError("invalid", `work order status ${wo.status} does not allow field documentation`); } return wo; } /** Client timestamp of an operation; future values (clock skew) are clamped to now. */ export function opTime(at?: string | null): Date { const now = new Date(); if (!at) return now; const d = new Date(at); if (Number.isNaN(d.getTime())) throw new ServiceError("invalid", "invalid timestamp"); return d.getTime() > now.getTime() + 60_000 ? now : d; } export async function audit( ctx: ServiceCtx, action: "create" | "update" | "delete", entity: string, entityId: string, before?: unknown, after?: unknown, ): Promise { await writeAuditLog({ tenantId: ctx.tenantId, actorId: ctx.userId, action, entity, entityId, before, after }); } /** Prisma unique-constraint violation (used for idempotent creates under races). */ export function isUniqueViolation(err: unknown): boolean { return (err as { code?: string })?.code === "P2002"; }