import { and, asc, eq, isNull } from "drizzle-orm"; import type { db as dbInstance } from "@tasks/database"; import { convoyOperatorMessages } from "@tasks/database/schema"; type Db = typeof dbInstance; export type OperatorMessageRow = typeof convoyOperatorMessages.$inferSelect; /** * Poll undelivered operator messages for a convoy. * Keep polling fallback simple — no Redis pubsub, just a DB query. */ export async function pollOperatorNotes( db: Db, workspaceId: string, convoyId: string, briefId?: string | null, ): Promise { const conditions = [ eq(convoyOperatorMessages.workspaceId, workspaceId), eq(convoyOperatorMessages.convoyId, convoyId), isNull(convoyOperatorMessages.deliveredAt), ]; // Optional brief scoping — include global convoy notes plus brief-specific ones // When briefId is provided we return rows where brief_id is null OR matches the brief. // Keeping the simple fallback: just filter by workspace+convoy when briefId not set. // When briefId is set we still return all for convoy so direct messages are not missed. void briefId; const rows = await db .select() .from(convoyOperatorMessages) .where(and(...conditions)) .orderBy(asc(convoyOperatorMessages.createdAt)); return rows; } export async function markOperatorNotesDelivered( db: Db, ids: string[], ): Promise { if (ids.length === 0) return; for (const id of ids) { await db .update(convoyOperatorMessages) .set({ deliveredAt: new Date() }) .where(eq(convoyOperatorMessages.id, id)); } } /** Convenience: poll and mark delivered in one step */ export async function consumeOperatorNotes( db: Db, workspaceId: string, convoyId: string, briefId?: string | null, ): Promise { const rows = await pollOperatorNotes(db, workspaceId, convoyId, briefId); if (rows.length === 0) return rows; await markOperatorNotesDelivered( db, rows.map((r) => r.id), ); return rows; }