import { randomBytes } from "@noble/hashes/utils.js" import { ConvexError, v } from "convex/values" import { mutation, query } from "./_generated/server" import type { Id } from "./_generated/dataModel" import type { MutationCtx, QueryCtx } from "./_generated/server" const SECRET = process.env.CONVEX_SYNC_SECRET ?? "dev-sync-secret" const VALID_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]) const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]) function normalizeEmail(value: string) { return value.trim().toLowerCase() } function generateProvisioningCode() { return Array.from(randomBytes(32), (b) => b.toString(16).padStart(2, "0")).join("") } type ImportedUser = { email: string name: string role?: string | null avatarUrl?: string | null teams?: string[] | null companySlug?: string | null } type ImportedQueue = { slug?: string | null name: string } type ImportedCompany = { slug: string name: string provisioningCode?: string | null isAvulso?: boolean | null cnpj?: string | null domain?: string | null phone?: string | null description?: string | null address?: string | null createdAt?: number | null updatedAt?: number | null } function normalizeRole(role: string | null | undefined) { if (!role) return "AGENT" const normalized = role.toUpperCase() if (VALID_ROLES.has(normalized)) return normalized // map legacy CUSTOMER to MANAGER if (normalized === "CUSTOMER") return "MANAGER" return "AGENT" } function slugify(value: string) { return value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-") .replace(/-+/g, "-") .toLowerCase() } function pruneUndefined>(input: T): T { for (const key of Object.keys(input)) { if (input[key] === undefined) { delete input[key] } } return input } type TicketSlaSnapshotRecord = { categoryId?: Id<"ticketCategories"> categoryName?: string priority?: string responseTargetMinutes?: number responseMode?: string solutionTargetMinutes?: number solutionMode?: string alertThreshold?: number pauseStatuses?: string[] } type ExportedSlaSnapshot = { categoryId?: string categoryName?: string priority?: string responseTargetMinutes?: number responseMode?: string solutionTargetMinutes?: number solutionMode?: string alertThreshold?: number pauseStatuses?: string[] } function serializeSlaSnapshot(snapshot?: TicketSlaSnapshotRecord | null): ExportedSlaSnapshot | undefined { if (!snapshot) return undefined const exported = pruneUndefined({ categoryId: snapshot.categoryId ? String(snapshot.categoryId) : undefined, categoryName: snapshot.categoryName, priority: snapshot.priority, responseTargetMinutes: snapshot.responseTargetMinutes, responseMode: snapshot.responseMode, solutionTargetMinutes: snapshot.solutionTargetMinutes, solutionMode: snapshot.solutionMode, alertThreshold: snapshot.alertThreshold, pauseStatuses: snapshot.pauseStatuses && snapshot.pauseStatuses.length > 0 ? snapshot.pauseStatuses : undefined, }) return Object.keys(exported).length > 0 ? exported : undefined } function normalizeImportedSlaSnapshot(snapshot: unknown): TicketSlaSnapshotRecord | undefined { if (!snapshot || typeof snapshot !== "object") return undefined const record = snapshot as Record const pauseStatuses = Array.isArray(record.pauseStatuses) ? record.pauseStatuses.filter((value): value is string => typeof value === "string") : undefined const normalized = pruneUndefined({ categoryName: typeof record.categoryName === "string" ? record.categoryName : undefined, priority: typeof record.priority === "string" ? record.priority : undefined, responseTargetMinutes: typeof record.responseTargetMinutes === "number" ? record.responseTargetMinutes : undefined, responseMode: typeof record.responseMode === "string" ? record.responseMode : undefined, solutionTargetMinutes: typeof record.solutionTargetMinutes === "number" ? record.solutionTargetMinutes : undefined, solutionMode: typeof record.solutionMode === "string" ? record.solutionMode : undefined, alertThreshold: typeof record.alertThreshold === "number" ? record.alertThreshold : undefined, pauseStatuses: pauseStatuses && pauseStatuses.length > 0 ? pauseStatuses : undefined, }) return Object.keys(normalized).length > 0 ? normalized : undefined } async function ensureUser( ctx: MutationCtx, tenantId: string, data: ImportedUser, cache: Map>, companyCache: Map> ) { if (cache.has(data.email)) { return cache.get(data.email)! } const existing = await ctx.db .query("users") .withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", data.email)) .first() const role = normalizeRole(data.role) const companyId = data.companySlug ? companyCache.get(data.companySlug) : undefined const record = existing ? (() => { const needsPatch = existing.name !== data.name || existing.role !== role || existing.avatarUrl !== (data.avatarUrl ?? undefined) || JSON.stringify(existing.teams ?? []) !== JSON.stringify(data.teams ?? []) || (existing.companyId ?? undefined) !== companyId if (needsPatch) { return ctx.db.patch(existing._id, { name: data.name, role, avatarUrl: data.avatarUrl ?? undefined, teams: data.teams ?? undefined, tenantId, companyId, }) } return Promise.resolve() })() : ctx.db.insert("users", { tenantId, email: data.email, name: data.name, role, avatarUrl: data.avatarUrl ?? undefined, teams: data.teams ?? undefined, companyId, }) const id = existing ? existing._id : ((await record) as Id<"users">) cache.set(data.email, id) return id } async function ensureQueue( ctx: MutationCtx, tenantId: string, data: ImportedQueue, cache: Map> ) { const slug = data.slug && data.slug.trim().length > 0 ? data.slug : slugify(data.name) if (cache.has(slug)) return cache.get(slug)! const bySlug = await ctx.db .query("queues") .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug)) .first() if (bySlug) { if (bySlug.name !== data.name) { await ctx.db.patch(bySlug._id, { name: data.name }) } cache.set(slug, bySlug._id) return bySlug._id } const byName = await ctx.db .query("queues") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", data.name)) .first() if (byName) { if (byName.slug !== slug) { await ctx.db.patch(byName._id, { slug }) } cache.set(slug, byName._id) return byName._id } const id = await ctx.db.insert("queues", { tenantId, name: data.name, slug, teamId: undefined, }) cache.set(slug, id) return id } async function ensureCompany( ctx: MutationCtx, tenantId: string, data: ImportedCompany, cache: Map> ) { const slug = data.slug || slugify(data.name) if (cache.has(slug)) { return cache.get(slug)! } const existing = await ctx.db .query("companies") .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug)) .first() const payload = pruneUndefined({ tenantId, name: data.name, slug, provisioningCode: data.provisioningCode ?? existing?.provisioningCode ?? generateProvisioningCode(), isAvulso: data.isAvulso ?? undefined, cnpj: data.cnpj ?? undefined, domain: data.domain ?? undefined, phone: data.phone ?? undefined, description: data.description ?? undefined, address: data.address ?? undefined, createdAt: data.createdAt ?? Date.now(), updatedAt: data.updatedAt ?? Date.now(), }) let id: Id<"companies"> if (existing) { const existingIsAvulso = existing.isAvulso ?? undefined const targetIsAvulso = payload.isAvulso ?? existingIsAvulso const targetCnpj = payload.cnpj ?? undefined const targetDomain = payload.domain ?? undefined const targetPhone = payload.phone ?? undefined const targetDescription = payload.description ?? undefined const targetAddress = payload.address ?? undefined const needsPatch = existing.name !== payload.name || existingIsAvulso !== targetIsAvulso || (existing.cnpj ?? undefined) !== targetCnpj || (existing.domain ?? undefined) !== targetDomain || (existing.phone ?? undefined) !== targetPhone || (existing.description ?? undefined) !== targetDescription || (existing.address ?? undefined) !== targetAddress || existing.provisioningCode !== payload.provisioningCode if (needsPatch) { await ctx.db.patch(existing._id, { name: payload.name, isAvulso: targetIsAvulso, cnpj: targetCnpj, domain: targetDomain, phone: targetPhone, description: targetDescription, address: targetAddress, provisioningCode: payload.provisioningCode, updatedAt: Date.now(), }) } id = existing._id } else { id = await ctx.db.insert("companies", payload) } cache.set(slug, id) return id } async function getTenantUsers(ctx: QueryCtx, tenantId: string) { return ctx.db .query("users") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() } async function getTenantQueues(ctx: QueryCtx, tenantId: string) { return ctx.db .query("queues") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() } async function getTenantCompanies(ctx: QueryCtx, tenantId: string) { return ctx.db .query("companies") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() } export const exportTenantSnapshot = query({ args: { secret: v.string(), tenantId: v.string(), }, handler: async (ctx, { secret, tenantId }) => { if (secret !== SECRET) { throw new ConvexError("CONVEX_SYNC_SECRET não configurada no backend") } const [users, queues, companies] = await Promise.all([ getTenantUsers(ctx, tenantId), getTenantQueues(ctx, tenantId), getTenantCompanies(ctx, tenantId), ]) const userMap = new Map(users.map((user) => [user._id, user])) const queueMap = new Map(queues.map((queue) => [queue._id, queue])) const companyMap = new Map(companies.map((company) => [company._id, company])) const tickets = await ctx.db .query("tickets") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() const ticketsWithRelations = [] for (const ticket of tickets) { const comments = await ctx.db .query("ticketComments") .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id)) .collect() const events = await ctx.db .query("ticketEvents") .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id)) .collect() const requester = userMap.get(ticket.requesterId) const assignee = ticket.assigneeId ? userMap.get(ticket.assigneeId) : undefined const queue = ticket.queueId ? queueMap.get(ticket.queueId) : undefined const company = ticket.companyId ? companyMap.get(ticket.companyId) : requester?.companyId ? companyMap.get(requester.companyId) : undefined if (!requester) { continue } ticketsWithRelations.push({ reference: ticket.reference, subject: ticket.subject, summary: ticket.summary ?? null, status: ticket.status, priority: ticket.priority, channel: ticket.channel, queueSlug: queue?.slug ?? undefined, requesterEmail: requester.email, assigneeEmail: assignee?.email ?? undefined, companySlug: company?.slug ?? undefined, dueAt: ticket.dueAt ?? undefined, firstResponseAt: ticket.firstResponseAt ?? undefined, resolvedAt: ticket.resolvedAt ?? undefined, closedAt: ticket.closedAt ?? undefined, createdAt: ticket.createdAt, updatedAt: ticket.updatedAt, tags: ticket.tags ?? [], slaSnapshot: serializeSlaSnapshot(ticket.slaSnapshot as TicketSlaSnapshotRecord | null), slaResponseDueAt: ticket.slaResponseDueAt ?? undefined, slaSolutionDueAt: ticket.slaSolutionDueAt ?? undefined, slaResponseStatus: ticket.slaResponseStatus ?? undefined, slaSolutionStatus: ticket.slaSolutionStatus ?? undefined, slaPausedAt: ticket.slaPausedAt ?? undefined, slaPausedBy: ticket.slaPausedBy ?? undefined, slaPausedMs: ticket.slaPausedMs ?? undefined, comments: comments .map((comment) => { const author = userMap.get(comment.authorId) if (!author) { return null } return { authorEmail: author.email, visibility: comment.visibility, body: comment.body, createdAt: comment.createdAt, updatedAt: comment.updatedAt, } }) .filter((value): value is { authorEmail: string visibility: string body: string createdAt: number updatedAt: number } => value !== null), events: events.map((event) => ({ type: event.type, payload: event.payload ?? {}, createdAt: event.createdAt, })), }) } return { tenantId, companies: companies.map((company) => ({ slug: company.slug, name: company.name, isAvulso: company.isAvulso ?? false, cnpj: company.cnpj ?? null, domain: company.domain ?? null, phone: company.phone ?? null, description: company.description ?? null, address: company.address ?? null, createdAt: company.createdAt, updatedAt: company.updatedAt, })), users: users.map((user) => ({ email: user.email, name: user.name, role: user.role ?? null, avatarUrl: user.avatarUrl ?? null, teams: user.teams ?? [], companySlug: user.companyId ? companyMap.get(user.companyId)?.slug ?? null : null, })), queues: queues.map((queue) => ({ name: queue.name, slug: queue.slug, })), tickets: ticketsWithRelations, } }, }) export const importPrismaSnapshot = mutation({ args: { secret: v.string(), snapshot: v.object({ tenantId: v.string(), companies: v.array( v.object({ slug: v.string(), name: v.string(), cnpj: v.optional(v.string()), domain: v.optional(v.string()), phone: v.optional(v.string()), description: v.optional(v.string()), address: v.optional(v.string()), createdAt: v.optional(v.number()), updatedAt: v.optional(v.number()), }) ), users: v.array( v.object({ email: v.string(), name: v.string(), role: v.optional(v.string()), avatarUrl: v.optional(v.string()), teams: v.optional(v.array(v.string())), companySlug: v.optional(v.string()), }) ), queues: v.array( v.object({ name: v.string(), slug: v.optional(v.string()), }) ), tickets: v.array( v.object({ reference: v.number(), subject: v.string(), summary: v.optional(v.string()), status: v.string(), priority: v.string(), channel: v.string(), queueSlug: v.optional(v.string()), requesterEmail: v.string(), assigneeEmail: v.optional(v.string()), companySlug: v.optional(v.string()), dueAt: v.optional(v.number()), firstResponseAt: v.optional(v.number()), resolvedAt: v.optional(v.number()), closedAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), tags: v.optional(v.array(v.string())), slaSnapshot: v.optional(v.any()), slaResponseDueAt: v.optional(v.number()), slaSolutionDueAt: v.optional(v.number()), slaResponseStatus: v.optional(v.string()), slaSolutionStatus: v.optional(v.string()), slaPausedAt: v.optional(v.number()), slaPausedBy: v.optional(v.string()), slaPausedMs: v.optional(v.number()), comments: v.array( v.object({ authorEmail: v.string(), visibility: v.string(), body: v.string(), createdAt: v.number(), updatedAt: v.number(), }) ), events: v.array( v.object({ type: v.string(), payload: v.optional(v.any()), createdAt: v.number(), }) ), }) ), }), }, handler: async (ctx, { secret, snapshot }) => { if (!SECRET) { throw new ConvexError("CONVEX_SYNC_SECRET não configurada no backend") } if (secret !== SECRET) { throw new ConvexError("Segredo inválido para sincronização") } const companyCache = new Map>() const userCache = new Map>() const queueCache = new Map>() for (const company of snapshot.companies) { await ensureCompany(ctx, snapshot.tenantId, company, companyCache) } for (const user of snapshot.users) { await ensureUser(ctx, snapshot.tenantId, user, userCache, companyCache) } for (const queue of snapshot.queues) { await ensureQueue(ctx, snapshot.tenantId, queue, queueCache) } const snapshotStaffEmails = new Set( snapshot.users .filter((user) => INTERNAL_STAFF_ROLES.has(normalizeRole(user.role ?? null))) .map((user) => normalizeEmail(user.email)) ) const existingTenantUsers = await ctx.db .query("users") .withIndex("by_tenant", (q) => q.eq("tenantId", snapshot.tenantId)) .collect() for (const user of existingTenantUsers) { const role = normalizeRole(user.role ?? null) if (INTERNAL_STAFF_ROLES.has(role) && !snapshotStaffEmails.has(normalizeEmail(user.email))) { await ctx.db.delete(user._id) } } let ticketsUpserted = 0 let commentsInserted = 0 let eventsInserted = 0 for (const ticket of snapshot.tickets) { const normalizedSnapshot = normalizeImportedSlaSnapshot(ticket.slaSnapshot) const slaPausedMs = typeof ticket.slaPausedMs === "number" ? ticket.slaPausedMs : undefined const requesterId = await ensureUser( ctx, snapshot.tenantId, { email: ticket.requesterEmail, name: ticket.requesterEmail, }, userCache, companyCache ) const assigneeId = ticket.assigneeEmail ? await ensureUser( ctx, snapshot.tenantId, { email: ticket.assigneeEmail, name: ticket.assigneeEmail, }, userCache, companyCache ) : undefined const queueId = ticket.queueSlug ? queueCache.get(ticket.queueSlug) ?? (await ensureQueue(ctx, snapshot.tenantId, { name: ticket.queueSlug, slug: ticket.queueSlug }, queueCache)) : undefined const companyId = ticket.companySlug ? companyCache.get(ticket.companySlug) ?? (await ensureCompany(ctx, snapshot.tenantId, { slug: ticket.companySlug, name: ticket.companySlug }, companyCache)) : undefined const existing = await ctx.db .query("tickets") .withIndex("by_tenant_reference", (q) => q.eq("tenantId", snapshot.tenantId).eq("reference", ticket.reference)) .first() const payload = pruneUndefined({ tenantId: snapshot.tenantId, reference: ticket.reference, subject: ticket.subject, summary: ticket.summary ?? undefined, status: ticket.status, priority: ticket.priority, channel: ticket.channel, queueId: queueId as Id<"queues"> | undefined, categoryId: undefined, subcategoryId: undefined, requesterId, assigneeId: assigneeId as Id<"users"> | undefined, working: false, slaPolicyId: undefined, companyId: companyId as Id<"companies"> | undefined, dueAt: ticket.dueAt ?? undefined, firstResponseAt: ticket.firstResponseAt ?? undefined, resolvedAt: ticket.resolvedAt ?? undefined, closedAt: ticket.closedAt ?? undefined, updatedAt: ticket.updatedAt, createdAt: ticket.createdAt, tags: ticket.tags && ticket.tags.length > 0 ? ticket.tags : undefined, slaSnapshot: normalizedSnapshot, slaResponseDueAt: ticket.slaResponseDueAt ?? undefined, slaSolutionDueAt: ticket.slaSolutionDueAt ?? undefined, slaResponseStatus: ticket.slaResponseStatus ?? undefined, slaSolutionStatus: ticket.slaSolutionStatus ?? undefined, slaPausedAt: ticket.slaPausedAt ?? undefined, slaPausedBy: ticket.slaPausedBy ?? undefined, slaPausedMs, customFields: undefined, totalWorkedMs: undefined, activeSessionId: undefined, }) let ticketId: Id<"tickets"> if (existing) { await ctx.db.patch(existing._id, payload) ticketId = existing._id } else { ticketId = await ctx.db.insert("tickets", payload) } ticketsUpserted += 1 const existingComments = await ctx.db .query("ticketComments") .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId)) .collect() for (const comment of existingComments) { await ctx.db.delete(comment._id) } const existingEvents = await ctx.db .query("ticketEvents") .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId)) .collect() for (const event of existingEvents) { await ctx.db.delete(event._id) } for (const comment of ticket.comments) { const authorId = await ensureUser( ctx, snapshot.tenantId, { email: comment.authorEmail, name: comment.authorEmail, }, userCache, companyCache ) await ctx.db.insert("ticketComments", { ticketId, authorId, visibility: comment.visibility, body: comment.body, attachments: [], createdAt: comment.createdAt, updatedAt: comment.updatedAt, }) commentsInserted += 1 } for (const event of ticket.events) { await ctx.db.insert("ticketEvents", { ticketId, type: event.type, payload: event.payload ?? {}, createdAt: event.createdAt, }) eventsInserted += 1 } } return { usersProcessed: userCache.size, queuesProcessed: queueCache.size, ticketsUpserted, commentsInserted, eventsInserted, } }, }) export const backfillTicketCommentAuthorSnapshots = mutation({ args: { limit: v.optional(v.number()), dryRun: v.optional(v.boolean()), }, handler: async (ctx, { limit, dryRun }) => { const effectiveDryRun = Boolean(dryRun) const maxUpdates = limit && limit > 0 ? limit : null const comments = await ctx.db.query("ticketComments").collect() let updated = 0 let skippedExisting = 0 let missingAuthors = 0 for (const comment of comments) { if (comment.authorSnapshot) { skippedExisting += 1 continue } if (maxUpdates !== null && updated >= maxUpdates) { break } const author = await ctx.db.get(comment.authorId) let name: string | null = author?.name ?? null const email: string | null = author?.email ?? null let avatarUrl: string | null = author?.avatarUrl ?? null const teams: string[] | undefined = (author?.teams ?? undefined) as string[] | undefined if (!author) { missingAuthors += 1 const events = await ctx.db .query("ticketEvents") .withIndex("by_ticket", (q) => q.eq("ticketId", comment.ticketId)) .collect() const matchingEvent = events.find( (event) => event.type === "COMMENT_ADDED" && event.createdAt === comment.createdAt, ) if (matchingEvent && matchingEvent.payload && typeof matchingEvent.payload === "object") { const payload = matchingEvent.payload as { authorName?: string; authorAvatar?: string } if (typeof payload.authorName === "string" && payload.authorName.trim().length > 0) { name = payload.authorName.trim() } if (typeof payload.authorAvatar === "string" && payload.authorAvatar.trim().length > 0) { avatarUrl = payload.authorAvatar } } } const snapshot = pruneUndefined({ name: name && name.trim().length > 0 ? name : "Usuário removido", email: email ?? undefined, avatarUrl: avatarUrl ?? undefined, teams: teams && teams.length > 0 ? teams : undefined, }) if (!effectiveDryRun) { await ctx.db.patch(comment._id, { authorSnapshot: snapshot }) } updated += 1 } return { dryRun: effectiveDryRun, totalComments: comments.length, updated, skippedExisting, missingAuthors, limit: maxUpdates, } }, }) export const syncMachineCompanyReferences = mutation({ args: { tenantId: v.optional(v.string()), dryRun: v.optional(v.boolean()), }, handler: async (ctx, { tenantId, dryRun }) => { const effectiveDryRun = Boolean(dryRun) const machines = tenantId && tenantId.trim().length > 0 ? await ctx.db .query("machines") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() : await ctx.db.query("machines").collect() const slugCache = new Map | null>() const summary = { total: machines.length, updated: 0, skippedMissingSlug: 0, skippedMissingCompany: 0, alreadyLinked: 0, } for (const machine of machines) { const slug = machine.companySlug ?? null if (!slug) { summary.skippedMissingSlug += 1 continue } const cacheKey = `${machine.tenantId}::${slug}` let companyId = slugCache.get(cacheKey) if (companyId === undefined) { const company = await ctx.db .query("companies") .withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", slug)) .unique() companyId = company?._id ?? null slugCache.set(cacheKey, companyId) } if (!companyId) { summary.skippedMissingCompany += 1 continue } if (machine.companyId === companyId) { summary.alreadyLinked += 1 continue } if (!effectiveDryRun) { await ctx.db.patch(machine._id, { companyId }) } summary.updated += 1 } return { dryRun: effectiveDryRun, ...summary, } }, }) export const backfillTicketSnapshots = mutation({ args: { tenantId: v.string(), limit: v.optional(v.number()) }, handler: async (ctx, { tenantId, limit }) => { const tickets = await ctx.db .query("tickets") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() let processed = 0 for (const t of tickets) { if (limit && processed >= limit) break const patch: Record = {} if (!t.requesterSnapshot) { const requester = await ctx.db.get(t.requesterId) if (requester) { patch.requesterSnapshot = { name: requester.name, email: requester.email, avatarUrl: requester.avatarUrl ?? undefined, teams: requester.teams ?? undefined, } } } if (t.assigneeId && !t.assigneeSnapshot) { const assignee = await ctx.db.get(t.assigneeId) if (assignee) { patch.assigneeSnapshot = { name: assignee.name, email: assignee.email, avatarUrl: assignee.avatarUrl ?? undefined, teams: assignee.teams ?? undefined, } } } if (!t.companySnapshot) { const companyId = t.companyId if (companyId) { const company = await ctx.db.get(companyId) if (company) { patch.companySnapshot = { name: company.name, slug: company.slug, isAvulso: company.isAvulso ?? undefined, } } } } if (Object.keys(patch).length > 0) { await ctx.db.patch(t._id, patch) } processed += 1 } return { processed } }, })