@adatechnology/meta-whatsapp-module 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.2_@swc+helpers@0.5.23__jiti@2.7.0_postcss@8.5.28_tsx@4.23.13_typescript@5.9.3_yaml@2.9.1/node_modules/tsup/assets/esm_shims.js","../src/createMetaWhatsAppModule.ts","../src/repositories/SessionRepository.ts","../src/schema/schema.ts","../src/repositories/MessageRepository.ts","../src/repositories/FlowGraphRepository.ts","../src/repositories/FlowGraphCache.ts","../src/repositories/SettingsRepository.ts","../src/use-cases/LogMessage.use-case.ts","../src/use-cases/SendMessage.use-case.ts","../src/use-cases/TakeoverConversation.use-case.ts","../src/use-cases/ReleaseConversation.use-case.ts","../src/use-cases/ListConversations.use-case.ts","../src/use-cases/ListMessages.use-case.ts","../src/use-cases/ListConversationDocuments.use-case.ts","../src/use-cases/ListCompanyDocuments.use-case.ts","../src/use-cases/DeleteConversation.use-case.ts","../src/use-cases/PurgeExpiredDocuments.use-case.ts","../src/repositories/DocumentRepository.ts","../src/use-cases/ExportConversation.use-case.ts","../src/use-cases/FlowGraph.use-cases.ts","../src/flows/FlowInterpreter.ts","../src/flows/createSendMediaAction.ts","../src/flows/createSendProductListAction.ts","../src/repositories/FlowMediaRepository.ts","../src/repositories/FlowMediaIdRepository.ts","../src/channel/WhatsAppChannelAdapter.ts","../src/channel/previewMedia.ts","../src/channel/ReceiveWebhook.use-case.ts","../src/channel/webhookSecurity.ts","../src/channel/IngestInboundMedia.use-case.ts","../src/use-cases/TranscribeAudio.use-case.ts","../src/transcription.types.ts","../src/channel/inboundDispatch.ts","../src/use-cases/resolveTranscriptionPolicy.ts","../src/use-cases/StorePreviewMedia.use-case.ts","../src/runMigrations.ts","../src/realtime/SseHub.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider'\nimport type { MetaWhatsAppDatabase } from './database.types'\nimport { FLOW_ACTION_KIND } from '@adatechnology/meta-whatsapp-contracts'\nimport type {\n CatalogPort,\n ChannelAdapterInterface,\n FlowActionHandler,\n FlowActionKind,\n MetaWhatsAppHooks,\n ObjectStorageInterface,\n CacheInterface,\n RealtimeNotifierInterface,\n SessionState,\n SubjectResolverInterface,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { SessionRepository } from './repositories/SessionRepository'\nimport { MessageRepository } from './repositories/MessageRepository'\nimport { FlowGraphRepository } from './repositories/FlowGraphRepository'\nimport { FlowGraphCache, DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS } from './repositories/FlowGraphCache'\nimport { SettingsRepository } from './repositories/SettingsRepository'\nimport { LogMessageUseCase, type MessageModerator } from './use-cases/LogMessage.use-case'\nimport { SendMessageUseCase } from './use-cases/SendMessage.use-case'\nimport { TakeoverConversationUseCase } from './use-cases/TakeoverConversation.use-case'\nimport { ReleaseConversationUseCase } from './use-cases/ReleaseConversation.use-case'\nimport { ListConversationsUseCase } from './use-cases/ListConversations.use-case'\nimport { ListMessagesUseCase } from './use-cases/ListMessages.use-case'\nimport { ListConversationDocumentsUseCase } from './use-cases/ListConversationDocuments.use-case'\nimport { ListCompanyDocumentsUseCase } from './use-cases/ListCompanyDocuments.use-case'\nimport { DeleteConversationUseCase } from './use-cases/DeleteConversation.use-case'\nimport { PurgeExpiredDocumentsUseCase } from './use-cases/PurgeExpiredDocuments.use-case'\nimport { DocumentRepository } from './repositories/DocumentRepository'\nimport { ExportConversationUseCase } from './use-cases/ExportConversation.use-case'\nimport {\n GetFlowGraphUseCase,\n ListFlowGraphsUseCase,\n CreateFlowGraphUseCase,\n SaveFlowGraphUseCase,\n DeleteFlowGraphUseCase,\n GetLiveFlowPositionsUseCase,\n} from './use-cases/FlowGraph.use-cases'\nimport { FlowInterpreter } from './flows/FlowInterpreter'\nimport { createSendMediaAction } from './flows/createSendMediaAction'\nimport { createSendProductListAction } from './flows/createSendProductListAction'\nimport { FlowMediaRepository } from './repositories/FlowMediaRepository'\nimport { FlowMediaIdRepository } from './repositories/FlowMediaIdRepository'\nimport { WhatsAppChannelAdapter, type PreviewMediaSupport } from './channel/WhatsAppChannelAdapter'\nimport { ReceiveWebhookUseCase } from './channel/ReceiveWebhook.use-case'\nimport { IngestInboundMediaUseCase } from './channel/IngestInboundMedia.use-case'\nimport { verifyWebhookChallenge, type NonceStoreInterface } from './channel/webhookSecurity'\nimport { TranscribeAudioUseCase, type TranscribeAudioDependencies } from './use-cases/TranscribeAudio.use-case'\nimport { createTranscriptionPolicyResolver } from './use-cases/resolveTranscriptionPolicy'\nimport { StorePreviewMediaUseCase } from './use-cases/StorePreviewMedia.use-case'\nimport { TRANSCRIPTION_MODE, type AudioTranscriber, type TranscriptionMode } from './transcription.types'\n\nexport interface MetaWhatsAppModuleConfig {\n phoneNumberId: string\n accessToken: string\n webhookVerifyToken: string\n appSecret: string\n wabaId?: string\n apiVersion?: string\n // Aponta para um mock local em dev/teste; undefined usa a Graph API real.\n baseUrl?: string\n /**\n * Catálogo do Meta Commerce que a vitrine no chat oferece. Sem ele — ou sem `providers.catalog`\n * — a action `send_product_list` não é registrada: nó que o editor oferece e que em silêncio não\n * faz nada é pior do que nó que não existe.\n */\n catalogId?: string\n}\n\nexport interface MetaWhatsAppModuleFeatures {\n // T4.4 — desliga o motor de grafo. Um produto cuja conversa é dirigida por código próprio\n // (o QuickCart tem o seu ConversationEngine) não deve nem carregar o interpretador, e o\n // módulo não pode presumir que todo consumidor quer fluxo visual.\n flowEngine?: boolean\n\n // Cache de leitura dos grafos. Desligado por omissão: cachear é decisão do host, que é quem\n // sabe se o Redis dele é compartilhado entre instâncias — cache por processo serviria versões\n // diferentes do mesmo fluxo depois de uma publicação. Ligar exige `providers.cache`; sem ele o\n // flag é ignorado, porque o módulo não abre conexão própria.\n flowGraphCache?: boolean | { ttlSeconds?: number }\n\n /**\n * Aceita mídia do simulador de conversa — o que faz o microfone aparecer no preview do cliente.\n *\n * **Desligado por omissão, e a decisão é consciente.** Ligado, o canal passa a aceitar id que não\n * veio da Meta: `preview-upload:<chave>` faz o servidor ler aquele objeto do storage. Em ambiente\n * de simulação isso é o recurso; em produção é leitura arbitrária do bucket por webhook forjado.\n *\n * Exige `providers.objectStorage` com `getObject` — sem os bytes não há o que devolver, e o flag é\n * ignorado em vez de produzir um canal que falha na primeira nota de voz.\n */\n previewMedia?: boolean\n}\n\n/**\n * Transcrição de áudio. Ausente = desligada, e as colunas ficam nulas (\"não avaliado\").\n *\n * `mode` é a decisão de produto que este objeto existe para carregar: `auto` transcreve toda nota de\n * voz recebida, na ingestão, onde o buffer já está em memória; `onDemand` só transcreve quando o\n * atendente pede, gastando cota apenas com áudio que alguém vai ler de fato.\n */\nexport interface MetaWhatsAppTranscriptionConfig {\n transcriber: AudioTranscriber\n /**\n * PADRÃO, não decisão final: vale para as empresas que não mexeram no interruptor do painel. As\n * configurações por empresa (`settings.transcriptionMode`) têm precedência.\n *\n * Padrão `onDemand` — o modo que não gasta cota sem alguém pedir.\n */\n mode?: TranscriptionMode\n /**\n * PADRÃO de ligado/desligado para empresas sem escolha registrada. `true` — injetar o transcritor\n * já é a declaração de que o host quer o recurso; quem controla por empresa usa o painel.\n */\n isEnabledByDefault?: boolean\n /** ISO 639-1 do produto (ex.: `'pt'`). Corta a detecção de idioma do engine. */\n languageHint?: string\n}\n\nexport interface MetaWhatsAppModuleProviders {\n objectStorage?: ObjectStorageInterface\n // Cache do host, compartilhado entre instâncias (ver CacheInterface). Hoje serve os grafos de\n // fluxo, que são lidos a cada mensagem recebida.\n cache?: CacheInterface\n realtime?: RealtimeNotifierInterface\n subjectResolver?: SubjectResolverInterface\n // Opcional por desenho — sem catálogo injetado o módulo sobe e funciona, só os recursos de\n // produto ficam desligados (ver .specs/features/meta-catalog-trio/spec.md §4).\n catalog?: CatalogPort\n // Marca mensagem ofensiva do cliente no transcript. Injetado, e não construído aqui, pelo mesmo\n // motivo dos outros providers: a lista de termos e o liga/desliga são de cada produto, e o módulo\n // não lê process.env. Ausente, as colunas ficam nulas — \"não avaliado\".\n moderator?: MessageModerator\n /**\n * Converte nota de voz em texto. Exige `objectStorage` com `getObject` para o modo sob demanda —\n * transcrever um áudio já salvo significa ler os bytes de volta.\n */\n transcription?: MetaWhatsAppTranscriptionConfig\n}\n\nexport interface CreateMetaWhatsAppModuleParams {\n db: MetaWhatsAppDatabase\n config: MetaWhatsAppModuleConfig\n // Cache compartilhado entre instâncias para o anti-replay do webhook.\n nonceStore: NonceStoreInterface\n // Estado inicial de uma sessão nova — o módulo não conhece a máquina de estados do produto.\n startState?: SessionState\n features?: MetaWhatsAppModuleFeatures\n providers?: MetaWhatsAppModuleProviders\n hooks?: MetaWhatsAppHooks\n}\n\n// T5.6 — costura conversa × canal. Tudo que é ambiente (db, credenciais, storage, cache,\n// realtime) entra por parâmetro: o módulo não lê process.env nem abre conexão própria, que é o\n// que permite ao host controlar pool, ciclo de vida e configuração por empresa.\nexport function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams) {\n const { db, config, nonceStore, providers = {}, hooks } = params\n const startState = params.startState ?? 'start'\n const flowEngineEnabled = params.features?.flowEngine ?? true\n\n const messageProvider = new WhatsAppMessageProvider({\n accessToken: config.accessToken,\n phoneNumberId: config.phoneNumberId,\n wabaId: config.wabaId,\n apiVersion: config.apiVersion,\n baseUrl: config.baseUrl,\n })\n /**\n * Só monta o suporte quando as DUAS condições existem: o host pediu e o storage sabe ler de volta.\n * Ligar o flag sem storage legível daria um canal que aceita o id do simulador e falha ao buscar.\n */\n const previewMediaSupport =\n params.features?.previewMedia && providers.objectStorage?.getObject\n ? {\n isEnabled: true,\n objectStorage: providers.objectStorage as PreviewMediaSupport['objectStorage'],\n }\n : undefined\n\n const channel: ChannelAdapterInterface = new WhatsAppChannelAdapter(messageProvider, previewMediaSupport)\n\n const sessionRepository = new SessionRepository(db)\n const messageRepository = new MessageRepository(db)\n const settingsRepository = new SettingsRepository(db)\n const flowGraphCacheFeature = params.features?.flowGraphCache ?? false\n const flowGraphCache =\n flowGraphCacheFeature && providers.cache\n ? new FlowGraphCache(\n providers.cache,\n typeof flowGraphCacheFeature === 'object'\n ? (flowGraphCacheFeature.ttlSeconds ?? DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS)\n : DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,\n )\n : undefined\n\n const flowGraphRepository = new FlowGraphRepository(db, flowGraphCache)\n const documentRepository = new DocumentRepository(db)\n const flowMediaRepository = new FlowMediaRepository(db)\n\n const logMessage = new LogMessageUseCase(\n sessionRepository,\n messageRepository,\n providers.realtime,\n providers.moderator,\n )\n const sendMessage = new SendMessageUseCase(\n channel,\n sessionRepository,\n logMessage,\n providers.objectStorage,\n documentRepository,\n )\n\n const receiveWebhook = new ReceiveWebhookUseCase({\n appSecret: config.appSecret,\n phoneNumberId: config.phoneNumberId,\n nonceStore,\n sessionRepository,\n messageRepository,\n logMessage,\n startState,\n hooks,\n realtime: providers.realtime,\n })\n\n // Só existe se a flag estiver ligada — não adianta o host \"não usar\" um interpretador que\n // ainda assim ficou instanciado e exposto na API pública do módulo.\n const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : undefined\n\n // `send_media` é a única action que o pacote registra sozinho — as demais dependem de regra do\n // produto. Condicionada a `getObject` porque sem os bytes o handler não teria o que enviar:\n // registrá-lo assim mesmo deixaria um nó que o editor oferece e que silenciosamente não faz nada.\n if (flowInterpreter && providers.objectStorage?.getObject) {\n flowInterpreter.registerFlowAction(\n FLOW_ACTION_KIND.SEND_MEDIA,\n createSendMediaAction({\n flowMediaRepository,\n objectStorage: providers.objectStorage as Parameters<typeof createSendMediaAction>[0]['objectStorage'],\n logMessage,\n startState,\n // Cache ligado por padrão, com o store do próprio módulo: a tabela é daqui e a coluna é\n // daqui, então exigir que cada host escrevesse o repositório faria todos reescreverem o\n // mesmo código — e, até escreverem, o mesmo binário continuaria subindo por cliente. Quem\n // quiser outro lugar (Redis) troca o `store`; quem não fizer nada já sai sem ressubir.\n mediaIdCache: { store: new FlowMediaIdRepository(db), senderKey: config.phoneNumberId },\n onError: hooks?.onFlowMediaError,\n }),\n )\n }\n\n // Vitrine no chat: precisa do catalogo injetado E do id do catalogo na Meta. Um sem o outro nao\n // envia nada — o `retailerId` so significa alguma coisa dentro de um catalogo.\n if (flowInterpreter && providers.catalog && config.catalogId) {\n flowInterpreter.registerFlowAction(\n FLOW_ACTION_KIND.SEND_PRODUCT_LIST,\n createSendProductListAction({\n catalog: providers.catalog,\n catalogId: config.catalogId,\n logMessage,\n startState,\n onError: hooks?.onFlowProductListError,\n }),\n )\n }\n\n const transcriptionMode = providers.transcription?.mode ?? TRANSCRIPTION_MODE.ON_DEMAND\n\n /**\n * Ambiente decide se é POSSÍVEL (o transcritor acima), settings decide se é para FAZER. Este\n * resolvedor junta os dois, a cada áudio, para o interruptor do painel valer sem deploy.\n */\n const resolveTranscriptionPolicy = providers.transcription\n ? createTranscriptionPolicyResolver({\n settingsRepository,\n defaults: {\n isEnabled: providers.transcription.isEnabledByDefault ?? true,\n mode: transcriptionMode,\n },\n })\n : undefined\n\n // Só existe com storage injetado — sem ele não há para onde copiar o binário, e devolver um\n // use-case que sempre falha seria pior do que a ausência ser visível no tipo.\n const ingestInboundMedia = providers.objectStorage\n ? new IngestInboundMediaUseCase(\n db,\n channel,\n providers.objectStorage,\n documentRepository,\n providers.transcription && resolveTranscriptionPolicy\n ? {\n transcriber: providers.transcription.transcriber,\n resolvePolicy: resolveTranscriptionPolicy,\n messageRepository,\n ...(providers.transcription.languageHint ? { languageHint: providers.transcription.languageHint } : {}),\n ...(hooks ? { hooks } : {}),\n }\n : undefined,\n )\n : undefined\n\n /**\n * Transcrição sob demanda e retomada de pendente. Exige `getObject`: transcrever um áudio já\n * salvo é ler os bytes de volta do storage, e sem esse método o use-case só saberia falhar —\n * a ausência no tipo é mais honesta que um botão que estoura no clique.\n *\n * O modo `auto` NÃO depende disto: ele transcreve dentro da ingestão, com o buffer que acabou de\n * baixar. Um host em `auto` sem `getObject` transcreve normalmente, só não oferece \"transcrever\n * de novo\".\n */\n const transcribeAudio =\n providers.transcription && providers.objectStorage?.getObject\n ? new TranscribeAudioUseCase({\n messageRepository,\n objectStorage: providers.objectStorage as TranscribeAudioDependencies['objectStorage'],\n transcriber: providers.transcription.transcriber,\n ...(resolveTranscriptionPolicy ? { resolvePolicy: resolveTranscriptionPolicy } : {}),\n ...(providers.transcription.languageHint ? { languageHint: providers.transcription.languageHint } : {}),\n ...(hooks ? { hooks } : {}),\n })\n : undefined\n\n /**\n * A leitura da biblioteca existe mesmo sem storage injetado: listar é consultar a tabela, e\n * devolver `undefined` aqui obrigaria o host a ramificar numa rota que funciona de qualquer jeito.\n * Quem precisa de storage é o download (URL assinada) — esse fica com o host, que já tem o\n * provider em mãos.\n */\n const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository)\n\n return {\n channel,\n // undefined quando providers.objectStorage não foi injetado.\n ingestInboundMedia,\n conversations: {\n log: logMessage,\n send: sendMessage,\n takeover: new TakeoverConversationUseCase(sessionRepository, providers.realtime),\n release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),\n list: new ListConversationsUseCase(sessionRepository),\n listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),\n listDocuments,\n // Biblioteca da empresa inteira, para uma tela de Documentos fora da conversa.\n listCompanyDocuments: new ListCompanyDocumentsUseCase(documentRepository),\n // Apaga a mídia no storage antes das linhas — a cascata da FK sozinha deixaria os binários\n // órfãos, já que a lista de uploadId vive justamente nas linhas que ela derruba.\n delete: new DeleteConversationUseCase(sessionRepository, documentRepository, providers.objectStorage),\n purgeExpiredDocuments: new PurgeExpiredDocumentsUseCase(documentRepository, providers.objectStorage),\n export: new ExportConversationUseCase(sessionRepository),\n // undefined quando transcrição não foi injetada, ou quando o storage não sabe ler de volta.\n // O painel consulta a ausência para decidir se desenha o botão \"transcrever\".\n transcribeAudio,\n repository: sessionRepository,\n messageRepository,\n documentRepository,\n },\n /**\n * `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a\n * capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —\n * é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.\n */\n transcription:\n providers.transcription && resolveTranscriptionPolicy\n ? { defaultMode: transcriptionMode, resolvePolicy: resolveTranscriptionPolicy }\n : undefined,\n settings: settingsRepository,\n /**\n * `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a\n * ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.\n */\n previewMedia: previewMediaSupport\n ? new StorePreviewMediaUseCase(\n providers.objectStorage!,\n () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n )\n : undefined,\n webhook: {\n receive: receiveWebhook,\n // GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.\n verifyChallenge: (query: { mode: string | null; token: string | null; challenge: string | null }) =>\n verifyWebhookChallenge({ ...query, expectedToken: config.webhookVerifyToken }),\n },\n // undefined quando features.flowEngine === false.\n flows: flowInterpreter\n ? {\n interpreter: flowInterpreter,\n registerFlowAction: (kind: FlowActionKind, handler: FlowActionHandler) =>\n flowInterpreter.registerFlowAction(kind, handler),\n get: new GetFlowGraphUseCase(flowGraphRepository),\n list: new ListFlowGraphsUseCase(flowGraphRepository),\n create: new CreateFlowGraphUseCase(flowGraphRepository),\n save: new SaveFlowGraphUseCase(flowGraphRepository),\n delete: new DeleteFlowGraphUseCase(flowGraphRepository),\n livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),\n repository: flowGraphRepository,\n // Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor\n // (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é\n // consultar a tabela, e só o ENVIO precisa dos bytes.\n mediaRepository: flowMediaRepository,\n }\n : undefined,\n catalog: providers.catalog,\n }\n}\n\nexport type MetaWhatsAppModule = ReturnType<typeof createMetaWhatsAppModule>\n","import { and, desc, eq, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type {\n ConversationSummary,\n MessageDirection,\n SessionMode,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { messages, sessions, type SessionRow } from '../schema/schema'\n\nexport interface ListConversationsFilters {\n page?: number\n limit?: number\n waitingHuman?: boolean\n search?: string\n}\n\nconst DEFAULT_LIMIT = 20\n\n// Expressões correlacionadas da lista de conversas, extraídas para poderem ser renderizadas e\n// verificadas sem banco (ver SessionRepository.test.ts).\n//\n// A correlação usa `${sessions}.company_id` e NÃO `${sessions.companyId}`: dentro da lista de\n// seleção o drizzle renderiza a referência de coluna sem qualificar a tabela, e um\n// `\"company_id\"` solto dentro do subselect resolve para a coluna de `m`. O resultado seria\n// `m.company_id = m.company_id` (sempre verdadeiro) e `m.session_id = m.id` (nunca) — zero\n// linhas, NULL, e nenhum erro.\nexport const conversationSummaryProjection = {\n lastContent: sql<string | null>`(\n select m.content from ${messages} m\n where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id\n order by m.created_at desc limit 1\n )`,\n lastDirection: sql<string | null>`(\n select m.direction from ${messages} m\n where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id\n order by m.created_at desc limit 1\n )`,\n // Entradas do cliente depois da última leitura do atendente. Sessão nunca lida conta\n // tudo — é o comportamento esperado de uma conversa que ninguém abriu ainda.\n unread: sql<number>`(\n select count(*)::int from ${messages} m\n where m.company_id = ${sessions}.company_id\n and m.session_id = ${sessions}.id\n and m.direction = 'inbound'\n and (${sessions}.last_agent_read_at is null or m.created_at > ${sessions}.last_agent_read_at)\n )`,\n} as const\n\n// Mescla do context feita pelo Postgres. Extraída para ser verificável sem banco (ver\n// SessionRepository.test.ts): o `::jsonb` é obrigatório — sem ele o parâmetro chega como text e\n// o `||` concatena strings em vez de mesclar objetos, corrompendo o context em silêncio.\nexport function sessionContextPatch(patch: Record<string, unknown>) {\n return sql`${sessions.context} || ${JSON.stringify(patch)}::jsonb`\n}\n\n// T3.2 — todo método recebe/filtra por companyId explicitamente (nunca lê de um campo do\n// payload do cliente); ver database.md \"Consistência e multiempresa\".\nexport class SessionRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n async getContext(companyId: string, whatsappNumber: string): Promise<SessionRow | undefined> {\n const [row] = await this.db\n .select()\n .from(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n .limit(1)\n return row\n }\n\n async getOrCreate(companyId: string, whatsappNumber: string, startState: SessionState): Promise<SessionRow> {\n const existing = await this.getContext(companyId, whatsappNumber)\n if (existing) return existing\n\n const [created] = await this.db\n .insert(sessions)\n .values({ companyId, whatsappNumber, currentState: startState })\n .onConflictDoUpdate({\n target: [sessions.companyId, sessions.whatsappNumber],\n set: { lastActivity: sql`now()` },\n })\n .returning()\n return created!\n }\n\n // O `context` jsonb é o ponto de extensão oficial para estado de sessão por produto: o módulo\n // não conhece a forma, o consumidor a declara em TSessionContext. Este setter SUBSTITUI o\n // objeto inteiro — para acumular respostas ao longo da conversa use patchContext.\n async setState<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n state: SessionState,\n context?: TSessionContext,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n currentState: state,\n ...(context !== undefined ? { context } : {}),\n lastActivity: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Mescla parcial do context, feita no banco (`||`) e não por read-modify-write no host: duas\n // mensagens do mesmo cliente processadas em paralelo sobrescreveriam uma à outra, e o campo\n // acumula justamente as respostas coletadas ao longo da conversa. Chave presente no patch\n // vence a existente; as demais permanecem.\n async patchContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n patch: Partial<TSessionContext>,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n context: sessionContextPatch(patch),\n lastActivity: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Leitura tipada do estado de sessão do produto. Devolve undefined quando a sessão não existe —\n // distinto de existir com context vazio, que devolve o objeto vazio.\n async readContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n ): Promise<TSessionContext | undefined> {\n const [row] = await this.db\n .select({ context: sessions.context })\n .from(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n .limit(1)\n return row?.context as TSessionContext | undefined\n }\n\n // Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.\n // Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).\n async setFlowPosition(\n companyId: string,\n whatsappNumber: string,\n flowKey: string | null,\n currentNodeId: string | null,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({ flowKey, currentNodeId, lastActivity: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Marca a chegada de uma mensagem DO CLIENTE. É este carimbo — e não lastActivity, que também\n // se move em ações do atendente/bot — que define a janela de 24h do WhatsApp: fora dela, só\n // template aprovado reabre a conversa (ver WindowExpiredError nos contracts).\n async touchInbound(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ lastInboundAt: sql`now()`, lastActivity: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Horas desde a última mensagem do cliente — undefined se ele nunca escreveu. O canal (Fase 5)\n // usa isto para decidir entre mensagem livre e template antes de chamar a Graph API.\n async hoursSinceLastInbound(companyId: string, whatsappNumber: string): Promise<number | undefined> {\n const session = await this.getContext(companyId, whatsappNumber)\n if (!session?.lastInboundAt) return undefined\n return (Date.now() - session.lastInboundAt.getTime()) / (1000 * 60 * 60)\n }\n\n // `clearHumanRequest` é opt-in: quem chama `setMode` direto para outros fins (ex.: trocar de\n // atendente sem mexer na fila) continua sem afetar `humanRequestedAt`. Só `takeover`/`release`\n // pedem a limpeza, porque só eles representam \"o pedido de atendimento foi resolvido\".\n async setMode(\n companyId: string,\n whatsappNumber: string,\n mode: SessionMode,\n assignedUserId?: string | null,\n clearHumanRequest = false,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n mode,\n assignedUserId: assignedUserId ?? null,\n ...(clearHumanRequest ? { humanRequestedAt: null } : {}),\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // T3.3 — takeover: atendente assume a conversa, tirando-a do modo bot.\n //\n // Limpa `humanRequestedAt`: sem isso a conversa nunca sai da fila `waitingHuman` da inbox,\n // mesmo depois de atendida — o filtro é só `humanRequestedAt is not null` (ver\n // `listByContextFilters`), então o campo tem que voltar a null quando o pedido é resolvido.\n async takeover(companyId: string, whatsappNumber: string, agentUserId: string): Promise<void> {\n await this.setMode(companyId, whatsappNumber, 'human', agentUserId, true)\n }\n\n // T3.3 — release: devolve a conversa ao bot.\n //\n // Também limpa `humanRequestedAt` pelo mesmo motivo do takeover: se o cliente pedir atendimento\n // de novo depois, `requestHuman` grava um novo timestamp normalmente.\n async release(companyId: string, whatsappNumber: string): Promise<void> {\n await this.setMode(companyId, whatsappNumber, 'bot', null, true)\n }\n\n /**\n * Apaga a sessão; a cascata das FKs leva mensagens e documentos.\n *\n * Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é\n * chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os\n * objetos antes deixa mídia órfã sendo cobrada para sempre.\n */\n async deleteByNumber(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .delete(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async requestHuman(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ humanRequestedAt: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async markRead(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ lastAgentReadAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async markAllRead(companyId: string, userId: string): Promise<number> {\n const result = await this.db\n .update(sessions)\n .set({ lastAgentReadAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.assignedUserId, userId)))\n return Number((result as unknown as { rowCount?: number }).rowCount ?? 0)\n }\n\n async listByContextFilters(companyId: string, filters: ListConversationsFilters): Promise<ConversationSummary[]> {\n const limit = filters.limit ?? DEFAULT_LIMIT\n const offset = ((filters.page ?? 1) - 1) * limit\n\n const conditions = [eq(sessions.companyId, companyId)]\n if (filters.waitingHuman) conditions.push(sql`${sessions.humanRequestedAt} is not null`)\n if (filters.search) conditions.push(sql`${sessions.whatsappNumber} ilike ${'%' + filters.search + '%'}`)\n\n const rows = await this.db\n .select({\n id: sessions.id,\n whatsappNumber: sessions.whatsappNumber,\n mode: sessions.mode,\n assignedUserId: sessions.assignedUserId,\n currentState: sessions.currentState,\n lastActivity: sessions.lastActivity,\n lastInboundAt: sessions.lastInboundAt,\n humanRequestedAt: sessions.humanRequestedAt,\n // Prévia e contagem saem de subquery correlacionada em vez de N+1 na volta: uma inbox\n // lista dezenas de conversas por página, e uma query por linha é o gargalo clássico\n // dessa tela. Ambos os campos são dados do próprio módulo — deixá-los para o host\n // obrigaria todo consumidor a reescrever o mesmo join contra tabelas que não são dele.\n ...conversationSummaryProjection,\n })\n .from(sessions)\n .where(and(...conditions))\n .orderBy(desc(sessions.lastActivity))\n .limit(limit)\n .offset(offset)\n\n return rows.map((row) => ({\n id: row.id,\n whatsappNumber: row.whatsappNumber,\n ...(row.lastContent !== null ? { lastContent: row.lastContent } : {}),\n ...(row.lastDirection !== null ? { lastDirection: row.lastDirection as MessageDirection } : {}),\n lastAt: row.lastActivity.toISOString(),\n lastInboundAt: row.lastInboundAt?.toISOString() ?? null,\n mode: row.mode as SessionMode,\n assignedUserId: row.assignedUserId,\n waitingHuman: row.humanRequestedAt !== null,\n unread: Number(row.unread),\n currentState: row.currentState,\n }))\n }\n\n // T3.3 — export: transcript completo de uma conversa, mensagens em ordem cronológica.\n async exportConversation(companyId: string, whatsappNumber: string) {\n const session = await this.getContext(companyId, whatsappNumber)\n if (!session) return null\n\n const rows = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, companyId), eq(messages.sessionId, session.id)))\n .orderBy(messages.createdAt)\n\n return { session, messages: rows }\n }\n}\n","import { sql } from 'drizzle-orm'\nimport {\n pgSchema,\n uuid,\n varchar,\n jsonb,\n timestamp,\n text,\n integer,\n boolean,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core'\nimport type { TranscriptionMode, TranscriptionStatus } from '../transcription.types'\n\n// Schema Postgres dedicado (T3.1/T3.5) — o módulo nunca escreve no schema `public` do host,\n// só ocupa este namespace próprio. O banco continua sendo um por produto; isto é apenas um\n// namespace dentro dele (ver rules/packages/pluggable-module.md §3).\nexport const metaWhatsAppSchema = pgSchema('meta_whatsapp')\n\nexport const sessions = metaWhatsAppSchema.table(\n 'sessions',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n // Multiempresa: toda entidade carrega companyId, vindo do contexto autenticado do host —\n // nunca de um campo livre do cliente (ver database.md \"Consistência e multiempresa\").\n companyId: uuid('company_id').notNull(),\n whatsappNumber: varchar('whatsapp_number', { length: 20 }).notNull(),\n // varchar (não enum): a máquina de estados de cada produto evolui de forma independente do módulo.\n currentState: varchar('current_state', { length: 64 }).notNull().default('start'),\n // Posição no grafo, quando o motor de fluxo está em uso — colunas separadas em vez de\n // convencionar \"flowKey:nodeId\" dentro de currentState: além de ser um contrato implícito que\n // nada garantia, a concatenação estourava o varchar(64) (flow key já é varchar(64) sozinha).\n flowKey: varchar('flow_key', { length: 64 }),\n currentNodeId: varchar('current_node_id', { length: 64 }),\n context: jsonb('context').$type<Record<string, unknown>>().notNull().default({}),\n mode: varchar('mode', { length: 12 }).notNull().default('bot'), // bot | human\n assignedUserId: uuid('assigned_user_id'),\n humanRequestedAt: timestamp('human_requested_at', { withTimezone: true }),\n lastInboundAt: timestamp('last_inbound_at', { withTimezone: true }),\n lastAgentReadAt: timestamp('last_agent_read_at', { withTimezone: true }),\n lastActivity: timestamp('last_activity', { withTimezone: true }).notNull().defaultNow(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n // Único por empresa (não globalmente) — dois hosts diferentes podem atender o mesmo número.\n uniqueIndex('idx_sessions_company_number').on(table.companyId, table.whatsappNumber),\n index('idx_sessions_company_mode').on(table.companyId, table.mode),\n // Alimenta getLiveFlowPositions (agregação por nó do grafo).\n index('idx_sessions_company_flow_node').on(table.companyId, table.flowKey, table.currentNodeId),\n ],\n)\n\nexport const messages = metaWhatsAppSchema.table(\n 'messages',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n sessionId: uuid('session_id')\n .notNull()\n .references(() => sessions.id, { onDelete: 'cascade' }),\n whatsappNumber: varchar('whatsapp_number', { length: 20 }).notNull(),\n direction: varchar('direction', { length: 12 }).notNull(), // inbound | outbound\n sender: varchar('sender', { length: 12 }).notNull(), // customer | bot | agent\n agentUserId: uuid('agent_user_id'),\n // 32, não 16: os tipos da própria Meta são curtos ('text', 'interactive'), mas o host rotula\n type:\n // a saída com o subtipo que enviou ('interactive_buttons' já tem 19). Apertar aqui só\n // transfere para o consumidor a escolha entre truncar o rótulo e estourar o insert.\n varchar('type', { length: 32 }).notNull().default('text'),\n content: text('content'),\n // T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por\n // uploadId dentro deste jsonb.\n payload: jsonb('payload').$type<Record<string, unknown>>(),\n waMessageId: varchar('wa_message_id', { length: 128 }),\n status: varchar('status', { length: 16 }),\n readAt: timestamp('read_at', { withTimezone: true }),\n // Moderação de conteúdo. `null` significa NÃO AVALIADO (moderação desligada, ou mensagem\n // anterior ao recurso) — diferente de `false`, que é avaliado e limpo. Colunas em vez de chave\n // dentro de `payload` porque \"listar o que foi sinalizado\" é consulta de operação, e índice\n // parcial sobre boolean resolve isso sem cavar jsonb.\n moderationFlagged: boolean('moderation_flagged'),\n moderationTerms: jsonb('moderation_terms').$type<string[]>(),\n /**\n * Transcrição de áudio. `null` em `transcription_status` significa NÃO AVALIADO — áudio nunca\n * pedido (modo sob demanda), transcrição desligada, mensagem anterior ao recurso, ou mensagem\n * que não é áudio. Diferente de `'done'` com texto vazio, que é áudio em silêncio já processado\n * e que NÃO deve ser reprocessado.\n *\n * Colunas em vez de chave em `payload` pelo mesmo motivo da moderação: \"quais áudios ficaram\n * pendentes\" e \"quais falharam\" são consultas de operação, e índice parcial resolve sem cavar\n * jsonb. Buscar texto de áudio também deixa de exigir varredura de payload.\n */\n transcriptionStatus: varchar('transcription_status', { length: 16 }).$type<TranscriptionStatus>(),\n transcriptionText: text('transcription_text'),\n transcriptionLanguage: varchar('transcription_language', { length: 32 }),\n /** Qual engine produziu. Com uma cadeia de engines, é o que responde \"por que esta saiu ruim\". */\n transcriptionEngine: varchar('transcription_engine', { length: 32 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_messages_session_created').on(table.sessionId, table.createdAt),\n index('idx_messages_company_number_created').on(table.companyId, table.whatsappNumber, table.createdAt),\n // Parcial e ÚNICO: é o que de fato garante idempotência de entrega. Um SELECT-antes-de-INSERT\n // não basta — a Meta reenvia a mesma entrega e várias instâncias do host processam em\n // paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque\n // mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs\n // não podem competir entre si por unicidade.\n uniqueIndex('idx_messages_company_wa_message_id')\n .on(table.companyId, table.waMessageId)\n .where(sql`${table.waMessageId} is not null`),\n // Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do\n // tamanho do transcript.\n index('idx_messages_moderation_flagged')\n .on(table.companyId, table.createdAt)\n .where(sql`${table.moderationFlagged}`),\n // Alimenta a varredura de retomada: \"quais áudios ficaram pendentes de transcrição\". Parcial\n // porque pendente é estado transitório e raro — o índice fica do tamanho da fila atrasada, não\n // do transcript inteiro.\n index('idx_messages_transcription_pending')\n .on(table.companyId, table.createdAt)\n .where(sql`${table.transcriptionStatus} = 'pending'`),\n ],\n)\n\n/**\n * Biblioteca de arquivos da conversa.\n *\n * Tabela própria, e não derivada de `messages.payload`: o painel precisa de `source` e `linkedAt`,\n * que não existem no payload; busca por nome de arquivo quer índice, não varredura de jsonb; e\n * `messageId` anulável é o que permite o atendente anexar documento à conversa sem que exista uma\n * mensagem correspondente.\n *\n * `sessionId` em cascata apaga a LINHA junto com a conversa, mas não o binário no storage — quem\n * apaga objeto é passo de aplicação (listar `uploadId` → apagar no storage → apagar a sessão).\n * Confiar só na FK deixaria objeto órfão sendo cobrado para sempre.\n */\nexport const documents = metaWhatsAppSchema.table(\n 'documents',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n sessionId: uuid('session_id')\n .notNull()\n .references(() => sessions.id, { onDelete: 'cascade' }),\n // Anulável e `set null`: apagar a mensagem não deve apagar o arquivo da biblioteca, e arquivo\n // do atendente nasce sem mensagem.\n messageId: uuid('message_id').references(() => messages.id, { onDelete: 'set null' }),\n uploadId: varchar('upload_id', { length: 256 }).notNull(),\n filename: varchar('filename', { length: 512 }).notNull(),\n mimeType: varchar('mime_type', { length: 128 }).notNull(),\n sizeBytes: integer('size_bytes').notNull(),\n // Do provider de storage, que é endereçado por conteúdo — serve para reconciliar storage\n // contra tabela e para detectar o mesmo binário chegando duas vezes.\n sha256: varchar('sha256', { length: 64 }),\n source: varchar('source', { length: 12 }).notNull(), // customer | agent | bot\n linkedAt: timestamp('linked_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_documents_session_linked').on(table.sessionId, table.linkedAt),\n // Para varredura de retenção por idade sem passar por sessão.\n index('idx_documents_company_linked').on(table.companyId, table.linkedAt),\n // O mesmo objeto não pode ser linkado duas vezes na mesma empresa: o job de ingestão é\n // reentregue por retry, e sem isto a reentrega criaria linha duplicada no painel.\n uniqueIndex('idx_documents_company_upload').on(table.companyId, table.uploadId),\n ],\n)\n\n// T4.1 — um grafo por fluxo de conversa. `nodes` guarda o Record<string, FlowNodeData> inteiro\n// como jsonb (mesmo shape do editor visual em conversations-ui/flows) — não normalizado em\n// linhas, porque o grafo é sempre lido/escrito como uma unidade pelo editor e pelo interpretador.\nexport const flowGraphs = metaWhatsAppSchema.table(\n 'flow_graphs',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n key: varchar('key', { length: 64 }).notNull(),\n label: varchar('label', { length: 120 }).notNull(),\n startNodeId: varchar('start_node_id', { length: 64 }).notNull(),\n nodes: jsonb('nodes').$type<Record<string, unknown>>().notNull().default({}),\n // Otimista: o editor manda a versão que carregou; salvar com versão desatualizada é\n // rejeitado pelo host (evita duas abas sobrescreverem uma a outra silenciosamente).\n version: integer('version').notNull().default(1),\n showInMenu: boolean('show_in_menu').notNull().default(false),\n menuOptionLabel: varchar('menu_option_label', { length: 64 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [uniqueIndex('idx_flow_graphs_company_key').on(table.companyId, table.key)],\n)\n\n/**\n * Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.\n *\n * Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA\n * CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o\n * mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é\n * enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.\n *\n * Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o\n * índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então\n * apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a\n * limpeza é passo de aplicação ao salvar o grafo.\n */\nexport const flowMedia = metaWhatsAppSchema.table(\n 'flow_media',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n flowKey: varchar('flow_key', { length: 64 }).notNull(),\n nodeId: varchar('node_id', { length: 64 }).notNull(),\n uploadId: varchar('upload_id', { length: 256 }).notNull(),\n filename: varchar('filename', { length: 512 }).notNull(),\n mimeType: varchar('mime_type', { length: 128 }).notNull(),\n sizeBytes: integer('size_bytes').notNull(),\n // Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e\n // um folder precisa de textos diferentes para cada um.\n caption: text('caption'),\n // Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e \"tabela antes\n // do folder\" é decisão de quem edita, não do banco.\n sortOrder: integer('sort_order').notNull().default(0),\n // Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha\n // perderia a ordem e a legenda já ajustadas.\n active: boolean('active').notNull().default(true),\n // Id do arquivo já subido para a Meta, por número remetente. Sem isto, o MESMO binário subia de\n // novo para cada cliente que passava pelo nó: a Meta aceita reusar o id por 30 dias.\n //\n // Mapa por `phone_number_id`, e não coluna única: o id é escopado ao número que envia, e uma\n // instalação com dois números mandaria o id de um pelo outro. A validade não é gravada de\n // propósito — confiar em \"30 dias\" calculados erra nos casos de borda, e quem decide é a\n // recusa da Meta, que devolve ao caminho de subir o binário.\n metaMediaIds: jsonb('meta_media_ids').$type<Record<string, string>>().notNull().default({}),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_flow_media_node').on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),\n // O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente\n // receberia o documento repetido.\n uniqueIndex('idx_flow_media_node_upload').on(table.companyId, table.flowKey, table.nodeId, table.uploadId),\n ],\n)\n\n// T5.5 — configuração de WhatsApp POR EMPRESA, em tabela do módulo. Deliberadamente separada do\n// app_config genérico do host: são chaves que só o módulo entende, e mantê-las aqui é o que\n// permite instalar/remover a capacidade sem migrar a tabela de configuração do produto.\nexport const settings = metaWhatsAppSchema.table('settings', {\n companyId: uuid('company_id').primaryKey(),\n templateName: varchar('template_name', { length: 128 }),\n templateLanguage: varchar('template_language', { length: 16 }).notNull().default('pt_BR'),\n // Mapa posicional {{1}}, {{2}}... → token resolvido no envio (ex.: '{clientName}').\n templateVariables: jsonb('template_variables').$type<string[]>().notNull().default([]),\n welcomeMessage: text('welcome_message'),\n farewellMessage: text('farewell_message'),\n /**\n * Política de transcrição desta empresa. Nulo é significativo: \"o painel não decidiu\", e aí vale\n * o padrão que o host injetou. Sem a distinção, atualizar o módulo desligaria a transcrição de\n * quem já a tinha ligada por ambiente.\n */\n transcriptionEnabled: boolean('transcription_enabled'),\n transcriptionMode: varchar('transcription_mode', { length: 16 }).$type<TranscriptionMode>(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport type SettingsRow = typeof settings.$inferSelect\nexport type NewSettingsRow = typeof settings.$inferInsert\n\nexport type SessionRow = typeof sessions.$inferSelect\nexport type NewSessionRow = typeof sessions.$inferInsert\nexport type MessageRow = typeof messages.$inferSelect\nexport type NewMessageRow = typeof messages.$inferInsert\nexport type FlowGraphRow = typeof flowGraphs.$inferSelect\nexport type NewFlowGraphRow = typeof flowGraphs.$inferInsert\nexport type DocumentRow = typeof documents.$inferSelect\nexport type NewDocumentRow = typeof documents.$inferInsert\nexport type FlowMediaRow = typeof flowMedia.$inferSelect\nexport type NewFlowMediaRow = typeof flowMedia.$inferInsert\n","import { and, eq, lt } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { MessageDirection, MessageSender, MessageStatus } from '@adatechnology/meta-whatsapp-contracts'\nimport { messages, type MessageRow, type NewMessageRow } from '../schema/schema'\nimport type { TranscriptionStatus } from '../transcription.types'\n\nexport interface InsertMessageParams {\n companyId: string\n sessionId: string\n whatsappNumber: string\n direction: MessageDirection\n sender: MessageSender\n agentUserId?: string | null\n type: string\n content?: string | null\n payload?: Record<string, unknown> | null\n waMessageId?: string | null\n status?: MessageStatus | null\n /** `undefined` deixa a coluna nula: não avaliado, distinto de avaliado e limpo. */\n moderationFlagged?: boolean | null\n moderationTerms?: string[] | null\n}\n\nexport interface ListMessagesParams {\n companyId: string\n sessionId: string\n limit?: number\n before?: string\n}\n\nexport interface SaveTranscriptionByWaMessageIdParams extends Omit<SaveTranscriptionParams, 'messageId'> {\n /**\n * Id da mensagem na Meta. É o único que quem processa o webhook conhece — o id do módulo só\n * existe depois da gravação, e obrigar o host a descobri-lo faria cada um escrever a própria\n * consulta por `wa_message_id`.\n */\n waMessageId: string\n}\n\nexport interface SaveTranscriptionParams {\n companyId: string\n messageId: string\n status: TranscriptionStatus\n /** Ausente em `pending`/`failed`/`unsupported`; vazio em `done` é silêncio já processado. */\n text?: string | null\n language?: string | null\n engine?: string | null\n}\n\nconst DEFAULT_LIMIT = 50\n\nexport class MessageRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n // Idempotente por (companyId, waMessageId) — o mesmo webhook chega mais de uma vez (a Meta\n // reenvia em instabilidade) e várias instâncias do host processam em paralelo. A garantia é o\n // índice único parcial no banco + onConflictDoNothing, NÃO um SELECT prévio: duas entregas\n // concorrentes passariam as duas pela checagem e inseririam duplicado.\n // Devolve undefined quando a mensagem já existia (nada foi inserido).\n async insertMessage(params: InsertMessageParams): Promise<MessageRow | undefined> {\n const values: NewMessageRow = {\n companyId: params.companyId,\n sessionId: params.sessionId,\n whatsappNumber: params.whatsappNumber,\n direction: params.direction,\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: params.type,\n content: params.content ?? null,\n payload: params.payload ?? null,\n waMessageId: params.waMessageId ?? null,\n status: params.status ?? null,\n moderationFlagged: params.moderationFlagged ?? null,\n moderationTerms: params.moderationTerms ?? null,\n }\n\n const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning()\n return created\n }\n\n async updateMessageStatus(\n companyId: string,\n waMessageId: string,\n status: MessageStatus,\n ): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({ status, ...(status === 'read' ? { readAt: new Date() } : {}) })\n .where(and(eq(messages.companyId, companyId), eq(messages.waMessageId, waMessageId)))\n .returning()\n return updated\n }\n\n /**\n * Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.\n *\n * Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para\n * responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever\n * o mesmo áudio uma segunda vez só para o painel ver.\n *\n * Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são\n * corridas normais, não erro.\n */\n async saveTranscriptionByWaMessageId(params: SaveTranscriptionByWaMessageIdParams): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({\n transcriptionStatus: params.status,\n ...(params.text !== undefined ? { transcriptionText: params.text } : {}),\n ...(params.language !== undefined ? { transcriptionLanguage: params.language } : {}),\n ...(params.engine !== undefined ? { transcriptionEngine: params.engine } : {}),\n })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.waMessageId, params.waMessageId)))\n .returning()\n return updated\n }\n\n async findById(companyId: string, messageId: string): Promise<MessageRow | undefined> {\n const [found] = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, companyId), eq(messages.id, messageId)))\n .limit(1)\n return found\n }\n\n /**\n * Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada\n * entre o enfileiramento e a execução do job) — não é erro, é corrida normal.\n *\n * `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar\n * atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de\n * outro engine da cadeia.\n */\n async saveTranscription(params: SaveTranscriptionParams): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({\n transcriptionStatus: params.status,\n ...(params.text !== undefined ? { transcriptionText: params.text } : {}),\n ...(params.language !== undefined ? { transcriptionLanguage: params.language } : {}),\n ...(params.engine !== undefined ? { transcriptionEngine: params.engine } : {}),\n })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n .returning()\n return updated\n }\n\n async listByConversation(params: ListMessagesParams): Promise<MessageRow[]> {\n const conditions = [eq(messages.companyId, params.companyId), eq(messages.sessionId, params.sessionId)]\n if (params.before) conditions.push(lt(messages.createdAt, new Date(params.before)))\n\n return this.db\n .select()\n .from(messages)\n .where(and(...conditions))\n .orderBy(messages.createdAt)\n .limit(params.limit ?? DEFAULT_LIMIT)\n }\n}\n","import { and, eq, isNotNull, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { flowGraphNodesSchema } from '@adatechnology/meta-whatsapp-contracts'\nimport type {\n FlowGraphData,\n FlowGraphSummary,\n FlowNodeData,\n LiveFlowPosition,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { sessions, flowGraphs, type FlowGraphRow } from '../schema/schema'\nimport type { FlowGraphCache } from './FlowGraphCache'\n\nexport class InvalidFlowGraphError extends Error {\n constructor(\n key: string,\n public readonly validationMessage: string,\n ) {\n super(`Grafo do fluxo \"${key}\" está malformado no banco: ${validationMessage}`)\n this.name = 'InvalidFlowGraphError'\n }\n}\n\n// O jsonb `nodes` entrou pelo editor (dado de origem cliente) e volta do banco como `unknown`.\n// Validar aqui, na fronteira, faz um grafo corrompido falhar com a causa explícita em vez de\n// virar comportamento estranho lá dentro do interpretador.\nfunction toContractGraph(row: FlowGraphRow): FlowGraphData {\n const parsed = flowGraphNodesSchema.safeParse(row.nodes)\n if (!parsed.success) throw new InvalidFlowGraphError(row.key, parsed.error.message)\n\n return {\n key: row.key,\n label: row.label,\n startNodeId: row.startNodeId,\n version: row.version,\n nodes: parsed.data as Record<string, FlowNodeData>,\n }\n}\n\nexport class OptimisticLockError extends Error {\n constructor(key: string) {\n super(`Fluxo \"${key}\" foi alterado por outra sessão — recarregue antes de salvar.`)\n this.name = 'OptimisticLockError'\n }\n}\n\n// T4.2 — CRUD de grafo de fluxo, mais a consulta de posições ao vivo (quantas sessões estão\n// em cada nó agora) que alimenta o liveCount no editor visual (FlowNodeCardData).\nexport class FlowGraphRepository {\n // Cache opcional: sem ele o repositório se comporta exatamente como antes, lendo sempre do\n // banco. É o host que decide se quer cachear e com qual provedor (ver CacheInterface).\n constructor(\n private readonly db: MetaWhatsAppDatabase,\n private readonly cache?: FlowGraphCache,\n ) {}\n\n async get(companyId: string, key: string): Promise<FlowGraphData | undefined> {\n const cached = await this.cache?.read(companyId, key)\n if (cached) return cached\n\n const [row] = await this.db\n .select()\n .from(flowGraphs)\n .where(and(eq(flowGraphs.companyId, companyId), eq(flowGraphs.key, key)))\n .limit(1)\n if (!row) return undefined\n\n const graph = toContractGraph(row)\n await this.cache?.write(companyId, graph)\n return graph\n }\n\n async list(companyId: string): Promise<FlowGraphSummary[]> {\n const rows = await this.db.select().from(flowGraphs).where(eq(flowGraphs.companyId, companyId))\n return rows.map((row) => ({\n key: row.key,\n label: row.label,\n nodeCount: Object.keys(row.nodes as Record<string, unknown>).length,\n showInMenu: row.showInMenu,\n menuOptionLabel: row.menuOptionLabel ?? undefined,\n updatedAt: row.updatedAt.toISOString(),\n }))\n }\n\n // Valida antes de gravar: aceitar um grafo malformado e só reclamar na leitura empurraria a\n // falha para o meio de uma conversa real, em vez de barrar quem salvou.\n private assertValidNodes(key: string, nodes: unknown): void {\n const parsed = flowGraphNodesSchema.safeParse(nodes)\n if (!parsed.success) throw new InvalidFlowGraphError(key, parsed.error.message)\n }\n\n async create(\n companyId: string,\n graph: Omit<FlowGraphData, 'version'> & { showInMenu?: boolean; menuOptionLabel?: string },\n ): Promise<FlowGraphData> {\n this.assertValidNodes(graph.key, graph.nodes)\n\n const [created] = await this.db\n .insert(flowGraphs)\n .values({\n companyId,\n key: graph.key,\n label: graph.label,\n startNodeId: graph.startNodeId,\n nodes: graph.nodes,\n showInMenu: graph.showInMenu ?? false,\n menuOptionLabel: graph.menuOptionLabel,\n })\n .returning()\n\n const createdGraph = toContractGraph(created!)\n await this.cache?.invalidate(companyId, createdGraph.key)\n return createdGraph\n }\n\n // Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —\n // senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).\n async save(companyId: string, graph: FlowGraphData, expectedVersion: number): Promise<FlowGraphData> {\n this.assertValidNodes(graph.key, graph.nodes)\n\n const rows = await this.db\n .update(flowGraphs)\n .set({\n label: graph.label,\n startNodeId: graph.startNodeId,\n nodes: graph.nodes,\n version: expectedVersion + 1,\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(flowGraphs.companyId, companyId),\n eq(flowGraphs.key, graph.key),\n eq(flowGraphs.version, expectedVersion),\n ),\n )\n .returning()\n\n if (rows.length === 0) throw new OptimisticLockError(graph.key)\n\n // Invalida em vez de reescrever a entrada com o grafo novo: reescrever perderia a corrida\n // contra uma leitura concorrente que já tivesse buscado a versão anterior e ainda não tivesse\n // gravado — o cache ficaria com o grafo velho, com TTL cheio pela frente.\n await this.cache?.invalidate(companyId, graph.key)\n return toContractGraph(rows[0]!)\n }\n\n async delete(companyId: string, key: string): Promise<void> {\n await this.db.delete(flowGraphs).where(and(eq(flowGraphs.companyId, companyId), eq(flowGraphs.key, key)))\n await this.cache?.invalidate(companyId, key)\n }\n\n // T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as\n // colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,\n // coberto por idx_sessions_company_flow_node) em vez de trazer toda sessão da empresa para\n // contar em memória.\n async getLiveFlowPositions(companyId: string): Promise<LiveFlowPosition[]> {\n const rows = await this.db\n .select({\n flowKey: sessions.flowKey,\n nodeId: sessions.currentNodeId,\n count: sql<number>`count(*)::int`,\n })\n .from(sessions)\n .where(\n and(\n eq(sessions.companyId, companyId),\n eq(sessions.mode, 'bot'),\n isNotNull(sessions.flowKey),\n isNotNull(sessions.currentNodeId),\n ),\n )\n .groupBy(sessions.flowKey, sessions.currentNodeId)\n\n return rows.map((row) => ({ flowKey: row.flowKey!, nodeId: row.nodeId!, count: row.count }))\n }\n}\n","import type { CacheInterface, FlowGraphData } from '@adatechnology/meta-whatsapp-contracts'\n\n// Namespace na chave para o cache do host poder ser compartilhado com o resto da aplicação sem\n// risco de colisão — o host escolhe onde o Redis mora, não como o módulo nomeia o que guarda.\nconst KEY_PREFIX = 'meta-whatsapp:flow-graph'\n\nexport const DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300\n\n/**\n * Cache de leitura dos grafos de fluxo, com invalidação na publicação.\n *\n * TTL **e** invalidação explícita, e não um dos dois: só TTL deixaria o cliente andando no grafo\n * antigo até a chave expirar, logo depois de alguém corrigir o fluxo no editor; só invalidação\n * deixaria cache envenenado para sempre se um `delete` se perdesse (Redis reiniciando, deploy no\n * meio da escrita).\n *\n * Toda operação é tolerante a falha por decisão de projeto: cache é aceleração, não dependência.\n * Redis fora do ar tem que degradar para leitura no banco — que é exatamente o comportamento de\n * quem não configura cache nenhum — em vez de derrubar a conversa do cliente.\n */\nexport class FlowGraphCache {\n constructor(\n private readonly provider: CacheInterface,\n private readonly ttlSeconds: number = DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,\n ) {}\n\n // companyId na chave, e não só a flowKey: a chave do fluxo é escolhida por quem edita e se\n // repete entre empresas — 'consorcio' existe em todas — então uma chave sem tenant serviria o\n // grafo de uma empresa para a conversa de outra.\n private keyFor(companyId: string, flowKey: string): string {\n return `${KEY_PREFIX}:${companyId}:${flowKey}`\n }\n\n async read(companyId: string, flowKey: string): Promise<FlowGraphData | undefined> {\n try {\n const cached = await this.provider.get(this.keyFor(companyId, flowKey))\n if (!cached) return undefined\n return JSON.parse(cached) as FlowGraphData\n } catch {\n // Inclui JSON corrompido na chave: vale o mesmo que ausência, e o banco resolve.\n return undefined\n }\n }\n\n async write(companyId: string, graph: FlowGraphData): Promise<void> {\n try {\n await this.provider.set(this.keyFor(companyId, graph.key), JSON.stringify(graph), this.ttlSeconds)\n } catch {\n // Não conseguir acelerar não é motivo para falhar a leitura que já deu certo.\n }\n }\n\n async invalidate(companyId: string, flowKey: string): Promise<void> {\n try {\n await this.provider.delete(this.keyFor(companyId, flowKey))\n } catch {\n // Engolir aqui é seguro porque existe TTL: a entrada some sozinha em no máximo ttlSeconds.\n // Sem o TTL como rede, este catch seria um bug — o grafo velho ficaria para sempre.\n }\n }\n}\n","import { eq } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { WhatsAppSettings } from '@adatechnology/meta-whatsapp-contracts'\nimport { settings, type SettingsRow } from '../schema/schema'\n\nfunction toContractSettings(row: SettingsRow): WhatsAppSettings {\n return {\n templateName: row.templateName ?? '',\n templateLanguage: row.templateLanguage,\n templateVariables: row.templateVariables,\n welcomeMessage: row.welcomeMessage ?? '',\n farewellMessage: row.farewellMessage ?? '',\n // `?? null` e não `?? false`: nulo é \"o painel não decidiu\", e é o que faz valer o padrão do\n // host. Colapsar para `false` desligaria quem ligou por ambiente.\n transcriptionEnabled: row.transcriptionEnabled ?? null,\n transcriptionMode: row.transcriptionMode ?? null,\n }\n}\n\nconst EMPTY_SETTINGS: WhatsAppSettings = {\n templateName: '',\n templateLanguage: 'pt_BR',\n templateVariables: [],\n welcomeMessage: '',\n farewellMessage: '',\n transcriptionEnabled: null,\n transcriptionMode: null,\n}\n\n// T5.5 — configuração de WhatsApp por empresa.\nexport class SettingsRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n // Empresa sem linha de configuração é o estado normal logo após instalar o módulo — devolve\n // os defaults em vez de undefined, para o chamador não precisar tratar \"ainda não configurado\"\n // como caso especial em todo lugar.\n async get(companyId: string): Promise<WhatsAppSettings> {\n const [row] = await this.db.select().from(settings).where(eq(settings.companyId, companyId)).limit(1)\n return row ? toContractSettings(row) : EMPTY_SETTINGS\n }\n\n async save(companyId: string, update: Partial<WhatsAppSettings>): Promise<WhatsAppSettings> {\n const current = await this.get(companyId)\n const merged = { ...current, ...update }\n\n const [row] = await this.db\n .insert(settings)\n .values({\n companyId,\n templateName: merged.templateName || null,\n templateLanguage: merged.templateLanguage,\n templateVariables: merged.templateVariables,\n welcomeMessage: merged.welcomeMessage || null,\n farewellMessage: merged.farewellMessage || null,\n // Sem `|| null`: `false` aqui é decisão explícita do painel (\"desligado para esta empresa\"),\n // e colapsá-lo para nulo faria a empresa voltar a herdar o padrão do host — exatamente o\n // oposto do que o operador acabou de pedir.\n transcriptionEnabled: merged.transcriptionEnabled,\n transcriptionMode: merged.transcriptionMode,\n })\n .onConflictDoUpdate({\n target: settings.companyId,\n set: {\n templateName: merged.templateName || null,\n templateLanguage: merged.templateLanguage,\n templateVariables: merged.templateVariables,\n welcomeMessage: merged.welcomeMessage || null,\n farewellMessage: merged.farewellMessage || null,\n transcriptionEnabled: merged.transcriptionEnabled,\n transcriptionMode: merged.transcriptionMode,\n updatedAt: new Date(),\n },\n })\n .returning()\n\n return toContractSettings(row!)\n }\n\n // Resolve {{1}}, {{2}}... a partir do mapa configurado + o contexto da conversa. Ex.: se\n // templateVariables = ['{clientName}', '{city}'], devolve [nome, cidade] na ordem posicional\n // que a Graph API espera nos bodyParameters.\n async resolveTemplateVariables(companyId: string, context: Record<string, unknown>): Promise<string[]> {\n const { templateVariables } = await this.get(companyId)\n return templateVariables.map((token) => {\n const key = token.replace(/^\\{|\\}$/g, '')\n const value = context[key]\n return value === undefined || value === null ? '' : String(value)\n })\n }\n}\n","import type { RealtimeNotifierInterface, SessionState } from '@adatechnology/meta-whatsapp-contracts'\nimport type { InsertMessageParams, MessageRepository } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { MessageRow } from '../schema/schema'\n\nexport type LogMessageParams = Omit<InsertMessageParams, 'sessionId'> & {\n startState: SessionState\n}\n\n/**\n * O contrato mínimo de `@adatechnology/text-moderation`, declarado aqui em vez de importado.\n *\n * Assim o módulo não ganha dependência de pacote para um recurso opcional, e — mais importante —\n * moderação não fica amarrada a WhatsApp: o que atravessa esta fronteira é texto, e qualquer canal\n * que passe a escrever no transcript por este use case herda a marcação sem tocar nada aqui.\n */\nexport type MessageModerator = {\n inspect: (text: string) => { isOffensive: boolean; matchedTerms: readonly string[] }\n}\n\n// T3.3 — ponto único de escrita no transcript: garante a sessão (getOrCreate), insere a\n// mensagem (idempotente por waMessageId) e notifica em tempo real. Usado tanto pelo canal\n// (T5.2, ao enviar) quanto pelo webhook (T5.1, ao receber).\nexport class LogMessageUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly messageRepository: MessageRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n private readonly moderator?: MessageModerator,\n ) {}\n\n async execute(params: LogMessageParams): Promise<MessageRow | undefined> {\n const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState)\n\n const saved = await this.messageRepository.insertMessage({\n ...params,\n sessionId: session.id,\n ...this.moderationOf(params),\n })\n // Entrega duplicada (mesmo waMessageId) — não emite evento nem re-carimba a janela.\n if (!saved) return undefined\n\n if (params.direction === 'inbound') {\n await this.sessionRepository.touchInbound(params.companyId, params.whatsappNumber)\n }\n\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'message', {\n direction: params.direction,\n sender: params.sender,\n })\n this.realtime?.emit('global', 'data-changed', {})\n\n return saved\n }\n\n // Só o que o cliente escreveu: marcar o que o próprio atendente ou o bot enviou não sinaliza\n // abuso, apenas sujaria o transcript com etiqueta na resposta de quem atende.\n private moderationOf(params: LogMessageParams): Pick<InsertMessageParams, 'moderationFlagged' | 'moderationTerms'> {\n if (!this.moderator || params.direction !== 'inbound') return {}\n\n const text = params.content?.trim()\n if (!text) return {}\n\n const verdict = this.moderator.inspect(text)\n\n return {\n moderationFlagged: verdict.isOffensive,\n moderationTerms: verdict.isOffensive ? [...verdict.matchedTerms] : null,\n }\n }\n}\n","import {\n WindowExpiredError,\n type ChannelAdapterInterface,\n type ObjectStorageInterface,\n type SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { LogMessageUseCase } from './LogMessage.use-case'\nimport type { MessageRow } from '../schema/schema'\n\nconst WHATSAPP_WINDOW_HOURS = 24\n\nexport type SendTextParams = {\n companyId: string\n whatsappNumber: string\n body: string\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\nexport type SendMediaParams = {\n companyId: string\n whatsappNumber: string\n buffer: Buffer\n mimeType: string\n filename: string\n caption?: string\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\nexport type SendTemplateParams = {\n companyId: string\n whatsappNumber: string\n templateName: string\n languageCode?: string\n bodyParameters?: string[]\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\n// T5.2 — envio pelo canal + registro no transcript, num único ponto.\n//\n// A checagem de janela acontece ANTES da chamada à Graph API: a Meta também rejeitaria\n// (o adapter traduz o erro dela para WindowExpiredError), mas gastar uma ida à rede para\n// descobrir algo que o nosso próprio lastInboundAt já sabe é desperdício — e o erro local\n// carrega há quantas horas o cliente sumiu, que a resposta da Meta não informa.\nexport class SendMessageUseCase {\n constructor(\n private readonly channel: ChannelAdapterInterface,\n private readonly sessionRepository: SessionRepository,\n private readonly logMessage: LogMessageUseCase,\n private readonly objectStorage?: ObjectStorageInterface,\n // Opcional pelo mesmo motivo do IngestInboundMedia: sem ele o envio continua funcionando, só\n // não entra na biblioteca de arquivos da conversa.\n private readonly documentRepository?: DocumentRepository,\n ) {}\n\n private async assertWithinWindow(companyId: string, whatsappNumber: string): Promise<void> {\n const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber)\n // Cliente que nunca escreveu: não há janela aberta, só template pode iniciar a conversa.\n if (hours === undefined) throw new WindowExpiredError()\n if (hours >= WHATSAPP_WINDOW_HOURS) throw new WindowExpiredError(hours)\n }\n\n async sendText(params: SendTextParams): Promise<MessageRow | undefined> {\n await this.assertWithinWindow(params.companyId, params.whatsappNumber)\n\n const { externalMessageId } = await this.channel.sendText(params.whatsappNumber, params.body)\n\n return this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: 'text',\n content: params.body,\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n }\n\n async sendMedia(params: SendMediaParams): Promise<MessageRow | undefined> {\n await this.assertWithinWindow(params.companyId, params.whatsappNumber)\n\n const { externalMessageId } = await this.channel.sendMedia({\n to: params.whatsappNumber,\n buffer: params.buffer,\n mimeType: params.mimeType,\n filename: params.filename,\n caption: params.caption,\n })\n\n // T5.4 — o binário vai para o storage do host e o transcript guarda só a referência.\n // Gravar base64 no jsonb (dívida herdada do bot) inchava a tabela de mensagens, tornava\n // qualquer SELECT da conversa caro e duplicava um dado que o storage já tem.\n const uploadId = this.objectStorage\n ? (\n await this.objectStorage.upload({\n buffer: params.buffer,\n mimeType: params.mimeType,\n key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`,\n })\n ).uploadId\n : undefined\n\n const saved = await this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: mediaTypeFor(params.mimeType),\n content: params.caption ?? params.filename,\n payload: { filename: params.filename, mimeType: params.mimeType, ...(uploadId ? { uploadId } : {}) },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n\n // Arquivo que o atendente manda ao cliente também é arquivo da conversa: quem atende depois\n // precisa achar o que já foi enviado sem rolar a thread inteira.\n //\n // Só quando `saved` existe — `undefined` significa entrega duplicada, e linkar aí criaria uma\n // segunda linha para a mesma mensagem. E só com `uploadId`: sem storage não há objeto para\n // apontar, e documento sem destino é linha morta no painel.\n if (saved && uploadId) {\n await this.documentRepository?.link({\n companyId: params.companyId,\n sessionId: saved.sessionId,\n messageId: saved.id,\n uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.buffer.length,\n source: params.sender,\n })\n }\n\n return saved\n }\n\n // Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece\n // para reabri-la.\n async sendTemplate(params: SendTemplateParams): Promise<MessageRow | undefined> {\n const { externalMessageId } = await this.channel.sendTemplate({\n to: params.whatsappNumber,\n templateName: params.templateName,\n languageCode: params.languageCode ?? 'pt_BR',\n bodyParameters: params.bodyParameters,\n })\n\n return this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: 'template',\n content: params.templateName,\n payload: { templateName: params.templateName, bodyParameters: params.bodyParameters ?? [] },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n }\n}\n\nfunction mediaTypeFor(mimeType: string): string {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (mimeType.startsWith('video/')) return 'video'\n return 'document'\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type TakeoverConversationParams = {\n companyId: string\n whatsappNumber: string\n agentUserId: string\n}\n\n// T3.3 — atendente assume a conversa (tira do bot). Emite evento para os clientes de admin\n// atualizarem a UI (badge \"atendido por\", desabilitar botão de takeover) em tempo real.\nexport class TakeoverConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n ) {}\n\n async execute(params: TakeoverConversationParams): Promise<void> {\n await this.sessionRepository.takeover(params.companyId, params.whatsappNumber, params.agentUserId)\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'mode-changed', {\n mode: 'human',\n assignedUserId: params.agentUserId,\n })\n this.realtime?.emit('global', 'data-changed', {})\n }\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type ReleaseConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\n// T3.3 — devolve a conversa ao bot.\nexport class ReleaseConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n ) {}\n\n async execute(params: ReleaseConversationParams): Promise<void> {\n await this.sessionRepository.release(params.companyId, params.whatsappNumber)\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'mode-changed', { mode: 'bot', assignedUserId: null })\n this.realtime?.emit('global', 'data-changed', {})\n }\n}\n","import type { ConversationSummary } from '@adatechnology/meta-whatsapp-contracts'\nimport type { ListConversationsFilters, SessionRepository } from '../repositories/SessionRepository'\n\nexport type ListConversationsParams = {\n companyId: string\n filters?: ListConversationsFilters\n}\n\n// T3.3 — listagem paginada/filtrada de conversas para a tela de inbox do host.\nexport class ListConversationsUseCase {\n constructor(private readonly sessionRepository: SessionRepository) {}\n\n async execute(params: ListConversationsParams): Promise<ConversationSummary[]> {\n return this.sessionRepository.listByContextFilters(params.companyId, params.filters ?? {})\n }\n}\n","import type { MessageRepository, ListMessagesParams as RepoListMessagesParams } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRow } from '../schema/schema'\n\nexport type ListMessagesParams = {\n companyId: string\n whatsappNumber: string\n limit?: number\n before?: string\n}\n\n// T3.3 — histórico paginado de uma conversa (resolve sessionId a partir do número antes de\n// delegar ao repositório de mensagens).\nexport class ListMessagesUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly messageRepository: MessageRepository,\n ) {}\n\n async execute(params: ListMessagesParams): Promise<MessageRow[]> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const repoParams: RepoListMessagesParams = {\n companyId: params.companyId,\n sessionId: session.id,\n limit: params.limit,\n before: params.before,\n }\n return this.messageRepository.listByConversation(repoParams)\n }\n}\n","import { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\n\nexport type ListConversationDocumentsParams = {\n companyId: string\n whatsappNumber: string\n search?: string\n /** Origens explícitas. O apelido de UI (\"Equipe\") é traduzido na borda HTTP, não aqui. */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\n/**\n * O que o `ConversationDocumentsPanel` do conversations-ui consome. O shape é o do pacote de UI\n * (`ConversationDocument`), com `linkedAt` já em ISO para não obrigar cada host a serializar.\n */\nexport type ConversationDocumentView = {\n /**\n * O `uploadId` (key no storage), e NÃO o id da linha.\n *\n * É este valor que o consumidor devolve para pedir a URL assinada ou montar o zip, então expor o\n * UUID da tabela aqui fazia o download apontar para um objeto inexistente — falha que só aparece\n * no clique, porque a assinatura é gerada sem consultar o bucket.\n */\n id: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: string\n}\n\n/** Espelha o `ConversationDocumentPage` do conversations-ui: lista da página + total no servidor. */\nexport type ConversationDocumentsPage = {\n documents: ConversationDocumentView[]\n total: number\n}\n\nexport class ListConversationDocumentsUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly documentRepository: DocumentRepository,\n ) {}\n\n async execute(params: ListConversationDocumentsParams): Promise<ConversationDocumentsPage> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const { rows, total } = await this.documentRepository.listByConversation({\n companyId: params.companyId,\n sessionId: session.id,\n ...(params.search ? { search: params.search } : {}),\n ...(params.sources && params.sources.length > 0 ? { sources: params.sources } : {}),\n ...(params.sortDirection ? { sortDirection: params.sortDirection } : {}),\n ...(params.page ? { page: params.page } : {}),\n ...(params.limit ? { limit: params.limit } : {}),\n })\n\n return {\n documents: rows.map((row) => ({\n id: row.uploadId,\n filename: row.filename,\n mimeType: row.mimeType,\n sizeBytes: row.sizeBytes,\n source: row.source,\n linkedAt: row.linkedAt.toISOString(),\n })),\n total,\n }\n }\n}\n","import type { DocumentRepository } from '../repositories/DocumentRepository'\n\nexport type ListCompanyDocumentsParams = {\n companyId: string\n search?: string\n /** Origens explícitas. O apelido de UI (\"Equipe\") é traduzido na borda HTTP, não aqui. */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\n/**\n * Um arquivo na biblioteca da empresa. Igual ao da conversa, mais `conversationId` — sem saber de\n * quem veio, uma lista global de anexos não responde nenhuma pergunta útil.\n */\nexport type CompanyDocumentView = {\n /** O `uploadId` (key no storage), pelo mesmo motivo do `ConversationDocumentView`. */\n id: string\n conversationId: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: string\n}\n\nexport type CompanyDocumentsPage = {\n documents: CompanyDocumentView[]\n total: number\n}\n\n/**\n * Biblioteca de arquivos de todas as conversas da empresa.\n *\n * Existe separada de `ListConversationDocumentsUseCase` porque a pergunta é outra: aquela parte de\n * uma conversa conhecida, esta varre a empresa e por isso precisa dizer de qual conversa cada\n * arquivo veio. Reaproveitar a primeira exigiria um `sessionId` opcional que muda o significado do\n * retorno — dois nomes claros custam menos que um parâmetro que dobra o comportamento.\n */\nexport class ListCompanyDocumentsUseCase {\n constructor(private readonly documentRepository: DocumentRepository) {}\n\n async execute(params: ListCompanyDocumentsParams): Promise<CompanyDocumentsPage> {\n const { rows, total } = await this.documentRepository.listByCompany({\n companyId: params.companyId,\n ...(params.search ? { search: params.search } : {}),\n ...(params.sources && params.sources.length > 0 ? { sources: params.sources } : {}),\n ...(params.sortDirection ? { sortDirection: params.sortDirection } : {}),\n ...(params.page ? { page: params.page } : {}),\n ...(params.limit ? { limit: params.limit } : {}),\n })\n\n return {\n documents: rows.map((row) => ({\n id: row.uploadId,\n conversationId: row.whatsappNumber,\n filename: row.filename,\n mimeType: row.mimeType,\n sizeBytes: row.sizeBytes,\n source: row.source,\n linkedAt: row.linkedAt.toISOString(),\n })),\n total,\n }\n }\n}\n","import { SessionNotFoundError, type ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\n\nexport type DeleteConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\nexport type DeleteConversationResult = {\n /** Objetos efetivamente apagados no storage. */\n deletedObjects: number\n /** Objetos que o storage recusou. A conversa NÃO é apagada quando isto é maior que zero. */\n failedObjects: readonly string[]\n}\n\n/**\n * Apaga a conversa e a mídia dela.\n *\n * A ordem é o ponto: **storage primeiro, banco depois**. A FK de `documents.session_id` é\n * `on delete cascade`, então apagar a sessão primeiro derrubaria as linhas e levaria embora a única\n * lista de `uploadId` existente — os binários ficariam órfãos, cobrados para sempre e inalcançáveis.\n *\n * Se algum objeto falhar, a conversa é preservada e o chamador recebe a lista. Apagar as linhas\n * \"mesmo assim\" transformaria uma falha visível e reexecutável em lixo silencioso no storage.\n */\nexport class DeleteConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly documentRepository: DocumentRepository,\n private readonly objectStorage?: ObjectStorageInterface,\n ) {}\n\n async execute(params: DeleteConversationParams): Promise<DeleteConversationResult> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const uploadIds = await this.documentRepository.listUploadIdsBySession(params.companyId, session.id)\n\n // Sem `delete` no storage não há como cumprir a promessa de apagar a mídia. Falhar aqui é\n // melhor que apagar o transcript e deixar os arquivos para trás sem ninguém saber.\n if (uploadIds.length > 0 && !this.objectStorage?.delete) {\n throw new Error('storage_delete_unsupported: a conversa tem arquivos e o storage injetado não implementa delete')\n }\n\n const failedObjects: string[] = []\n let deletedObjects = 0\n\n for (const uploadId of uploadIds) {\n try {\n await this.objectStorage?.delete?.(uploadId)\n deletedObjects++\n } catch {\n failedObjects.push(uploadId)\n }\n }\n\n if (failedObjects.length > 0) return { deletedObjects, failedObjects }\n\n // A cascata cuida de messages e documents; a sessão é a raiz.\n await this.sessionRepository.deleteByNumber(params.companyId, params.whatsappNumber)\n\n return { deletedObjects, failedObjects: [] }\n }\n}\n","import type { ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\n\nexport type PurgeExpiredDocumentsParams = {\n companyId: string\n /** Dias de retenção. O produto configura; o módulo não escolhe política de dado pessoal. */\n retentionDays: number\n /** Teto por execução, para o job não segurar conexão nem storage por tempo indefinido. */\n batchSize?: number\n /** Instante de referência — injetado para o teste não depender do relógio. */\n now?: Date\n}\n\nexport type PurgeExpiredDocumentsResult = {\n purged: number\n failed: readonly string[]\n}\n\nconst DEFAULT_BATCH_SIZE = 50\nconst HOURS_IN_DAY = 24\nconst MILLISECONDS_IN_HOUR = 60 * 60 * 1000\n\n/**\n * Apaga documento vencido: objeto no storage primeiro, linha depois — a mesma ordem do\n * `DeleteConversationUseCase`, e pelo mesmo motivo.\n *\n * A linha só cai quando o objeto caiu. Contar como purgado sem ter apagado o binário deixaria lixo\n * pago no storage sem nenhum registro que o alcance depois: a linha era o único ponteiro.\n */\nexport class PurgeExpiredDocumentsUseCase {\n constructor(\n private readonly documentRepository: DocumentRepository,\n private readonly objectStorage?: ObjectStorageInterface,\n ) {}\n\n async execute(params: PurgeExpiredDocumentsParams): Promise<PurgeExpiredDocumentsResult> {\n if (!this.objectStorage?.delete) {\n throw new Error('storage_delete_unsupported: retenção exige um storage que implemente delete')\n }\n\n const reference = params.now ?? new Date()\n const olderThan = new Date(reference.getTime() - params.retentionDays * HOURS_IN_DAY * MILLISECONDS_IN_HOUR)\n\n const expired = await this.documentRepository.listExpired(\n params.companyId,\n olderThan,\n params.batchSize ?? DEFAULT_BATCH_SIZE,\n )\n\n const failed: string[] = []\n let purged = 0\n\n for (const document of expired) {\n try {\n await this.objectStorage.delete(document.uploadId)\n } catch {\n // Guarda e segue: um objeto problemático não deve impedir a limpeza dos outros, e a próxima\n // execução tenta de novo porque a linha continua lá.\n failed.push(document.uploadId)\n continue\n }\n\n await this.documentRepository.deleteById(params.companyId, document.id)\n purged++\n }\n\n return { purged, failed }\n }\n}\n","import { and, asc, count, desc, eq, ilike, inArray, lt, or, type SQL } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { documents, sessions, type DocumentRow, type NewDocumentRow } from '../schema/schema'\n\nexport interface LinkDocumentParams {\n companyId: string\n sessionId: string\n messageId?: string | null\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n sha256?: string | null\n source: string\n}\n\nexport interface ListDocumentsParams {\n companyId: string\n sessionId: string\n search?: string\n /**\n * Origens aceitas, como lista explícita (`['agent', 'bot']`) e não como apelido de UI.\n *\n * Agrupamento tipo \"Equipe\" é vocabulário de tela e muda por produto; traduzir aqui obrigaria o\n * módulo a conhecer o rótulo de cada host. Quem recebe o apelido na borda é a rota.\n */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\nexport interface ListCompanyDocumentsParams {\n companyId: string\n search?: string\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\nexport interface CompanyDocumentRow {\n id: string\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: Date\n whatsappNumber: string\n}\n\nexport interface ListCompanyDocumentsResult {\n rows: CompanyDocumentRow[]\n total: number\n}\n\nexport interface ListDocumentsResult {\n rows: DocumentRow[]\n /** Total no servidor, ANTES do corte de página — é o que permite calcular a última página. */\n total: number\n}\n\nconst DEFAULT_LIMIT = 50\n\n/**\n * O filtro de busca da biblioteca da empresa: nome do arquivo OU telefone da conversa.\n *\n * O telefone é comparado só pelos dígitos porque a tela mostra \"+55 (11) 94444-3333\" e o banco\n * guarda \"5511944443333\" — quem copia o número da tela e cola na busca casaria zero linhas se o\n * termo fosse usado cru. Por isso a mesma palavra vira dois predicados: o nome mantém a pontuação\n * (faz parte do arquivo), o número perde.\n *\n * Termo sem dígito nenhum não gera o predicado do telefone — `%%` casaria toda conversa e a busca\n * por nome deixaria de filtrar.\n */\nexport function companyDocumentSearch(search: string | undefined): SQL | undefined {\n const term = search?.trim()\n if (!term) return undefined\n\n const digits = term.replace(/\\D/g, '')\n const byFilename = ilike(documents.filename, `%${term}%`)\n if (!digits) return byFilename\n\n return or(byFilename, ilike(sessions.whatsappNumber, `%${digits}%`))\n}\n\nexport class DocumentRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n /**\n * Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o\n * job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma\n * checagem, duplicando a linha no painel.\n *\n * Devolve `undefined` quando o documento já estava linkado.\n */\n async link(params: LinkDocumentParams): Promise<DocumentRow | undefined> {\n const values: NewDocumentRow = {\n companyId: params.companyId,\n sessionId: params.sessionId,\n messageId: params.messageId ?? null,\n uploadId: params.uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.sizeBytes,\n sha256: params.sha256 ?? null,\n source: params.source,\n }\n\n const [created] = await this.db.insert(documents).values(values).onConflictDoNothing().returning()\n return created\n }\n\n async listByConversation(params: ListDocumentsParams): Promise<ListDocumentsResult> {\n const filters = [eq(documents.companyId, params.companyId), eq(documents.sessionId, params.sessionId)]\n // `ilike` com % nas duas pontas: o painel busca por parte do nome, e o atendente não sabe como\n // o arquivo chegou nomeado.\n if (params.search) filters.push(ilike(documents.filename, `%${params.search}%`))\n // Lista vazia é tratada como ausência de filtro, não como \"nenhuma origem aceita\": um\n // `inArray` com `[]` gera `false` e devolveria zero linhas para quem só quis dizer \"todas\".\n if (params.sources && params.sources.length > 0) {\n filters.push(inArray(documents.source, [...params.sources]))\n }\n\n const where = and(...filters)\n const limit = params.limit ?? DEFAULT_LIMIT\n const page = params.page && params.page > 0 ? params.page : 1\n const orderBy = params.sortDirection === 'asc' ? asc(documents.linkedAt) : desc(documents.linkedAt)\n\n // Duas consultas com o MESMO `where`: a contagem tem de ignorar o corte de página, senão o\n // total viraria o tamanho da página e a paginação nunca sairia da primeira.\n const [rows, counted] = await Promise.all([\n this.db\n .select()\n .from(documents)\n .where(where)\n .orderBy(orderBy)\n .limit(limit)\n .offset((page - 1) * limit),\n this.db.select({ value: count() }).from(documents).where(where),\n ])\n\n return { rows, total: counted[0]?.value ?? 0 }\n }\n\n /**\n * A biblioteca da EMPRESA inteira, não de uma conversa.\n *\n * A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.\n *\n * Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,\n * arquivo sem essa referência é inútil: o atendente vê \"comprovante.pdf\" e não sabe de quem.\n *\n * Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a\n * varredura de retenção.\n */\n async listByCompany(params: ListCompanyDocumentsParams): Promise<ListCompanyDocumentsResult> {\n const filters = [eq(documents.companyId, params.companyId)]\n const search = companyDocumentSearch(params.search)\n if (search) filters.push(search)\n if (params.sources && params.sources.length > 0) {\n filters.push(inArray(documents.source, [...params.sources]))\n }\n\n const where = and(...filters)\n const limit = params.limit ?? DEFAULT_LIMIT\n const page = params.page && params.page > 0 ? params.page : 1\n const orderBy = params.sortDirection === 'asc' ? asc(documents.linkedAt) : desc(documents.linkedAt)\n\n const [rows, counted] = await Promise.all([\n this.db\n .select({\n id: documents.id,\n uploadId: documents.uploadId,\n filename: documents.filename,\n mimeType: documents.mimeType,\n sizeBytes: documents.sizeBytes,\n source: documents.source,\n linkedAt: documents.linkedAt,\n // Só o número: o NOME do cliente é dado do produto (tabela própria dele), não do\n // módulo. Quem quiser exibir \"Marina Alves\" enriquece na borda.\n whatsappNumber: sessions.whatsappNumber,\n })\n .from(documents)\n .innerJoin(sessions, eq(documents.sessionId, sessions.id))\n .where(where)\n .orderBy(orderBy)\n .limit(limit)\n .offset((page - 1) * limit),\n // O mesmo join da listagem, e não só `from(documents)`: a busca pode citar\n // `sessions.whatsapp_number`, e uma contagem sem a tabela na cláusula não compila — pior,\n // se compilasse, o total divergiria das linhas e a paginação prometeria páginas vazias.\n this.db\n .select({ value: count() })\n .from(documents)\n .innerJoin(sessions, eq(documents.sessionId, sessions.id))\n .where(where),\n ])\n\n return { rows, total: counted[0]?.value ?? 0 }\n }\n\n /**\n * Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o\n * download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.\n */\n async findByUploadId(companyId: string, uploadId: string): Promise<DocumentRow | undefined> {\n const [row] = await this.db\n .select()\n .from(documents)\n .where(and(eq(documents.companyId, companyId), eq(documents.uploadId, uploadId)))\n .limit(1)\n\n return row\n }\n\n /**\n * Os objetos a apagar no storage antes de a linha sumir.\n *\n * Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa\n * precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.\n */\n async listUploadIdsBySession(companyId: string, sessionId: string): Promise<string[]> {\n const rows = await this.db\n .select({ uploadId: documents.uploadId })\n .from(documents)\n .where(and(eq(documents.companyId, companyId), eq(documents.sessionId, sessionId)))\n\n return rows.map((row) => row.uploadId)\n }\n\n /** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */\n async listExpired(companyId: string, olderThan: Date, limit = DEFAULT_LIMIT): Promise<DocumentRow[]> {\n return this.db\n .select()\n .from(documents)\n .where(and(eq(documents.companyId, companyId), lt(documents.linkedAt, olderThan)))\n .orderBy(documents.linkedAt)\n .limit(limit)\n }\n\n async deleteById(companyId: string, id: string): Promise<void> {\n await this.db.delete(documents).where(and(eq(documents.companyId, companyId), eq(documents.id, id)))\n }\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRow, SessionRow } from '../schema/schema'\n\nexport type ExportConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\nexport type ExportConversationResult = {\n session: SessionRow\n messages: MessageRow[]\n}\n\n// T3.3 — transcript completo para export (ex.: anexar num chamado de suporte, auditoria).\nexport class ExportConversationUseCase {\n constructor(private readonly sessionRepository: SessionRepository) {}\n\n async execute(params: ExportConversationParams): Promise<ExportConversationResult> {\n const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber)\n if (!result) throw new SessionNotFoundError(params.whatsappNumber)\n return result\n }\n}\n","import type { FlowGraphData, FlowGraphSummary } from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowGraphRepository } from '../repositories/FlowGraphRepository'\n\n// T4.2 — CRUD de grafo, agrupado num único arquivo (cada use-case é uma classe fina sobre o\n// mesmo repositório; não há regra de negócio própria a proteger na sua separação em arquivos\n// individuais como fizemos para conversation/session).\n\nexport class GetFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string; key: string }): Promise<FlowGraphData | undefined> {\n return this.flowGraphRepository.get(params.companyId, params.key)\n }\n}\n\nexport class ListFlowGraphsUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string }): Promise<FlowGraphSummary[]> {\n return this.flowGraphRepository.list(params.companyId)\n }\n}\n\nexport type CreateFlowGraphParams = {\n companyId: string\n key: string\n label: string\n startNodeId: string\n nodes: FlowGraphData['nodes']\n showInMenu?: boolean\n menuOptionLabel?: string\n}\n\nexport class CreateFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: CreateFlowGraphParams): Promise<FlowGraphData> {\n return this.flowGraphRepository.create(params.companyId, params)\n }\n}\n\nexport type SaveFlowGraphParams = {\n companyId: string\n graph: FlowGraphData\n expectedVersion: number\n}\n\n// Salvar já é publicar — o motor de fluxo lê o grafo em tempo real (ver rules/packages/\n// pluggable-module.md e tasks.md T4.2). A validação de conteúdo (destinos quebrados, opções\n// duplicadas, etc.) é responsabilidade do editor (conversations-ui/flows validateGraph), não\n// deste use-case — aqui só garantimos a consistência otimista da escrita.\nexport class SaveFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: SaveFlowGraphParams): Promise<FlowGraphData> {\n return this.flowGraphRepository.save(params.companyId, params.graph, params.expectedVersion)\n }\n}\n\nexport class DeleteFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string; key: string }): Promise<void> {\n return this.flowGraphRepository.delete(params.companyId, params.key)\n }\n}\n\nexport class GetLiveFlowPositionsUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string }) {\n return this.flowGraphRepository.getLiveFlowPositions(params.companyId)\n }\n}\n","import type {\n ChannelAdapterInterface,\n ConversationSession,\n FlowActionHandler,\n FlowActionKind,\n FlowActionResult,\n FlowGraphData,\n FlowNodeData,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { isCrossFlowTarget, crossFlowKey } from '@adatechnology/meta-whatsapp-contracts'\n\nexport interface FlowStepInput {\n graph: FlowGraphData\n currentNodeId: string\n userAnswer?: string\n context: Record<string, unknown>\n session: ConversationSession\n channel: ChannelAdapterInterface\n}\n\nexport type FlowStepResult =\n | { kind: 'awaiting-answer'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'advanced'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'cross-flow'; flowKey: string; context: Record<string, unknown> }\n | { kind: 'terminal'; context: Record<string, unknown> }\n\n// Resultado de run(): nunca 'advanced' (o laço já consumiu esses), mas pode ser\n// 'max-steps-exceeded' quando o grafo tem um ciclo de nós automáticos.\nexport type FlowRunResult = (\n | { kind: 'awaiting-answer'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'cross-flow'; flowKey: string; context: Record<string, unknown> }\n | { kind: 'terminal'; context: Record<string, unknown> }\n | { kind: 'max-steps-exceeded'; context: Record<string, unknown> }\n) & {\n // Caminho percorrido, do nó inicial ao final — serve para depurar um ciclo (os nós repetidos\n // aparecem aqui) e para o host registrar a trilha da conversa.\n visited: string[]\n steps: number\n}\n\nconst DEFAULT_MAX_STEPS = 50\n\nfunction evaluateCondition(node: FlowNodeData, context: Record<string, unknown>): boolean {\n if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false\n const actual = context[node.conditionContextKey]\n const expected = node.conditionValue\n\n switch (node.conditionOperator) {\n case '==':\n return String(actual) === expected\n case '!=':\n return String(actual) !== expected\n case 'contains':\n return typeof actual === 'string' && actual.includes(expected)\n case '>':\n case '>=':\n case '<':\n case '<=': {\n const actualNum = Number(actual)\n const expectedNum = Number(expected)\n if (Number.isNaN(actualNum) || Number.isNaN(expectedNum)) return false\n if (node.conditionOperator === '>') return actualNum > expectedNum\n if (node.conditionOperator === '>=') return actualNum >= expectedNum\n if (node.conditionOperator === '<') return actualNum < expectedNum\n return actualNum <= expectedNum\n }\n default:\n return false\n }\n}\n\nfunction resolveNext(node: FlowNodeData, answerId: string | undefined): string | undefined {\n if (!node.next) return undefined\n if (typeof node.next === 'string') return node.next\n if (answerId && node.next.byAnswer[answerId]) return node.next.byAnswer[answerId]\n return node.next.default\n}\n\n// T4.3 — interpreta um grafo de fluxo nó a nó. `registerFlowAction` é o único jeito de um\n// `actionKind` fazer algo: o interpretador nunca conhece 'trigger_simulation' ou qualquer outro\n// caso de negócio do host — ele só invoca o handler registrado (ver providers.ts,\n// FlowActionRegistry) e segue o `next` que o handler devolver, se houver.\nexport class FlowInterpreter {\n private readonly actionHandlers = new Map<FlowActionKind, FlowActionHandler>()\n\n registerFlowAction(kind: FlowActionKind, handler: FlowActionHandler): void {\n this.actionHandlers.set(kind, handler)\n }\n\n async step(input: FlowStepInput): Promise<FlowStepResult> {\n const node = input.graph.nodes[input.currentNodeId]\n if (!node) return { kind: 'terminal', context: input.context }\n\n if (node.type === 'condition') {\n const branch = evaluateCondition(node, input.context) ? 'true' : 'false'\n return this.moveTo(input, resolveNext(node, branch))\n }\n\n if (node.type === 'action') {\n const handler = node.actionKind ? this.actionHandlers.get(node.actionKind) : undefined\n if (!handler) return { kind: 'terminal', context: input.context }\n\n // Runtime de uma função `void` é sempre `undefined` — cast seguro para poder encadear\n // `?.` (TS não deixa acessar propriedade num tipo `void | {...}` mesmo com optional chaining).\n const result = (await handler({\n node,\n session: input.session,\n channel: input.channel,\n context: input.context,\n })) as FlowActionResult | undefined\n\n const contextAfterAction = result?.context ? { ...input.context, ...result.context } : input.context\n const nextNodeId = result?.next ?? resolveNext(node, undefined)\n return this.moveTo({ ...input, context: contextAfterAction }, nextNodeId)\n }\n\n const isChoice = node.type === 'menu' || node.questionType === 'choice'\n\n // Sem resposta do usuário ainda: este é o nó atual, aguardando input — não avança.\n if (input.userAnswer === undefined) {\n return { kind: 'awaiting-answer', nodeId: input.currentNodeId, context: input.context }\n }\n\n const updatedContext = node.contextKey ? { ...input.context, [node.contextKey]: input.userAnswer } : input.context\n const answerId = isChoice ? input.userAnswer : undefined\n const nextNodeId = resolveNext(node, answerId)\n\n return this.moveTo({ ...input, context: updatedContext }, nextNodeId)\n }\n\n private moveTo(input: FlowStepInput, nextNodeId: string | undefined): FlowStepResult {\n if (!nextNodeId) return { kind: 'terminal', context: input.context }\n if (isCrossFlowTarget(nextNodeId))\n return { kind: 'cross-flow', flowKey: crossFlowKey(nextNodeId), context: input.context }\n if (!input.graph.nodes[nextNodeId]) return { kind: 'terminal', context: input.context }\n return { kind: 'advanced', nodeId: nextNodeId, context: input.context }\n }\n\n // Avança pelo grafo até parar num ponto que exige algo de fora: uma pergunta esperando resposta\n // do cliente, o fim do fluxo, ou um salto para outro fluxo. Nós de condição e ação não pedem\n // input nenhum, então parar neles obrigaria o chamador a reimplementar este laço — e sem\n // proteção, um grafo com ciclo de condições (A→B→A) o prenderia para sempre.\n //\n // O corte por `maxSteps` é uma rede de segurança de runtime, não a defesa principal: um ciclo é\n // erro de autoria e deveria ser barrado na publicação. Como validateGraph (no editor) hoje\n // detecta nó inalcançável mas NÃO detecta ciclo, um grafo cíclico é publicável — então o\n // runtime precisa sobreviver a ele em vez de travar o processo que atende o webhook.\n async run(input: FlowStepInput, options?: { maxSteps?: number }): Promise<FlowRunResult> {\n const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS\n const visited: string[] = [input.currentNodeId]\n\n let current: FlowStepInput = input\n let steps = 0\n\n while (steps < maxSteps) {\n const result = await this.step(current)\n steps++\n\n if (result.kind !== 'advanced') return { ...result, visited, steps }\n\n visited.push(result.nodeId)\n // Só a primeira iteração consome a resposta do cliente; daí em diante o laço percorre\n // nós automáticos (condição/ação). Manter userAnswer faria a mesma resposta ser\n // reaplicada em cada pergunta seguinte, pulando-as sem o cliente ter respondido.\n current = { ...current, currentNodeId: result.nodeId, context: result.context, userAnswer: undefined }\n }\n\n return { kind: 'max-steps-exceeded', context: current.context, visited, steps }\n }\n}\n","import type {\n ChannelAdapterInterface,\n FlowActionHandler,\n ObjectStorageInterface,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowMediaRepository } from '../repositories/FlowMediaRepository'\nimport type { LogMessageParams } from '../use-cases/LogMessage.use-case'\n\n/**\n * Porta de escrita no transcript, e não a classe `LogMessageUseCase`.\n *\n * Exigir a classe amarraria a action a quem já guarda as mensagens nas tabelas do módulo — um host\n * em migração, com o transcript ainda no schema dele, não conseguiria usar a única action built-in\n * sem gravar o envio numa tabela que o painel dele não lê. O que a action precisa é só de um lugar\n * para registrar o que saiu.\n */\n/**\n * Onde guardar o `mediaId` que a Meta devolveu para cada arquivo da biblioteca.\n *\n * Porta, e não tabela: o módulo não sabe se o host guarda isso numa coluna, no Redis ou em memória.\n * O que ele precisa é reaproveitar o id em vez de ressubir o mesmo binário para cada cliente que\n * passa pelo nó — a Meta aceita reusar por 30 dias.\n */\nexport type FlowMediaIdStore = {\n get(params: { flowMediaId: string; senderKey: string }): Promise<string | undefined>\n set(params: { flowMediaId: string; senderKey: string; mediaId: string }): Promise<void>\n clear(params: { flowMediaId: string; senderKey: string }): Promise<void>\n}\n\n/**\n * O `senderKey` vem junto do store, e não solto, para não existir a forma inválida.\n *\n * O `mediaId` é escopado ao número remetente (`phone_number_id`) na Meta: cachear sem separar por\n * número faz o id de um número ser mandado pelo outro, e o envio falha. Exigi-lo dentro do mesmo\n * objeto torna \"liguei o cache e esqueci o número\" impossível de escrever.\n */\nexport type FlowMediaIdCache = {\n readonly store: FlowMediaIdStore\n readonly senderKey: string\n}\n\nexport type FlowMediaTranscriptLogger = {\n execute(params: LogMessageParams): Promise<unknown>\n}\n\nexport type CreateSendMediaActionParams = {\n flowMediaRepository: FlowMediaRepository\n // Precisa de `getObject` — sem ele não há como reenviar o arquivo, e o módulo nem registra\n // esta action (ver createMetaWhatsAppModule).\n objectStorage: ObjectStorageInterface & { getObject: NonNullable<ObjectStorageInterface['getObject']> }\n logMessage: FlowMediaTranscriptLogger\n /**\n * Reaproveitamento do arquivo já subido. Capacidade por ausência: sem o cache, cada envio ressobe\n * o binário — exatamente o comportamento anterior, sem flag para desligar.\n */\n mediaIdCache?: FlowMediaIdCache\n startState: SessionState\n // Falha de envio de UM arquivo não deve derrubar a conversa — o cliente ficaria travado num nó\n // automático por causa de um PDF. O host recebe o erro por aqui para alertar/observar.\n onError?: (error: unknown, details: { flowKey: string; nodeId: string; uploadId: string }) => void\n}\n\nfunction mediaTypeFor(mimeType: string): string {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (mimeType.startsWith('video/')) return 'video'\n return 'document'\n}\n\n/**\n * Action `send_media`: ao passar pelo nó, envia os arquivos que a biblioteca tem anexados a ele.\n *\n * É a única action built-in que o módulo implementa de ponta a ponta — as outras\n * (`handoff`, `send_product_list`) dependem de regra de negócio do produto. Esta não: \"mandar\n * estes arquivos ao chegar aqui\" é comportamento de canal, e replicá-la em cada host seria\n * copiar o mesmo código com os mesmos bugs.\n *\n * O nó não guarda os arquivos em `actionParams` de propósito. Se guardasse, trocar o material\n * exigiria editar e republicar o grafo — e o ponto todo é o contrário: quem cuida do conteúdo\n * troca o arquivo na biblioteca e o fluxo continua igual.\n */\n/**\n * Envia um anexo, reaproveitando o arquivo já subido quando houver cache.\n *\n * O caminho rápido não faz NENHUMA das duas coisas caras: não baixa o binário do storage e não o\n * sobe para a Meta. Sobra a chamada de envio.\n *\n * O erro no caminho rápido cai no caminho lento em vez de propagar. O motivo é que a única falha\n * esperada ali — id expirado, ou apagado do lado da Meta — é indistinguível de uma falha de rede\n * sem inspecionar código de erro da Graph API, e tratar as duas igual custa uma tentativa a mais no\n * pior caso e acerta nos dois. Sem isso, um id vencido pararia de entregar material silenciosamente\n * até alguém notar.\n */\nasync function sendAttachment(params: {\n attachment: { id: string; uploadId: string; mimeType: string; filename: string; caption: string | null }\n channel: Pick<ChannelAdapterInterface, 'sendMedia'>\n to: string\n objectStorage: { getObject: (uploadId: string) => Promise<Buffer> }\n cache?: FlowMediaIdCache\n}): Promise<{ externalMessageId: string | null }> {\n const { attachment, channel, to, cache } = params\n const common = {\n to,\n mimeType: attachment.mimeType,\n filename: attachment.filename,\n caption: attachment.caption ?? undefined,\n }\n const cacheKey = cache ? { flowMediaId: attachment.id, senderKey: cache.senderKey } : undefined\n\n if (cache && cacheKey) {\n const knownMediaId = await cache.store.get(cacheKey)\n if (knownMediaId) {\n try {\n return await channel.sendMedia({ ...common, mediaId: knownMediaId })\n } catch {\n await cache.store.clear(cacheKey)\n }\n }\n }\n\n const buffer = await params.objectStorage.getObject(attachment.uploadId)\n const result = await channel.sendMedia({ ...common, buffer })\n\n // Grava depois do envio bem-sucedido: id que a Meta ainda não confirmou não vale cachear.\n if (cache && cacheKey && result.mediaId) await cache.store.set({ ...cacheKey, mediaId: result.mediaId })\n\n return result\n}\n\nexport function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler {\n return async ({ node, session, channel }) => {\n // `flowKey` nulo significa conversa fora de fluxo — não há de onde ler a biblioteca.\n if (!session.flowKey) return\n\n const location = { companyId: session.companyId, flowKey: session.flowKey, nodeId: node.id }\n const attachments = await params.flowMediaRepository.listActive(location)\n\n // Sequencial, e não Promise.all: o cliente vê as mensagens na ordem em que chegam, e disparar\n // em paralelo entregaria o folder antes da tabela de preços conforme a latência de cada upload.\n for (const attachment of attachments) {\n try {\n const { externalMessageId } = await sendAttachment({\n attachment,\n channel,\n to: session.whatsappNumber,\n objectStorage: params.objectStorage,\n ...(params.mediaIdCache ? { cache: params.mediaIdCache } : {}),\n })\n\n /**\n * Registra no transcript, mas NÃO liga em `documents`: aquela tabela tem índice único em\n * (companyId, uploadId), então o mesmo arquivo da biblioteca — que por definição vai para\n * todo cliente que passar pelo nó — falharia já no segundo envio. O `uploadId` fica no\n * payload, que é o que o painel precisa para renderizar e baixar.\n */\n await params.logMessage.execute({\n companyId: session.companyId,\n whatsappNumber: session.whatsappNumber,\n direction: 'outbound',\n sender: 'bot',\n agentUserId: null,\n type: mediaTypeFor(attachment.mimeType),\n content: attachment.caption ?? attachment.filename,\n payload: {\n filename: attachment.filename,\n mimeType: attachment.mimeType,\n uploadId: attachment.uploadId,\n flowMediaId: attachment.id,\n },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n } catch (error) {\n params.onError?.(error, { flowKey: location.flowKey, nodeId: node.id, uploadId: attachment.uploadId })\n }\n }\n }\n}\n","import type {\n CatalogPort,\n CatalogProduct,\n FlowActionHandler,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowMediaTranscriptLogger } from './createSendMediaAction'\n\n/** Tetos da Meta para `interactive.product_list`. Acima disso a mensagem inteira é recusada. */\nexport const PRODUCT_LIST_LIMIT = {\n ITEMS: 30,\n SECTIONS: 10,\n} as const\n\nexport type CreateSendProductListActionParams = {\n catalog: CatalogPort\n catalogId: string\n logMessage: FlowMediaTranscriptLogger\n startState: SessionState\n /** Falha de UMA vitrine não pode travar a conversa; o host observa por aqui. */\n onError?: (error: unknown, details: { flowKey: string; nodeId: string }) => void\n}\n\ntype ProductListParams = {\n readonly headerText?: string\n readonly bodyText?: string\n readonly footerText?: string\n readonly search?: string\n readonly sectionTitle?: string\n}\n\n/**\n * Action `send_product_list`: ao passar pelo nó, envia a vitrine do catálogo publicado na Meta.\n *\n * O nó guarda **critério**, não a lista de produtos. Congelar `retailerId` em `actionParams` faria\n * o fluxo continuar oferecendo o item esgotado — ou o item excluído, que a Meta recusa junto com a\n * mensagem inteira. O que o operador edita é o texto e o filtro; o estoque manda no resto.\n *\n * Só produto em estoque entra. Vitrine que mostra o que não tem produz a pior conversa possível:\n * o cliente escolhe, responde, e recebe \"acabou\".\n */\nexport function createSendProductListAction(params: CreateSendProductListActionParams): FlowActionHandler {\n return async ({ node, session, channel }) => {\n // Canal sem vitrine (widget, ou dublê de teste): a capacidade some por ausência do método.\n if (!channel.sendProductList) return\n\n const actionParams = (node.actionParams ?? {}) as ProductListParams\n\n try {\n const available = await listAvailableProducts({\n catalog: params.catalog,\n catalogId: params.catalogId,\n ...(actionParams.search ? { search: actionParams.search } : {}),\n })\n\n // Sem item disponível não sai vitrine vazia: a Meta recusa `sections` sem produto, e o nó\n // seguinte do fluxo é quem sabe o que dizer para quem chegou com o estoque zerado.\n if (available.length === 0) return\n\n const bodyText = actionParams.bodyText ?? 'Veja o que temos disponível:'\n\n const { externalMessageId } = await channel.sendProductList({\n to: session.whatsappNumber,\n headerText: actionParams.headerText ?? 'Nossos produtos',\n body: bodyText,\n ...(actionParams.footerText ? { footerText: actionParams.footerText } : {}),\n sections: [\n {\n title: actionParams.sectionTitle ?? 'Disponíveis',\n retailerIds: available.map((product) => product.retailerId),\n },\n ],\n })\n\n /**\n * O transcript guarda o texto e a contagem, e não o preço de cada item: o painel precisa\n * saber que a vitrine saiu, e o histórico de conversa não é lugar de tabela de preços — ele\n * envelheceria errado no dia seguinte ao reajuste.\n */\n await params.logMessage.execute({\n companyId: session.companyId,\n whatsappNumber: session.whatsappNumber,\n direction: 'outbound',\n sender: 'bot',\n agentUserId: null,\n type: 'interactive',\n content: bodyText,\n payload: { kind: 'product_list', productCount: available.length },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n } catch (error) {\n params.onError?.(error, { flowKey: session.flowKey ?? '', nodeId: node.id })\n }\n }\n}\n\nasync function listAvailableProducts(params: {\n catalog: CatalogPort\n catalogId: string\n search?: string\n}): Promise<readonly CatalogProduct[]> {\n const products = await params.catalog.listProducts({\n catalogId: params.catalogId,\n ...(params.search ? { search: params.search } : {}),\n })\n\n return products.filter((product) => product.availability === 'in stock').slice(0, PRODUCT_LIST_LIMIT.ITEMS)\n}\n","import { and, asc, eq, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { flowMedia, type FlowMediaRow } from '../schema/schema'\n\nexport type FlowMediaLocation = {\n companyId: string\n flowKey: string\n nodeId: string\n}\n\nexport type AttachFlowMediaParams = FlowMediaLocation & {\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n caption?: string\n sortOrder?: number\n}\n\nexport type UpdateFlowMediaParams = {\n companyId: string\n id: string\n caption?: string | null\n sortOrder?: number\n active?: boolean\n}\n\n// Biblioteca de mídia por nó de fluxo. Toda consulta é escopada por companyId — a chave do fluxo\n// é escolhida por quem edita e repete entre empresas, então filtrar só por (flowKey, nodeId)\n// vazaria arquivo de um tenant para outro.\nexport class FlowMediaRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n private locationFilter(location: FlowMediaLocation) {\n return and(\n eq(flowMedia.companyId, location.companyId),\n eq(flowMedia.flowKey, location.flowKey),\n eq(flowMedia.nodeId, location.nodeId),\n )\n }\n\n // O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:\n // é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente\n // justamente o material que alguém desligou.\n async listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]> {\n return this.db\n .select()\n .from(flowMedia)\n .where(and(this.locationFilter(location), eq(flowMedia.active, true)))\n .orderBy(asc(flowMedia.sortOrder), asc(flowMedia.createdAt))\n }\n\n // Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para\n // poder ser religado.\n async listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]> {\n return this.db\n .select()\n .from(flowMedia)\n .where(this.locationFilter(location))\n .orderBy(asc(flowMedia.sortOrder), asc(flowMedia.createdAt))\n }\n\n /**\n * Anexa um arquivo já existente no storage ao nó.\n *\n * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no\n * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de\n * quem está montando o fluxo.\n */\n async attach(params: AttachFlowMediaParams): Promise<FlowMediaRow> {\n const [row] = await this.db\n .insert(flowMedia)\n .values({\n companyId: params.companyId,\n flowKey: params.flowKey,\n nodeId: params.nodeId,\n uploadId: params.uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.sizeBytes,\n caption: params.caption ?? null,\n sortOrder: params.sortOrder ?? 0,\n })\n .onConflictDoUpdate({\n target: [flowMedia.companyId, flowMedia.flowKey, flowMedia.nodeId, flowMedia.uploadId],\n set: {\n caption: params.caption ?? null,\n sortOrder: params.sortOrder ?? 0,\n active: true,\n updatedAt: sql`now()`,\n },\n })\n .returning()\n\n return row as FlowMediaRow\n }\n\n async update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined> {\n const [row] = await this.db\n .update(flowMedia)\n .set({\n ...(params.caption !== undefined ? { caption: params.caption } : {}),\n ...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),\n ...(params.active !== undefined ? { active: params.active } : {}),\n updatedAt: sql`now()`,\n })\n .where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.id, params.id)))\n .returning()\n\n return row as FlowMediaRow | undefined\n }\n\n /**\n * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a\n * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o\n * host, que é dono da biblioteca de arquivos.\n */\n async detach(params: { companyId: string; id: string }): Promise<void> {\n await this.db.delete(flowMedia).where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.id, params.id)))\n }\n\n // Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.\n async detachRemovedNodes(params: { companyId: string; flowKey: string; existingNodeIds: string[] }): Promise<void> {\n const condition =\n params.existingNodeIds.length === 0 ? undefined : sql`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`\n\n await this.db\n .delete(flowMedia)\n .where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.flowKey, params.flowKey), condition))\n }\n}\n","import { eq, sql } from 'drizzle-orm'\n\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { FlowMediaIdStore } from '../flows/createSendMediaAction'\nimport { flowMedia } from '../schema/schema'\n\n/**\n * Store padrão do cache de `mediaId`, na própria linha da biblioteca de mídia.\n *\n * A porta `FlowMediaIdStore` existe para o host poder escolher Redis ou memória. Esta é a\n * implementação que o módulo já traz pronta, para o caso comum não exigir nada de quem instala: a\n * tabela é do módulo, a coluna é do módulo, e sem isso todo host reescreveria o mesmo repositório.\n *\n * Um mapa por `phone_number_id` na mesma linha, e não uma tabela à parte: o id não existe sem o\n * arquivo, morre com ele, e a linha é lida de qualquer forma no envio.\n */\nexport class FlowMediaIdRepository implements FlowMediaIdStore {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n async get(params: { flowMediaId: string; senderKey: string }): Promise<string | undefined> {\n const rows = await this.db\n .select({ ids: flowMedia.metaMediaIds })\n .from(flowMedia)\n .where(eq(flowMedia.id, params.flowMediaId))\n .limit(1)\n\n return rows[0]?.ids?.[params.senderKey]\n }\n\n /**\n * Grava só a chave deste número.\n *\n * `jsonb_set` no banco, e não ler-alterar-escrever na aplicação: dois clientes passando pelo nó\n * ao mesmo tempo com números diferentes leriam o mesmo mapa e o último gravaria por cima,\n * apagando o id do outro. A escrita atômica não tem essa janela.\n */\n async set(params: { flowMediaId: string; senderKey: string; mediaId: string }): Promise<void> {\n await this.db\n .update(flowMedia)\n .set({\n metaMediaIds: sql`jsonb_set(${flowMedia.metaMediaIds}, ARRAY[${params.senderKey}::text], to_jsonb(${params.mediaId}::text), true)`,\n })\n .where(eq(flowMedia.id, params.flowMediaId))\n }\n\n /** Remove só a chave deste número — o id do outro número continua válido. */\n async clear(params: { flowMediaId: string; senderKey: string }): Promise<void> {\n await this.db\n .update(flowMedia)\n .set({ metaMediaIds: sql`${flowMedia.metaMediaIds} - ${params.senderKey}::text` })\n .where(eq(flowMedia.id, params.flowMediaId))\n }\n}\n","import { WhatsAppWindowExpiredError as ProviderWindowExpiredError } from '@adatechnology/meta-graph-core'\nimport type { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider'\nimport {\n WindowExpiredError,\n type ChannelAdapterInterface,\n type ObjectStorageInterface,\n type SendChannelMediaParams,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { resolvePreviewUploadId } from './previewMedia'\n\n/**\n * Leitura de mídia do simulador de conversa.\n *\n * **Atrás de flag de propósito, e não ligado por omissão.** Aceitar id que não veio da Meta é\n * exatamente o que um webhook forjado exploraria: bastaria mandar `preview-upload:<chave>` para\n * fazer o servidor ler um objeto arbitrário do storage e devolvê-lo. Num ambiente de simulação isso\n * é o recurso; em produção é leitura arbitrária. Quem liga assume, e a decisão fica visível no\n * lugar onde o módulo é montado.\n */\nexport type PreviewMediaSupport = {\n readonly isEnabled: boolean\n readonly objectStorage: ObjectStorageInterface & {\n getObject: NonNullable<ObjectStorageInterface['getObject']>\n }\n /** Mime a devolver, já que o storage guarda bytes e não o tipo. Padrão `audio/ogg`. */\n readonly defaultMimeType?: string\n}\n\n// T5.2 — implementa a porta ChannelAdapterInterface sobre o meta-whatsapp-provider. A camada de\n// conversa fala só com esta porta e nunca com a Graph API, o que é o que permite trocar o canal\n// (ou usar um dublê em teste) sem tocar no motor de fluxo.\n//\n// Também traduz os erros do provider (que são da família MetaGraphError) para os erros de\n// domínio do módulo — o host trata WindowExpiredError sem precisar conhecer meta-graph-core.\nexport class WhatsAppChannelAdapter implements ChannelAdapterInterface {\n constructor(\n private readonly messages: WhatsAppMessageProvider,\n /**\n * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante\n * que atualizar o pacote não abre nada em quem não pediu.\n */\n private readonly previewMedia?: PreviewMediaSupport,\n ) {}\n\n private async translateErrors<TResult>(operation: () => Promise<TResult>): Promise<TResult> {\n try {\n return await operation()\n } catch (error) {\n if (error instanceof ProviderWindowExpiredError) throw new WindowExpiredError()\n throw error\n }\n }\n\n async sendText(to: string, body: string): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() => this.messages.sendText(to, body))\n return { externalMessageId: result.waMessageId }\n }\n\n async sendMedia(params: SendChannelMediaParams): Promise<{\n externalMessageId: string | null\n mediaId?: string | undefined\n }> {\n const result = await this.translateErrors(() => this.messages.sendMedia(params))\n return { externalMessageId: result.waMessageId, mediaId: result.mediaId }\n }\n\n async sendTemplate(params: {\n to: string\n templateName: string\n languageCode: string\n bodyParameters?: string[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() => this.messages.sendTemplate(params))\n return { externalMessageId: result.waMessageId }\n }\n\n async sendInteractiveList(params: {\n to: string\n body: string\n buttonLabel: string\n rows: { id: string; title: string }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendInteractiveList({\n to: params.to,\n bodyText: params.body,\n buttonText: params.buttonLabel,\n sections: [{ title: params.buttonLabel, rows: params.rows }],\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n async sendInteractiveButtons(params: {\n to: string\n body: string\n buttons: { id: string; title: string }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendInteractiveButtons({\n to: params.to,\n bodyText: params.body,\n buttons: params.buttons,\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n async sendProductList(params: {\n to: string\n headerText: string\n body: string\n footerText?: string\n sections: { title: string; retailerIds: string[] }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendProductListMessage({\n to: params.to,\n headerText: params.headerText,\n bodyText: params.body,\n ...(params.footerText ? { footerText: params.footerText } : {}),\n sections: params.sections,\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n /**\n * Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.\n *\n * O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e\n * tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.\n */\n async fetchMediaAsBase64(mediaId: string): Promise<{ data: string; mimeType: string }> {\n const uploadId = this.previewMedia?.isEnabled ? resolvePreviewUploadId(mediaId) : undefined\n\n if (uploadId) {\n const buffer = await this.previewMedia!.objectStorage.getObject(uploadId)\n return {\n data: buffer.toString('base64'),\n mimeType: this.previewMedia!.defaultMimeType ?? 'audio/ogg',\n }\n }\n\n return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId))\n }\n}\n","/**\n * A convenção de mídia do simulador mora em `@adatechnology/meta-whatsapp-contracts`.\n *\n * Reexportada aqui só por conveniência de quem já importa do módulo. A definição NÃO é duplicada de\n * propósito: o front também precisa dela, e foi justamente uma cópia de cada lado — com comentários\n * pedindo \"precisa bater com a outra\" — que fez o recurso existir num produto e faltar no outro.\n */\n\nexport {\n PREVIEW_MEDIA_ID_PREFIX,\n toPreviewMediaId,\n resolvePreviewUploadId,\n} from '@adatechnology/meta-whatsapp-contracts'\n","import {\n whatsAppWebhookPayloadSchema,\n resolveContactProfileName,\n whatsAppTemplateStatusUpdateSchema,\n whatsAppPhoneNumberQualityUpdateSchema,\n WHATSAPP_WEBHOOK_FIELDS,\n type MetaWhatsAppHooks,\n type WhatsAppWebhookChange,\n type SessionState,\n type WhatsAppMessage,\n type WhatsAppStatus,\n type MessageStatus,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { LogMessageUseCase } from '../use-cases/LogMessage.use-case'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport {\n verifyWebhookSignature,\n claimWebhookDelivery,\n confirmWebhookDelivery,\n type NonceStoreInterface,\n} from './webhookSecurity'\nimport { extractMediaDescriptor } from './IngestInboundMedia.use-case'\nimport {\n InboundEffectsDispatcher,\n buildInboundJobId,\n extractAnswer,\n type InboundDispatchJob,\n type InboundDispatchQueueInterface,\n} from './inboundDispatch'\n\nexport type ReceiveWebhookParams = {\n companyId: string\n rawBody: Buffer | string\n signatureHeader: string | null | undefined\n}\n\nexport type ReceiveWebhookResult = {\n // Sempre 200 para a Meta, mesmo em duplicata — ela desativa webhooks que respondem erro.\n duplicate: boolean\n messagesProcessed: number\n statusesProcessed: number\n // Eventos de outro número da mesma WABA. Contados em vez de silenciados: um filtro que descarta\n // sem deixar rastro é indistinguível de um webhook que parou de chegar.\n ignoredForeignNumber: number\n // Eventos de conta (template, qualidade do número) entregues aos hooks do host.\n accountEventsProcessed: number\n // Chegaram e não foram tratados — field sem handler, ou corpo fora do schema.\n unhandledEvents: number\n}\n\nconst EMPTY_RESULT: Omit<ReceiveWebhookResult, 'duplicate'> = {\n messagesProcessed: 0,\n statusesProcessed: 0,\n ignoredForeignNumber: 0,\n accountEventsProcessed: 0,\n unhandledEvents: 0,\n}\n\n// Texto legível para o transcript. Uma mensagem interativa traz título E id; guardamos o título\n// (é o que o cliente viu), com o id como fallback quando a Meta não manda título.\nfunction extractContent(message: WhatsAppMessage): string | null {\n if (message.text?.body) return message.text.body\n const interactive = message.interactive\n if (interactive?.button_reply) return interactive.button_reply.title || interactive.button_reply.id\n if (interactive?.list_reply) return interactive.list_reply.title || interactive.list_reply.id\n if (message.order) {\n const items = message.order.product_items.reduce((sum, item) => sum + item.quantity, 0)\n return `Pedido: ${items} item(ns)`\n }\n return message.image?.caption ?? message.document?.caption ?? null\n}\n\nfunction extractPayload(message: WhatsAppMessage): Record<string, unknown> | null {\n const payload: Record<string, unknown> = {}\n if (message.interactive) payload['interactive'] = message.interactive\n if (message.order) payload['order'] = message.order\n if (message.image) payload['image'] = message.image\n if (message.audio) payload['audio'] = message.audio\n if (message.video) payload['video'] = message.video\n if (message.document) payload['document'] = message.document\n if (message.sticker) payload['sticker'] = message.sticker\n if (message.context?.referred_product) payload['referredProduct'] = message.context.referred_product\n return Object.keys(payload).length > 0 ? payload : null\n}\n\nconst ACCOUNT_EVENT_FIELDS: readonly string[] = [\n WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE,\n WHATSAPP_WEBHOOK_FIELDS.PHONE_NUMBER_QUALITY_UPDATE,\n]\n\nfunction isAccountEventField(field: string | undefined): boolean {\n return field !== undefined && ACCOUNT_EVENT_FIELDS.includes(field)\n}\n\n// T5.1 — porta de entrada do canal. Ordem deliberada: assinatura → anti-replay → parse → efeito.\n// Verificar a assinatura antes de qualquer parse evita gastar CPU (e superfície do zod) com\n// corpo não autenticado.\nexport class ReceiveWebhookUseCase {\n constructor(\n private readonly params: {\n appSecret: string\n phoneNumberId: string\n nonceStore: NonceStoreInterface\n sessionRepository: SessionRepository\n messageRepository: MessageRepository\n logMessage: LogMessageUseCase\n startState: SessionState\n hooks?: MetaWhatsAppHooks\n realtime?: RealtimeNotifierInterface\n /**\n * Sem fila o módulo se comporta como sempre: os hooks rodam dentro da requisição do webhook.\n * Configurá-la é o que faz a conversa sobreviver a um deploy no meio do atendimento.\n */\n inboundQueue?: InboundDispatchQueueInterface\n },\n ) {\n this.dispatcher = new InboundEffectsDispatcher({\n sessionRepository: params.sessionRepository,\n ...(params.hooks ? { hooks: params.hooks } : {}),\n ...(params.realtime ? { realtime: params.realtime } : {}),\n })\n }\n\n private readonly dispatcher: InboundEffectsDispatcher\n\n async execute(input: ReceiveWebhookParams): Promise<ReceiveWebhookResult> {\n verifyWebhookSignature({\n rawBody: input.rawBody,\n signatureHeader: input.signatureHeader,\n appSecret: this.params.appSecret,\n })\n\n const claimed = await claimWebhookDelivery({\n nonceStore: this.params.nonceStore,\n signatureHeader: input.signatureHeader!,\n })\n if (!claimed) return { duplicate: true, ...EMPTY_RESULT }\n\n const rawText = typeof input.rawBody === 'string' ? input.rawBody : input.rawBody.toString('utf8')\n // `safeParse`, não `parse`: um envelope que não bate com o schema não pode derrubar a entrega\n // inteira. A Meta desativa webhook que responde erro com frequência, e o corpo já veio\n // autenticado pela assinatura — então o que resta é reportar ao host e responder 200.\n const parsed = whatsAppWebhookPayloadSchema.safeParse(JSON.parse(rawText))\n if (!parsed.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: undefined,\n reason: 'invalid-shape',\n value: rawText,\n })\n return { duplicate: false, ...EMPTY_RESULT, unhandledEvents: 1 }\n }\n const payload = parsed.data\n\n let messagesProcessed = 0\n let statusesProcessed = 0\n let ignoredForeignNumber = 0\n let accountEventsProcessed = 0\n let unhandledEvents = 0\n\n for (const entry of payload.entry) {\n for (const change of entry.changes) {\n // Eventos de conta são resolvidos antes do filtro de número: eles não têm `metadata`, e o\n // filtro abaixo depende dele. O `phone_number_quality_update` traz o número no próprio\n // corpo (`display_phone_number`), não em `metadata`.\n if (isAccountEventField(change.field)) {\n const handled = await this.handleAccountEvent(change)\n if (handled) accountEventsProcessed++\n else unhandledEvents++\n continue\n }\n\n // Uma WABA comporta vários números, e a Meta entrega os eventos de TODOS eles para cada app\n // inscrito na conta — a inscrição é por WABA, não por número. Sem este filtro, uma instância\n // responde o cliente de um número que não é dela e grava esse contato na própria base.\n // Metadata ausente segue processando: só a Meta preenche esse campo, e não há como decidir\n // pertencimento sem ele.\n const targetNumber = change.value.metadata?.phone_number_id\n if (targetNumber && targetNumber !== this.params.phoneNumberId) {\n ignoredForeignNumber++\n continue\n }\n\n for (const message of change.value.messages ?? []) {\n // O nome sai do `change`, e não da mensagem: a Meta o entrega em `contacts`, ao lado das\n // mensagens, e não dentro de cada uma.\n const profileName = resolveContactProfileName({ contacts: change.value.contacts, from: message.from })\n await this.handleMessage(input.companyId, message, profileName)\n messagesProcessed++\n }\n for (const status of change.value.statuses ?? []) {\n await this.handleStatus(input.companyId, status)\n statusesProcessed++\n }\n\n // Field desconhecido que também não trouxe nada de conversa: sem isto, um campo assinado\n // por engano no painel seria indistinguível de webhook mudo.\n const carriedConversationData =\n (change.value.messages?.length ?? 0) > 0 ||\n (change.value.message_echoes?.length ?? 0) > 0 ||\n (change.value.statuses?.length ?? 0) > 0\n if (!carriedConversationData) {\n unhandledEvents++\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'unknown-field',\n value: change.value,\n })\n }\n }\n }\n\n // Só aqui a entrega vira \"processada\". Se qualquer coisa acima lançar, o claim curto expira e\n // a reentrega da Meta ainda encontra a porta aberta — que é o ponto de todo este desenho.\n await confirmWebhookDelivery({\n nonceStore: this.params.nonceStore,\n signatureHeader: input.signatureHeader!,\n })\n\n return {\n duplicate: false,\n messagesProcessed,\n statusesProcessed,\n ignoredForeignNumber,\n accountEventsProcessed,\n unhandledEvents,\n }\n }\n\n // Devolve `false` quando o corpo não bate com o schema do field — o chamador conta como não\n // tratado. O host é avisado nos dois casos, mas com `reason` diferente.\n private async handleAccountEvent(change: WhatsAppWebhookChange): Promise<boolean> {\n if (change.field === WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE) {\n const update = whatsAppTemplateStatusUpdateSchema.safeParse(change.value)\n if (!update.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'invalid-shape',\n value: change.value,\n })\n return false\n }\n await this.params.hooks?.onTemplateStatusUpdate?.(update.data)\n return true\n }\n\n const quality = whatsAppPhoneNumberQualityUpdateSchema.safeParse(change.value)\n if (!quality.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'invalid-shape',\n value: change.value,\n })\n return false\n }\n await this.params.hooks?.onPhoneNumberQualityUpdate?.(quality.data)\n return true\n }\n\n private async handleMessage(companyId: string, message: WhatsAppMessage, profileName?: string): Promise<void> {\n // Persistir fica na requisição de propósito: é escrita local, custa pouco, e é o que garante\n // que a mensagem do cliente existe no banco mesmo que tudo depois dela falhe.\n const saved = await this.params.logMessage.execute({\n companyId,\n whatsappNumber: message.from,\n direction: 'inbound',\n sender: 'customer',\n type: message.type,\n content: extractContent(message),\n payload: extractPayload(message),\n waMessageId: message.id,\n status: 'received',\n startState: this.params.startState,\n })\n // Mensagem repetida (mesmo waMessageId já gravado): não dispara o hook de novo, senão a\n // regra de negócio do host rodaria duas vezes para a mesma mensagem do cliente.\n if (!saved) return\n\n const media = extractMediaDescriptor(saved)\n await this.dispatch({\n kind: 'message',\n companyId,\n message,\n savedMessageId: saved.id,\n ...(media ? { media } : {}),\n ...(profileName ? { profileName } : {}),\n receivedAt: Date.now(),\n })\n }\n\n private async handleStatus(companyId: string, status: WhatsAppStatus): Promise<void> {\n const updated = await this.params.messageRepository.updateMessageStatus(\n companyId,\n status.id,\n status.status as MessageStatus,\n )\n if (!updated) return\n\n this.params.realtime?.emit(`conv:${updated.whatsappNumber}`, 'message-status', {\n waMessageId: status.id,\n status: status.status,\n })\n\n await this.dispatch({\n kind: 'status',\n companyId,\n status,\n whatsappNumber: updated.whatsappNumber,\n receivedAt: Date.now(),\n })\n }\n\n // Com fila configurada, os efeitos saem da requisição do webhook; sem ela, rodam aqui mesmo e o\n // comportamento é o de sempre. É o mesmo `InboundEffectsDispatcher` nos dois caminhos.\n private async dispatch(job: InboundDispatchJob): Promise<void> {\n if (this.params.inboundQueue) {\n await this.params.inboundQueue.enqueue(job, { jobId: buildInboundJobId(job) })\n return\n }\n await this.dispatcher.run(job)\n }\n}\n\nexport { extractAnswer, extractContent }\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * O algoritmo mora em `meta-graph-core`, compartilhado com os outros objetos que a Meta assina.\n * Aqui fica só a tradução para o vocabulário deste domínio: o erro lançado e o namespace do nonce.\n */\n\nimport {\n WEBHOOK_CLAIM_TTL_SECONDS,\n WEBHOOK_NONCE_TTL_SECONDS,\n buildWebhookDeliveryKey,\n isValidWebhookChallenge,\n isValidWebhookSignature,\n} from '@adatechnology/meta-graph-core'\nimport { InvalidWebhookSignatureError } from '@adatechnology/meta-whatsapp-contracts'\n\nexport interface NonceStoreInterface {\n setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>\n /**\n * Estende a chave já reivindicada para a janela cheia. Um SET simples, sem NX.\n *\n * Opcional só por compatibilidade com hosts que ainda não o implementam: sem ele o claim curto\n * expira, e a Meta pode reentregar uma entrega já processada — trabalho repetido, que a dedupe\n * por `waMessageId` ainda segura antes de virar efeito visível ao cliente. Implementar é o\n * caminho correto.\n */\n confirm?(key: string, ttlSeconds: number): Promise<void>\n}\n\nexport { WEBHOOK_CLAIM_TTL_SECONDS, WEBHOOK_NONCE_TTL_SECONDS }\n\nconst WEBHOOK_NONCE_NAMESPACE = 'meta-whatsapp'\n\nexport function verifyWebhookChallenge(params: {\n mode: string | null\n token: string | null\n challenge: string | null\n expectedToken: string\n}): string {\n if (!isValidWebhookChallenge(params)) throw new InvalidWebhookSignatureError()\n return params.challenge as string\n}\n\nexport function verifyWebhookSignature(params: {\n rawBody: Buffer | string\n signatureHeader: string | null | undefined\n appSecret: string\n}): void {\n if (!isValidWebhookSignature(params)) throw new InvalidWebhookSignatureError()\n}\n\nfunction deliveryKey(signatureHeader: string): string {\n return buildWebhookDeliveryKey({\n namespace: WEBHOOK_NONCE_NAMESPACE,\n signatureHeader,\n })\n}\n\n/**\n * Reivindica a entrega por pouco tempo. O par obrigatório é `confirmWebhookDelivery` ao fim do\n * processamento — ver `WEBHOOK_CLAIM_TTL_SECONDS` para o porquê dos dois tempos.\n */\nexport async function claimWebhookDelivery(params: {\n nonceStore: NonceStoreInterface\n signatureHeader: string\n ttlSeconds?: number\n}): Promise<boolean> {\n return params.nonceStore.setIfAbsent(\n deliveryKey(params.signatureHeader),\n params.ttlSeconds ?? WEBHOOK_CLAIM_TTL_SECONDS,\n )\n}\n\n/** Só depois da entrega processada por inteiro: é isto que fecha a janela anti-replay. */\nexport async function confirmWebhookDelivery(params: {\n nonceStore: NonceStoreInterface\n signatureHeader: string\n ttlSeconds?: number\n}): Promise<void> {\n await params.nonceStore.confirm?.(deliveryKey(params.signatureHeader), params.ttlSeconds ?? WEBHOOK_NONCE_TTL_SECONDS)\n}\n","import { eq, and } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type {\n ChannelAdapterInterface,\n MetaWhatsAppHooks,\n ObjectStorageInterface,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport { messages, type MessageRow } from '../schema/schema'\nimport { resolveFailureStatus } from '../use-cases/TranscribeAudio.use-case'\nimport type { TranscriptionPolicyResolver } from '../use-cases/resolveTranscriptionPolicy'\nimport {\n TRANSCRIPTION_MODE,\n TRANSCRIPTION_STATUS,\n isAudioMimeType,\n transcriptionRetryAfterSeconds,\n type AudioTranscriber,\n type TranscriptionStatus,\n} from '../transcription.types'\n\nexport type IngestInboundMediaParams = {\n companyId: string\n messageId: string\n // ID da mídia na Meta — a URL de download expira, então o binário precisa ser copiado para o\n // storage do host antes de sumir.\n sourceMediaId: string\n mimeType: string\n filename?: string\n}\n\nexport type IngestInboundMediaResult = {\n uploadId: string\n // true quando a mídia já estava no storage e nada foi baixado de novo.\n alreadyIngested: boolean\n /**\n * Só presente quando a transcrição automática rodou nesta execução. Ausente é o normal: mídia que\n * não é áudio, transcrição desligada, modo sob demanda, ou mídia já ingerida antes.\n */\n transcription?: { status: TranscriptionStatus }\n}\n\n/**\n * Transcrição durante a ingestão — o modo `auto`.\n *\n * Entra aqui, e não em use-case separado, por um motivo só: neste ponto o buffer do áudio ACABOU de\n * ser baixado e está em memória. Transcrever fora daqui custaria um segundo download do storage por\n * áudio, e o `TranscribeAudioUseCase` existe justamente para esse caso (sob demanda e retomada).\n */\nexport type IngestTranscriptionOptions = {\n transcriber: AudioTranscriber\n messageRepository: MessageRepository\n /**\n * Política POR EMPRESA, resolvida a cada áudio. Não é um `mode` fixo porque o interruptor mora nas\n * configurações da empresa: um valor capturado na construção do use-case congelaria a escolha até\n * o próximo deploy, e o worker é um processo longo — o operador mexeria no painel e nada mudaria.\n */\n resolvePolicy: TranscriptionPolicyResolver\n languageHint?: string\n hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>\n}\n\n// T5.3 — copia mídia recebida da Meta para o storage do host.\n//\n// Não reimplementa S3: delega ao ObjectStorageInterface que o host injeta (object-storage-provider\n// ou o que ele usar). O módulo só orquestra \"baixa da Meta → entrega ao storage → referencia na\n// mensagem\".\n//\n// Idempotente por sourceMediaId gravado no payload da mensagem: o job pode ser reentregue (fila\n// com retry, webhook reprocessado) e baixar de novo custaria banda e geraria um segundo objeto\n// órfão no storage para o mesmo binário.\nexport class IngestInboundMediaUseCase {\n constructor(\n private readonly db: MetaWhatsAppDatabase,\n private readonly channel: ChannelAdapterInterface,\n private readonly objectStorage: ObjectStorageInterface,\n // Opcional: sem ele a mídia continua sendo copiada e referenciada no payload, só não entra na\n // biblioteca da conversa. Mantém compatível quem já usava o use case antes da tabela existir.\n private readonly documentRepository?: DocumentRepository,\n // Último e opcional para não quebrar quem já constrói este use-case posicionalmente. Ausente,\n // a ingestão se comporta exatamente como antes da transcrição existir.\n private readonly transcription?: IngestTranscriptionOptions,\n ) {}\n\n async execute(params: IngestInboundMediaParams): Promise<IngestInboundMediaResult> {\n const [message] = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n .limit(1)\n\n if (!message) throw new Error(`Mensagem ${params.messageId} não encontrada para ingestão de mídia`)\n\n const payload = (message.payload ?? {}) as Record<string, unknown>\n\n // Já ingerida: devolve o uploadId existente sem tocar na rede.\n //\n // Não tenta transcrever aqui de propósito. Sem o download não há buffer, e reler do storage\n // duplicaria o `TranscribeAudioUseCase`. Retomada de transcrição pendente é trabalho DELE — o\n // `onTranscriptionDeferred` carrega o `uploadId` exatamente para o host enfileirar uma\n // transcrição, não uma segunda ingestão.\n if (payload['uploadId'] && payload['sourceMediaId'] === params.sourceMediaId) {\n return { uploadId: String(payload['uploadId']), alreadyIngested: true }\n }\n\n const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId)\n const buffer = Buffer.from(data, 'base64')\n\n const { uploadId } = await this.objectStorage.upload({\n buffer,\n mimeType: mimeType || params.mimeType,\n key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`,\n })\n\n // Guarda sourceMediaId junto do uploadId: é o que torna a próxima execução idempotente.\n const updatedPayload: Record<string, unknown> = {\n ...payload,\n uploadId,\n sourceMediaId: params.sourceMediaId,\n mimeType: mimeType || params.mimeType,\n // O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o\n // exibe ao lado do tipo. Sem gravar aqui, a UI mostraria \"PDF\" sem o \"· 180 KB\", e buscá-lo\n // depois custaria uma consulta à tabela de documentos por mensagem renderizada.\n sizeBytes: buffer.length,\n ...(params.filename ? { filename: params.filename } : {}),\n }\n\n await this.db\n .update(messages)\n .set({ payload: updatedPayload })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n\n // Entra na biblioteca da conversa. Idempotente por (companyId, uploadId) no repositório, o que\n // importa aqui: a chave do objeto deriva de sourceMediaId, então a reentrega do job produz o\n // mesmo uploadId e o link não duplica.\n await this.documentRepository?.link({\n companyId: params.companyId,\n sessionId: message.sessionId,\n messageId: message.id,\n uploadId,\n // Áudio e sticker chegam sem nome; sem um rótulo o painel mostraria linha vazia.\n filename: params.filename ?? `${params.sourceMediaId}`,\n mimeType: mimeType || params.mimeType,\n sizeBytes: buffer.length,\n source: message.sender,\n })\n\n const transcription = await this.transcribeIfAuto({\n companyId: params.companyId,\n message,\n uploadId,\n buffer,\n mimeType: mimeType || params.mimeType,\n })\n\n return { uploadId, alreadyIngested: false, ...(transcription ? { transcription } : {}) }\n }\n\n /**\n * Transcreve o áudio recém-baixado, quando o modo é `auto`.\n *\n * **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da\n * conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o\n * retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir\n * um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`\n * avisa quem sabe reenfileirar.\n */\n private async transcribeIfAuto(context: {\n companyId: string\n message: MessageRow\n uploadId: string\n buffer: Buffer\n mimeType: string\n }): Promise<{ status: TranscriptionStatus } | undefined> {\n const transcription = this.transcription\n if (!transcription) return undefined\n // Vídeo, imagem e documento passam sem tocar no engine — checado ANTES de consultar a política,\n // para um PDF não custar uma leitura de `settings`.\n if (!isAudioMimeType(context.mimeType)) return undefined\n\n const policy = await transcription.resolvePolicy(context.companyId)\n if (!policy.isEnabled || policy.mode !== TRANSCRIPTION_MODE.AUTO) return undefined\n\n /**\n * Já transcrito, não transcreve de novo.\n *\n * Quem chega antes é normalmente o próprio host: o grafo de conversa transcreve no webhook\n * porque precisa do texto para responder ao cliente, e grava o resultado na mensagem. Sem esta\n * guarda o job de ingestão transcreveria o MESMO áudio uma segunda vez — duas cobranças e duas\n * chamadas ao engine para um resultado que já estava no banco.\n *\n * Reduz a corrida, não a elimina: host e worker podem ler `null` no mesmo instante. Aí um dos\n * dois grava por cima com o mesmo texto, o que é inofensivo — o desperdício fica na janela\n * estreita entre a gravação de um e a leitura do outro, em vez de acontecer sempre.\n *\n * A releitura é obrigatória. A linha de `context.message` foi lida no começo de `execute()`,\n * ANTES de baixar o binário da Meta — e é durante esse download que o host costuma gravar a\n * transcrição dele. Confiar na linha velha faria a guarda falhar justamente no caso comum.\n */\n const current = await transcription.messageRepository.findById(context.companyId, context.message.id)\n if (current?.transcriptionStatus === TRANSCRIPTION_STATUS.DONE) return undefined\n\n try {\n const result = await transcription.transcriber.transcribe({\n buffer: context.buffer,\n mimeType: context.mimeType,\n ...(transcription.languageHint ? { languageHint: transcription.languageHint } : {}),\n })\n\n await transcription.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.message.id,\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n })\n\n return { status: TRANSCRIPTION_STATUS.DONE }\n } catch (error) {\n return this.recordTranscriptionFailure(context, error)\n }\n }\n\n private async recordTranscriptionFailure(\n context: { companyId: string; message: MessageRow; uploadId: string },\n error: unknown,\n ): Promise<{ status: TranscriptionStatus }> {\n const status = resolveFailureStatus(error)\n\n await this.transcription?.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.message.id,\n status,\n })\n\n if (status === TRANSCRIPTION_STATUS.PENDING) {\n const retryAfterSeconds = transcriptionRetryAfterSeconds(error)\n await this.transcription?.hooks?.onTranscriptionDeferred?.({\n companyId: context.companyId,\n messageId: context.message.id,\n whatsappNumber: context.message.whatsappNumber,\n uploadId: context.uploadId,\n ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),\n reason: retryAfterSeconds !== undefined ? 'rate-limited' : 'transient-failure',\n error,\n })\n }\n\n return { status }\n }\n}\n\n// Extrai o descritor de mídia de uma mensagem já persistida — o webhook guarda o objeto cru da\n// Meta (image/audio/video/document/sticker) no payload, e a ingestão precisa saber qual é.\nexport function extractMediaDescriptor(\n message: MessageRow,\n): { sourceMediaId: string; mimeType: string; filename?: string } | undefined {\n const payload = (message.payload ?? {}) as Record<string, unknown>\n for (const key of ['image', 'audio', 'video', 'document', 'sticker'] as const) {\n const media = payload[key] as { id?: string; mime_type?: string; filename?: string } | undefined\n if (media?.id) {\n return {\n sourceMediaId: media.id,\n mimeType: media.mime_type ?? 'application/octet-stream',\n filename: media.filename,\n }\n }\n }\n return undefined\n}\n","import {\n AudioNotIngestedError,\n MessageNotAudioError,\n TranscriptionDisabledError,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { MetaWhatsAppHooks, ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport type { MessageRow } from '../schema/schema'\nimport type { TranscriptionPolicyResolver } from './resolveTranscriptionPolicy'\nimport {\n TRANSCRIPTION_STATUS,\n isAudioMimeType,\n isRetriableTranscriptionError,\n isUnsupportedTranscriptionError,\n transcriptionRetryAfterSeconds,\n type AudioTranscriber,\n type TranscriptionStatus,\n} from '../transcription.types'\n\n/**\n * Storage com leitura garantida. `getObject` é opcional no contrato, então quem monta este use-case\n * precisa provar que o método existe — sem os bytes não há o que transcrever, e um use-case que\n * sempre falha é pior do que a ausência ser visível no tipo.\n */\ntype ReadableObjectStorage = ObjectStorageInterface & {\n getObject: NonNullable<ObjectStorageInterface['getObject']>\n}\n\nexport type TranscribeAudioParams = {\n companyId: string\n messageId: string\n /**\n * Refaz mesmo com transcrição já salva. Serve ao \"transcrever de novo\" depois de trocar de engine\n * — sem isto, um resultado ruim do engine antigo ficaria congelado para sempre.\n */\n force?: boolean\n}\n\nexport type TranscribeAudioResult = {\n status: TranscriptionStatus\n text: string | null\n language: string | null\n engine: string | null\n /** `true` quando devolveu o que já estava salvo, sem gastar cota. */\n alreadyTranscribed: boolean\n}\n\nexport type TranscribeAudioDependencies = {\n messageRepository: MessageRepository\n objectStorage: ReadableObjectStorage\n transcriber: AudioTranscriber\n /**\n * Política por empresa. Ausente, a transcrição sob demanda não consulta configuração nenhuma e\n * atende sempre — que é o comportamento de quem controla o liga/desliga só por ambiente.\n */\n resolvePolicy?: TranscriptionPolicyResolver\n /** ISO 639-1 do produto. Informar corta a detecção do Whisper e evita pt-BR curto virar espanhol. */\n languageHint?: string\n hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>\n}\n\n/**\n * Transcreve o áudio de UMA mensagem já persistida e ingerida.\n *\n * Serve aos dois modos: é o que o painel chama no botão \"transcrever\" (`onDemand`) e é o que o host\n * chama ao retomar um `pending` reenfileirado. O modo `auto` não passa por aqui — ele transcreve\n * dentro da ingestão, onde o buffer já está em memória e não custa um segundo download.\n *\n * Idempotente por `transcription_status`: chamar de novo num `'done'` devolve o que está salvo em\n * vez de gastar cota transcrevendo o mesmo áudio.\n */\nexport class TranscribeAudioUseCase {\n constructor(private readonly dependencies: TranscribeAudioDependencies) {}\n\n async execute(params: TranscribeAudioParams): Promise<TranscribeAudioResult> {\n const message = await this.dependencies.messageRepository.findById(params.companyId, params.messageId)\n if (!message) throw new Error(`Mensagem ${params.messageId} não encontrada para transcrição`)\n\n /**\n * Devolver o que já está salvo vem ANTES da checagem de política, e de propósito: o texto já\n * existe, já aparece na conversa, e desligar o recurso depois não deve apagar nem esconder o que\n * foi transcrito enquanto estava ligado. A política governa gastar cota, não ler o passado.\n */\n if (message.transcriptionStatus === TRANSCRIPTION_STATUS.DONE && !params.force) {\n return {\n status: TRANSCRIPTION_STATUS.DONE,\n text: message.transcriptionText,\n language: message.transcriptionLanguage,\n engine: message.transcriptionEngine,\n alreadyTranscribed: true,\n }\n }\n\n // Daqui para baixo gasta cota, então a política manda.\n if (this.dependencies.resolvePolicy) {\n const policy = await this.dependencies.resolvePolicy(params.companyId)\n if (!policy.isEnabled) throw new TranscriptionDisabledError()\n }\n\n const audio = extractAudioReference(message)\n const buffer = await this.dependencies.objectStorage.getObject(audio.uploadId)\n\n return this.transcribeBuffer({ ...params, buffer, mimeType: audio.mimeType, uploadId: audio.uploadId, message })\n }\n\n private async transcribeBuffer(context: {\n companyId: string\n messageId: string\n buffer: Buffer\n mimeType: string\n uploadId: string\n message: MessageRow\n }): Promise<TranscribeAudioResult> {\n try {\n const result = await this.dependencies.transcriber.transcribe({\n buffer: context.buffer,\n mimeType: context.mimeType,\n ...(this.dependencies.languageHint ? { languageHint: this.dependencies.languageHint } : {}),\n })\n\n await this.dependencies.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.messageId,\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n })\n\n return {\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n alreadyTranscribed: false,\n }\n } catch (error) {\n await this.persistFailure(context, error)\n throw error\n }\n }\n\n /**\n * Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:\n * reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.\n */\n private async persistFailure(\n context: { companyId: string; messageId: string; uploadId: string; message: MessageRow },\n error: unknown,\n ): Promise<void> {\n const status = resolveFailureStatus(error)\n\n await this.dependencies.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.messageId,\n status,\n })\n\n if (status !== TRANSCRIPTION_STATUS.PENDING) return\n\n const retryAfterSeconds = transcriptionRetryAfterSeconds(error)\n await this.dependencies.hooks?.onTranscriptionDeferred?.({\n companyId: context.companyId,\n messageId: context.messageId,\n whatsappNumber: context.message.whatsappNumber,\n uploadId: context.uploadId,\n ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),\n reason: retryAfterSeconds !== undefined ? 'rate-limited' : 'transient-failure',\n error,\n })\n }\n}\n\nexport function resolveFailureStatus(error: unknown): TranscriptionStatus {\n if (isUnsupportedTranscriptionError(error)) return TRANSCRIPTION_STATUS.UNSUPPORTED\n return isRetriableTranscriptionError(error) ? TRANSCRIPTION_STATUS.PENDING : TRANSCRIPTION_STATUS.FAILED\n}\n\n/**\n * Onde o áudio está e em que formato.\n *\n * Lê do `payload`, que é onde a ingestão gravou `uploadId` e `mimeType` — a tabela de documentos\n * também tem o dado, mas exigiria uma consulta a mais por mensagem e não é a fonte que a ingestão\n * atualiza primeiro.\n */\nfunction extractAudioReference(message: MessageRow): { uploadId: string; mimeType: string } {\n const payload = (message.payload ?? {}) as Record<string, unknown>\n const audio = payload['audio'] as { mime_type?: string } | undefined\n const mimeType = typeof payload['mimeType'] === 'string' ? payload['mimeType'] : audio?.mime_type\n\n // `type` do host é rótulo livre (varchar(32)), então a checagem real é o mime — é ele que diz se\n // existe áudio para transcrever, independente de como o produto nomeou a mensagem.\n if (!isAudioMimeType(mimeType) && !audio) {\n throw new MessageNotAudioError(message.id, message.type)\n }\n\n const uploadId = payload['uploadId']\n // Sem uploadId a ingestão ainda não terminou. O áudio existe na Meta, mas a URL de lá expira e\n // não é este use-case que a busca — quem copia é a ingestão, e ela é assíncrona.\n if (typeof uploadId !== 'string' || uploadId.length === 0) {\n throw new AudioNotIngestedError(message.id)\n }\n\n return { uploadId, mimeType: mimeType ?? 'audio/ogg' }\n}\n","/**\n * Vocabulário de transcrição de áudio do módulo.\n *\n * Fica em arquivo próprio porque o schema e os dois use-cases (ingestão automática e sob demanda)\n * precisam dos mesmos tipos, e pendurá-los em qualquer um dos três faria os outros dois importarem\n * de dentro de uma camada que não é a deles.\n */\n\nimport type { TranscriptionMode } from '@adatechnology/meta-whatsapp-contracts'\n\nexport const TRANSCRIPTION_STATUS = {\n /** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */\n PENDING: 'pending',\n /** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */\n DONE: 'done',\n /** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */\n FAILED: 'failed',\n /** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */\n UNSUPPORTED: 'unsupported',\n} as const\n\nexport type TranscriptionStatus = (typeof TRANSCRIPTION_STATUS)[keyof typeof TRANSCRIPTION_STATUS]\n\n/**\n * Quando transcrever.\n *\n * `auto` transcreve durante a ingestão da mídia, onde o buffer do áudio JÁ está em memória — não\n * custa um segundo download do storage. `onDemand` só transcreve quando o atendente pede, o que\n * troca latência na interface por não gastar cota com áudio que ninguém vai ler.\n *\n * O tipo vem do contrato (o painel escolhe, a API transporta, o módulo obedece) e `satisfies`\n * garante em tempo de compilação que estes valores continuam sendo exatamente os de lá — sem isso,\n * um modo novo no contrato passaria despercebido aqui.\n */\nexport const TRANSCRIPTION_MODE = {\n AUTO: 'auto',\n ON_DEMAND: 'onDemand',\n} as const satisfies Record<string, TranscriptionMode>\n\nexport type { TranscriptionMode }\n\n/**\n * O contrato mínimo de `@adatechnology/audio-transcription-provider`, declarado aqui em vez de\n * importado — mesma decisão do `MessageModerator` em `LogMessage.use-case.ts`.\n *\n * Assim o módulo não ganha dependência de pacote por um recurso opcional, e transcrição não fica\n * amarrada a WhatsApp: o que atravessa esta fronteira é um buffer de áudio e um mime.\n */\nexport type AudioTranscriber = {\n readonly name: string\n transcribe: (input: {\n buffer: Buffer\n mimeType: string\n languageHint?: string\n }) => Promise<{ text: string; language?: string; durationSeconds?: number; engine: string }>\n}\n\n/**\n * Recorte do erro do provider que o módulo precisa ler para escolher entre `'pending'` e\n * `'failed'`. Estrutural, não `instanceof`: o provider é opcional e pode nem estar instalado, e um\n * `instanceof` contra classe ausente não compila.\n */\nexport function isRetriableTranscriptionError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return true\n const isRetriable = (error as { isRetriable?: unknown }).isRetriable\n // Ausente conta como retriável: erro sem carimbo é quase sempre rede, e insistir é barato.\n return typeof isRetriable === 'boolean' ? isRetriable : true\n}\n\nexport function isUnsupportedTranscriptionError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as { name?: unknown }).name === 'TranscriptionUnsupportedError'\n )\n}\n\nexport function transcriptionRetryAfterSeconds(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const retryAfter = (error as { retryAfterSeconds?: unknown }).retryAfterSeconds\n return typeof retryAfter === 'number' ? retryAfter : undefined\n}\n\n/** Mime de áudio? Só áudio é transcrito — vídeo, imagem e documento passam sem tocar no engine. */\nexport function isAudioMimeType(mimeType: string | undefined | null): boolean {\n return typeof mimeType === 'string' && mimeType.trim().toLowerCase().startsWith('audio/')\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Separa a entrega do webhook em duas metades: o que precisa acontecer antes de responder à Meta\n * (persistir a mensagem, que é escrita local e rápida) e o que pode esperar alguns milissegundos\n * (os hooks do host, que costumam falar com rede — motor de fluxo, IA, integrações).\n *\n * A segunda metade rodava dentro da requisição do webhook. Um deploy no meio de uma conversa\n * matava a chamada em voo, e o cliente ficava sem resposta sem que nada registrasse a perda. Com\n * uma fila no host, derrubar o processo vira atraso: o job espera e é retomado.\n */\n\nimport type {\n ConversationSession,\n MetaWhatsAppHooks,\n RealtimeNotifierInterface,\n WhatsAppMessage,\n WhatsAppStatus,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { SessionRow } from '../schema/schema'\n\nexport type InboundMediaDescriptor = {\n sourceMediaId: string\n mimeType: string\n filename?: string\n}\n\nexport type InboundMessageEffectsJob = {\n kind: 'message'\n companyId: string\n message: WhatsAppMessage\n savedMessageId: string\n media?: InboundMediaDescriptor\n /**\n * Nome de perfil de quem mandou. Vai no JOB e não é relido depois porque o job pode passar por\n * fila: o payload da Meta não existe mais quando o worker roda.\n *\n * Opcional, e job antigo sem o campo continua válido — é o que permite subir o módulo sem drenar\n * a fila antes.\n */\n profileName?: string\n receivedAt: number\n}\n\nexport type InboundStatusEffectsJob = {\n kind: 'status'\n companyId: string\n status: WhatsAppStatus\n whatsappNumber: string\n receivedAt: number\n}\n\nexport type InboundDispatchJob = InboundMessageEffectsJob | InboundStatusEffectsJob\n\n/**\n * Porta de fila. O módulo não escolhe a tecnologia — BullMQ, SQS, o que o host já tiver — mas\n * exige dela duas garantias, e sem as duas o padrão não entrega o que promete:\n *\n * 1. **Durabilidade**: o job sobrevive à morte do processo que o enfileirou. Uma fila em memória\n * reintroduz exatamente a perda que este desenho existe para evitar.\n * 2. **Retentativa com backoff**: o destino dos hooks (n8n, IA, API de terceiro) também cai em\n * deploy. Sem retry, o job falha uma vez e a conversa morre do mesmo jeito.\n *\n * `jobId` é estável e derivado da mensagem: reentrega da Meta e re-enfileiramento produzem o mesmo\n * id, e a fila descarta o segundo em vez de rodar o efeito duas vezes.\n */\nexport interface InboundDispatchQueueInterface {\n enqueue(job: InboundDispatchJob, options: { jobId: string }): Promise<void>\n}\n\nexport function buildInboundJobId(job: InboundDispatchJob): string {\n return job.kind === 'message'\n ? `wa-inbound-message:${job.message.id}`\n : `wa-inbound-status:${job.status.id}:${job.status.status}`\n}\n\nexport function toSessionContract(row: SessionRow): ConversationSession {\n return {\n id: row.id,\n companyId: row.companyId,\n whatsappNumber: row.whatsappNumber,\n currentState: row.currentState,\n flowKey: row.flowKey,\n currentNodeId: row.currentNodeId,\n context: row.context,\n mode: row.mode as ConversationSession['mode'],\n assignedUserId: row.assignedUserId,\n humanRequestedAt: row.humanRequestedAt?.toISOString() ?? null,\n lastInboundAt: row.lastInboundAt?.toISOString() ?? null,\n lastAgentReadAt: row.lastAgentReadAt?.toISOString() ?? null,\n lastActivity: row.lastActivity.toISOString(),\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n }\n}\n\n/**\n * A resposta que o motor de fluxo deve consumir: para interativo é o ID da opção (estável), não\n * o título (que muda quando alguém edita o texto do botão no editor).\n */\nexport function extractAnswer(message: WhatsAppMessage): string | undefined {\n const interactive = message.interactive\n if (interactive?.button_reply) return interactive.button_reply.id\n if (interactive?.list_reply) return interactive.list_reply.id\n return message.text?.body\n}\n\n/**\n * Os efeitos de host de uma entrega já persistida. Vive fora do `ReceiveWebhookUseCase` porque\n * roda nos dois lados: inline, quando o host não configurou fila, e dentro do worker, quando\n * configurou. Um só corpo de regra para os dois caminhos — duas cópias divergiriam.\n */\nexport class InboundEffectsDispatcher {\n constructor(\n private readonly params: {\n sessionRepository: SessionRepository\n hooks?: MetaWhatsAppHooks\n realtime?: RealtimeNotifierInterface\n },\n ) {}\n\n async run(job: InboundDispatchJob): Promise<void> {\n if (job.kind === 'message') return this.runMessageEffects(job)\n return this.runStatusEffects(job)\n }\n\n async runMessageEffects(job: InboundMessageEffectsJob): Promise<void> {\n const { companyId, message, savedMessageId, media } = job\n\n // Antes de qualquer retorno antecipado: os `return` abaixo (atendimento humano, sessão ausente)\n // são sobre o FLUXO DO BOT, e mídia precisa ser copiada da Meta de qualquer forma. Justamente em\n // atendimento humano é que o cliente manda documento para o atendente — deixar a ingestão\n // depois desse `return` perderia esses arquivos, e a URL da Meta expira sem segunda chance.\n if (media) {\n await this.params.hooks?.onMediaReceived?.({\n companyId,\n messageId: savedMessageId,\n whatsappNumber: message.from,\n sourceMediaId: media.sourceMediaId,\n mimeType: media.mimeType,\n ...(media.filename ? { filename: media.filename } : {}),\n })\n }\n\n const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from)\n if (!sessionRow) return\n\n // Conversa em atendimento humano não é processada pelo bot — o atendente responde.\n if (sessionRow.mode === 'human') return\n\n const outcome = await this.params.hooks?.onMessageReceived?.(\n message,\n toSessionContract(sessionRow),\n job.profileName === undefined ? {} : { profileName: job.profileName },\n )\n // 'handled' = o host já respondeu e assumiu a mensagem; o módulo não segue com o fluxo.\n if (outcome?.outcome === 'handled') return\n\n // 'continue' (ou sem hook): quem dirige o motor de fluxo é o host via runFlowStep — o\n // webhook só entrega o sinal. Expor a resposta extraída evita que cada host reimplemente\n // a lógica de \"qual é a resposta do cliente\" para interativo vs texto.\n void extractAnswer(message)\n }\n\n async runStatusEffects(job: InboundStatusEffectsJob): Promise<void> {\n const sessionRow = await this.params.sessionRepository.getContext(job.companyId, job.whatsappNumber)\n await this.params.hooks?.onStatusUpdate?.(job.status, sessionRow ? toSessionContract(sessionRow) : null)\n }\n}\n\n/**\n * Ponto de entrada do worker do host: recebe o job que a fila devolveu e roda os efeitos.\n *\n * Deixe a exceção propagar. É ela que faz a fila contar a tentativa e reagendar com backoff;\n * capturar aqui para logar transforma falha recuperável em mensagem perdida em silêncio.\n */\nexport class ProcessInboundDispatchUseCase {\n constructor(private readonly dispatcher: InboundEffectsDispatcher) {}\n\n async execute(job: InboundDispatchJob): Promise<void> {\n await this.dispatcher.run(job)\n }\n}\n","import type { SettingsRepository } from '../repositories/SettingsRepository'\nimport { TRANSCRIPTION_MODE, type TranscriptionMode } from '../transcription.types'\n\n/**\n * Política efetiva de transcrição de uma empresa.\n *\n * A separação que este arquivo existe para manter: **ambiente decide se é POSSÍVEL, settings decide\n * se é para FAZER.** A capacidade (engine, chave, storage que sabe reler) é injetada pelo host e é\n * por deploy — chave de API não vai para tabela de configuração de tenant. Já \"transcrever ou não\" e\n * \"automático ou sob demanda\" são decisão de operação de cada empresa, e pedir deploy para mudar\n * isso é o que transforma um interruptor em ticket.\n */\nexport type TranscriptionPolicy = {\n readonly isEnabled: boolean\n readonly mode: TranscriptionMode\n}\n\nexport type TranscriptionPolicyDefaults = {\n /** O que vale quando o painel não decidiu. Tipicamente vem do ambiente do host. */\n readonly isEnabled: boolean\n readonly mode: TranscriptionMode\n}\n\nexport type ResolveTranscriptionPolicyDependencies = {\n readonly settingsRepository: SettingsRepository\n readonly defaults: TranscriptionPolicyDefaults\n}\n\n/**\n * Resolve a política por empresa, com o padrão do host como base.\n *\n * Uma consulta a `settings` por áudio transcrito. Não é cacheado de propósito: é uma leitura por\n * chave primária, acontece uma vez por nota de voz (não por mensagem), e cachear introduziria a\n * pergunta \"por quanto tempo o operador continua vendo o interruptor antigo depois de mexer nele\" —\n * custo real, para economizar um índice único.\n */\nexport function createTranscriptionPolicyResolver(dependencies: ResolveTranscriptionPolicyDependencies) {\n return async function resolveTranscriptionPolicy(companyId: string): Promise<TranscriptionPolicy> {\n const settings = await dependencies.settingsRepository.get(companyId)\n\n return {\n // `??` e não `||`: `false` gravado é decisão explícita de desligar, e `||` a trocaria pelo\n // padrão do host — desligar no painel não faria nada num deploy com transcrição ligada.\n isEnabled: settings.transcriptionEnabled ?? dependencies.defaults.isEnabled,\n mode: normalizeMode(settings.transcriptionMode) ?? dependencies.defaults.mode,\n }\n }\n}\n\nexport type TranscriptionPolicyResolver = ReturnType<typeof createTranscriptionPolicyResolver>\n\n/**\n * A coluna é `varchar`, então o banco aceita qualquer string — um valor legado ou digitado à mão não\n * pode virar um modo que ninguém trata. Desconhecido cai no padrão do host.\n */\nfunction normalizeMode(value: string | null): TranscriptionMode | undefined {\n if (value === TRANSCRIPTION_MODE.AUTO) return TRANSCRIPTION_MODE.AUTO\n if (value === TRANSCRIPTION_MODE.ON_DEMAND) return TRANSCRIPTION_MODE.ON_DEMAND\n return undefined\n}\n","import type { ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport { toPreviewMediaId } from '../channel/previewMedia'\n\n/**\n * Guarda um arquivo gravado no simulador e devolve o id que o webhook vai referenciar.\n *\n * Existe como use-case para o host só precisar da ROTA: receber o corpo, chamar isto, devolver o\n * `mediaId`. A parte que erra — onde gravar, com que chave, como marcar o id para o adaptador\n * reconhecer depois — fica aqui, num lugar só, e não em cada produto.\n *\n * O SDK para exatamente na porta do HTTP: registrar endpoint é do host, e um pacote que abrisse rota\n * no servidor de quem o instala decidiria caminho, autenticação e versionamento no lugar dele.\n */\nexport type StorePreviewMediaParams = {\n companyId: string\n /** Bytes do arquivo. Quem converte de base64 é a rota — o use-case não conhece transporte. */\n buffer: Buffer\n mimeType: string\n filename?: string\n}\n\nexport type StorePreviewMediaResult = {\n /** Já com o prefixo: é isto que o simulador manda no webhook. */\n mediaId: string\n uploadId: string\n}\n\nexport class StorePreviewMediaUseCase {\n constructor(\n private readonly objectStorage: ObjectStorageInterface,\n /**\n * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque\n * um teste precisa de chave previsível.\n */\n private readonly generateKeySuffix: () => string,\n ) {}\n\n async execute(params: StorePreviewMediaParams): Promise<StorePreviewMediaResult> {\n /**\n * Namespace próprio (`preview/`) e não junto da mídia real.\n *\n * Arquivo de simulação e arquivo de cliente têm ciclo de vida diferente: a retenção varre a\n * biblioteca da conversa, e misturar os dois faria a limpeza pensar duas vezes sobre o que é\n * dado de verdade. Separado, apagar tudo que é teste é apagar um prefixo.\n */\n const key = `meta-whatsapp/${params.companyId}/preview/${this.generateKeySuffix()}`\n\n const { uploadId } = await this.objectStorage.upload({\n buffer: params.buffer,\n mimeType: params.mimeType,\n key,\n })\n\n return { mediaId: toPreviewMediaId(uploadId), uploadId }\n }\n}\n","import { join } from 'node:path'\nimport type { MetaWhatsAppDatabase } from './database.types'\n\n// Migrations append-only com journal próprio — nunca colide com as migrations do host, que roda\n// as delas por um caminho totalmente separado (ver rules/packages/pluggable-module.md §3.2).\n//\n// O journal vive em `drizzle.meta_whatsapp_migrations` (schema `drizzle`, o padrão do\n// drizzle-kit; o nome DA TABELA é que isola o módulo — o host usa `__drizzle_migrations` ali do\n// lado, sem conflito). Deliberadamente NÃO fica dentro de `meta_whatsapp`: a primeira migration\n// do módulo é justamente quem cria esse schema (CREATE SCHEMA), então apontar o journal para lá\n// gera uma corrida entre o migrator garantindo o schema da tabela de controle e a migration\n// tentando criá-lo de novo — o erro \"schema meta_whatsapp already exists\".\n//\n// Para resetar um banco de teste é preciso derrubar os dois: o schema meta_whatsapp E o journal\n// em drizzle.meta_whatsapp_migrations. Só dropar o schema faz o migrator pular tudo na próxima\n// execução, deixando o banco sem as tabelas e sem erro aparente.\nexport const META_WHATSAPP_MIGRATIONS_TABLE = 'meta_whatsapp_migrations'\n\n// Pasta das migrations embarcadas, para o host que prefere chamar o próprio migrator.\nexport function metaWhatsAppMigrationsFolder(): string {\n return join(__dirname, 'migrations')\n}\n\nexport type RunMetaWhatsAppMigrationsParams = {\n db: MetaWhatsAppDatabase\n // O `migrate` do driver do host — `drizzle-orm/node-postgres/migrator`,\n // `drizzle-orm/bun-sql/migrator`, etc. O módulo não escolhe driver pelo host.\n migrate: (db: never, config: { migrationsFolder: string; migrationsTable?: string }) => Promise<void>\n}\n\nexport async function runMetaWhatsAppMigrations(params: RunMetaWhatsAppMigrationsParams): Promise<void> {\n await params.migrate(params.db as never, {\n migrationsFolder: metaWhatsAppMigrationsFolder(),\n migrationsTable: META_WHATSAPP_MIGRATIONS_TABLE,\n })\n}\n","import { randomUUID } from 'node:crypto'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type SseListener = (event: string, payload: Record<string, unknown>) => void\n\n// Porta mínima de cache para o ticket de conexão SSE — qualquer TTL cache do host serve\n// (Redis, in-memory); o módulo não assume Redis especificamente.\nexport interface TicketStoreInterface {\n set(key: string, value: string, ttlSeconds: number): Promise<void>\n get(key: string): Promise<string | null>\n delete(key: string): Promise<void>\n}\n\n// Publica localmente e, se um relay for injetado (ex.: Redis pub/sub), propaga entre processos\n// — várias instâncias do host atrás de um load balancer recebem o mesmo evento (T3.4).\nexport interface RealtimeRelay {\n publish(channel: string, message: string): Promise<void>\n subscribe(channel: string, onMessage: (message: string) => void): Promise<() => void>\n}\n\nconst TICKET_TTL_SECONDS = 60\n\n// T3.4 — hub de tempo real atrás da porta RealtimeNotifierInterface (contracts). O host pode\n// trocar por WebSocket ou desligar sem o resto do módulo saber — ver providers.ts.\ntype RelayEnvelope = {\n originId: string\n event: string\n payload: Record<string, unknown>\n}\n\nexport class SseHub implements RealtimeNotifierInterface {\n private readonly localListeners = new Map<string, Set<SseListener>>()\n // Uma assinatura de relay por CANAL, compartilhada por todos os listeners locais dele — uma\n // por listener faria o relay entregar N cópias num canal com N conexões abertas.\n private readonly relaySubscriptions = new Map<string, Promise<() => void>>()\n // Identifica esta instância do processo para descartar o próprio eco (pub/sub entrega a\n // mensagem a todos os inscritos, inclusive quem publicou).\n private readonly instanceId = randomUUID()\n\n constructor(private readonly relay?: RealtimeRelay) {}\n\n emit(channel: string, event: string, payload: Record<string, unknown>): void {\n this.dispatchLocal(channel, event, payload)\n if (this.relay) {\n const envelope: RelayEnvelope = { originId: this.instanceId, event, payload }\n this.relay.publish(channel, JSON.stringify(envelope)).catch(() => undefined)\n }\n }\n\n private dispatchLocal(channel: string, event: string, payload: Record<string, unknown>): void {\n const listeners = this.localListeners.get(channel)\n if (!listeners) return\n for (const listener of listeners) listener(event, payload)\n }\n\n async subscribe(channel: string, listener: SseListener): Promise<() => void> {\n if (!this.localListeners.has(channel)) this.localListeners.set(channel, new Set())\n this.localListeners.get(channel)!.add(listener)\n\n if (this.relay && !this.relaySubscriptions.has(channel)) {\n this.relaySubscriptions.set(\n channel,\n this.relay.subscribe(channel, (message) => {\n try {\n const envelope = JSON.parse(message) as RelayEnvelope\n // Eco do que nós mesmos publicamos: dispatchLocal já entregou em emit().\n if (envelope.originId === this.instanceId) return\n this.dispatchLocal(channel, envelope.event, envelope.payload)\n } catch {\n // mensagem de relay malformada — ignora, não derruba a conexão\n }\n }),\n )\n }\n\n return () => {\n const listeners = this.localListeners.get(channel)\n listeners?.delete(listener)\n // Último listener do canal saiu: solta a assinatura do relay e a chave do Map, senão\n // canais efêmeros (conv:<numero>) se acumulam pelo uptime inteiro do processo.\n if (listeners && listeners.size === 0) {\n this.localListeners.delete(channel)\n const pendingUnsubscribe = this.relaySubscriptions.get(channel)\n this.relaySubscriptions.delete(channel)\n pendingUnsubscribe?.then((unsubscribe) => unsubscribe()).catch(() => undefined)\n }\n }\n }\n}\n\n// EventSource do browser não manda headers customizados — o ticket vai na query string da URL\n// de conexão e expira em 60s, curto o bastante para não valer a pena capturar e reusar depois.\nexport async function issueSseTicket(\n store: TicketStoreInterface,\n companyId: string,\n whatsappNumber: string,\n): Promise<string> {\n const ticket = randomUUID()\n await store.set(`sse:ticket:${ticket}`, JSON.stringify({ companyId, whatsappNumber }), TICKET_TTL_SECONDS)\n return ticket\n}\n\nexport async function redeemSseTicket(\n store: TicketStoreInterface,\n ticket: string,\n): Promise<{ companyId: string; whatsappNumber: string } | null> {\n const raw = await store.get(`sse:ticket:${ticket}`)\n if (!raw) return null\n await store.delete(`sse:ticket:${ticket}`)\n return JSON.parse(raw) as { companyId: string; whatsappNumber: string }\n}\n"],"mappings":"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","names":["path","fileURLToPath","getFilename","fileURLToPath","url","getDirname","path","dirname","__dirname","WhatsAppMessageProvider","FLOW_ACTION_KIND","and","desc","eq","sql","sql","pgSchema","uuid","varchar","jsonb","timestamp","text","integer","boolean","index","uniqueIndex","metaWhatsAppSchema","sessions","table","id","primaryKey","defaultRandom","companyId","notNull","whatsappNumber","length","currentState","default","flowKey","currentNodeId","context","$type","mode","assignedUserId","humanRequestedAt","withTimezone","lastInboundAt","lastAgentReadAt","lastActivity","defaultNow","createdAt","updatedAt","on","messages","sessionId","references","onDelete","direction","sender","agentUserId","type","content","payload","waMessageId","status","readAt","moderationFlagged","moderationTerms","transcriptionStatus","transcriptionText","transcriptionLanguage","transcriptionEngine","where","documents","messageId","uploadId","filename","mimeType","sizeBytes","sha256","source","linkedAt","flowGraphs","key","label","startNodeId","nodes","version","showInMenu","menuOptionLabel","flowMedia","nodeId","caption","sortOrder","active","metaMediaIds","settings","templateName","templateLanguage","templateVariables","welcomeMessage","farewellMessage","transcriptionEnabled","transcriptionMode","DEFAULT_LIMIT","conversationSummaryProjection","lastContent","sql","messages","sessions","lastDirection","unread","sessionContextPatch","patch","context","JSON","stringify","SessionRepository","db","getContext","companyId","whatsappNumber","row","select","from","where","and","eq","limit","getOrCreate","startState","existing","created","insert","values","currentState","onConflictDoUpdate","target","set","lastActivity","returning","setState","state","update","undefined","updatedAt","patchContext","readContext","setFlowPosition","flowKey","currentNodeId","touchInbound","lastInboundAt","hoursSinceLastInbound","session","Date","now","getTime","setMode","mode","assignedUserId","clearHumanRequest","humanRequestedAt","takeover","agentUserId","release","deleteByNumber","delete","requestHuman","markRead","lastAgentReadAt","markAllRead","userId","result","Number","rowCount","listByContextFilters","filters","offset","page","conditions","waitingHuman","push","search","rows","id","orderBy","desc","map","lastAt","toISOString","exportConversation","sessionId","createdAt","and","eq","lt","DEFAULT_LIMIT","MessageRepository","db","insertMessage","params","values","companyId","sessionId","whatsappNumber","direction","sender","agentUserId","type","content","payload","waMessageId","status","moderationFlagged","moderationTerms","created","insert","messages","onConflictDoNothing","returning","updateMessageStatus","updated","update","set","readAt","Date","where","and","eq","saveTranscriptionByWaMessageId","transcriptionStatus","text","undefined","transcriptionText","language","transcriptionLanguage","engine","transcriptionEngine","findById","messageId","found","select","from","id","limit","saveTranscription","listByConversation","conditions","before","push","lt","createdAt","orderBy","and","eq","isNotNull","sql","flowGraphNodesSchema","InvalidFlowGraphError","Error","key","validationMessage","name","toContractGraph","row","parsed","flowGraphNodesSchema","safeParse","nodes","success","error","message","label","startNodeId","version","data","OptimisticLockError","FlowGraphRepository","db","cache","get","companyId","cached","read","select","from","flowGraphs","where","and","eq","limit","undefined","graph","write","list","rows","map","nodeCount","Object","keys","length","showInMenu","menuOptionLabel","updatedAt","toISOString","assertValidNodes","create","created","insert","values","returning","createdGraph","invalidate","save","expectedVersion","update","set","Date","delete","getLiveFlowPositions","flowKey","sessions","nodeId","currentNodeId","count","sql","mode","isNotNull","groupBy","KEY_PREFIX","DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS","FlowGraphCache","provider","ttlSeconds","keyFor","companyId","flowKey","read","cached","get","undefined","JSON","parse","write","graph","set","key","stringify","invalidate","delete","eq","toContractSettings","row","templateName","templateLanguage","templateVariables","welcomeMessage","farewellMessage","transcriptionEnabled","transcriptionMode","EMPTY_SETTINGS","SettingsRepository","db","get","companyId","select","from","settings","where","eq","limit","save","update","current","merged","insert","values","onConflictDoUpdate","target","set","updatedAt","Date","returning","resolveTemplateVariables","context","map","token","key","replace","value","undefined","String","LogMessageUseCase","sessionRepository","messageRepository","realtime","moderator","execute","params","session","getOrCreate","companyId","whatsappNumber","startState","saved","insertMessage","sessionId","id","moderationOf","undefined","direction","touchInbound","emit","sender","text","content","trim","verdict","inspect","moderationFlagged","isOffensive","moderationTerms","matchedTerms","WindowExpiredError","WHATSAPP_WINDOW_HOURS","SendMessageUseCase","channel","sessionRepository","logMessage","objectStorage","documentRepository","assertWithinWindow","companyId","whatsappNumber","hours","hoursSinceLastInbound","undefined","WindowExpiredError","sendText","params","externalMessageId","body","execute","direction","sender","agentUserId","type","content","waMessageId","status","startState","sendMedia","to","buffer","mimeType","filename","caption","uploadId","upload","key","Date","now","saved","mediaTypeFor","payload","link","sessionId","messageId","id","sizeBytes","length","source","sendTemplate","templateName","languageCode","bodyParameters","startsWith","TakeoverConversationUseCase","sessionRepository","realtime","execute","params","takeover","companyId","whatsappNumber","agentUserId","emit","mode","assignedUserId","ReleaseConversationUseCase","sessionRepository","realtime","execute","params","release","companyId","whatsappNumber","emit","mode","assignedUserId","ListConversationsUseCase","sessionRepository","execute","params","listByContextFilters","companyId","filters","SessionNotFoundError","ListMessagesUseCase","sessionRepository","messageRepository","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","repoParams","sessionId","id","limit","before","listByConversation","SessionNotFoundError","ListConversationDocumentsUseCase","sessionRepository","documentRepository","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","rows","total","listByConversation","sessionId","id","search","sources","length","sortDirection","page","limit","documents","map","row","uploadId","filename","mimeType","sizeBytes","source","linkedAt","toISOString","ListCompanyDocumentsUseCase","documentRepository","execute","params","rows","total","listByCompany","companyId","search","sources","length","sortDirection","page","limit","documents","map","row","id","uploadId","conversationId","whatsappNumber","filename","mimeType","sizeBytes","source","linkedAt","toISOString","SessionNotFoundError","DeleteConversationUseCase","sessionRepository","documentRepository","objectStorage","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","uploadIds","listUploadIdsBySession","id","length","delete","Error","failedObjects","deletedObjects","uploadId","push","deleteByNumber","DEFAULT_BATCH_SIZE","HOURS_IN_DAY","MILLISECONDS_IN_HOUR","PurgeExpiredDocumentsUseCase","documentRepository","objectStorage","execute","params","delete","Error","reference","now","Date","olderThan","getTime","retentionDays","expired","listExpired","companyId","batchSize","failed","purged","document","uploadId","push","deleteById","id","and","asc","count","desc","eq","ilike","inArray","lt","or","DEFAULT_LIMIT","companyDocumentSearch","search","term","trim","undefined","digits","replace","byFilename","ilike","documents","filename","or","sessions","whatsappNumber","DocumentRepository","db","link","params","values","companyId","sessionId","messageId","uploadId","mimeType","sizeBytes","sha256","source","created","insert","onConflictDoNothing","returning","listByConversation","filters","eq","push","sources","length","inArray","where","and","limit","page","orderBy","sortDirection","asc","linkedAt","desc","rows","counted","Promise","all","select","from","offset","value","count","total","listByCompany","id","innerJoin","findByUploadId","row","listUploadIdsBySession","map","listExpired","olderThan","lt","deleteById","delete","SessionNotFoundError","ExportConversationUseCase","sessionRepository","execute","params","result","exportConversation","companyId","whatsappNumber","SessionNotFoundError","GetFlowGraphUseCase","flowGraphRepository","execute","params","get","companyId","key","ListFlowGraphsUseCase","list","CreateFlowGraphUseCase","create","SaveFlowGraphUseCase","save","graph","expectedVersion","DeleteFlowGraphUseCase","delete","GetLiveFlowPositionsUseCase","getLiveFlowPositions","isCrossFlowTarget","crossFlowKey","DEFAULT_MAX_STEPS","evaluateCondition","node","context","conditionContextKey","conditionOperator","conditionValue","actual","expected","String","includes","actualNum","Number","expectedNum","isNaN","resolveNext","answerId","next","undefined","byAnswer","default","FlowInterpreter","actionHandlers","Map","registerFlowAction","kind","handler","set","step","input","graph","nodes","currentNodeId","type","branch","moveTo","actionKind","get","result","session","channel","contextAfterAction","nextNodeId","isChoice","questionType","userAnswer","nodeId","updatedContext","contextKey","isCrossFlowTarget","flowKey","crossFlowKey","run","options","maxSteps","visited","current","steps","push","mediaTypeFor","mimeType","startsWith","sendAttachment","params","attachment","channel","to","cache","common","filename","caption","undefined","cacheKey","flowMediaId","id","senderKey","knownMediaId","store","get","sendMedia","mediaId","clear","buffer","objectStorage","getObject","uploadId","result","set","createSendMediaAction","node","session","flowKey","location","companyId","nodeId","attachments","flowMediaRepository","listActive","externalMessageId","whatsappNumber","mediaIdCache","logMessage","execute","direction","sender","agentUserId","type","content","payload","waMessageId","status","startState","error","onError","PRODUCT_LIST_LIMIT","ITEMS","SECTIONS","createSendProductListAction","params","node","session","channel","sendProductList","actionParams","available","listAvailableProducts","catalog","catalogId","search","length","bodyText","externalMessageId","to","whatsappNumber","headerText","body","footerText","sections","title","sectionTitle","retailerIds","map","product","retailerId","logMessage","execute","companyId","direction","sender","agentUserId","type","content","payload","kind","productCount","waMessageId","status","startState","error","onError","flowKey","nodeId","id","products","listProducts","filter","availability","slice","and","asc","eq","sql","FlowMediaRepository","db","locationFilter","location","and","eq","flowMedia","companyId","flowKey","nodeId","listActive","select","from","where","active","orderBy","asc","sortOrder","createdAt","listAll","attach","params","row","insert","values","uploadId","filename","mimeType","sizeBytes","caption","onConflictDoUpdate","target","set","updatedAt","sql","returning","update","undefined","id","detach","delete","detachRemovedNodes","condition","existingNodeIds","length","eq","sql","FlowMediaIdRepository","db","get","params","rows","select","ids","flowMedia","metaMediaIds","from","where","eq","id","flowMediaId","limit","senderKey","set","update","sql","mediaId","clear","WhatsAppWindowExpiredError","ProviderWindowExpiredError","WindowExpiredError","PREVIEW_MEDIA_ID_PREFIX","toPreviewMediaId","resolvePreviewUploadId","WhatsAppChannelAdapter","messages","previewMedia","translateErrors","operation","error","ProviderWindowExpiredError","WindowExpiredError","sendText","to","body","result","externalMessageId","waMessageId","sendMedia","params","mediaId","sendTemplate","sendInteractiveList","bodyText","buttonText","buttonLabel","sections","title","rows","sendInteractiveButtons","buttons","sendProductList","sendProductListMessage","headerText","footerText","fetchMediaAsBase64","uploadId","isEnabled","resolvePreviewUploadId","undefined","buffer","objectStorage","getObject","data","toString","mimeType","defaultMimeType","whatsAppWebhookPayloadSchema","resolveContactProfileName","whatsAppTemplateStatusUpdateSchema","whatsAppPhoneNumberQualityUpdateSchema","WHATSAPP_WEBHOOK_FIELDS","WEBHOOK_CLAIM_TTL_SECONDS","WEBHOOK_NONCE_TTL_SECONDS","buildWebhookDeliveryKey","isValidWebhookChallenge","isValidWebhookSignature","InvalidWebhookSignatureError","WEBHOOK_NONCE_NAMESPACE","verifyWebhookChallenge","params","isValidWebhookChallenge","InvalidWebhookSignatureError","challenge","verifyWebhookSignature","isValidWebhookSignature","deliveryKey","signatureHeader","buildWebhookDeliveryKey","namespace","claimWebhookDelivery","nonceStore","setIfAbsent","ttlSeconds","WEBHOOK_CLAIM_TTL_SECONDS","confirmWebhookDelivery","confirm","WEBHOOK_NONCE_TTL_SECONDS","eq","and","AudioNotIngestedError","MessageNotAudioError","TranscriptionDisabledError","TRANSCRIPTION_STATUS","PENDING","DONE","FAILED","UNSUPPORTED","TRANSCRIPTION_MODE","AUTO","ON_DEMAND","isRetriableTranscriptionError","error","isRetriable","isUnsupportedTranscriptionError","name","transcriptionRetryAfterSeconds","undefined","retryAfter","retryAfterSeconds","isAudioMimeType","mimeType","trim","toLowerCase","startsWith","TranscribeAudioUseCase","dependencies","execute","params","message","messageRepository","findById","companyId","messageId","Error","transcriptionStatus","TRANSCRIPTION_STATUS","DONE","force","status","text","transcriptionText","language","transcriptionLanguage","engine","transcriptionEngine","alreadyTranscribed","resolvePolicy","policy","isEnabled","TranscriptionDisabledError","audio","extractAudioReference","buffer","objectStorage","getObject","uploadId","transcribeBuffer","mimeType","context","result","transcriber","transcribe","languageHint","saveTranscription","error","persistFailure","resolveFailureStatus","PENDING","retryAfterSeconds","transcriptionRetryAfterSeconds","hooks","onTranscriptionDeferred","whatsappNumber","undefined","reason","isUnsupportedTranscriptionError","UNSUPPORTED","isRetriableTranscriptionError","FAILED","payload","mime_type","isAudioMimeType","MessageNotAudioError","id","type","length","AudioNotIngestedError","IngestInboundMediaUseCase","db","channel","objectStorage","documentRepository","transcription","execute","params","message","select","from","messages","where","and","eq","companyId","id","messageId","limit","Error","payload","sourceMediaId","uploadId","String","alreadyIngested","data","mimeType","fetchMediaAsBase64","buffer","Buffer","upload","key","updatedPayload","sizeBytes","length","filename","update","set","link","sessionId","source","sender","transcribeIfAuto","context","undefined","isAudioMimeType","policy","resolvePolicy","isEnabled","mode","TRANSCRIPTION_MODE","AUTO","current","messageRepository","findById","transcriptionStatus","TRANSCRIPTION_STATUS","DONE","result","transcriber","transcribe","languageHint","saveTranscription","status","text","language","engine","error","recordTranscriptionFailure","resolveFailureStatus","PENDING","retryAfterSeconds","transcriptionRetryAfterSeconds","hooks","onTranscriptionDeferred","whatsappNumber","reason","extractMediaDescriptor","media","mime_type","buildInboundJobId","job","kind","message","id","status","toSessionContract","row","companyId","whatsappNumber","currentState","flowKey","currentNodeId","context","mode","assignedUserId","humanRequestedAt","toISOString","lastInboundAt","lastAgentReadAt","lastActivity","createdAt","updatedAt","extractAnswer","interactive","button_reply","list_reply","text","body","InboundEffectsDispatcher","params","run","runMessageEffects","runStatusEffects","savedMessageId","media","hooks","onMediaReceived","messageId","from","sourceMediaId","mimeType","filename","sessionRow","sessionRepository","getContext","outcome","onMessageReceived","profileName","undefined","onStatusUpdate","ProcessInboundDispatchUseCase","dispatcher","execute","EMPTY_RESULT","messagesProcessed","statusesProcessed","ignoredForeignNumber","accountEventsProcessed","unhandledEvents","extractContent","message","text","body","interactive","button_reply","title","id","list_reply","order","items","product_items","reduce","sum","item","quantity","image","caption","document","extractPayload","payload","audio","video","sticker","context","referred_product","Object","keys","length","ACCOUNT_EVENT_FIELDS","WHATSAPP_WEBHOOK_FIELDS","TEMPLATE_STATUS_UPDATE","PHONE_NUMBER_QUALITY_UPDATE","isAccountEventField","field","undefined","includes","ReceiveWebhookUseCase","params","dispatcher","InboundEffectsDispatcher","sessionRepository","hooks","realtime","execute","input","verifyWebhookSignature","rawBody","signatureHeader","appSecret","claimed","claimWebhookDelivery","nonceStore","duplicate","rawText","toString","parsed","whatsAppWebhookPayloadSchema","safeParse","JSON","parse","success","onUnhandledWebhookEvent","reason","value","data","entry","change","changes","handled","handleAccountEvent","targetNumber","metadata","phone_number_id","phoneNumberId","messages","profileName","resolveContactProfileName","contacts","from","handleMessage","companyId","status","statuses","handleStatus","carriedConversationData","message_echoes","confirmWebhookDelivery","update","whatsAppTemplateStatusUpdateSchema","onTemplateStatusUpdate","quality","whatsAppPhoneNumberQualityUpdateSchema","onPhoneNumberQualityUpdate","saved","logMessage","whatsappNumber","direction","sender","type","content","waMessageId","startState","media","extractMediaDescriptor","dispatch","kind","savedMessageId","receivedAt","Date","now","updated","messageRepository","updateMessageStatus","emit","job","inboundQueue","enqueue","jobId","buildInboundJobId","run","createTranscriptionPolicyResolver","dependencies","resolveTranscriptionPolicy","companyId","settings","settingsRepository","get","isEnabled","transcriptionEnabled","defaults","mode","normalizeMode","transcriptionMode","value","TRANSCRIPTION_MODE","AUTO","ON_DEMAND","undefined","StorePreviewMediaUseCase","objectStorage","generateKeySuffix","execute","params","key","companyId","uploadId","upload","buffer","mimeType","mediaId","toPreviewMediaId","createMetaWhatsAppModule","params","db","config","nonceStore","providers","hooks","startState","flowEngineEnabled","features","flowEngine","messageProvider","WhatsAppMessageProvider","accessToken","phoneNumberId","wabaId","apiVersion","baseUrl","previewMediaSupport","previewMedia","objectStorage","getObject","isEnabled","undefined","channel","WhatsAppChannelAdapter","sessionRepository","SessionRepository","messageRepository","MessageRepository","settingsRepository","SettingsRepository","flowGraphCacheFeature","flowGraphCache","cache","FlowGraphCache","ttlSeconds","DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS","flowGraphRepository","FlowGraphRepository","documentRepository","DocumentRepository","flowMediaRepository","FlowMediaRepository","logMessage","LogMessageUseCase","realtime","moderator","sendMessage","SendMessageUseCase","receiveWebhook","ReceiveWebhookUseCase","appSecret","flowInterpreter","FlowInterpreter","registerFlowAction","FLOW_ACTION_KIND","SEND_MEDIA","createSendMediaAction","mediaIdCache","store","FlowMediaIdRepository","senderKey","onError","onFlowMediaError","catalog","catalogId","SEND_PRODUCT_LIST","createSendProductListAction","onFlowProductListError","transcriptionMode","transcription","mode","TRANSCRIPTION_MODE","ON_DEMAND","resolveTranscriptionPolicy","createTranscriptionPolicyResolver","defaults","isEnabledByDefault","ingestInboundMedia","IngestInboundMediaUseCase","transcriber","resolvePolicy","languageHint","transcribeAudio","TranscribeAudioUseCase","listDocuments","ListConversationDocumentsUseCase","conversations","log","send","takeover","TakeoverConversationUseCase","release","ReleaseConversationUseCase","list","ListConversationsUseCase","listMessages","ListMessagesUseCase","listCompanyDocuments","ListCompanyDocumentsUseCase","delete","DeleteConversationUseCase","purgeExpiredDocuments","PurgeExpiredDocumentsUseCase","export","ExportConversationUseCase","repository","defaultMode","settings","StorePreviewMediaUseCase","Date","now","Math","random","toString","slice","webhook","receive","verifyChallenge","query","verifyWebhookChallenge","expectedToken","webhookVerifyToken","flows","interpreter","kind","handler","get","GetFlowGraphUseCase","ListFlowGraphsUseCase","create","CreateFlowGraphUseCase","save","SaveFlowGraphUseCase","DeleteFlowGraphUseCase","livePositions","GetLiveFlowPositionsUseCase","mediaRepository","join","META_WHATSAPP_MIGRATIONS_TABLE","metaWhatsAppMigrationsFolder","join","__dirname","runMetaWhatsAppMigrations","params","migrate","db","migrationsFolder","migrationsTable","randomUUID","TICKET_TTL_SECONDS","SseHub","localListeners","Map","relaySubscriptions","instanceId","randomUUID","relay","emit","channel","event","payload","dispatchLocal","envelope","originId","publish","JSON","stringify","catch","undefined","listeners","get","listener","subscribe","has","set","Set","add","message","parse","delete","size","pendingUnsubscribe","then","unsubscribe","issueSseTicket","store","companyId","whatsappNumber","ticket","redeemSseTicket","raw"]}
|
|
1
|
+
{"version":3,"sources":["../../../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.2_@swc+helpers@0.5.23__jiti@2.7.0_postcss@8.5.28_tsx@4.23.13_typescript@5.9.3_yaml@2.9.1/node_modules/tsup/assets/esm_shims.js","../src/createMetaWhatsAppModule.ts","../src/repositories/SessionRepository.ts","../src/schema/schema.ts","../src/repositories/MessageRepository.ts","../src/repositories/FlowGraphRepository.ts","../src/repositories/FlowGraphCache.ts","../src/repositories/SettingsRepository.ts","../src/use-cases/LogMessage.use-case.ts","../src/use-cases/SendMessage.use-case.ts","../src/use-cases/TakeoverConversation.use-case.ts","../src/use-cases/ReleaseConversation.use-case.ts","../src/use-cases/ListConversations.use-case.ts","../src/use-cases/ListMessages.use-case.ts","../src/use-cases/ListConversationDocuments.use-case.ts","../src/use-cases/ListCompanyDocuments.use-case.ts","../src/use-cases/DeleteConversation.use-case.ts","../src/use-cases/PurgeExpiredDocuments.use-case.ts","../src/repositories/DocumentRepository.ts","../src/use-cases/ExportConversation.use-case.ts","../src/use-cases/FlowGraph.use-cases.ts","../src/flows/FlowInterpreter.ts","../src/flows/createSendMediaAction.ts","../src/flows/createSendProductListAction.ts","../src/repositories/FlowMediaRepository.ts","../src/repositories/FlowMediaIdRepository.ts","../src/channel/WhatsAppChannelAdapter.ts","../src/channel/previewMedia.ts","../src/channel/ReceiveWebhook.use-case.ts","../src/channel/webhookSecurity.ts","../src/channel/IngestInboundMedia.use-case.ts","../src/use-cases/TranscribeAudio.use-case.ts","../src/transcription.types.ts","../src/channel/inboundDispatch.ts","../src/use-cases/resolveTranscriptionPolicy.ts","../src/use-cases/StorePreviewMedia.use-case.ts","../src/runMigrations.ts","../src/realtime/SseHub.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider'\nimport type { MetaWhatsAppDatabase } from './database.types'\nimport { FLOW_ACTION_KIND } from '@adatechnology/meta-whatsapp-contracts'\nimport type {\n CatalogPort,\n ChannelAdapterInterface,\n FlowActionHandler,\n FlowActionKind,\n MetaWhatsAppHooks,\n ObjectStorageInterface,\n CacheInterface,\n RealtimeNotifierInterface,\n SessionState,\n SubjectResolverInterface,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { SessionRepository } from './repositories/SessionRepository'\nimport { MessageRepository } from './repositories/MessageRepository'\nimport { FlowGraphRepository } from './repositories/FlowGraphRepository'\nimport { FlowGraphCache, DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS } from './repositories/FlowGraphCache'\nimport { SettingsRepository } from './repositories/SettingsRepository'\nimport { LogMessageUseCase, type MessageModerator } from './use-cases/LogMessage.use-case'\nimport { SendMessageUseCase } from './use-cases/SendMessage.use-case'\nimport { TakeoverConversationUseCase } from './use-cases/TakeoverConversation.use-case'\nimport { ReleaseConversationUseCase } from './use-cases/ReleaseConversation.use-case'\nimport { ListConversationsUseCase } from './use-cases/ListConversations.use-case'\nimport { ListMessagesUseCase } from './use-cases/ListMessages.use-case'\nimport { ListConversationDocumentsUseCase } from './use-cases/ListConversationDocuments.use-case'\nimport { ListCompanyDocumentsUseCase } from './use-cases/ListCompanyDocuments.use-case'\nimport { DeleteConversationUseCase } from './use-cases/DeleteConversation.use-case'\nimport { PurgeExpiredDocumentsUseCase } from './use-cases/PurgeExpiredDocuments.use-case'\nimport { DocumentRepository } from './repositories/DocumentRepository'\nimport { ExportConversationUseCase } from './use-cases/ExportConversation.use-case'\nimport {\n GetFlowGraphUseCase,\n ListFlowGraphsUseCase,\n CreateFlowGraphUseCase,\n SaveFlowGraphUseCase,\n DeleteFlowGraphUseCase,\n GetLiveFlowPositionsUseCase,\n} from './use-cases/FlowGraph.use-cases'\nimport { FlowInterpreter } from './flows/FlowInterpreter'\nimport { createSendMediaAction } from './flows/createSendMediaAction'\nimport { createSendProductListAction } from './flows/createSendProductListAction'\nimport { FlowMediaRepository } from './repositories/FlowMediaRepository'\nimport { FlowMediaIdRepository } from './repositories/FlowMediaIdRepository'\nimport { WhatsAppChannelAdapter, type PreviewMediaSupport } from './channel/WhatsAppChannelAdapter'\nimport { ReceiveWebhookUseCase } from './channel/ReceiveWebhook.use-case'\nimport { IngestInboundMediaUseCase } from './channel/IngestInboundMedia.use-case'\nimport { verifyWebhookChallenge, type NonceStoreInterface } from './channel/webhookSecurity'\nimport { TranscribeAudioUseCase, type TranscribeAudioDependencies } from './use-cases/TranscribeAudio.use-case'\nimport { createTranscriptionPolicyResolver } from './use-cases/resolveTranscriptionPolicy'\nimport { StorePreviewMediaUseCase } from './use-cases/StorePreviewMedia.use-case'\nimport { TRANSCRIPTION_MODE, type AudioTranscriber, type TranscriptionMode } from './transcription.types'\n\nexport interface MetaWhatsAppModuleConfig {\n phoneNumberId: string\n accessToken: string\n webhookVerifyToken: string\n appSecret: string\n wabaId?: string\n apiVersion?: string\n // Aponta para um mock local em dev/teste; undefined usa a Graph API real.\n baseUrl?: string\n /**\n * Catálogo do Meta Commerce que a vitrine no chat oferece. Sem ele — ou sem `providers.catalog`\n * — a action `send_product_list` não é registrada: nó que o editor oferece e que em silêncio não\n * faz nada é pior do que nó que não existe.\n */\n catalogId?: string\n}\n\nexport interface MetaWhatsAppModuleFeatures {\n // T4.4 — desliga o motor de grafo. Um produto cuja conversa é dirigida por código próprio\n // (o QuickCart tem o seu ConversationEngine) não deve nem carregar o interpretador, e o\n // módulo não pode presumir que todo consumidor quer fluxo visual.\n flowEngine?: boolean\n\n // Cache de leitura dos grafos. Desligado por omissão: cachear é decisão do host, que é quem\n // sabe se o Redis dele é compartilhado entre instâncias — cache por processo serviria versões\n // diferentes do mesmo fluxo depois de uma publicação. Ligar exige `providers.cache`; sem ele o\n // flag é ignorado, porque o módulo não abre conexão própria.\n flowGraphCache?: boolean | { ttlSeconds?: number }\n\n /**\n * Aceita mídia do simulador de conversa — o que faz o microfone aparecer no preview do cliente.\n *\n * **Desligado por omissão, e a decisão é consciente.** Ligado, o canal passa a aceitar id que não\n * veio da Meta: `preview-upload:<chave>` faz o servidor ler aquele objeto do storage. Em ambiente\n * de simulação isso é o recurso; em produção é leitura arbitrária do bucket por webhook forjado.\n *\n * Exige `providers.objectStorage` com `getObject` — sem os bytes não há o que devolver, e o flag é\n * ignorado em vez de produzir um canal que falha na primeira nota de voz.\n */\n previewMedia?: boolean\n}\n\n/**\n * Transcrição de áudio. Ausente = desligada, e as colunas ficam nulas (\"não avaliado\").\n *\n * `mode` é a decisão de produto que este objeto existe para carregar: `auto` transcreve toda nota de\n * voz recebida, na ingestão, onde o buffer já está em memória; `onDemand` só transcreve quando o\n * atendente pede, gastando cota apenas com áudio que alguém vai ler de fato.\n */\nexport interface MetaWhatsAppTranscriptionConfig {\n transcriber: AudioTranscriber\n /**\n * PADRÃO, não decisão final: vale para as empresas que não mexeram no interruptor do painel. As\n * configurações por empresa (`settings.transcriptionMode`) têm precedência.\n *\n * Padrão `onDemand` — o modo que não gasta cota sem alguém pedir.\n */\n mode?: TranscriptionMode\n /**\n * PADRÃO de ligado/desligado para empresas sem escolha registrada. `true` — injetar o transcritor\n * já é a declaração de que o host quer o recurso; quem controla por empresa usa o painel.\n */\n isEnabledByDefault?: boolean\n /** ISO 639-1 do produto (ex.: `'pt'`). Corta a detecção de idioma do engine. */\n languageHint?: string\n}\n\nexport interface MetaWhatsAppModuleProviders {\n objectStorage?: ObjectStorageInterface\n // Cache do host, compartilhado entre instâncias (ver CacheInterface). Hoje serve os grafos de\n // fluxo, que são lidos a cada mensagem recebida.\n cache?: CacheInterface\n realtime?: RealtimeNotifierInterface\n subjectResolver?: SubjectResolverInterface\n // Opcional por desenho — sem catálogo injetado o módulo sobe e funciona, só os recursos de\n // produto ficam desligados (ver .specs/features/meta-catalog-trio/spec.md §4).\n catalog?: CatalogPort\n // Marca mensagem ofensiva do cliente no transcript. Injetado, e não construído aqui, pelo mesmo\n // motivo dos outros providers: a lista de termos e o liga/desliga são de cada produto, e o módulo\n // não lê process.env. Ausente, as colunas ficam nulas — \"não avaliado\".\n moderator?: MessageModerator\n /**\n * Converte nota de voz em texto. Exige `objectStorage` com `getObject` para o modo sob demanda —\n * transcrever um áudio já salvo significa ler os bytes de volta.\n */\n transcription?: MetaWhatsAppTranscriptionConfig\n}\n\nexport interface CreateMetaWhatsAppModuleParams {\n db: MetaWhatsAppDatabase\n config: MetaWhatsAppModuleConfig\n // Cache compartilhado entre instâncias para o anti-replay do webhook.\n nonceStore: NonceStoreInterface\n // Estado inicial de uma sessão nova — o módulo não conhece a máquina de estados do produto.\n startState?: SessionState\n features?: MetaWhatsAppModuleFeatures\n providers?: MetaWhatsAppModuleProviders\n hooks?: MetaWhatsAppHooks\n}\n\n// T5.6 — costura conversa × canal. Tudo que é ambiente (db, credenciais, storage, cache,\n// realtime) entra por parâmetro: o módulo não lê process.env nem abre conexão própria, que é o\n// que permite ao host controlar pool, ciclo de vida e configuração por empresa.\nexport function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams) {\n const { db, config, nonceStore, providers = {}, hooks } = params\n const startState = params.startState ?? 'start'\n const flowEngineEnabled = params.features?.flowEngine ?? true\n\n const messageProvider = new WhatsAppMessageProvider({\n accessToken: config.accessToken,\n phoneNumberId: config.phoneNumberId,\n wabaId: config.wabaId,\n apiVersion: config.apiVersion,\n baseUrl: config.baseUrl,\n })\n /**\n * Só monta o suporte quando as DUAS condições existem: o host pediu e o storage sabe ler de volta.\n * Ligar o flag sem storage legível daria um canal que aceita o id do simulador e falha ao buscar.\n */\n const previewMediaSupport =\n params.features?.previewMedia && providers.objectStorage?.getObject\n ? {\n isEnabled: true,\n objectStorage: providers.objectStorage as PreviewMediaSupport['objectStorage'],\n }\n : undefined\n\n const channel: ChannelAdapterInterface = new WhatsAppChannelAdapter(messageProvider, previewMediaSupport)\n\n const sessionRepository = new SessionRepository(db)\n const messageRepository = new MessageRepository(db)\n const settingsRepository = new SettingsRepository(db)\n const flowGraphCacheFeature = params.features?.flowGraphCache ?? false\n const flowGraphCache =\n flowGraphCacheFeature && providers.cache\n ? new FlowGraphCache(\n providers.cache,\n typeof flowGraphCacheFeature === 'object'\n ? (flowGraphCacheFeature.ttlSeconds ?? DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS)\n : DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,\n )\n : undefined\n\n const flowGraphRepository = new FlowGraphRepository(db, flowGraphCache)\n const documentRepository = new DocumentRepository(db)\n const flowMediaRepository = new FlowMediaRepository(db)\n\n const logMessage = new LogMessageUseCase(\n sessionRepository,\n messageRepository,\n providers.realtime,\n providers.moderator,\n )\n const sendMessage = new SendMessageUseCase(\n channel,\n sessionRepository,\n logMessage,\n providers.objectStorage,\n documentRepository,\n )\n\n const receiveWebhook = new ReceiveWebhookUseCase({\n appSecret: config.appSecret,\n phoneNumberId: config.phoneNumberId,\n nonceStore,\n sessionRepository,\n messageRepository,\n logMessage,\n startState,\n hooks,\n realtime: providers.realtime,\n })\n\n // Só existe se a flag estiver ligada — não adianta o host \"não usar\" um interpretador que\n // ainda assim ficou instanciado e exposto na API pública do módulo.\n const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : undefined\n\n // `send_media` é a única action que o pacote registra sozinho — as demais dependem de regra do\n // produto. Condicionada a `getObject` porque sem os bytes o handler não teria o que enviar:\n // registrá-lo assim mesmo deixaria um nó que o editor oferece e que silenciosamente não faz nada.\n if (flowInterpreter && providers.objectStorage?.getObject) {\n flowInterpreter.registerFlowAction(\n FLOW_ACTION_KIND.SEND_MEDIA,\n createSendMediaAction({\n flowMediaRepository,\n objectStorage: providers.objectStorage as Parameters<typeof createSendMediaAction>[0]['objectStorage'],\n logMessage,\n startState,\n // Cache ligado por padrão, com o store do próprio módulo: a tabela é daqui e a coluna é\n // daqui, então exigir que cada host escrevesse o repositório faria todos reescreverem o\n // mesmo código — e, até escreverem, o mesmo binário continuaria subindo por cliente. Quem\n // quiser outro lugar (Redis) troca o `store`; quem não fizer nada já sai sem ressubir.\n mediaIdCache: { store: new FlowMediaIdRepository(db), senderKey: config.phoneNumberId },\n onError: hooks?.onFlowMediaError,\n }),\n )\n }\n\n // Vitrine no chat: precisa do catalogo injetado E do id do catalogo na Meta. Um sem o outro nao\n // envia nada — o `retailerId` so significa alguma coisa dentro de um catalogo.\n if (flowInterpreter && providers.catalog && config.catalogId) {\n flowInterpreter.registerFlowAction(\n FLOW_ACTION_KIND.SEND_PRODUCT_LIST,\n createSendProductListAction({\n catalog: providers.catalog,\n catalogId: config.catalogId,\n logMessage,\n startState,\n onError: hooks?.onFlowProductListError,\n }),\n )\n }\n\n const transcriptionMode = providers.transcription?.mode ?? TRANSCRIPTION_MODE.ON_DEMAND\n\n /**\n * Ambiente decide se é POSSÍVEL (o transcritor acima), settings decide se é para FAZER. Este\n * resolvedor junta os dois, a cada áudio, para o interruptor do painel valer sem deploy.\n */\n const resolveTranscriptionPolicy = providers.transcription\n ? createTranscriptionPolicyResolver({\n settingsRepository,\n defaults: {\n isEnabled: providers.transcription.isEnabledByDefault ?? true,\n mode: transcriptionMode,\n },\n })\n : undefined\n\n // Só existe com storage injetado — sem ele não há para onde copiar o binário, e devolver um\n // use-case que sempre falha seria pior do que a ausência ser visível no tipo.\n const ingestInboundMedia = providers.objectStorage\n ? new IngestInboundMediaUseCase(\n db,\n channel,\n providers.objectStorage,\n documentRepository,\n providers.transcription && resolveTranscriptionPolicy\n ? {\n transcriber: providers.transcription.transcriber,\n resolvePolicy: resolveTranscriptionPolicy,\n messageRepository,\n ...(providers.transcription.languageHint ? { languageHint: providers.transcription.languageHint } : {}),\n ...(hooks ? { hooks } : {}),\n }\n : undefined,\n )\n : undefined\n\n /**\n * Transcrição sob demanda e retomada de pendente. Exige `getObject`: transcrever um áudio já\n * salvo é ler os bytes de volta do storage, e sem esse método o use-case só saberia falhar —\n * a ausência no tipo é mais honesta que um botão que estoura no clique.\n *\n * O modo `auto` NÃO depende disto: ele transcreve dentro da ingestão, com o buffer que acabou de\n * baixar. Um host em `auto` sem `getObject` transcreve normalmente, só não oferece \"transcrever\n * de novo\".\n */\n const transcribeAudio =\n providers.transcription && providers.objectStorage?.getObject\n ? new TranscribeAudioUseCase({\n messageRepository,\n objectStorage: providers.objectStorage as TranscribeAudioDependencies['objectStorage'],\n transcriber: providers.transcription.transcriber,\n ...(resolveTranscriptionPolicy ? { resolvePolicy: resolveTranscriptionPolicy } : {}),\n ...(providers.transcription.languageHint ? { languageHint: providers.transcription.languageHint } : {}),\n ...(hooks ? { hooks } : {}),\n })\n : undefined\n\n /**\n * A leitura da biblioteca existe mesmo sem storage injetado: listar é consultar a tabela, e\n * devolver `undefined` aqui obrigaria o host a ramificar numa rota que funciona de qualquer jeito.\n * Quem precisa de storage é o download (URL assinada) — esse fica com o host, que já tem o\n * provider em mãos.\n */\n const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository)\n\n return {\n channel,\n // undefined quando providers.objectStorage não foi injetado.\n ingestInboundMedia,\n conversations: {\n log: logMessage,\n send: sendMessage,\n takeover: new TakeoverConversationUseCase(sessionRepository, providers.realtime),\n release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),\n list: new ListConversationsUseCase(sessionRepository),\n listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),\n listDocuments,\n // Biblioteca da empresa inteira, para uma tela de Documentos fora da conversa.\n listCompanyDocuments: new ListCompanyDocumentsUseCase(documentRepository),\n // Apaga a mídia no storage antes das linhas — a cascata da FK sozinha deixaria os binários\n // órfãos, já que a lista de uploadId vive justamente nas linhas que ela derruba.\n delete: new DeleteConversationUseCase(sessionRepository, documentRepository, providers.objectStorage),\n purgeExpiredDocuments: new PurgeExpiredDocumentsUseCase(documentRepository, providers.objectStorage),\n export: new ExportConversationUseCase(sessionRepository),\n // undefined quando transcrição não foi injetada, ou quando o storage não sabe ler de volta.\n // O painel consulta a ausência para decidir se desenha o botão \"transcrever\".\n transcribeAudio,\n repository: sessionRepository,\n messageRepository,\n documentRepository,\n },\n /**\n * `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a\n * capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —\n * é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.\n */\n transcription:\n providers.transcription && resolveTranscriptionPolicy\n ? { defaultMode: transcriptionMode, resolvePolicy: resolveTranscriptionPolicy }\n : undefined,\n settings: settingsRepository,\n /**\n * `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a\n * ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.\n */\n previewMedia: previewMediaSupport\n ? new StorePreviewMediaUseCase(\n providers.objectStorage!,\n () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n )\n : undefined,\n webhook: {\n receive: receiveWebhook,\n // GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.\n verifyChallenge: (query: { mode: string | null; token: string | null; challenge: string | null }) =>\n verifyWebhookChallenge({ ...query, expectedToken: config.webhookVerifyToken }),\n },\n // undefined quando features.flowEngine === false.\n flows: flowInterpreter\n ? {\n interpreter: flowInterpreter,\n registerFlowAction: (kind: FlowActionKind, handler: FlowActionHandler) =>\n flowInterpreter.registerFlowAction(kind, handler),\n get: new GetFlowGraphUseCase(flowGraphRepository),\n list: new ListFlowGraphsUseCase(flowGraphRepository),\n create: new CreateFlowGraphUseCase(flowGraphRepository),\n save: new SaveFlowGraphUseCase(flowGraphRepository),\n delete: new DeleteFlowGraphUseCase(flowGraphRepository),\n livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),\n repository: flowGraphRepository,\n // Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor\n // (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é\n // consultar a tabela, e só o ENVIO precisa dos bytes.\n mediaRepository: flowMediaRepository,\n }\n : undefined,\n catalog: providers.catalog,\n }\n}\n\nexport type MetaWhatsAppModule = ReturnType<typeof createMetaWhatsAppModule>\n","import { and, desc, eq, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type {\n ConversationSummary,\n MessageDirection,\n SessionMode,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { messages, sessions, type SessionRow } from '../schema/schema'\n\nexport interface ListConversationsFilters {\n page?: number\n limit?: number\n waitingHuman?: boolean\n search?: string\n}\n\nconst DEFAULT_LIMIT = 20\n\n// Expressões correlacionadas da lista de conversas, extraídas para poderem ser renderizadas e\n// verificadas sem banco (ver SessionRepository.test.ts).\n//\n// A correlação usa `${sessions}.company_id` e NÃO `${sessions.companyId}`: dentro da lista de\n// seleção o drizzle renderiza a referência de coluna sem qualificar a tabela, e um\n// `\"company_id\"` solto dentro do subselect resolve para a coluna de `m`. O resultado seria\n// `m.company_id = m.company_id` (sempre verdadeiro) e `m.session_id = m.id` (nunca) — zero\n// linhas, NULL, e nenhum erro.\nexport const conversationSummaryProjection = {\n lastContent: sql<string | null>`(\n select m.content from ${messages} m\n where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id\n order by m.created_at desc limit 1\n )`,\n lastDirection: sql<string | null>`(\n select m.direction from ${messages} m\n where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id\n order by m.created_at desc limit 1\n )`,\n // Entradas do cliente depois da última leitura do atendente. Sessão nunca lida conta\n // tudo — é o comportamento esperado de uma conversa que ninguém abriu ainda.\n unread: sql<number>`(\n select count(*)::int from ${messages} m\n where m.company_id = ${sessions}.company_id\n and m.session_id = ${sessions}.id\n and m.direction = 'inbound'\n and (${sessions}.last_agent_read_at is null or m.created_at > ${sessions}.last_agent_read_at)\n )`,\n} as const\n\n// Mescla do context feita pelo Postgres. Extraída para ser verificável sem banco (ver\n// SessionRepository.test.ts): o `::jsonb` é obrigatório — sem ele o parâmetro chega como text e\n// o `||` concatena strings em vez de mesclar objetos, corrompendo o context em silêncio.\nexport function sessionContextPatch(patch: Record<string, unknown>) {\n return sql`${sessions.context} || ${JSON.stringify(patch)}::jsonb`\n}\n\n// T3.2 — todo método recebe/filtra por companyId explicitamente (nunca lê de um campo do\n// payload do cliente); ver database.md \"Consistência e multiempresa\".\nexport class SessionRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n async getContext(companyId: string, whatsappNumber: string): Promise<SessionRow | undefined> {\n const [row] = await this.db\n .select()\n .from(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n .limit(1)\n return row\n }\n\n async getOrCreate(companyId: string, whatsappNumber: string, startState: SessionState): Promise<SessionRow> {\n const existing = await this.getContext(companyId, whatsappNumber)\n if (existing) return existing\n\n const [created] = await this.db\n .insert(sessions)\n .values({ companyId, whatsappNumber, currentState: startState })\n .onConflictDoUpdate({\n target: [sessions.companyId, sessions.whatsappNumber],\n set: { lastActivity: sql`now()` },\n })\n .returning()\n return created!\n }\n\n // O `context` jsonb é o ponto de extensão oficial para estado de sessão por produto: o módulo\n // não conhece a forma, o consumidor a declara em TSessionContext. Este setter SUBSTITUI o\n // objeto inteiro — para acumular respostas ao longo da conversa use patchContext.\n async setState<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n state: SessionState,\n context?: TSessionContext,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n currentState: state,\n ...(context !== undefined ? { context } : {}),\n lastActivity: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Mescla parcial do context, feita no banco (`||`) e não por read-modify-write no host: duas\n // mensagens do mesmo cliente processadas em paralelo sobrescreveriam uma à outra, e o campo\n // acumula justamente as respostas coletadas ao longo da conversa. Chave presente no patch\n // vence a existente; as demais permanecem.\n async patchContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n patch: Partial<TSessionContext>,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n context: sessionContextPatch(patch),\n lastActivity: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Leitura tipada do estado de sessão do produto. Devolve undefined quando a sessão não existe —\n // distinto de existir com context vazio, que devolve o objeto vazio.\n async readContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(\n companyId: string,\n whatsappNumber: string,\n ): Promise<TSessionContext | undefined> {\n const [row] = await this.db\n .select({ context: sessions.context })\n .from(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n .limit(1)\n return row?.context as TSessionContext | undefined\n }\n\n // Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.\n // Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).\n async setFlowPosition(\n companyId: string,\n whatsappNumber: string,\n flowKey: string | null,\n currentNodeId: string | null,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({ flowKey, currentNodeId, lastActivity: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Marca a chegada de uma mensagem DO CLIENTE. É este carimbo — e não lastActivity, que também\n // se move em ações do atendente/bot — que define a janela de 24h do WhatsApp: fora dela, só\n // template aprovado reabre a conversa (ver WindowExpiredError nos contracts).\n async touchInbound(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ lastInboundAt: sql`now()`, lastActivity: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // Horas desde a última mensagem do cliente — undefined se ele nunca escreveu. O canal (Fase 5)\n // usa isto para decidir entre mensagem livre e template antes de chamar a Graph API.\n async hoursSinceLastInbound(companyId: string, whatsappNumber: string): Promise<number | undefined> {\n const session = await this.getContext(companyId, whatsappNumber)\n if (!session?.lastInboundAt) return undefined\n return (Date.now() - session.lastInboundAt.getTime()) / (1000 * 60 * 60)\n }\n\n // `clearHumanRequest` é opt-in: quem chama `setMode` direto para outros fins (ex.: trocar de\n // atendente sem mexer na fila) continua sem afetar `humanRequestedAt`. Só `takeover`/`release`\n // pedem a limpeza, porque só eles representam \"o pedido de atendimento foi resolvido\".\n async setMode(\n companyId: string,\n whatsappNumber: string,\n mode: SessionMode,\n assignedUserId?: string | null,\n clearHumanRequest = false,\n ): Promise<void> {\n await this.db\n .update(sessions)\n .set({\n mode,\n assignedUserId: assignedUserId ?? null,\n ...(clearHumanRequest ? { humanRequestedAt: null } : {}),\n updatedAt: sql`now()`,\n })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n // T3.3 — takeover: atendente assume a conversa, tirando-a do modo bot.\n //\n // Limpa `humanRequestedAt`: sem isso a conversa nunca sai da fila `waitingHuman` da inbox,\n // mesmo depois de atendida — o filtro é só `humanRequestedAt is not null` (ver\n // `listByContextFilters`), então o campo tem que voltar a null quando o pedido é resolvido.\n async takeover(companyId: string, whatsappNumber: string, agentUserId: string): Promise<void> {\n await this.setMode(companyId, whatsappNumber, 'human', agentUserId, true)\n }\n\n // T3.3 — release: devolve a conversa ao bot.\n //\n // Também limpa `humanRequestedAt` pelo mesmo motivo do takeover: se o cliente pedir atendimento\n // de novo depois, `requestHuman` grava um novo timestamp normalmente.\n async release(companyId: string, whatsappNumber: string): Promise<void> {\n await this.setMode(companyId, whatsappNumber, 'bot', null, true)\n }\n\n /**\n * Apaga a sessão; a cascata das FKs leva mensagens e documentos.\n *\n * Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é\n * chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os\n * objetos antes deixa mídia órfã sendo cobrada para sempre.\n */\n async deleteByNumber(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .delete(sessions)\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async requestHuman(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ humanRequestedAt: sql`now()`, updatedAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async markRead(companyId: string, whatsappNumber: string): Promise<void> {\n await this.db\n .update(sessions)\n .set({ lastAgentReadAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)))\n }\n\n async markAllRead(companyId: string, userId: string): Promise<number> {\n const result = await this.db\n .update(sessions)\n .set({ lastAgentReadAt: sql`now()` })\n .where(and(eq(sessions.companyId, companyId), eq(sessions.assignedUserId, userId)))\n return Number((result as unknown as { rowCount?: number }).rowCount ?? 0)\n }\n\n async listByContextFilters(companyId: string, filters: ListConversationsFilters): Promise<ConversationSummary[]> {\n const limit = filters.limit ?? DEFAULT_LIMIT\n const offset = ((filters.page ?? 1) - 1) * limit\n\n const conditions = [eq(sessions.companyId, companyId)]\n if (filters.waitingHuman) conditions.push(sql`${sessions.humanRequestedAt} is not null`)\n if (filters.search) conditions.push(sql`${sessions.whatsappNumber} ilike ${'%' + filters.search + '%'}`)\n\n const rows = await this.db\n .select({\n id: sessions.id,\n whatsappNumber: sessions.whatsappNumber,\n mode: sessions.mode,\n assignedUserId: sessions.assignedUserId,\n currentState: sessions.currentState,\n lastActivity: sessions.lastActivity,\n lastInboundAt: sessions.lastInboundAt,\n humanRequestedAt: sessions.humanRequestedAt,\n // Prévia e contagem saem de subquery correlacionada em vez de N+1 na volta: uma inbox\n // lista dezenas de conversas por página, e uma query por linha é o gargalo clássico\n // dessa tela. Ambos os campos são dados do próprio módulo — deixá-los para o host\n // obrigaria todo consumidor a reescrever o mesmo join contra tabelas que não são dele.\n ...conversationSummaryProjection,\n })\n .from(sessions)\n .where(and(...conditions))\n .orderBy(desc(sessions.lastActivity))\n .limit(limit)\n .offset(offset)\n\n return rows.map((row) => ({\n id: row.id,\n whatsappNumber: row.whatsappNumber,\n ...(row.lastContent !== null ? { lastContent: row.lastContent } : {}),\n ...(row.lastDirection !== null ? { lastDirection: row.lastDirection as MessageDirection } : {}),\n lastAt: row.lastActivity.toISOString(),\n lastInboundAt: row.lastInboundAt?.toISOString() ?? null,\n mode: row.mode as SessionMode,\n assignedUserId: row.assignedUserId,\n waitingHuman: row.humanRequestedAt !== null,\n unread: Number(row.unread),\n currentState: row.currentState,\n }))\n }\n\n // T3.3 — export: transcript completo de uma conversa, mensagens em ordem cronológica.\n async exportConversation(companyId: string, whatsappNumber: string) {\n const session = await this.getContext(companyId, whatsappNumber)\n if (!session) return null\n\n const rows = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, companyId), eq(messages.sessionId, session.id)))\n .orderBy(messages.createdAt)\n\n return { session, messages: rows }\n }\n}\n","import { sql } from 'drizzle-orm'\nimport {\n pgSchema,\n uuid,\n varchar,\n jsonb,\n timestamp,\n text,\n integer,\n boolean,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core'\nimport type { TranscriptionMode, TranscriptionStatus } from '../transcription.types'\n\n// Schema Postgres dedicado (T3.1/T3.5) — o módulo nunca escreve no schema `public` do host,\n// só ocupa este namespace próprio. O banco continua sendo um por produto; isto é apenas um\n// namespace dentro dele (ver rules/packages/pluggable-module.md §3).\nexport const metaWhatsAppSchema = pgSchema('meta_whatsapp')\n\nexport const sessions = metaWhatsAppSchema.table(\n 'sessions',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n // Multiempresa: toda entidade carrega companyId, vindo do contexto autenticado do host —\n // nunca de um campo livre do cliente (ver database.md \"Consistência e multiempresa\").\n companyId: uuid('company_id').notNull(),\n whatsappNumber: varchar('whatsapp_number', { length: 20 }).notNull(),\n // varchar (não enum): a máquina de estados de cada produto evolui de forma independente do módulo.\n currentState: varchar('current_state', { length: 64 }).notNull().default('start'),\n // Posição no grafo, quando o motor de fluxo está em uso — colunas separadas em vez de\n // convencionar \"flowKey:nodeId\" dentro de currentState: além de ser um contrato implícito que\n // nada garantia, a concatenação estourava o varchar(64) (flow key já é varchar(64) sozinha).\n flowKey: varchar('flow_key', { length: 64 }),\n currentNodeId: varchar('current_node_id', { length: 64 }),\n context: jsonb('context').$type<Record<string, unknown>>().notNull().default({}),\n mode: varchar('mode', { length: 12 }).notNull().default('bot'), // bot | human\n assignedUserId: uuid('assigned_user_id'),\n humanRequestedAt: timestamp('human_requested_at', { withTimezone: true }),\n lastInboundAt: timestamp('last_inbound_at', { withTimezone: true }),\n lastAgentReadAt: timestamp('last_agent_read_at', { withTimezone: true }),\n lastActivity: timestamp('last_activity', { withTimezone: true }).notNull().defaultNow(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n // Único por empresa (não globalmente) — dois hosts diferentes podem atender o mesmo número.\n uniqueIndex('idx_sessions_company_number').on(table.companyId, table.whatsappNumber),\n index('idx_sessions_company_mode').on(table.companyId, table.mode),\n // Alimenta getLiveFlowPositions (agregação por nó do grafo).\n index('idx_sessions_company_flow_node').on(table.companyId, table.flowKey, table.currentNodeId),\n ],\n)\n\nexport const messages = metaWhatsAppSchema.table(\n 'messages',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n sessionId: uuid('session_id')\n .notNull()\n .references(() => sessions.id, { onDelete: 'cascade' }),\n whatsappNumber: varchar('whatsapp_number', { length: 20 }).notNull(),\n direction: varchar('direction', { length: 12 }).notNull(), // inbound | outbound\n sender: varchar('sender', { length: 12 }).notNull(), // customer | bot | agent\n agentUserId: uuid('agent_user_id'),\n // 32, não 16: os tipos da própria Meta são curtos ('text', 'interactive'), mas o host rotula\n type:\n // a saída com o subtipo que enviou ('interactive_buttons' já tem 19). Apertar aqui só\n // transfere para o consumidor a escolha entre truncar o rótulo e estourar o insert.\n varchar('type', { length: 32 }).notNull().default('text'),\n content: text('content'),\n // T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por\n // uploadId dentro deste jsonb.\n payload: jsonb('payload').$type<Record<string, unknown>>(),\n waMessageId: varchar('wa_message_id', { length: 128 }),\n status: varchar('status', { length: 16 }),\n readAt: timestamp('read_at', { withTimezone: true }),\n // Moderação de conteúdo. `null` significa NÃO AVALIADO (moderação desligada, ou mensagem\n // anterior ao recurso) — diferente de `false`, que é avaliado e limpo. Colunas em vez de chave\n // dentro de `payload` porque \"listar o que foi sinalizado\" é consulta de operação, e índice\n // parcial sobre boolean resolve isso sem cavar jsonb.\n moderationFlagged: boolean('moderation_flagged'),\n moderationTerms: jsonb('moderation_terms').$type<string[]>(),\n /**\n * Transcrição de áudio. `null` em `transcription_status` significa NÃO AVALIADO — áudio nunca\n * pedido (modo sob demanda), transcrição desligada, mensagem anterior ao recurso, ou mensagem\n * que não é áudio. Diferente de `'done'` com texto vazio, que é áudio em silêncio já processado\n * e que NÃO deve ser reprocessado.\n *\n * Colunas em vez de chave em `payload` pelo mesmo motivo da moderação: \"quais áudios ficaram\n * pendentes\" e \"quais falharam\" são consultas de operação, e índice parcial resolve sem cavar\n * jsonb. Buscar texto de áudio também deixa de exigir varredura de payload.\n */\n transcriptionStatus: varchar('transcription_status', { length: 16 }).$type<TranscriptionStatus>(),\n transcriptionText: text('transcription_text'),\n transcriptionLanguage: varchar('transcription_language', { length: 32 }),\n /** Qual engine produziu. Com uma cadeia de engines, é o que responde \"por que esta saiu ruim\". */\n transcriptionEngine: varchar('transcription_engine', { length: 32 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_messages_session_created').on(table.sessionId, table.createdAt),\n index('idx_messages_company_number_created').on(table.companyId, table.whatsappNumber, table.createdAt),\n // Parcial e ÚNICO: é o que de fato garante idempotência de entrega. Um SELECT-antes-de-INSERT\n // não basta — a Meta reenvia a mesma entrega e várias instâncias do host processam em\n // paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque\n // mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs\n // não podem competir entre si por unicidade.\n uniqueIndex('idx_messages_company_wa_message_id')\n .on(table.companyId, table.waMessageId)\n .where(sql`${table.waMessageId} is not null`),\n // Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do\n // tamanho do transcript.\n index('idx_messages_moderation_flagged')\n .on(table.companyId, table.createdAt)\n .where(sql`${table.moderationFlagged}`),\n // Alimenta a varredura de retomada: \"quais áudios ficaram pendentes de transcrição\". Parcial\n // porque pendente é estado transitório e raro — o índice fica do tamanho da fila atrasada, não\n // do transcript inteiro.\n index('idx_messages_transcription_pending')\n .on(table.companyId, table.createdAt)\n .where(sql`${table.transcriptionStatus} = 'pending'`),\n ],\n)\n\n/**\n * Biblioteca de arquivos da conversa.\n *\n * Tabela própria, e não derivada de `messages.payload`: o painel precisa de `source` e `linkedAt`,\n * que não existem no payload; busca por nome de arquivo quer índice, não varredura de jsonb; e\n * `messageId` anulável é o que permite o atendente anexar documento à conversa sem que exista uma\n * mensagem correspondente.\n *\n * `sessionId` em cascata apaga a LINHA junto com a conversa, mas não o binário no storage — quem\n * apaga objeto é passo de aplicação (listar `uploadId` → apagar no storage → apagar a sessão).\n * Confiar só na FK deixaria objeto órfão sendo cobrado para sempre.\n */\nexport const documents = metaWhatsAppSchema.table(\n 'documents',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n sessionId: uuid('session_id')\n .notNull()\n .references(() => sessions.id, { onDelete: 'cascade' }),\n // Anulável e `set null`: apagar a mensagem não deve apagar o arquivo da biblioteca, e arquivo\n // do atendente nasce sem mensagem.\n messageId: uuid('message_id').references(() => messages.id, { onDelete: 'set null' }),\n uploadId: varchar('upload_id', { length: 256 }).notNull(),\n filename: varchar('filename', { length: 512 }).notNull(),\n mimeType: varchar('mime_type', { length: 128 }).notNull(),\n sizeBytes: integer('size_bytes').notNull(),\n // Do provider de storage, que é endereçado por conteúdo — serve para reconciliar storage\n // contra tabela e para detectar o mesmo binário chegando duas vezes.\n sha256: varchar('sha256', { length: 64 }),\n source: varchar('source', { length: 12 }).notNull(), // customer | agent | bot\n linkedAt: timestamp('linked_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_documents_session_linked').on(table.sessionId, table.linkedAt),\n // Para varredura de retenção por idade sem passar por sessão.\n index('idx_documents_company_linked').on(table.companyId, table.linkedAt),\n // O mesmo objeto não pode ser linkado duas vezes na mesma empresa: o job de ingestão é\n // reentregue por retry, e sem isto a reentrega criaria linha duplicada no painel.\n uniqueIndex('idx_documents_company_upload').on(table.companyId, table.uploadId),\n ],\n)\n\n// T4.1 — um grafo por fluxo de conversa. `nodes` guarda o Record<string, FlowNodeData> inteiro\n// como jsonb (mesmo shape do editor visual em conversations-ui/flows) — não normalizado em\n// linhas, porque o grafo é sempre lido/escrito como uma unidade pelo editor e pelo interpretador.\nexport const flowGraphs = metaWhatsAppSchema.table(\n 'flow_graphs',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n key: varchar('key', { length: 64 }).notNull(),\n label: varchar('label', { length: 120 }).notNull(),\n startNodeId: varchar('start_node_id', { length: 64 }).notNull(),\n nodes: jsonb('nodes').$type<Record<string, unknown>>().notNull().default({}),\n // Otimista: o editor manda a versão que carregou; salvar com versão desatualizada é\n // rejeitado pelo host (evita duas abas sobrescreverem uma a outra silenciosamente).\n version: integer('version').notNull().default(1),\n showInMenu: boolean('show_in_menu').notNull().default(false),\n menuOptionLabel: varchar('menu_option_label', { length: 64 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [uniqueIndex('idx_flow_graphs_company_key').on(table.companyId, table.key)],\n)\n\n/**\n * Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.\n *\n * Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA\n * CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o\n * mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é\n * enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.\n *\n * Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o\n * índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então\n * apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a\n * limpeza é passo de aplicação ao salvar o grafo.\n */\nexport const flowMedia = metaWhatsAppSchema.table(\n 'flow_media',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n companyId: uuid('company_id').notNull(),\n flowKey: varchar('flow_key', { length: 64 }).notNull(),\n nodeId: varchar('node_id', { length: 64 }).notNull(),\n uploadId: varchar('upload_id', { length: 256 }).notNull(),\n filename: varchar('filename', { length: 512 }).notNull(),\n mimeType: varchar('mime_type', { length: 128 }).notNull(),\n sizeBytes: integer('size_bytes').notNull(),\n // Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e\n // um folder precisa de textos diferentes para cada um.\n caption: text('caption'),\n // Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e \"tabela antes\n // do folder\" é decisão de quem edita, não do banco.\n sortOrder: integer('sort_order').notNull().default(0),\n // Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha\n // perderia a ordem e a legenda já ajustadas.\n active: boolean('active').notNull().default(true),\n // Id do arquivo já subido para a Meta, por número remetente. Sem isto, o MESMO binário subia de\n // novo para cada cliente que passava pelo nó: a Meta aceita reusar o id por 30 dias.\n //\n // Mapa por `phone_number_id`, e não coluna única: o id é escopado ao número que envia, e uma\n // instalação com dois números mandaria o id de um pelo outro. A validade não é gravada de\n // propósito — confiar em \"30 dias\" calculados erra nos casos de borda, e quem decide é a\n // recusa da Meta, que devolve ao caminho de subir o binário.\n metaMediaIds: jsonb('meta_media_ids').$type<Record<string, string>>().notNull().default({}),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => [\n index('idx_flow_media_node').on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),\n // O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente\n // receberia o documento repetido.\n uniqueIndex('idx_flow_media_node_upload').on(table.companyId, table.flowKey, table.nodeId, table.uploadId),\n ],\n)\n\n// T5.5 — configuração de WhatsApp POR EMPRESA, em tabela do módulo. Deliberadamente separada do\n// app_config genérico do host: são chaves que só o módulo entende, e mantê-las aqui é o que\n// permite instalar/remover a capacidade sem migrar a tabela de configuração do produto.\nexport const settings = metaWhatsAppSchema.table('settings', {\n companyId: uuid('company_id').primaryKey(),\n templateName: varchar('template_name', { length: 128 }),\n templateLanguage: varchar('template_language', { length: 16 }).notNull().default('pt_BR'),\n // Mapa posicional {{1}}, {{2}}... → token resolvido no envio (ex.: '{clientName}').\n templateVariables: jsonb('template_variables').$type<string[]>().notNull().default([]),\n welcomeMessage: text('welcome_message'),\n farewellMessage: text('farewell_message'),\n /**\n * Política de transcrição desta empresa. Nulo é significativo: \"o painel não decidiu\", e aí vale\n * o padrão que o host injetou. Sem a distinção, atualizar o módulo desligaria a transcrição de\n * quem já a tinha ligada por ambiente.\n */\n transcriptionEnabled: boolean('transcription_enabled'),\n transcriptionMode: varchar('transcription_mode', { length: 16 }).$type<TranscriptionMode>(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\nexport type SettingsRow = typeof settings.$inferSelect\nexport type NewSettingsRow = typeof settings.$inferInsert\n\nexport type SessionRow = typeof sessions.$inferSelect\nexport type NewSessionRow = typeof sessions.$inferInsert\nexport type MessageRow = typeof messages.$inferSelect\nexport type NewMessageRow = typeof messages.$inferInsert\nexport type FlowGraphRow = typeof flowGraphs.$inferSelect\nexport type NewFlowGraphRow = typeof flowGraphs.$inferInsert\nexport type DocumentRow = typeof documents.$inferSelect\nexport type NewDocumentRow = typeof documents.$inferInsert\nexport type FlowMediaRow = typeof flowMedia.$inferSelect\nexport type NewFlowMediaRow = typeof flowMedia.$inferInsert\n","import { and, eq, lt } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { MessageDirection, MessageSender, MessageStatus } from '@adatechnology/meta-whatsapp-contracts'\nimport { messages, type MessageRow, type NewMessageRow } from '../schema/schema'\nimport type { TranscriptionStatus } from '../transcription.types'\n\nexport interface InsertMessageParams {\n companyId: string\n sessionId: string\n whatsappNumber: string\n direction: MessageDirection\n sender: MessageSender\n agentUserId?: string | null\n type: string\n content?: string | null\n payload?: Record<string, unknown> | null\n waMessageId?: string | null\n status?: MessageStatus | null\n /** `undefined` deixa a coluna nula: não avaliado, distinto de avaliado e limpo. */\n moderationFlagged?: boolean | null\n moderationTerms?: string[] | null\n}\n\nexport interface ListMessagesParams {\n companyId: string\n sessionId: string\n limit?: number\n before?: string\n}\n\nexport interface SaveTranscriptionByWaMessageIdParams extends Omit<SaveTranscriptionParams, 'messageId'> {\n /**\n * Id da mensagem na Meta. É o único que quem processa o webhook conhece — o id do módulo só\n * existe depois da gravação, e obrigar o host a descobri-lo faria cada um escrever a própria\n * consulta por `wa_message_id`.\n */\n waMessageId: string\n}\n\nexport interface SaveTranscriptionParams {\n companyId: string\n messageId: string\n status: TranscriptionStatus\n /** Ausente em `pending`/`failed`/`unsupported`; vazio em `done` é silêncio já processado. */\n text?: string | null\n language?: string | null\n engine?: string | null\n}\n\nconst DEFAULT_LIMIT = 50\n\nexport class MessageRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n // Idempotente por (companyId, waMessageId) — o mesmo webhook chega mais de uma vez (a Meta\n // reenvia em instabilidade) e várias instâncias do host processam em paralelo. A garantia é o\n // índice único parcial no banco + onConflictDoNothing, NÃO um SELECT prévio: duas entregas\n // concorrentes passariam as duas pela checagem e inseririam duplicado.\n // Devolve undefined quando a mensagem já existia (nada foi inserido).\n async insertMessage(params: InsertMessageParams): Promise<MessageRow | undefined> {\n const values: NewMessageRow = {\n companyId: params.companyId,\n sessionId: params.sessionId,\n whatsappNumber: params.whatsappNumber,\n direction: params.direction,\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: params.type,\n content: params.content ?? null,\n payload: params.payload ?? null,\n waMessageId: params.waMessageId ?? null,\n status: params.status ?? null,\n moderationFlagged: params.moderationFlagged ?? null,\n moderationTerms: params.moderationTerms ?? null,\n }\n\n const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning()\n return created\n }\n\n async updateMessageStatus(\n companyId: string,\n waMessageId: string,\n status: MessageStatus,\n ): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({ status, ...(status === 'read' ? { readAt: new Date() } : {}) })\n .where(and(eq(messages.companyId, companyId), eq(messages.waMessageId, waMessageId)))\n .returning()\n return updated\n }\n\n /**\n * Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.\n *\n * Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para\n * responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever\n * o mesmo áudio uma segunda vez só para o painel ver.\n *\n * Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são\n * corridas normais, não erro.\n */\n async saveTranscriptionByWaMessageId(params: SaveTranscriptionByWaMessageIdParams): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({\n transcriptionStatus: params.status,\n ...(params.text !== undefined ? { transcriptionText: params.text } : {}),\n ...(params.language !== undefined ? { transcriptionLanguage: params.language } : {}),\n ...(params.engine !== undefined ? { transcriptionEngine: params.engine } : {}),\n })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.waMessageId, params.waMessageId)))\n .returning()\n return updated\n }\n\n async findById(companyId: string, messageId: string): Promise<MessageRow | undefined> {\n const [found] = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, companyId), eq(messages.id, messageId)))\n .limit(1)\n return found\n }\n\n /**\n * Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada\n * entre o enfileiramento e a execução do job) — não é erro, é corrida normal.\n *\n * `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar\n * atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de\n * outro engine da cadeia.\n */\n async saveTranscription(params: SaveTranscriptionParams): Promise<MessageRow | undefined> {\n const [updated] = await this.db\n .update(messages)\n .set({\n transcriptionStatus: params.status,\n ...(params.text !== undefined ? { transcriptionText: params.text } : {}),\n ...(params.language !== undefined ? { transcriptionLanguage: params.language } : {}),\n ...(params.engine !== undefined ? { transcriptionEngine: params.engine } : {}),\n })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n .returning()\n return updated\n }\n\n async listByConversation(params: ListMessagesParams): Promise<MessageRow[]> {\n const conditions = [eq(messages.companyId, params.companyId), eq(messages.sessionId, params.sessionId)]\n if (params.before) conditions.push(lt(messages.createdAt, new Date(params.before)))\n\n return this.db\n .select()\n .from(messages)\n .where(and(...conditions))\n .orderBy(messages.createdAt)\n .limit(params.limit ?? DEFAULT_LIMIT)\n }\n}\n","import { and, eq, isNotNull, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { flowGraphNodesSchema } from '@adatechnology/meta-whatsapp-contracts'\nimport type {\n FlowGraphData,\n FlowGraphSummary,\n FlowNodeData,\n LiveFlowPosition,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { sessions, flowGraphs, type FlowGraphRow } from '../schema/schema'\nimport type { FlowGraphCache } from './FlowGraphCache'\n\nexport class InvalidFlowGraphError extends Error {\n constructor(\n key: string,\n public readonly validationMessage: string,\n ) {\n super(`Grafo do fluxo \"${key}\" está malformado no banco: ${validationMessage}`)\n this.name = 'InvalidFlowGraphError'\n }\n}\n\n// O jsonb `nodes` entrou pelo editor (dado de origem cliente) e volta do banco como `unknown`.\n// Validar aqui, na fronteira, faz um grafo corrompido falhar com a causa explícita em vez de\n// virar comportamento estranho lá dentro do interpretador.\nfunction toContractGraph(row: FlowGraphRow): FlowGraphData {\n const parsed = flowGraphNodesSchema.safeParse(row.nodes)\n if (!parsed.success) throw new InvalidFlowGraphError(row.key, parsed.error.message)\n\n return {\n key: row.key,\n label: row.label,\n startNodeId: row.startNodeId,\n version: row.version,\n nodes: parsed.data as Record<string, FlowNodeData>,\n }\n}\n\nexport class OptimisticLockError extends Error {\n constructor(key: string) {\n super(`Fluxo \"${key}\" foi alterado por outra sessão — recarregue antes de salvar.`)\n this.name = 'OptimisticLockError'\n }\n}\n\n// T4.2 — CRUD de grafo de fluxo, mais a consulta de posições ao vivo (quantas sessões estão\n// em cada nó agora) que alimenta o liveCount no editor visual (FlowNodeCardData).\nexport class FlowGraphRepository {\n // Cache opcional: sem ele o repositório se comporta exatamente como antes, lendo sempre do\n // banco. É o host que decide se quer cachear e com qual provedor (ver CacheInterface).\n constructor(\n private readonly db: MetaWhatsAppDatabase,\n private readonly cache?: FlowGraphCache,\n ) {}\n\n async get(companyId: string, key: string): Promise<FlowGraphData | undefined> {\n const cached = await this.cache?.read(companyId, key)\n if (cached) return cached\n\n const [row] = await this.db\n .select()\n .from(flowGraphs)\n .where(and(eq(flowGraphs.companyId, companyId), eq(flowGraphs.key, key)))\n .limit(1)\n if (!row) return undefined\n\n const graph = toContractGraph(row)\n await this.cache?.write(companyId, graph)\n return graph\n }\n\n async list(companyId: string): Promise<FlowGraphSummary[]> {\n const rows = await this.db.select().from(flowGraphs).where(eq(flowGraphs.companyId, companyId))\n return rows.map((row) => ({\n key: row.key,\n label: row.label,\n nodeCount: Object.keys(row.nodes as Record<string, unknown>).length,\n showInMenu: row.showInMenu,\n menuOptionLabel: row.menuOptionLabel ?? undefined,\n updatedAt: row.updatedAt.toISOString(),\n }))\n }\n\n // Valida antes de gravar: aceitar um grafo malformado e só reclamar na leitura empurraria a\n // falha para o meio de uma conversa real, em vez de barrar quem salvou.\n private assertValidNodes(key: string, nodes: unknown): void {\n const parsed = flowGraphNodesSchema.safeParse(nodes)\n if (!parsed.success) throw new InvalidFlowGraphError(key, parsed.error.message)\n }\n\n async create(\n companyId: string,\n graph: Omit<FlowGraphData, 'version'> & { showInMenu?: boolean; menuOptionLabel?: string },\n ): Promise<FlowGraphData> {\n this.assertValidNodes(graph.key, graph.nodes)\n\n const [created] = await this.db\n .insert(flowGraphs)\n .values({\n companyId,\n key: graph.key,\n label: graph.label,\n startNodeId: graph.startNodeId,\n nodes: graph.nodes,\n showInMenu: graph.showInMenu ?? false,\n menuOptionLabel: graph.menuOptionLabel,\n })\n .returning()\n\n const createdGraph = toContractGraph(created!)\n await this.cache?.invalidate(companyId, createdGraph.key)\n return createdGraph\n }\n\n // Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —\n // senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).\n async save(companyId: string, graph: FlowGraphData, expectedVersion: number): Promise<FlowGraphData> {\n this.assertValidNodes(graph.key, graph.nodes)\n\n const rows = await this.db\n .update(flowGraphs)\n .set({\n label: graph.label,\n startNodeId: graph.startNodeId,\n nodes: graph.nodes,\n version: expectedVersion + 1,\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(flowGraphs.companyId, companyId),\n eq(flowGraphs.key, graph.key),\n eq(flowGraphs.version, expectedVersion),\n ),\n )\n .returning()\n\n if (rows.length === 0) throw new OptimisticLockError(graph.key)\n\n // Invalida em vez de reescrever a entrada com o grafo novo: reescrever perderia a corrida\n // contra uma leitura concorrente que já tivesse buscado a versão anterior e ainda não tivesse\n // gravado — o cache ficaria com o grafo velho, com TTL cheio pela frente.\n await this.cache?.invalidate(companyId, graph.key)\n return toContractGraph(rows[0]!)\n }\n\n async delete(companyId: string, key: string): Promise<void> {\n await this.db.delete(flowGraphs).where(and(eq(flowGraphs.companyId, companyId), eq(flowGraphs.key, key)))\n await this.cache?.invalidate(companyId, key)\n }\n\n // T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as\n // colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,\n // coberto por idx_sessions_company_flow_node) em vez de trazer toda sessão da empresa para\n // contar em memória.\n async getLiveFlowPositions(companyId: string): Promise<LiveFlowPosition[]> {\n const rows = await this.db\n .select({\n flowKey: sessions.flowKey,\n nodeId: sessions.currentNodeId,\n count: sql<number>`count(*)::int`,\n })\n .from(sessions)\n .where(\n and(\n eq(sessions.companyId, companyId),\n eq(sessions.mode, 'bot'),\n isNotNull(sessions.flowKey),\n isNotNull(sessions.currentNodeId),\n ),\n )\n .groupBy(sessions.flowKey, sessions.currentNodeId)\n\n return rows.map((row) => ({ flowKey: row.flowKey!, nodeId: row.nodeId!, count: row.count }))\n }\n}\n","import type { CacheInterface, FlowGraphData } from '@adatechnology/meta-whatsapp-contracts'\n\n// Namespace na chave para o cache do host poder ser compartilhado com o resto da aplicação sem\n// risco de colisão — o host escolhe onde o Redis mora, não como o módulo nomeia o que guarda.\nconst KEY_PREFIX = 'meta-whatsapp:flow-graph'\n\nexport const DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300\n\n/**\n * Cache de leitura dos grafos de fluxo, com invalidação na publicação.\n *\n * TTL **e** invalidação explícita, e não um dos dois: só TTL deixaria o cliente andando no grafo\n * antigo até a chave expirar, logo depois de alguém corrigir o fluxo no editor; só invalidação\n * deixaria cache envenenado para sempre se um `delete` se perdesse (Redis reiniciando, deploy no\n * meio da escrita).\n *\n * Toda operação é tolerante a falha por decisão de projeto: cache é aceleração, não dependência.\n * Redis fora do ar tem que degradar para leitura no banco — que é exatamente o comportamento de\n * quem não configura cache nenhum — em vez de derrubar a conversa do cliente.\n */\nexport class FlowGraphCache {\n constructor(\n private readonly provider: CacheInterface,\n private readonly ttlSeconds: number = DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,\n ) {}\n\n // companyId na chave, e não só a flowKey: a chave do fluxo é escolhida por quem edita e se\n // repete entre empresas — 'consorcio' existe em todas — então uma chave sem tenant serviria o\n // grafo de uma empresa para a conversa de outra.\n private keyFor(companyId: string, flowKey: string): string {\n return `${KEY_PREFIX}:${companyId}:${flowKey}`\n }\n\n async read(companyId: string, flowKey: string): Promise<FlowGraphData | undefined> {\n try {\n const cached = await this.provider.get(this.keyFor(companyId, flowKey))\n if (!cached) return undefined\n return JSON.parse(cached) as FlowGraphData\n } catch {\n // Inclui JSON corrompido na chave: vale o mesmo que ausência, e o banco resolve.\n return undefined\n }\n }\n\n async write(companyId: string, graph: FlowGraphData): Promise<void> {\n try {\n await this.provider.set(this.keyFor(companyId, graph.key), JSON.stringify(graph), this.ttlSeconds)\n } catch {\n // Não conseguir acelerar não é motivo para falhar a leitura que já deu certo.\n }\n }\n\n async invalidate(companyId: string, flowKey: string): Promise<void> {\n try {\n await this.provider.delete(this.keyFor(companyId, flowKey))\n } catch {\n // Engolir aqui é seguro porque existe TTL: a entrada some sozinha em no máximo ttlSeconds.\n // Sem o TTL como rede, este catch seria um bug — o grafo velho ficaria para sempre.\n }\n }\n}\n","import { eq } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { WhatsAppSettings } from '@adatechnology/meta-whatsapp-contracts'\nimport { settings, type SettingsRow } from '../schema/schema'\n\nfunction toContractSettings(row: SettingsRow): WhatsAppSettings {\n return {\n templateName: row.templateName ?? '',\n templateLanguage: row.templateLanguage,\n templateVariables: row.templateVariables,\n welcomeMessage: row.welcomeMessage ?? '',\n farewellMessage: row.farewellMessage ?? '',\n // `?? null` e não `?? false`: nulo é \"o painel não decidiu\", e é o que faz valer o padrão do\n // host. Colapsar para `false` desligaria quem ligou por ambiente.\n transcriptionEnabled: row.transcriptionEnabled ?? null,\n transcriptionMode: row.transcriptionMode ?? null,\n }\n}\n\nconst EMPTY_SETTINGS: WhatsAppSettings = {\n templateName: '',\n templateLanguage: 'pt_BR',\n templateVariables: [],\n welcomeMessage: '',\n farewellMessage: '',\n transcriptionEnabled: null,\n transcriptionMode: null,\n}\n\n// T5.5 — configuração de WhatsApp por empresa.\nexport class SettingsRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n // Empresa sem linha de configuração é o estado normal logo após instalar o módulo — devolve\n // os defaults em vez de undefined, para o chamador não precisar tratar \"ainda não configurado\"\n // como caso especial em todo lugar.\n async get(companyId: string): Promise<WhatsAppSettings> {\n const [row] = await this.db.select().from(settings).where(eq(settings.companyId, companyId)).limit(1)\n return row ? toContractSettings(row) : EMPTY_SETTINGS\n }\n\n async save(companyId: string, update: Partial<WhatsAppSettings>): Promise<WhatsAppSettings> {\n const current = await this.get(companyId)\n const merged = { ...current, ...update }\n\n const [row] = await this.db\n .insert(settings)\n .values({\n companyId,\n templateName: merged.templateName || null,\n templateLanguage: merged.templateLanguage,\n templateVariables: merged.templateVariables,\n welcomeMessage: merged.welcomeMessage || null,\n farewellMessage: merged.farewellMessage || null,\n // Sem `|| null`: `false` aqui é decisão explícita do painel (\"desligado para esta empresa\"),\n // e colapsá-lo para nulo faria a empresa voltar a herdar o padrão do host — exatamente o\n // oposto do que o operador acabou de pedir.\n transcriptionEnabled: merged.transcriptionEnabled,\n transcriptionMode: merged.transcriptionMode,\n })\n .onConflictDoUpdate({\n target: settings.companyId,\n set: {\n templateName: merged.templateName || null,\n templateLanguage: merged.templateLanguage,\n templateVariables: merged.templateVariables,\n welcomeMessage: merged.welcomeMessage || null,\n farewellMessage: merged.farewellMessage || null,\n transcriptionEnabled: merged.transcriptionEnabled,\n transcriptionMode: merged.transcriptionMode,\n updatedAt: new Date(),\n },\n })\n .returning()\n\n return toContractSettings(row!)\n }\n\n // Resolve {{1}}, {{2}}... a partir do mapa configurado + o contexto da conversa. Ex.: se\n // templateVariables = ['{clientName}', '{city}'], devolve [nome, cidade] na ordem posicional\n // que a Graph API espera nos bodyParameters.\n async resolveTemplateVariables(companyId: string, context: Record<string, unknown>): Promise<string[]> {\n const { templateVariables } = await this.get(companyId)\n return templateVariables.map((token) => {\n const key = token.replace(/^\\{|\\}$/g, '')\n const value = context[key]\n return value === undefined || value === null ? '' : String(value)\n })\n }\n}\n","import type { RealtimeNotifierInterface, SessionState } from '@adatechnology/meta-whatsapp-contracts'\nimport type { InsertMessageParams, MessageRepository } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { MessageRow } from '../schema/schema'\n\nexport type LogMessageParams = Omit<InsertMessageParams, 'sessionId'> & {\n startState: SessionState\n}\n\n/**\n * O contrato mínimo de `@adatechnology/text-moderation`, declarado aqui em vez de importado.\n *\n * Assim o módulo não ganha dependência de pacote para um recurso opcional, e — mais importante —\n * moderação não fica amarrada a WhatsApp: o que atravessa esta fronteira é texto, e qualquer canal\n * que passe a escrever no transcript por este use case herda a marcação sem tocar nada aqui.\n */\nexport type MessageModerator = {\n inspect: (text: string) => { isOffensive: boolean; matchedTerms: readonly string[] }\n}\n\n// T3.3 — ponto único de escrita no transcript: garante a sessão (getOrCreate), insere a\n// mensagem (idempotente por waMessageId) e notifica em tempo real. Usado tanto pelo canal\n// (T5.2, ao enviar) quanto pelo webhook (T5.1, ao receber).\nexport class LogMessageUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly messageRepository: MessageRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n private readonly moderator?: MessageModerator,\n ) {}\n\n async execute(params: LogMessageParams): Promise<MessageRow | undefined> {\n const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState)\n\n const saved = await this.messageRepository.insertMessage({\n ...params,\n sessionId: session.id,\n ...this.moderationOf(params),\n })\n // Entrega duplicada (mesmo waMessageId) — não emite evento nem re-carimba a janela.\n if (!saved) return undefined\n\n if (params.direction === 'inbound') {\n await this.sessionRepository.touchInbound(params.companyId, params.whatsappNumber)\n }\n\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'message', {\n direction: params.direction,\n sender: params.sender,\n })\n this.realtime?.emit('global', 'data-changed', {})\n\n return saved\n }\n\n // Só o que o cliente escreveu: marcar o que o próprio atendente ou o bot enviou não sinaliza\n // abuso, apenas sujaria o transcript com etiqueta na resposta de quem atende.\n private moderationOf(params: LogMessageParams): Pick<InsertMessageParams, 'moderationFlagged' | 'moderationTerms'> {\n if (!this.moderator || params.direction !== 'inbound') return {}\n\n const text = params.content?.trim()\n if (!text) return {}\n\n const verdict = this.moderator.inspect(text)\n\n return {\n moderationFlagged: verdict.isOffensive,\n moderationTerms: verdict.isOffensive ? [...verdict.matchedTerms] : null,\n }\n }\n}\n","import {\n WindowExpiredError,\n type ChannelAdapterInterface,\n type ObjectStorageInterface,\n type SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { LogMessageUseCase } from './LogMessage.use-case'\nimport type { MessageRow } from '../schema/schema'\n\nconst WHATSAPP_WINDOW_HOURS = 24\n\nexport type SendTextParams = {\n companyId: string\n whatsappNumber: string\n body: string\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\nexport type SendMediaParams = {\n companyId: string\n whatsappNumber: string\n buffer: Buffer\n mimeType: string\n filename: string\n caption?: string\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\nexport type SendTemplateParams = {\n companyId: string\n whatsappNumber: string\n templateName: string\n languageCode?: string\n bodyParameters?: string[]\n sender: 'bot' | 'agent'\n agentUserId?: string\n startState: SessionState\n}\n\n// T5.2 — envio pelo canal + registro no transcript, num único ponto.\n//\n// A checagem de janela acontece ANTES da chamada à Graph API: a Meta também rejeitaria\n// (o adapter traduz o erro dela para WindowExpiredError), mas gastar uma ida à rede para\n// descobrir algo que o nosso próprio lastInboundAt já sabe é desperdício — e o erro local\n// carrega há quantas horas o cliente sumiu, que a resposta da Meta não informa.\nexport class SendMessageUseCase {\n constructor(\n private readonly channel: ChannelAdapterInterface,\n private readonly sessionRepository: SessionRepository,\n private readonly logMessage: LogMessageUseCase,\n private readonly objectStorage?: ObjectStorageInterface,\n // Opcional pelo mesmo motivo do IngestInboundMedia: sem ele o envio continua funcionando, só\n // não entra na biblioteca de arquivos da conversa.\n private readonly documentRepository?: DocumentRepository,\n ) {}\n\n private async assertWithinWindow(companyId: string, whatsappNumber: string): Promise<void> {\n const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber)\n // Cliente que nunca escreveu: não há janela aberta, só template pode iniciar a conversa.\n if (hours === undefined) throw new WindowExpiredError()\n if (hours >= WHATSAPP_WINDOW_HOURS) throw new WindowExpiredError(hours)\n }\n\n async sendText(params: SendTextParams): Promise<MessageRow | undefined> {\n await this.assertWithinWindow(params.companyId, params.whatsappNumber)\n\n const { externalMessageId } = await this.channel.sendText(params.whatsappNumber, params.body)\n\n return this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: 'text',\n content: params.body,\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n }\n\n async sendMedia(params: SendMediaParams): Promise<MessageRow | undefined> {\n await this.assertWithinWindow(params.companyId, params.whatsappNumber)\n\n const { externalMessageId } = await this.channel.sendMedia({\n to: params.whatsappNumber,\n buffer: params.buffer,\n mimeType: params.mimeType,\n filename: params.filename,\n caption: params.caption,\n })\n\n // T5.4 — o binário vai para o storage do host e o transcript guarda só a referência.\n // Gravar base64 no jsonb (dívida herdada do bot) inchava a tabela de mensagens, tornava\n // qualquer SELECT da conversa caro e duplicava um dado que o storage já tem.\n const uploadId = this.objectStorage\n ? (\n await this.objectStorage.upload({\n buffer: params.buffer,\n mimeType: params.mimeType,\n key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`,\n })\n ).uploadId\n : undefined\n\n const saved = await this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: mediaTypeFor(params.mimeType),\n content: params.caption ?? params.filename,\n payload: { filename: params.filename, mimeType: params.mimeType, ...(uploadId ? { uploadId } : {}) },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n\n // Arquivo que o atendente manda ao cliente também é arquivo da conversa: quem atende depois\n // precisa achar o que já foi enviado sem rolar a thread inteira.\n //\n // Só quando `saved` existe — `undefined` significa entrega duplicada, e linkar aí criaria uma\n // segunda linha para a mesma mensagem. E só com `uploadId`: sem storage não há objeto para\n // apontar, e documento sem destino é linha morta no painel.\n if (saved && uploadId) {\n await this.documentRepository?.link({\n companyId: params.companyId,\n sessionId: saved.sessionId,\n messageId: saved.id,\n uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.buffer.length,\n source: params.sender,\n })\n }\n\n return saved\n }\n\n // Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece\n // para reabri-la.\n async sendTemplate(params: SendTemplateParams): Promise<MessageRow | undefined> {\n const { externalMessageId } = await this.channel.sendTemplate({\n to: params.whatsappNumber,\n templateName: params.templateName,\n languageCode: params.languageCode ?? 'pt_BR',\n bodyParameters: params.bodyParameters,\n })\n\n return this.logMessage.execute({\n companyId: params.companyId,\n whatsappNumber: params.whatsappNumber,\n direction: 'outbound',\n sender: params.sender,\n agentUserId: params.agentUserId ?? null,\n type: 'template',\n content: params.templateName,\n payload: { templateName: params.templateName, bodyParameters: params.bodyParameters ?? [] },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n }\n}\n\nfunction mediaTypeFor(mimeType: string): string {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (mimeType.startsWith('video/')) return 'video'\n return 'document'\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type TakeoverConversationParams = {\n companyId: string\n whatsappNumber: string\n agentUserId: string\n}\n\n// T3.3 — atendente assume a conversa (tira do bot). Emite evento para os clientes de admin\n// atualizarem a UI (badge \"atendido por\", desabilitar botão de takeover) em tempo real.\nexport class TakeoverConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n ) {}\n\n async execute(params: TakeoverConversationParams): Promise<void> {\n await this.sessionRepository.takeover(params.companyId, params.whatsappNumber, params.agentUserId)\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'mode-changed', {\n mode: 'human',\n assignedUserId: params.agentUserId,\n })\n this.realtime?.emit('global', 'data-changed', {})\n }\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type ReleaseConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\n// T3.3 — devolve a conversa ao bot.\nexport class ReleaseConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly realtime?: RealtimeNotifierInterface,\n ) {}\n\n async execute(params: ReleaseConversationParams): Promise<void> {\n await this.sessionRepository.release(params.companyId, params.whatsappNumber)\n this.realtime?.emit(`conv:${params.whatsappNumber}`, 'mode-changed', { mode: 'bot', assignedUserId: null })\n this.realtime?.emit('global', 'data-changed', {})\n }\n}\n","import type { ConversationSummary } from '@adatechnology/meta-whatsapp-contracts'\nimport type { ListConversationsFilters, SessionRepository } from '../repositories/SessionRepository'\n\nexport type ListConversationsParams = {\n companyId: string\n filters?: ListConversationsFilters\n}\n\n// T3.3 — listagem paginada/filtrada de conversas para a tela de inbox do host.\nexport class ListConversationsUseCase {\n constructor(private readonly sessionRepository: SessionRepository) {}\n\n async execute(params: ListConversationsParams): Promise<ConversationSummary[]> {\n return this.sessionRepository.listByContextFilters(params.companyId, params.filters ?? {})\n }\n}\n","import type { MessageRepository, ListMessagesParams as RepoListMessagesParams } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRow } from '../schema/schema'\n\nexport type ListMessagesParams = {\n companyId: string\n whatsappNumber: string\n limit?: number\n before?: string\n}\n\n// T3.3 — histórico paginado de uma conversa (resolve sessionId a partir do número antes de\n// delegar ao repositório de mensagens).\nexport class ListMessagesUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly messageRepository: MessageRepository,\n ) {}\n\n async execute(params: ListMessagesParams): Promise<MessageRow[]> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const repoParams: RepoListMessagesParams = {\n companyId: params.companyId,\n sessionId: session.id,\n limit: params.limit,\n before: params.before,\n }\n return this.messageRepository.listByConversation(repoParams)\n }\n}\n","import { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\n\nexport type ListConversationDocumentsParams = {\n companyId: string\n whatsappNumber: string\n search?: string\n /** Origens explícitas. O apelido de UI (\"Equipe\") é traduzido na borda HTTP, não aqui. */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\n/**\n * O que o `ConversationDocumentsPanel` do conversations-ui consome. O shape é o do pacote de UI\n * (`ConversationDocument`), com `linkedAt` já em ISO para não obrigar cada host a serializar.\n */\nexport type ConversationDocumentView = {\n /**\n * O `uploadId` (key no storage), e NÃO o id da linha.\n *\n * É este valor que o consumidor devolve para pedir a URL assinada ou montar o zip, então expor o\n * UUID da tabela aqui fazia o download apontar para um objeto inexistente — falha que só aparece\n * no clique, porque a assinatura é gerada sem consultar o bucket.\n */\n id: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: string\n}\n\n/** Espelha o `ConversationDocumentPage` do conversations-ui: lista da página + total no servidor. */\nexport type ConversationDocumentsPage = {\n documents: ConversationDocumentView[]\n total: number\n}\n\nexport class ListConversationDocumentsUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly documentRepository: DocumentRepository,\n ) {}\n\n async execute(params: ListConversationDocumentsParams): Promise<ConversationDocumentsPage> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const { rows, total } = await this.documentRepository.listByConversation({\n companyId: params.companyId,\n sessionId: session.id,\n ...(params.search ? { search: params.search } : {}),\n ...(params.sources && params.sources.length > 0 ? { sources: params.sources } : {}),\n ...(params.sortDirection ? { sortDirection: params.sortDirection } : {}),\n ...(params.page ? { page: params.page } : {}),\n ...(params.limit ? { limit: params.limit } : {}),\n })\n\n return {\n documents: rows.map((row) => ({\n id: row.uploadId,\n filename: row.filename,\n mimeType: row.mimeType,\n sizeBytes: row.sizeBytes,\n source: row.source,\n linkedAt: row.linkedAt.toISOString(),\n })),\n total,\n }\n }\n}\n","import type { DocumentRepository } from '../repositories/DocumentRepository'\n\nexport type ListCompanyDocumentsParams = {\n companyId: string\n search?: string\n /** Origens explícitas. O apelido de UI (\"Equipe\") é traduzido na borda HTTP, não aqui. */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\n/**\n * Um arquivo na biblioteca da empresa. Igual ao da conversa, mais `conversationId` — sem saber de\n * quem veio, uma lista global de anexos não responde nenhuma pergunta útil.\n */\nexport type CompanyDocumentView = {\n /** O `uploadId` (key no storage), pelo mesmo motivo do `ConversationDocumentView`. */\n id: string\n conversationId: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: string\n}\n\nexport type CompanyDocumentsPage = {\n documents: CompanyDocumentView[]\n total: number\n}\n\n/**\n * Biblioteca de arquivos de todas as conversas da empresa.\n *\n * Existe separada de `ListConversationDocumentsUseCase` porque a pergunta é outra: aquela parte de\n * uma conversa conhecida, esta varre a empresa e por isso precisa dizer de qual conversa cada\n * arquivo veio. Reaproveitar a primeira exigiria um `sessionId` opcional que muda o significado do\n * retorno — dois nomes claros custam menos que um parâmetro que dobra o comportamento.\n */\nexport class ListCompanyDocumentsUseCase {\n constructor(private readonly documentRepository: DocumentRepository) {}\n\n async execute(params: ListCompanyDocumentsParams): Promise<CompanyDocumentsPage> {\n const { rows, total } = await this.documentRepository.listByCompany({\n companyId: params.companyId,\n ...(params.search ? { search: params.search } : {}),\n ...(params.sources && params.sources.length > 0 ? { sources: params.sources } : {}),\n ...(params.sortDirection ? { sortDirection: params.sortDirection } : {}),\n ...(params.page ? { page: params.page } : {}),\n ...(params.limit ? { limit: params.limit } : {}),\n })\n\n return {\n documents: rows.map((row) => ({\n id: row.uploadId,\n conversationId: row.whatsappNumber,\n filename: row.filename,\n mimeType: row.mimeType,\n sizeBytes: row.sizeBytes,\n source: row.source,\n linkedAt: row.linkedAt.toISOString(),\n })),\n total,\n }\n }\n}\n","import { SessionNotFoundError, type ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\n\nexport type DeleteConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\nexport type DeleteConversationResult = {\n /** Objetos efetivamente apagados no storage. */\n deletedObjects: number\n /** Objetos que o storage recusou. A conversa NÃO é apagada quando isto é maior que zero. */\n failedObjects: readonly string[]\n}\n\n/**\n * Apaga a conversa e a mídia dela.\n *\n * A ordem é o ponto: **storage primeiro, banco depois**. A FK de `documents.session_id` é\n * `on delete cascade`, então apagar a sessão primeiro derrubaria as linhas e levaria embora a única\n * lista de `uploadId` existente — os binários ficariam órfãos, cobrados para sempre e inalcançáveis.\n *\n * Se algum objeto falhar, a conversa é preservada e o chamador recebe a lista. Apagar as linhas\n * \"mesmo assim\" transformaria uma falha visível e reexecutável em lixo silencioso no storage.\n */\nexport class DeleteConversationUseCase {\n constructor(\n private readonly sessionRepository: SessionRepository,\n private readonly documentRepository: DocumentRepository,\n private readonly objectStorage?: ObjectStorageInterface,\n ) {}\n\n async execute(params: DeleteConversationParams): Promise<DeleteConversationResult> {\n const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber)\n if (!session) throw new SessionNotFoundError(params.whatsappNumber)\n\n const uploadIds = await this.documentRepository.listUploadIdsBySession(params.companyId, session.id)\n\n // Sem `delete` no storage não há como cumprir a promessa de apagar a mídia. Falhar aqui é\n // melhor que apagar o transcript e deixar os arquivos para trás sem ninguém saber.\n if (uploadIds.length > 0 && !this.objectStorage?.delete) {\n throw new Error('storage_delete_unsupported: a conversa tem arquivos e o storage injetado não implementa delete')\n }\n\n const failedObjects: string[] = []\n let deletedObjects = 0\n\n for (const uploadId of uploadIds) {\n try {\n await this.objectStorage?.delete?.(uploadId)\n deletedObjects++\n } catch {\n failedObjects.push(uploadId)\n }\n }\n\n if (failedObjects.length > 0) return { deletedObjects, failedObjects }\n\n // A cascata cuida de messages e documents; a sessão é a raiz.\n await this.sessionRepository.deleteByNumber(params.companyId, params.whatsappNumber)\n\n return { deletedObjects, failedObjects: [] }\n }\n}\n","import type { ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\n\nexport type PurgeExpiredDocumentsParams = {\n companyId: string\n /** Dias de retenção. O produto configura; o módulo não escolhe política de dado pessoal. */\n retentionDays: number\n /** Teto por execução, para o job não segurar conexão nem storage por tempo indefinido. */\n batchSize?: number\n /** Instante de referência — injetado para o teste não depender do relógio. */\n now?: Date\n}\n\nexport type PurgeExpiredDocumentsResult = {\n purged: number\n failed: readonly string[]\n}\n\nconst DEFAULT_BATCH_SIZE = 50\nconst HOURS_IN_DAY = 24\nconst MILLISECONDS_IN_HOUR = 60 * 60 * 1000\n\n/**\n * Apaga documento vencido: objeto no storage primeiro, linha depois — a mesma ordem do\n * `DeleteConversationUseCase`, e pelo mesmo motivo.\n *\n * A linha só cai quando o objeto caiu. Contar como purgado sem ter apagado o binário deixaria lixo\n * pago no storage sem nenhum registro que o alcance depois: a linha era o único ponteiro.\n */\nexport class PurgeExpiredDocumentsUseCase {\n constructor(\n private readonly documentRepository: DocumentRepository,\n private readonly objectStorage?: ObjectStorageInterface,\n ) {}\n\n async execute(params: PurgeExpiredDocumentsParams): Promise<PurgeExpiredDocumentsResult> {\n if (!this.objectStorage?.delete) {\n throw new Error('storage_delete_unsupported: retenção exige um storage que implemente delete')\n }\n\n const reference = params.now ?? new Date()\n const olderThan = new Date(reference.getTime() - params.retentionDays * HOURS_IN_DAY * MILLISECONDS_IN_HOUR)\n\n const expired = await this.documentRepository.listExpired(\n params.companyId,\n olderThan,\n params.batchSize ?? DEFAULT_BATCH_SIZE,\n )\n\n const failed: string[] = []\n let purged = 0\n\n for (const document of expired) {\n try {\n await this.objectStorage.delete(document.uploadId)\n } catch {\n // Guarda e segue: um objeto problemático não deve impedir a limpeza dos outros, e a próxima\n // execução tenta de novo porque a linha continua lá.\n failed.push(document.uploadId)\n continue\n }\n\n await this.documentRepository.deleteById(params.companyId, document.id)\n purged++\n }\n\n return { purged, failed }\n }\n}\n","import { and, asc, count, desc, eq, ilike, inArray, lt, or, type SQL } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { documents, sessions, type DocumentRow, type NewDocumentRow } from '../schema/schema'\n\nexport interface LinkDocumentParams {\n companyId: string\n sessionId: string\n messageId?: string | null\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n sha256?: string | null\n source: string\n}\n\nexport interface ListDocumentsParams {\n companyId: string\n sessionId: string\n search?: string\n /**\n * Origens aceitas, como lista explícita (`['agent', 'bot']`) e não como apelido de UI.\n *\n * Agrupamento tipo \"Equipe\" é vocabulário de tela e muda por produto; traduzir aqui obrigaria o\n * módulo a conhecer o rótulo de cada host. Quem recebe o apelido na borda é a rota.\n */\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\nexport interface ListCompanyDocumentsParams {\n companyId: string\n search?: string\n sources?: readonly string[]\n sortDirection?: 'asc' | 'desc'\n page?: number\n limit?: number\n}\n\nexport interface CompanyDocumentRow {\n id: string\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n source: string\n linkedAt: Date\n whatsappNumber: string\n}\n\nexport interface ListCompanyDocumentsResult {\n rows: CompanyDocumentRow[]\n total: number\n}\n\nexport interface ListDocumentsResult {\n rows: DocumentRow[]\n /** Total no servidor, ANTES do corte de página — é o que permite calcular a última página. */\n total: number\n}\n\nconst DEFAULT_LIMIT = 50\n\n/**\n * O filtro de busca da biblioteca da empresa: nome do arquivo OU telefone da conversa.\n *\n * O telefone é comparado só pelos dígitos porque a tela mostra \"+55 (11) 94444-3333\" e o banco\n * guarda \"5511944443333\" — quem copia o número da tela e cola na busca casaria zero linhas se o\n * termo fosse usado cru. Por isso a mesma palavra vira dois predicados: o nome mantém a pontuação\n * (faz parte do arquivo), o número perde.\n *\n * Termo sem dígito nenhum não gera o predicado do telefone — `%%` casaria toda conversa e a busca\n * por nome deixaria de filtrar.\n */\nexport function companyDocumentSearch(search: string | undefined): SQL | undefined {\n const term = search?.trim()\n if (!term) return undefined\n\n const digits = term.replace(/\\D/g, '')\n const byFilename = ilike(documents.filename, `%${term}%`)\n if (!digits) return byFilename\n\n return or(byFilename, ilike(sessions.whatsappNumber, `%${digits}%`))\n}\n\nexport class DocumentRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n /**\n * Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o\n * job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma\n * checagem, duplicando a linha no painel.\n *\n * Devolve `undefined` quando o documento já estava linkado.\n */\n async link(params: LinkDocumentParams): Promise<DocumentRow | undefined> {\n const values: NewDocumentRow = {\n companyId: params.companyId,\n sessionId: params.sessionId,\n messageId: params.messageId ?? null,\n uploadId: params.uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.sizeBytes,\n sha256: params.sha256 ?? null,\n source: params.source,\n }\n\n const [created] = await this.db.insert(documents).values(values).onConflictDoNothing().returning()\n return created\n }\n\n async listByConversation(params: ListDocumentsParams): Promise<ListDocumentsResult> {\n const filters = [eq(documents.companyId, params.companyId), eq(documents.sessionId, params.sessionId)]\n // `ilike` com % nas duas pontas: o painel busca por parte do nome, e o atendente não sabe como\n // o arquivo chegou nomeado.\n if (params.search) filters.push(ilike(documents.filename, `%${params.search}%`))\n // Lista vazia é tratada como ausência de filtro, não como \"nenhuma origem aceita\": um\n // `inArray` com `[]` gera `false` e devolveria zero linhas para quem só quis dizer \"todas\".\n if (params.sources && params.sources.length > 0) {\n filters.push(inArray(documents.source, [...params.sources]))\n }\n\n const where = and(...filters)\n const limit = params.limit ?? DEFAULT_LIMIT\n const page = params.page && params.page > 0 ? params.page : 1\n const orderBy = params.sortDirection === 'asc' ? asc(documents.linkedAt) : desc(documents.linkedAt)\n\n // Duas consultas com o MESMO `where`: a contagem tem de ignorar o corte de página, senão o\n // total viraria o tamanho da página e a paginação nunca sairia da primeira.\n const [rows, counted] = await Promise.all([\n this.db\n .select()\n .from(documents)\n .where(where)\n .orderBy(orderBy)\n .limit(limit)\n .offset((page - 1) * limit),\n this.db.select({ value: count() }).from(documents).where(where),\n ])\n\n return { rows, total: counted[0]?.value ?? 0 }\n }\n\n /**\n * A biblioteca da EMPRESA inteira, não de uma conversa.\n *\n * A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.\n *\n * Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,\n * arquivo sem essa referência é inútil: o atendente vê \"comprovante.pdf\" e não sabe de quem.\n *\n * Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a\n * varredura de retenção.\n */\n async listByCompany(params: ListCompanyDocumentsParams): Promise<ListCompanyDocumentsResult> {\n const filters = [eq(documents.companyId, params.companyId)]\n const search = companyDocumentSearch(params.search)\n if (search) filters.push(search)\n if (params.sources && params.sources.length > 0) {\n filters.push(inArray(documents.source, [...params.sources]))\n }\n\n const where = and(...filters)\n const limit = params.limit ?? DEFAULT_LIMIT\n const page = params.page && params.page > 0 ? params.page : 1\n const orderBy = params.sortDirection === 'asc' ? asc(documents.linkedAt) : desc(documents.linkedAt)\n\n const [rows, counted] = await Promise.all([\n this.db\n .select({\n id: documents.id,\n uploadId: documents.uploadId,\n filename: documents.filename,\n mimeType: documents.mimeType,\n sizeBytes: documents.sizeBytes,\n source: documents.source,\n linkedAt: documents.linkedAt,\n // Só o número: o NOME do cliente é dado do produto (tabela própria dele), não do\n // módulo. Quem quiser exibir \"Marina Alves\" enriquece na borda.\n whatsappNumber: sessions.whatsappNumber,\n })\n .from(documents)\n .innerJoin(sessions, eq(documents.sessionId, sessions.id))\n .where(where)\n .orderBy(orderBy)\n .limit(limit)\n .offset((page - 1) * limit),\n // O mesmo join da listagem, e não só `from(documents)`: a busca pode citar\n // `sessions.whatsapp_number`, e uma contagem sem a tabela na cláusula não compila — pior,\n // se compilasse, o total divergiria das linhas e a paginação prometeria páginas vazias.\n this.db\n .select({ value: count() })\n .from(documents)\n .innerJoin(sessions, eq(documents.sessionId, sessions.id))\n .where(where),\n ])\n\n return { rows, total: counted[0]?.value ?? 0 }\n }\n\n /**\n * Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o\n * download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.\n */\n async findByUploadId(companyId: string, uploadId: string): Promise<DocumentRow | undefined> {\n const [row] = await this.db\n .select()\n .from(documents)\n .where(and(eq(documents.companyId, companyId), eq(documents.uploadId, uploadId)))\n .limit(1)\n\n return row\n }\n\n /**\n * Os objetos a apagar no storage antes de a linha sumir.\n *\n * Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa\n * precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.\n */\n async listUploadIdsBySession(companyId: string, sessionId: string): Promise<string[]> {\n const rows = await this.db\n .select({ uploadId: documents.uploadId })\n .from(documents)\n .where(and(eq(documents.companyId, companyId), eq(documents.sessionId, sessionId)))\n\n return rows.map((row) => row.uploadId)\n }\n\n /** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */\n async listExpired(companyId: string, olderThan: Date, limit = DEFAULT_LIMIT): Promise<DocumentRow[]> {\n return this.db\n .select()\n .from(documents)\n .where(and(eq(documents.companyId, companyId), lt(documents.linkedAt, olderThan)))\n .orderBy(documents.linkedAt)\n .limit(limit)\n }\n\n async deleteById(companyId: string, id: string): Promise<void> {\n await this.db.delete(documents).where(and(eq(documents.companyId, companyId), eq(documents.id, id)))\n }\n}\n","import type { SessionRepository } from '../repositories/SessionRepository'\nimport { SessionNotFoundError } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRow, SessionRow } from '../schema/schema'\n\nexport type ExportConversationParams = {\n companyId: string\n whatsappNumber: string\n}\n\nexport type ExportConversationResult = {\n session: SessionRow\n messages: MessageRow[]\n}\n\n// T3.3 — transcript completo para export (ex.: anexar num chamado de suporte, auditoria).\nexport class ExportConversationUseCase {\n constructor(private readonly sessionRepository: SessionRepository) {}\n\n async execute(params: ExportConversationParams): Promise<ExportConversationResult> {\n const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber)\n if (!result) throw new SessionNotFoundError(params.whatsappNumber)\n return result\n }\n}\n","import type { FlowGraphData, FlowGraphSummary } from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowGraphRepository } from '../repositories/FlowGraphRepository'\n\n// T4.2 — CRUD de grafo, agrupado num único arquivo (cada use-case é uma classe fina sobre o\n// mesmo repositório; não há regra de negócio própria a proteger na sua separação em arquivos\n// individuais como fizemos para conversation/session).\n\nexport class GetFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string; key: string }): Promise<FlowGraphData | undefined> {\n return this.flowGraphRepository.get(params.companyId, params.key)\n }\n}\n\nexport class ListFlowGraphsUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string }): Promise<FlowGraphSummary[]> {\n return this.flowGraphRepository.list(params.companyId)\n }\n}\n\nexport type CreateFlowGraphParams = {\n companyId: string\n key: string\n label: string\n startNodeId: string\n nodes: FlowGraphData['nodes']\n showInMenu?: boolean\n menuOptionLabel?: string\n}\n\nexport class CreateFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: CreateFlowGraphParams): Promise<FlowGraphData> {\n return this.flowGraphRepository.create(params.companyId, params)\n }\n}\n\nexport type SaveFlowGraphParams = {\n companyId: string\n graph: FlowGraphData\n expectedVersion: number\n}\n\n// Salvar já é publicar — o motor de fluxo lê o grafo em tempo real (ver rules/packages/\n// pluggable-module.md e tasks.md T4.2). A validação de conteúdo (destinos quebrados, opções\n// duplicadas, etc.) é responsabilidade do editor (conversations-ui/flows validateGraph), não\n// deste use-case — aqui só garantimos a consistência otimista da escrita.\nexport class SaveFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: SaveFlowGraphParams): Promise<FlowGraphData> {\n return this.flowGraphRepository.save(params.companyId, params.graph, params.expectedVersion)\n }\n}\n\nexport class DeleteFlowGraphUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string; key: string }): Promise<void> {\n return this.flowGraphRepository.delete(params.companyId, params.key)\n }\n}\n\nexport class GetLiveFlowPositionsUseCase {\n constructor(private readonly flowGraphRepository: FlowGraphRepository) {}\n async execute(params: { companyId: string }) {\n return this.flowGraphRepository.getLiveFlowPositions(params.companyId)\n }\n}\n","import type {\n ChannelAdapterInterface,\n ConversationSession,\n FlowActionHandler,\n FlowActionKind,\n FlowActionResult,\n FlowGraphData,\n FlowNodeData,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { isCrossFlowTarget, crossFlowKey } from '@adatechnology/meta-whatsapp-contracts'\n\nexport interface FlowStepInput {\n graph: FlowGraphData\n currentNodeId: string\n userAnswer?: string\n context: Record<string, unknown>\n session: ConversationSession\n channel: ChannelAdapterInterface\n}\n\nexport type FlowStepResult =\n | { kind: 'awaiting-answer'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'advanced'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'cross-flow'; flowKey: string; context: Record<string, unknown> }\n | { kind: 'terminal'; context: Record<string, unknown> }\n\n// Resultado de run(): nunca 'advanced' (o laço já consumiu esses), mas pode ser\n// 'max-steps-exceeded' quando o grafo tem um ciclo de nós automáticos.\nexport type FlowRunResult = (\n | { kind: 'awaiting-answer'; nodeId: string; context: Record<string, unknown> }\n | { kind: 'cross-flow'; flowKey: string; context: Record<string, unknown> }\n | { kind: 'terminal'; context: Record<string, unknown> }\n | { kind: 'max-steps-exceeded'; context: Record<string, unknown> }\n) & {\n // Caminho percorrido, do nó inicial ao final — serve para depurar um ciclo (os nós repetidos\n // aparecem aqui) e para o host registrar a trilha da conversa.\n visited: string[]\n steps: number\n}\n\nconst DEFAULT_MAX_STEPS = 50\n\nfunction evaluateCondition(node: FlowNodeData, context: Record<string, unknown>): boolean {\n if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false\n const actual = context[node.conditionContextKey]\n const expected = node.conditionValue\n\n switch (node.conditionOperator) {\n case '==':\n return String(actual) === expected\n case '!=':\n return String(actual) !== expected\n case 'contains':\n return typeof actual === 'string' && actual.includes(expected)\n case '>':\n case '>=':\n case '<':\n case '<=': {\n const actualNum = Number(actual)\n const expectedNum = Number(expected)\n if (Number.isNaN(actualNum) || Number.isNaN(expectedNum)) return false\n if (node.conditionOperator === '>') return actualNum > expectedNum\n if (node.conditionOperator === '>=') return actualNum >= expectedNum\n if (node.conditionOperator === '<') return actualNum < expectedNum\n return actualNum <= expectedNum\n }\n default:\n return false\n }\n}\n\nfunction resolveNext(node: FlowNodeData, answerId: string | undefined): string | undefined {\n if (!node.next) return undefined\n if (typeof node.next === 'string') return node.next\n if (answerId && node.next.byAnswer[answerId]) return node.next.byAnswer[answerId]\n return node.next.default\n}\n\n// T4.3 — interpreta um grafo de fluxo nó a nó. `registerFlowAction` é o único jeito de um\n// `actionKind` fazer algo: o interpretador nunca conhece 'trigger_simulation' ou qualquer outro\n// caso de negócio do host — ele só invoca o handler registrado (ver providers.ts,\n// FlowActionRegistry) e segue o `next` que o handler devolver, se houver.\nexport class FlowInterpreter {\n private readonly actionHandlers = new Map<FlowActionKind, FlowActionHandler>()\n\n registerFlowAction(kind: FlowActionKind, handler: FlowActionHandler): void {\n this.actionHandlers.set(kind, handler)\n }\n\n async step(input: FlowStepInput): Promise<FlowStepResult> {\n const node = input.graph.nodes[input.currentNodeId]\n if (!node) return { kind: 'terminal', context: input.context }\n\n if (node.type === 'condition') {\n const branch = evaluateCondition(node, input.context) ? 'true' : 'false'\n return this.moveTo(input, resolveNext(node, branch))\n }\n\n if (node.type === 'action') {\n const handler = node.actionKind ? this.actionHandlers.get(node.actionKind) : undefined\n if (!handler) return { kind: 'terminal', context: input.context }\n\n // Runtime de uma função `void` é sempre `undefined` — cast seguro para poder encadear\n // `?.` (TS não deixa acessar propriedade num tipo `void | {...}` mesmo com optional chaining).\n const result = (await handler({\n node,\n session: input.session,\n channel: input.channel,\n context: input.context,\n })) as FlowActionResult | undefined\n\n const contextAfterAction = result?.context ? { ...input.context, ...result.context } : input.context\n const nextNodeId = result?.next ?? resolveNext(node, undefined)\n return this.moveTo({ ...input, context: contextAfterAction }, nextNodeId)\n }\n\n const isChoice = node.type === 'menu' || node.questionType === 'choice'\n\n // Sem resposta do usuário ainda: este é o nó atual, aguardando input — não avança.\n if (input.userAnswer === undefined) {\n return { kind: 'awaiting-answer', nodeId: input.currentNodeId, context: input.context }\n }\n\n const updatedContext = node.contextKey ? { ...input.context, [node.contextKey]: input.userAnswer } : input.context\n const answerId = isChoice ? input.userAnswer : undefined\n const nextNodeId = resolveNext(node, answerId)\n\n return this.moveTo({ ...input, context: updatedContext }, nextNodeId)\n }\n\n private moveTo(input: FlowStepInput, nextNodeId: string | undefined): FlowStepResult {\n if (!nextNodeId) return { kind: 'terminal', context: input.context }\n if (isCrossFlowTarget(nextNodeId))\n return { kind: 'cross-flow', flowKey: crossFlowKey(nextNodeId), context: input.context }\n if (!input.graph.nodes[nextNodeId]) return { kind: 'terminal', context: input.context }\n return { kind: 'advanced', nodeId: nextNodeId, context: input.context }\n }\n\n // Avança pelo grafo até parar num ponto que exige algo de fora: uma pergunta esperando resposta\n // do cliente, o fim do fluxo, ou um salto para outro fluxo. Nós de condição e ação não pedem\n // input nenhum, então parar neles obrigaria o chamador a reimplementar este laço — e sem\n // proteção, um grafo com ciclo de condições (A→B→A) o prenderia para sempre.\n //\n // O corte por `maxSteps` é uma rede de segurança de runtime, não a defesa principal: um ciclo é\n // erro de autoria e deveria ser barrado na publicação. Como validateGraph (no editor) hoje\n // detecta nó inalcançável mas NÃO detecta ciclo, um grafo cíclico é publicável — então o\n // runtime precisa sobreviver a ele em vez de travar o processo que atende o webhook.\n async run(input: FlowStepInput, options?: { maxSteps?: number }): Promise<FlowRunResult> {\n const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS\n const visited: string[] = [input.currentNodeId]\n\n let current: FlowStepInput = input\n let steps = 0\n\n while (steps < maxSteps) {\n const result = await this.step(current)\n steps++\n\n if (result.kind !== 'advanced') return { ...result, visited, steps }\n\n visited.push(result.nodeId)\n // Só a primeira iteração consome a resposta do cliente; daí em diante o laço percorre\n // nós automáticos (condição/ação). Manter userAnswer faria a mesma resposta ser\n // reaplicada em cada pergunta seguinte, pulando-as sem o cliente ter respondido.\n current = { ...current, currentNodeId: result.nodeId, context: result.context, userAnswer: undefined }\n }\n\n return { kind: 'max-steps-exceeded', context: current.context, visited, steps }\n }\n}\n","import type {\n ChannelAdapterInterface,\n FlowActionHandler,\n ObjectStorageInterface,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowMediaRepository } from '../repositories/FlowMediaRepository'\nimport type { LogMessageParams } from '../use-cases/LogMessage.use-case'\n\n/**\n * Porta de escrita no transcript, e não a classe `LogMessageUseCase`.\n *\n * Exigir a classe amarraria a action a quem já guarda as mensagens nas tabelas do módulo — um host\n * em migração, com o transcript ainda no schema dele, não conseguiria usar a única action built-in\n * sem gravar o envio numa tabela que o painel dele não lê. O que a action precisa é só de um lugar\n * para registrar o que saiu.\n */\n/**\n * Onde guardar o `mediaId` que a Meta devolveu para cada arquivo da biblioteca.\n *\n * Porta, e não tabela: o módulo não sabe se o host guarda isso numa coluna, no Redis ou em memória.\n * O que ele precisa é reaproveitar o id em vez de ressubir o mesmo binário para cada cliente que\n * passa pelo nó — a Meta aceita reusar por 30 dias.\n */\nexport type FlowMediaIdStore = {\n get(params: { flowMediaId: string; senderKey: string }): Promise<string | undefined>\n set(params: { flowMediaId: string; senderKey: string; mediaId: string }): Promise<void>\n clear(params: { flowMediaId: string; senderKey: string }): Promise<void>\n}\n\n/**\n * O `senderKey` vem junto do store, e não solto, para não existir a forma inválida.\n *\n * O `mediaId` é escopado ao número remetente (`phone_number_id`) na Meta: cachear sem separar por\n * número faz o id de um número ser mandado pelo outro, e o envio falha. Exigi-lo dentro do mesmo\n * objeto torna \"liguei o cache e esqueci o número\" impossível de escrever.\n */\nexport type FlowMediaIdCache = {\n readonly store: FlowMediaIdStore\n readonly senderKey: string\n}\n\nexport type FlowMediaTranscriptLogger = {\n execute(params: LogMessageParams): Promise<unknown>\n}\n\nexport type CreateSendMediaActionParams = {\n flowMediaRepository: FlowMediaRepository\n // Precisa de `getObject` — sem ele não há como reenviar o arquivo, e o módulo nem registra\n // esta action (ver createMetaWhatsAppModule).\n objectStorage: ObjectStorageInterface & { getObject: NonNullable<ObjectStorageInterface['getObject']> }\n logMessage: FlowMediaTranscriptLogger\n /**\n * Reaproveitamento do arquivo já subido. Capacidade por ausência: sem o cache, cada envio ressobe\n * o binário — exatamente o comportamento anterior, sem flag para desligar.\n */\n mediaIdCache?: FlowMediaIdCache\n startState: SessionState\n // Falha de envio de UM arquivo não deve derrubar a conversa — o cliente ficaria travado num nó\n // automático por causa de um PDF. O host recebe o erro por aqui para alertar/observar.\n onError?: (error: unknown, details: { flowKey: string; nodeId: string; uploadId: string }) => void\n}\n\nfunction mediaTypeFor(mimeType: string): string {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (mimeType.startsWith('video/')) return 'video'\n return 'document'\n}\n\n/**\n * Action `send_media`: ao passar pelo nó, envia os arquivos que a biblioteca tem anexados a ele.\n *\n * É a única action built-in que o módulo implementa de ponta a ponta — as outras\n * (`handoff`, `send_product_list`) dependem de regra de negócio do produto. Esta não: \"mandar\n * estes arquivos ao chegar aqui\" é comportamento de canal, e replicá-la em cada host seria\n * copiar o mesmo código com os mesmos bugs.\n *\n * O nó não guarda os arquivos em `actionParams` de propósito. Se guardasse, trocar o material\n * exigiria editar e republicar o grafo — e o ponto todo é o contrário: quem cuida do conteúdo\n * troca o arquivo na biblioteca e o fluxo continua igual.\n */\n/**\n * Envia um anexo, reaproveitando o arquivo já subido quando houver cache.\n *\n * O caminho rápido não faz NENHUMA das duas coisas caras: não baixa o binário do storage e não o\n * sobe para a Meta. Sobra a chamada de envio.\n *\n * O erro no caminho rápido cai no caminho lento em vez de propagar. O motivo é que a única falha\n * esperada ali — id expirado, ou apagado do lado da Meta — é indistinguível de uma falha de rede\n * sem inspecionar código de erro da Graph API, e tratar as duas igual custa uma tentativa a mais no\n * pior caso e acerta nos dois. Sem isso, um id vencido pararia de entregar material silenciosamente\n * até alguém notar.\n */\nasync function sendAttachment(params: {\n attachment: { id: string; uploadId: string; mimeType: string; filename: string; caption: string | null }\n channel: Pick<ChannelAdapterInterface, 'sendMedia'>\n to: string\n objectStorage: { getObject: (uploadId: string) => Promise<Buffer> }\n cache?: FlowMediaIdCache\n}): Promise<{ externalMessageId: string | null }> {\n const { attachment, channel, to, cache } = params\n const common = {\n to,\n mimeType: attachment.mimeType,\n filename: attachment.filename,\n caption: attachment.caption ?? undefined,\n }\n const cacheKey = cache ? { flowMediaId: attachment.id, senderKey: cache.senderKey } : undefined\n\n if (cache && cacheKey) {\n const knownMediaId = await cache.store.get(cacheKey)\n if (knownMediaId) {\n try {\n return await channel.sendMedia({ ...common, mediaId: knownMediaId })\n } catch {\n await cache.store.clear(cacheKey)\n }\n }\n }\n\n const buffer = await params.objectStorage.getObject(attachment.uploadId)\n const result = await channel.sendMedia({ ...common, buffer })\n\n // Grava depois do envio bem-sucedido: id que a Meta ainda não confirmou não vale cachear.\n if (cache && cacheKey && result.mediaId) await cache.store.set({ ...cacheKey, mediaId: result.mediaId })\n\n return result\n}\n\nexport function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler {\n return async ({ node, session, channel }) => {\n // `flowKey` nulo significa conversa fora de fluxo — não há de onde ler a biblioteca.\n if (!session.flowKey) return\n\n const location = { companyId: session.companyId, flowKey: session.flowKey, nodeId: node.id }\n const attachments = await params.flowMediaRepository.listActive(location)\n\n // Sequencial, e não Promise.all: o cliente vê as mensagens na ordem em que chegam, e disparar\n // em paralelo entregaria o folder antes da tabela de preços conforme a latência de cada upload.\n for (const attachment of attachments) {\n try {\n const { externalMessageId } = await sendAttachment({\n attachment,\n channel,\n to: session.whatsappNumber,\n objectStorage: params.objectStorage,\n ...(params.mediaIdCache ? { cache: params.mediaIdCache } : {}),\n })\n\n /**\n * Registra no transcript, mas NÃO liga em `documents`: aquela tabela tem índice único em\n * (companyId, uploadId), então o mesmo arquivo da biblioteca — que por definição vai para\n * todo cliente que passar pelo nó — falharia já no segundo envio. O `uploadId` fica no\n * payload, que é o que o painel precisa para renderizar e baixar.\n */\n await params.logMessage.execute({\n companyId: session.companyId,\n whatsappNumber: session.whatsappNumber,\n direction: 'outbound',\n sender: 'bot',\n agentUserId: null,\n type: mediaTypeFor(attachment.mimeType),\n content: attachment.caption ?? attachment.filename,\n payload: {\n filename: attachment.filename,\n mimeType: attachment.mimeType,\n uploadId: attachment.uploadId,\n flowMediaId: attachment.id,\n },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n } catch (error) {\n params.onError?.(error, { flowKey: location.flowKey, nodeId: node.id, uploadId: attachment.uploadId })\n }\n }\n }\n}\n","import type {\n CatalogPort,\n CatalogProduct,\n FlowActionHandler,\n SessionState,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { FlowMediaTranscriptLogger } from './createSendMediaAction'\n\n/** Tetos da Meta para `interactive.product_list`. Acima disso a mensagem inteira é recusada. */\nexport const PRODUCT_LIST_LIMIT = {\n ITEMS: 30,\n SECTIONS: 10,\n} as const\n\nexport type CreateSendProductListActionParams = {\n catalog: CatalogPort\n catalogId: string\n logMessage: FlowMediaTranscriptLogger\n startState: SessionState\n /** Falha de UMA vitrine não pode travar a conversa; o host observa por aqui. */\n onError?: (error: unknown, details: { flowKey: string; nodeId: string }) => void\n}\n\ntype ProductListParams = {\n readonly headerText?: string\n readonly bodyText?: string\n readonly footerText?: string\n readonly search?: string\n readonly sectionTitle?: string\n}\n\n/**\n * Action `send_product_list`: ao passar pelo nó, envia a vitrine do catálogo publicado na Meta.\n *\n * O nó guarda **critério**, não a lista de produtos. Congelar `retailerId` em `actionParams` faria\n * o fluxo continuar oferecendo o item esgotado — ou o item excluído, que a Meta recusa junto com a\n * mensagem inteira. O que o operador edita é o texto e o filtro; o estoque manda no resto.\n *\n * Só produto em estoque entra. Vitrine que mostra o que não tem produz a pior conversa possível:\n * o cliente escolhe, responde, e recebe \"acabou\".\n */\nexport function createSendProductListAction(params: CreateSendProductListActionParams): FlowActionHandler {\n return async ({ node, session, channel }) => {\n // Canal sem vitrine (widget, ou dublê de teste): a capacidade some por ausência do método.\n if (!channel.sendProductList) return\n\n const actionParams = (node.actionParams ?? {}) as ProductListParams\n\n try {\n const available = await listAvailableProducts({\n catalog: params.catalog,\n catalogId: params.catalogId,\n ...(actionParams.search ? { search: actionParams.search } : {}),\n })\n\n // Sem item disponível não sai vitrine vazia: a Meta recusa `sections` sem produto, e o nó\n // seguinte do fluxo é quem sabe o que dizer para quem chegou com o estoque zerado.\n if (available.length === 0) return\n\n const bodyText = actionParams.bodyText ?? 'Veja o que temos disponível:'\n\n const { externalMessageId } = await channel.sendProductList({\n to: session.whatsappNumber,\n headerText: actionParams.headerText ?? 'Nossos produtos',\n body: bodyText,\n ...(actionParams.footerText ? { footerText: actionParams.footerText } : {}),\n sections: [\n {\n title: actionParams.sectionTitle ?? 'Disponíveis',\n retailerIds: available.map((product) => product.retailerId),\n },\n ],\n })\n\n /**\n * O transcript guarda o texto e a contagem, e não o preço de cada item: o painel precisa\n * saber que a vitrine saiu, e o histórico de conversa não é lugar de tabela de preços — ele\n * envelheceria errado no dia seguinte ao reajuste.\n */\n await params.logMessage.execute({\n companyId: session.companyId,\n whatsappNumber: session.whatsappNumber,\n direction: 'outbound',\n sender: 'bot',\n agentUserId: null,\n type: 'interactive',\n content: bodyText,\n payload: { kind: 'product_list', productCount: available.length },\n waMessageId: externalMessageId,\n status: 'sent',\n startState: params.startState,\n })\n } catch (error) {\n params.onError?.(error, { flowKey: session.flowKey ?? '', nodeId: node.id })\n }\n }\n}\n\nasync function listAvailableProducts(params: {\n catalog: CatalogPort\n catalogId: string\n search?: string\n}): Promise<readonly CatalogProduct[]> {\n const products = await params.catalog.listProducts({\n catalogId: params.catalogId,\n ...(params.search ? { search: params.search } : {}),\n })\n\n return products.filter((product) => product.availability === 'in stock').slice(0, PRODUCT_LIST_LIMIT.ITEMS)\n}\n","import { and, asc, eq, sql } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport { flowMedia, type FlowMediaRow } from '../schema/schema'\n\nexport type FlowMediaLocation = {\n companyId: string\n flowKey: string\n nodeId: string\n}\n\nexport type AttachFlowMediaParams = FlowMediaLocation & {\n uploadId: string\n filename: string\n mimeType: string\n sizeBytes: number\n caption?: string\n sortOrder?: number\n}\n\nexport type UpdateFlowMediaParams = {\n companyId: string\n id: string\n caption?: string | null\n sortOrder?: number\n active?: boolean\n}\n\n// Biblioteca de mídia por nó de fluxo. Toda consulta é escopada por companyId — a chave do fluxo\n// é escolhida por quem edita e repete entre empresas, então filtrar só por (flowKey, nodeId)\n// vazaria arquivo de um tenant para outro.\nexport class FlowMediaRepository {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n private locationFilter(location: FlowMediaLocation) {\n return and(\n eq(flowMedia.companyId, location.companyId),\n eq(flowMedia.flowKey, location.flowKey),\n eq(flowMedia.nodeId, location.nodeId),\n )\n }\n\n // O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:\n // é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente\n // justamente o material que alguém desligou.\n async listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]> {\n return this.db\n .select()\n .from(flowMedia)\n .where(and(this.locationFilter(location), eq(flowMedia.active, true)))\n .orderBy(asc(flowMedia.sortOrder), asc(flowMedia.createdAt))\n }\n\n // Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para\n // poder ser religado.\n async listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]> {\n return this.db\n .select()\n .from(flowMedia)\n .where(this.locationFilter(location))\n .orderBy(asc(flowMedia.sortOrder), asc(flowMedia.createdAt))\n }\n\n /**\n * Anexa um arquivo já existente no storage ao nó.\n *\n * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no\n * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de\n * quem está montando o fluxo.\n */\n async attach(params: AttachFlowMediaParams): Promise<FlowMediaRow> {\n const [row] = await this.db\n .insert(flowMedia)\n .values({\n companyId: params.companyId,\n flowKey: params.flowKey,\n nodeId: params.nodeId,\n uploadId: params.uploadId,\n filename: params.filename,\n mimeType: params.mimeType,\n sizeBytes: params.sizeBytes,\n caption: params.caption ?? null,\n sortOrder: params.sortOrder ?? 0,\n })\n .onConflictDoUpdate({\n target: [flowMedia.companyId, flowMedia.flowKey, flowMedia.nodeId, flowMedia.uploadId],\n set: {\n caption: params.caption ?? null,\n sortOrder: params.sortOrder ?? 0,\n active: true,\n updatedAt: sql`now()`,\n },\n })\n .returning()\n\n return row as FlowMediaRow\n }\n\n async update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined> {\n const [row] = await this.db\n .update(flowMedia)\n .set({\n ...(params.caption !== undefined ? { caption: params.caption } : {}),\n ...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),\n ...(params.active !== undefined ? { active: params.active } : {}),\n updatedAt: sql`now()`,\n })\n .where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.id, params.id)))\n .returning()\n\n return row as FlowMediaRow | undefined\n }\n\n /**\n * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a\n * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o\n * host, que é dono da biblioteca de arquivos.\n */\n async detach(params: { companyId: string; id: string }): Promise<void> {\n await this.db.delete(flowMedia).where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.id, params.id)))\n }\n\n // Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.\n async detachRemovedNodes(params: { companyId: string; flowKey: string; existingNodeIds: string[] }): Promise<void> {\n const condition =\n params.existingNodeIds.length === 0 ? undefined : sql`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`\n\n await this.db\n .delete(flowMedia)\n .where(and(eq(flowMedia.companyId, params.companyId), eq(flowMedia.flowKey, params.flowKey), condition))\n }\n}\n","import { eq, sql } from 'drizzle-orm'\n\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type { FlowMediaIdStore } from '../flows/createSendMediaAction'\nimport { flowMedia } from '../schema/schema'\n\n/**\n * Store padrão do cache de `mediaId`, na própria linha da biblioteca de mídia.\n *\n * A porta `FlowMediaIdStore` existe para o host poder escolher Redis ou memória. Esta é a\n * implementação que o módulo já traz pronta, para o caso comum não exigir nada de quem instala: a\n * tabela é do módulo, a coluna é do módulo, e sem isso todo host reescreveria o mesmo repositório.\n *\n * Um mapa por `phone_number_id` na mesma linha, e não uma tabela à parte: o id não existe sem o\n * arquivo, morre com ele, e a linha é lida de qualquer forma no envio.\n */\nexport class FlowMediaIdRepository implements FlowMediaIdStore {\n constructor(private readonly db: MetaWhatsAppDatabase) {}\n\n async get(params: { flowMediaId: string; senderKey: string }): Promise<string | undefined> {\n const rows = await this.db\n .select({ ids: flowMedia.metaMediaIds })\n .from(flowMedia)\n .where(eq(flowMedia.id, params.flowMediaId))\n .limit(1)\n\n return rows[0]?.ids?.[params.senderKey]\n }\n\n /**\n * Grava só a chave deste número.\n *\n * `jsonb_set` no banco, e não ler-alterar-escrever na aplicação: dois clientes passando pelo nó\n * ao mesmo tempo com números diferentes leriam o mesmo mapa e o último gravaria por cima,\n * apagando o id do outro. A escrita atômica não tem essa janela.\n */\n async set(params: { flowMediaId: string; senderKey: string; mediaId: string }): Promise<void> {\n await this.db\n .update(flowMedia)\n .set({\n metaMediaIds: sql`jsonb_set(${flowMedia.metaMediaIds}, ARRAY[${params.senderKey}::text], to_jsonb(${params.mediaId}::text), true)`,\n })\n .where(eq(flowMedia.id, params.flowMediaId))\n }\n\n /** Remove só a chave deste número — o id do outro número continua válido. */\n async clear(params: { flowMediaId: string; senderKey: string }): Promise<void> {\n await this.db\n .update(flowMedia)\n .set({ metaMediaIds: sql`${flowMedia.metaMediaIds} - ${params.senderKey}::text` })\n .where(eq(flowMedia.id, params.flowMediaId))\n }\n}\n","import { WhatsAppWindowExpiredError as ProviderWindowExpiredError } from '@adatechnology/meta-graph-core'\nimport type { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider'\nimport {\n WindowExpiredError,\n type ChannelAdapterInterface,\n type ObjectStorageInterface,\n type SendChannelMediaParams,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport { resolvePreviewUploadId } from './previewMedia'\n\n/**\n * Leitura de mídia do simulador de conversa.\n *\n * **Atrás de flag de propósito, e não ligado por omissão.** Aceitar id que não veio da Meta é\n * exatamente o que um webhook forjado exploraria: bastaria mandar `preview-upload:<chave>` para\n * fazer o servidor ler um objeto arbitrário do storage e devolvê-lo. Num ambiente de simulação isso\n * é o recurso; em produção é leitura arbitrária. Quem liga assume, e a decisão fica visível no\n * lugar onde o módulo é montado.\n */\nexport type PreviewMediaSupport = {\n readonly isEnabled: boolean\n readonly objectStorage: ObjectStorageInterface & {\n getObject: NonNullable<ObjectStorageInterface['getObject']>\n }\n /** Mime a devolver, já que o storage guarda bytes e não o tipo. Padrão `audio/ogg`. */\n readonly defaultMimeType?: string\n}\n\n// T5.2 — implementa a porta ChannelAdapterInterface sobre o meta-whatsapp-provider. A camada de\n// conversa fala só com esta porta e nunca com a Graph API, o que é o que permite trocar o canal\n// (ou usar um dublê em teste) sem tocar no motor de fluxo.\n//\n// Também traduz os erros do provider (que são da família MetaGraphError) para os erros de\n// domínio do módulo — o host trata WindowExpiredError sem precisar conhecer meta-graph-core.\nexport class WhatsAppChannelAdapter implements ChannelAdapterInterface {\n constructor(\n private readonly messages: WhatsAppMessageProvider,\n /**\n * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante\n * que atualizar o pacote não abre nada em quem não pediu.\n */\n private readonly previewMedia?: PreviewMediaSupport,\n ) {}\n\n private async translateErrors<TResult>(operation: () => Promise<TResult>): Promise<TResult> {\n try {\n return await operation()\n } catch (error) {\n if (error instanceof ProviderWindowExpiredError) throw new WindowExpiredError()\n throw error\n }\n }\n\n async sendText(to: string, body: string): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() => this.messages.sendText(to, body))\n return { externalMessageId: result.waMessageId }\n }\n\n async sendMedia(params: SendChannelMediaParams): Promise<{\n externalMessageId: string | null\n mediaId?: string | undefined\n }> {\n const result = await this.translateErrors(() => this.messages.sendMedia(params))\n return { externalMessageId: result.waMessageId, mediaId: result.mediaId }\n }\n\n async sendTemplate(params: {\n to: string\n templateName: string\n languageCode: string\n bodyParameters?: string[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() => this.messages.sendTemplate(params))\n return { externalMessageId: result.waMessageId }\n }\n\n async sendInteractiveList(params: {\n to: string\n body: string\n buttonLabel: string\n rows: { id: string; title: string }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendInteractiveList({\n to: params.to,\n bodyText: params.body,\n buttonText: params.buttonLabel,\n sections: [{ title: params.buttonLabel, rows: params.rows }],\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n async sendInteractiveButtons(params: {\n to: string\n body: string\n buttons: { id: string; title: string }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendInteractiveButtons({\n to: params.to,\n bodyText: params.body,\n buttons: params.buttons,\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n async sendProductList(params: {\n to: string\n headerText: string\n body: string\n footerText?: string\n sections: { title: string; retailerIds: string[] }[]\n }): Promise<{ externalMessageId: string | null }> {\n const result = await this.translateErrors(() =>\n this.messages.sendProductListMessage({\n to: params.to,\n headerText: params.headerText,\n bodyText: params.body,\n ...(params.footerText ? { footerText: params.footerText } : {}),\n sections: params.sections,\n }),\n )\n return { externalMessageId: result.waMessageId }\n }\n\n /**\n * Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.\n *\n * O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e\n * tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.\n */\n async fetchMediaAsBase64(mediaId: string): Promise<{ data: string; mimeType: string }> {\n const uploadId = this.previewMedia?.isEnabled ? resolvePreviewUploadId(mediaId) : undefined\n\n if (uploadId) {\n const buffer = await this.previewMedia!.objectStorage.getObject(uploadId)\n return {\n data: buffer.toString('base64'),\n mimeType: this.previewMedia!.defaultMimeType ?? 'audio/ogg',\n }\n }\n\n return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId))\n }\n}\n","/**\n * A convenção de mídia do simulador mora em `@adatechnology/meta-whatsapp-contracts`.\n *\n * Reexportada aqui só por conveniência de quem já importa do módulo. A definição NÃO é duplicada de\n * propósito: o front também precisa dela, e foi justamente uma cópia de cada lado — com comentários\n * pedindo \"precisa bater com a outra\" — que fez o recurso existir num produto e faltar no outro.\n */\n\nexport {\n PREVIEW_MEDIA_ID_PREFIX,\n toPreviewMediaId,\n resolvePreviewUploadId,\n} from '@adatechnology/meta-whatsapp-contracts'\n","import {\n whatsAppWebhookPayloadSchema,\n resolveContactProfileName,\n whatsAppTemplateStatusUpdateSchema,\n whatsAppPhoneNumberQualityUpdateSchema,\n WHATSAPP_WEBHOOK_FIELDS,\n type MetaWhatsAppHooks,\n type WhatsAppWebhookChange,\n type SessionState,\n type WhatsAppMessage,\n type WhatsAppStatus,\n type MessageStatus,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { LogMessageUseCase } from '../use-cases/LogMessage.use-case'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport {\n verifyWebhookSignature,\n claimWebhookDelivery,\n confirmWebhookDelivery,\n type NonceStoreInterface,\n} from './webhookSecurity'\nimport { extractMediaDescriptor } from './IngestInboundMedia.use-case'\nimport {\n InboundEffectsDispatcher,\n buildInboundJobId,\n extractAnswer,\n type InboundDispatchJob,\n type InboundDispatchQueueInterface,\n} from './inboundDispatch'\n\nexport type ReceiveWebhookParams = {\n companyId: string\n rawBody: Buffer | string\n signatureHeader: string | null | undefined\n}\n\nexport type ReceiveWebhookResult = {\n // Sempre 200 para a Meta, mesmo em duplicata — ela desativa webhooks que respondem erro.\n duplicate: boolean\n messagesProcessed: number\n statusesProcessed: number\n // Eventos de outro número da mesma WABA. Contados em vez de silenciados: um filtro que descarta\n // sem deixar rastro é indistinguível de um webhook que parou de chegar.\n ignoredForeignNumber: number\n // Eventos de conta (template, qualidade do número) entregues aos hooks do host.\n accountEventsProcessed: number\n // Chegaram e não foram tratados — field sem handler, ou corpo fora do schema.\n unhandledEvents: number\n}\n\nconst EMPTY_RESULT: Omit<ReceiveWebhookResult, 'duplicate'> = {\n messagesProcessed: 0,\n statusesProcessed: 0,\n ignoredForeignNumber: 0,\n accountEventsProcessed: 0,\n unhandledEvents: 0,\n}\n\n// Texto legível para o transcript. Uma mensagem interativa traz título E id; guardamos o título\n// (é o que o cliente viu), com o id como fallback quando a Meta não manda título.\nfunction extractContent(message: WhatsAppMessage): string | null {\n if (message.text?.body) return message.text.body\n const interactive = message.interactive\n if (interactive?.button_reply) return interactive.button_reply.title || interactive.button_reply.id\n if (interactive?.list_reply) return interactive.list_reply.title || interactive.list_reply.id\n if (message.order) {\n const items = message.order.product_items.reduce((sum, item) => sum + item.quantity, 0)\n return `Pedido: ${items} item(ns)`\n }\n if (message.location) {\n const label = message.location.name ?? message.location.address\n return label ? `📍 Localização: ${label}` : '📍 Localização'\n }\n return message.image?.caption ?? message.document?.caption ?? null\n}\n\nfunction extractPayload(message: WhatsAppMessage): Record<string, unknown> | null {\n const payload: Record<string, unknown> = {}\n if (message.interactive) payload['interactive'] = message.interactive\n if (message.order) payload['order'] = message.order\n if (message.image) payload['image'] = message.image\n if (message.audio) payload['audio'] = message.audio\n if (message.video) payload['video'] = message.video\n if (message.document) payload['document'] = message.document\n if (message.sticker) payload['sticker'] = message.sticker\n if (message.location) payload['location'] = message.location\n if (message.context?.referred_product) payload['referredProduct'] = message.context.referred_product\n return Object.keys(payload).length > 0 ? payload : null\n}\n\nconst ACCOUNT_EVENT_FIELDS: readonly string[] = [\n WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE,\n WHATSAPP_WEBHOOK_FIELDS.PHONE_NUMBER_QUALITY_UPDATE,\n]\n\nfunction isAccountEventField(field: string | undefined): boolean {\n return field !== undefined && ACCOUNT_EVENT_FIELDS.includes(field)\n}\n\n// T5.1 — porta de entrada do canal. Ordem deliberada: assinatura → anti-replay → parse → efeito.\n// Verificar a assinatura antes de qualquer parse evita gastar CPU (e superfície do zod) com\n// corpo não autenticado.\nexport class ReceiveWebhookUseCase {\n constructor(\n private readonly params: {\n appSecret: string\n phoneNumberId: string\n nonceStore: NonceStoreInterface\n sessionRepository: SessionRepository\n messageRepository: MessageRepository\n logMessage: LogMessageUseCase\n startState: SessionState\n hooks?: MetaWhatsAppHooks\n realtime?: RealtimeNotifierInterface\n /**\n * Sem fila o módulo se comporta como sempre: os hooks rodam dentro da requisição do webhook.\n * Configurá-la é o que faz a conversa sobreviver a um deploy no meio do atendimento.\n */\n inboundQueue?: InboundDispatchQueueInterface\n },\n ) {\n this.dispatcher = new InboundEffectsDispatcher({\n sessionRepository: params.sessionRepository,\n ...(params.hooks ? { hooks: params.hooks } : {}),\n ...(params.realtime ? { realtime: params.realtime } : {}),\n })\n }\n\n private readonly dispatcher: InboundEffectsDispatcher\n\n async execute(input: ReceiveWebhookParams): Promise<ReceiveWebhookResult> {\n verifyWebhookSignature({\n rawBody: input.rawBody,\n signatureHeader: input.signatureHeader,\n appSecret: this.params.appSecret,\n })\n\n const claimed = await claimWebhookDelivery({\n nonceStore: this.params.nonceStore,\n signatureHeader: input.signatureHeader!,\n })\n if (!claimed) return { duplicate: true, ...EMPTY_RESULT }\n\n const rawText = typeof input.rawBody === 'string' ? input.rawBody : input.rawBody.toString('utf8')\n // `safeParse`, não `parse`: um envelope que não bate com o schema não pode derrubar a entrega\n // inteira. A Meta desativa webhook que responde erro com frequência, e o corpo já veio\n // autenticado pela assinatura — então o que resta é reportar ao host e responder 200.\n const parsed = whatsAppWebhookPayloadSchema.safeParse(JSON.parse(rawText))\n if (!parsed.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: undefined,\n reason: 'invalid-shape',\n value: rawText,\n })\n return { duplicate: false, ...EMPTY_RESULT, unhandledEvents: 1 }\n }\n const payload = parsed.data\n\n let messagesProcessed = 0\n let statusesProcessed = 0\n let ignoredForeignNumber = 0\n let accountEventsProcessed = 0\n let unhandledEvents = 0\n\n for (const entry of payload.entry) {\n for (const change of entry.changes) {\n // Eventos de conta são resolvidos antes do filtro de número: eles não têm `metadata`, e o\n // filtro abaixo depende dele. O `phone_number_quality_update` traz o número no próprio\n // corpo (`display_phone_number`), não em `metadata`.\n if (isAccountEventField(change.field)) {\n const handled = await this.handleAccountEvent(change)\n if (handled) accountEventsProcessed++\n else unhandledEvents++\n continue\n }\n\n // Uma WABA comporta vários números, e a Meta entrega os eventos de TODOS eles para cada app\n // inscrito na conta — a inscrição é por WABA, não por número. Sem este filtro, uma instância\n // responde o cliente de um número que não é dela e grava esse contato na própria base.\n // Metadata ausente segue processando: só a Meta preenche esse campo, e não há como decidir\n // pertencimento sem ele.\n const targetNumber = change.value.metadata?.phone_number_id\n if (targetNumber && targetNumber !== this.params.phoneNumberId) {\n ignoredForeignNumber++\n continue\n }\n\n for (const message of change.value.messages ?? []) {\n // O nome sai do `change`, e não da mensagem: a Meta o entrega em `contacts`, ao lado das\n // mensagens, e não dentro de cada uma.\n const profileName = resolveContactProfileName({ contacts: change.value.contacts, from: message.from })\n await this.handleMessage(input.companyId, message, profileName)\n messagesProcessed++\n }\n for (const status of change.value.statuses ?? []) {\n await this.handleStatus(input.companyId, status)\n statusesProcessed++\n }\n\n // Field desconhecido que também não trouxe nada de conversa: sem isto, um campo assinado\n // por engano no painel seria indistinguível de webhook mudo.\n const carriedConversationData =\n (change.value.messages?.length ?? 0) > 0 ||\n (change.value.message_echoes?.length ?? 0) > 0 ||\n (change.value.statuses?.length ?? 0) > 0\n if (!carriedConversationData) {\n unhandledEvents++\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'unknown-field',\n value: change.value,\n })\n }\n }\n }\n\n // Só aqui a entrega vira \"processada\". Se qualquer coisa acima lançar, o claim curto expira e\n // a reentrega da Meta ainda encontra a porta aberta — que é o ponto de todo este desenho.\n await confirmWebhookDelivery({\n nonceStore: this.params.nonceStore,\n signatureHeader: input.signatureHeader!,\n })\n\n return {\n duplicate: false,\n messagesProcessed,\n statusesProcessed,\n ignoredForeignNumber,\n accountEventsProcessed,\n unhandledEvents,\n }\n }\n\n // Devolve `false` quando o corpo não bate com o schema do field — o chamador conta como não\n // tratado. O host é avisado nos dois casos, mas com `reason` diferente.\n private async handleAccountEvent(change: WhatsAppWebhookChange): Promise<boolean> {\n if (change.field === WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE) {\n const update = whatsAppTemplateStatusUpdateSchema.safeParse(change.value)\n if (!update.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'invalid-shape',\n value: change.value,\n })\n return false\n }\n await this.params.hooks?.onTemplateStatusUpdate?.(update.data)\n return true\n }\n\n const quality = whatsAppPhoneNumberQualityUpdateSchema.safeParse(change.value)\n if (!quality.success) {\n await this.params.hooks?.onUnhandledWebhookEvent?.({\n field: change.field,\n reason: 'invalid-shape',\n value: change.value,\n })\n return false\n }\n await this.params.hooks?.onPhoneNumberQualityUpdate?.(quality.data)\n return true\n }\n\n private async handleMessage(companyId: string, message: WhatsAppMessage, profileName?: string): Promise<void> {\n // Persistir fica na requisição de propósito: é escrita local, custa pouco, e é o que garante\n // que a mensagem do cliente existe no banco mesmo que tudo depois dela falhe.\n const saved = await this.params.logMessage.execute({\n companyId,\n whatsappNumber: message.from,\n direction: 'inbound',\n sender: 'customer',\n type: message.type,\n content: extractContent(message),\n payload: extractPayload(message),\n waMessageId: message.id,\n status: 'received',\n startState: this.params.startState,\n })\n // Mensagem repetida (mesmo waMessageId já gravado): não dispara o hook de novo, senão a\n // regra de negócio do host rodaria duas vezes para a mesma mensagem do cliente.\n if (!saved) return\n\n const media = extractMediaDescriptor(saved)\n await this.dispatch({\n kind: 'message',\n companyId,\n message,\n savedMessageId: saved.id,\n ...(media ? { media } : {}),\n ...(profileName ? { profileName } : {}),\n receivedAt: Date.now(),\n })\n }\n\n private async handleStatus(companyId: string, status: WhatsAppStatus): Promise<void> {\n const updated = await this.params.messageRepository.updateMessageStatus(\n companyId,\n status.id,\n status.status as MessageStatus,\n )\n if (!updated) return\n\n this.params.realtime?.emit(`conv:${updated.whatsappNumber}`, 'message-status', {\n waMessageId: status.id,\n status: status.status,\n })\n\n await this.dispatch({\n kind: 'status',\n companyId,\n status,\n whatsappNumber: updated.whatsappNumber,\n receivedAt: Date.now(),\n })\n }\n\n // Com fila configurada, os efeitos saem da requisição do webhook; sem ela, rodam aqui mesmo e o\n // comportamento é o de sempre. É o mesmo `InboundEffectsDispatcher` nos dois caminhos.\n private async dispatch(job: InboundDispatchJob): Promise<void> {\n if (this.params.inboundQueue) {\n await this.params.inboundQueue.enqueue(job, { jobId: buildInboundJobId(job) })\n return\n }\n await this.dispatcher.run(job)\n }\n}\n\nexport { extractAnswer, extractContent }\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * O algoritmo mora em `meta-graph-core`, compartilhado com os outros objetos que a Meta assina.\n * Aqui fica só a tradução para o vocabulário deste domínio: o erro lançado e o namespace do nonce.\n */\n\nimport {\n WEBHOOK_CLAIM_TTL_SECONDS,\n WEBHOOK_NONCE_TTL_SECONDS,\n buildWebhookDeliveryKey,\n isValidWebhookChallenge,\n isValidWebhookSignature,\n} from '@adatechnology/meta-graph-core'\nimport { InvalidWebhookSignatureError } from '@adatechnology/meta-whatsapp-contracts'\n\nexport interface NonceStoreInterface {\n setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>\n /**\n * Estende a chave já reivindicada para a janela cheia. Um SET simples, sem NX.\n *\n * Opcional só por compatibilidade com hosts que ainda não o implementam: sem ele o claim curto\n * expira, e a Meta pode reentregar uma entrega já processada — trabalho repetido, que a dedupe\n * por `waMessageId` ainda segura antes de virar efeito visível ao cliente. Implementar é o\n * caminho correto.\n */\n confirm?(key: string, ttlSeconds: number): Promise<void>\n}\n\nexport { WEBHOOK_CLAIM_TTL_SECONDS, WEBHOOK_NONCE_TTL_SECONDS }\n\nconst WEBHOOK_NONCE_NAMESPACE = 'meta-whatsapp'\n\nexport function verifyWebhookChallenge(params: {\n mode: string | null\n token: string | null\n challenge: string | null\n expectedToken: string\n}): string {\n if (!isValidWebhookChallenge(params)) throw new InvalidWebhookSignatureError()\n return params.challenge as string\n}\n\nexport function verifyWebhookSignature(params: {\n rawBody: Buffer | string\n signatureHeader: string | null | undefined\n appSecret: string\n}): void {\n if (!isValidWebhookSignature(params)) throw new InvalidWebhookSignatureError()\n}\n\nfunction deliveryKey(signatureHeader: string): string {\n return buildWebhookDeliveryKey({\n namespace: WEBHOOK_NONCE_NAMESPACE,\n signatureHeader,\n })\n}\n\n/**\n * Reivindica a entrega por pouco tempo. O par obrigatório é `confirmWebhookDelivery` ao fim do\n * processamento — ver `WEBHOOK_CLAIM_TTL_SECONDS` para o porquê dos dois tempos.\n */\nexport async function claimWebhookDelivery(params: {\n nonceStore: NonceStoreInterface\n signatureHeader: string\n ttlSeconds?: number\n}): Promise<boolean> {\n return params.nonceStore.setIfAbsent(\n deliveryKey(params.signatureHeader),\n params.ttlSeconds ?? WEBHOOK_CLAIM_TTL_SECONDS,\n )\n}\n\n/** Só depois da entrega processada por inteiro: é isto que fecha a janela anti-replay. */\nexport async function confirmWebhookDelivery(params: {\n nonceStore: NonceStoreInterface\n signatureHeader: string\n ttlSeconds?: number\n}): Promise<void> {\n await params.nonceStore.confirm?.(deliveryKey(params.signatureHeader), params.ttlSeconds ?? WEBHOOK_NONCE_TTL_SECONDS)\n}\n","import { eq, and } from 'drizzle-orm'\nimport type { MetaWhatsAppDatabase } from '../database.types'\nimport type {\n ChannelAdapterInterface,\n MetaWhatsAppHooks,\n ObjectStorageInterface,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { DocumentRepository } from '../repositories/DocumentRepository'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport { messages, type MessageRow } from '../schema/schema'\nimport { resolveFailureStatus } from '../use-cases/TranscribeAudio.use-case'\nimport type { TranscriptionPolicyResolver } from '../use-cases/resolveTranscriptionPolicy'\nimport {\n TRANSCRIPTION_MODE,\n TRANSCRIPTION_STATUS,\n isAudioMimeType,\n transcriptionRetryAfterSeconds,\n type AudioTranscriber,\n type TranscriptionStatus,\n} from '../transcription.types'\n\nexport type IngestInboundMediaParams = {\n companyId: string\n messageId: string\n // ID da mídia na Meta — a URL de download expira, então o binário precisa ser copiado para o\n // storage do host antes de sumir.\n sourceMediaId: string\n mimeType: string\n filename?: string\n}\n\nexport type IngestInboundMediaResult = {\n uploadId: string\n // true quando a mídia já estava no storage e nada foi baixado de novo.\n alreadyIngested: boolean\n /**\n * Só presente quando a transcrição automática rodou nesta execução. Ausente é o normal: mídia que\n * não é áudio, transcrição desligada, modo sob demanda, ou mídia já ingerida antes.\n */\n transcription?: { status: TranscriptionStatus }\n}\n\n/**\n * Transcrição durante a ingestão — o modo `auto`.\n *\n * Entra aqui, e não em use-case separado, por um motivo só: neste ponto o buffer do áudio ACABOU de\n * ser baixado e está em memória. Transcrever fora daqui custaria um segundo download do storage por\n * áudio, e o `TranscribeAudioUseCase` existe justamente para esse caso (sob demanda e retomada).\n */\nexport type IngestTranscriptionOptions = {\n transcriber: AudioTranscriber\n messageRepository: MessageRepository\n /**\n * Política POR EMPRESA, resolvida a cada áudio. Não é um `mode` fixo porque o interruptor mora nas\n * configurações da empresa: um valor capturado na construção do use-case congelaria a escolha até\n * o próximo deploy, e o worker é um processo longo — o operador mexeria no painel e nada mudaria.\n */\n resolvePolicy: TranscriptionPolicyResolver\n languageHint?: string\n hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>\n}\n\n// T5.3 — copia mídia recebida da Meta para o storage do host.\n//\n// Não reimplementa S3: delega ao ObjectStorageInterface que o host injeta (object-storage-provider\n// ou o que ele usar). O módulo só orquestra \"baixa da Meta → entrega ao storage → referencia na\n// mensagem\".\n//\n// Idempotente por sourceMediaId gravado no payload da mensagem: o job pode ser reentregue (fila\n// com retry, webhook reprocessado) e baixar de novo custaria banda e geraria um segundo objeto\n// órfão no storage para o mesmo binário.\nexport class IngestInboundMediaUseCase {\n constructor(\n private readonly db: MetaWhatsAppDatabase,\n private readonly channel: ChannelAdapterInterface,\n private readonly objectStorage: ObjectStorageInterface,\n // Opcional: sem ele a mídia continua sendo copiada e referenciada no payload, só não entra na\n // biblioteca da conversa. Mantém compatível quem já usava o use case antes da tabela existir.\n private readonly documentRepository?: DocumentRepository,\n // Último e opcional para não quebrar quem já constrói este use-case posicionalmente. Ausente,\n // a ingestão se comporta exatamente como antes da transcrição existir.\n private readonly transcription?: IngestTranscriptionOptions,\n ) {}\n\n async execute(params: IngestInboundMediaParams): Promise<IngestInboundMediaResult> {\n const [message] = await this.db\n .select()\n .from(messages)\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n .limit(1)\n\n if (!message) throw new Error(`Mensagem ${params.messageId} não encontrada para ingestão de mídia`)\n\n const payload = (message.payload ?? {}) as Record<string, unknown>\n\n // Já ingerida: devolve o uploadId existente sem tocar na rede.\n //\n // Não tenta transcrever aqui de propósito. Sem o download não há buffer, e reler do storage\n // duplicaria o `TranscribeAudioUseCase`. Retomada de transcrição pendente é trabalho DELE — o\n // `onTranscriptionDeferred` carrega o `uploadId` exatamente para o host enfileirar uma\n // transcrição, não uma segunda ingestão.\n if (payload['uploadId'] && payload['sourceMediaId'] === params.sourceMediaId) {\n return { uploadId: String(payload['uploadId']), alreadyIngested: true }\n }\n\n const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId)\n const buffer = Buffer.from(data, 'base64')\n\n const { uploadId } = await this.objectStorage.upload({\n buffer,\n mimeType: mimeType || params.mimeType,\n key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`,\n })\n\n // Guarda sourceMediaId junto do uploadId: é o que torna a próxima execução idempotente.\n const updatedPayload: Record<string, unknown> = {\n ...payload,\n uploadId,\n sourceMediaId: params.sourceMediaId,\n mimeType: mimeType || params.mimeType,\n // O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o\n // exibe ao lado do tipo. Sem gravar aqui, a UI mostraria \"PDF\" sem o \"· 180 KB\", e buscá-lo\n // depois custaria uma consulta à tabela de documentos por mensagem renderizada.\n sizeBytes: buffer.length,\n ...(params.filename ? { filename: params.filename } : {}),\n }\n\n await this.db\n .update(messages)\n .set({ payload: updatedPayload })\n .where(and(eq(messages.companyId, params.companyId), eq(messages.id, params.messageId)))\n\n // Entra na biblioteca da conversa. Idempotente por (companyId, uploadId) no repositório, o que\n // importa aqui: a chave do objeto deriva de sourceMediaId, então a reentrega do job produz o\n // mesmo uploadId e o link não duplica.\n await this.documentRepository?.link({\n companyId: params.companyId,\n sessionId: message.sessionId,\n messageId: message.id,\n uploadId,\n // Áudio e sticker chegam sem nome; sem um rótulo o painel mostraria linha vazia.\n filename: params.filename ?? `${params.sourceMediaId}`,\n mimeType: mimeType || params.mimeType,\n sizeBytes: buffer.length,\n source: message.sender,\n })\n\n const transcription = await this.transcribeIfAuto({\n companyId: params.companyId,\n message,\n uploadId,\n buffer,\n mimeType: mimeType || params.mimeType,\n })\n\n return { uploadId, alreadyIngested: false, ...(transcription ? { transcription } : {}) }\n }\n\n /**\n * Transcreve o áudio recém-baixado, quando o modo é `auto`.\n *\n * **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da\n * conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o\n * retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir\n * um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`\n * avisa quem sabe reenfileirar.\n */\n private async transcribeIfAuto(context: {\n companyId: string\n message: MessageRow\n uploadId: string\n buffer: Buffer\n mimeType: string\n }): Promise<{ status: TranscriptionStatus } | undefined> {\n const transcription = this.transcription\n if (!transcription) return undefined\n // Vídeo, imagem e documento passam sem tocar no engine — checado ANTES de consultar a política,\n // para um PDF não custar uma leitura de `settings`.\n if (!isAudioMimeType(context.mimeType)) return undefined\n\n const policy = await transcription.resolvePolicy(context.companyId)\n if (!policy.isEnabled || policy.mode !== TRANSCRIPTION_MODE.AUTO) return undefined\n\n /**\n * Já transcrito, não transcreve de novo.\n *\n * Quem chega antes é normalmente o próprio host: o grafo de conversa transcreve no webhook\n * porque precisa do texto para responder ao cliente, e grava o resultado na mensagem. Sem esta\n * guarda o job de ingestão transcreveria o MESMO áudio uma segunda vez — duas cobranças e duas\n * chamadas ao engine para um resultado que já estava no banco.\n *\n * Reduz a corrida, não a elimina: host e worker podem ler `null` no mesmo instante. Aí um dos\n * dois grava por cima com o mesmo texto, o que é inofensivo — o desperdício fica na janela\n * estreita entre a gravação de um e a leitura do outro, em vez de acontecer sempre.\n *\n * A releitura é obrigatória. A linha de `context.message` foi lida no começo de `execute()`,\n * ANTES de baixar o binário da Meta — e é durante esse download que o host costuma gravar a\n * transcrição dele. Confiar na linha velha faria a guarda falhar justamente no caso comum.\n */\n const current = await transcription.messageRepository.findById(context.companyId, context.message.id)\n if (current?.transcriptionStatus === TRANSCRIPTION_STATUS.DONE) return undefined\n\n try {\n const result = await transcription.transcriber.transcribe({\n buffer: context.buffer,\n mimeType: context.mimeType,\n ...(transcription.languageHint ? { languageHint: transcription.languageHint } : {}),\n })\n\n await transcription.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.message.id,\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n })\n\n return { status: TRANSCRIPTION_STATUS.DONE }\n } catch (error) {\n return this.recordTranscriptionFailure(context, error)\n }\n }\n\n private async recordTranscriptionFailure(\n context: { companyId: string; message: MessageRow; uploadId: string },\n error: unknown,\n ): Promise<{ status: TranscriptionStatus }> {\n const status = resolveFailureStatus(error)\n\n await this.transcription?.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.message.id,\n status,\n })\n\n if (status === TRANSCRIPTION_STATUS.PENDING) {\n const retryAfterSeconds = transcriptionRetryAfterSeconds(error)\n await this.transcription?.hooks?.onTranscriptionDeferred?.({\n companyId: context.companyId,\n messageId: context.message.id,\n whatsappNumber: context.message.whatsappNumber,\n uploadId: context.uploadId,\n ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),\n reason: retryAfterSeconds !== undefined ? 'rate-limited' : 'transient-failure',\n error,\n })\n }\n\n return { status }\n }\n}\n\n// Extrai o descritor de mídia de uma mensagem já persistida — o webhook guarda o objeto cru da\n// Meta (image/audio/video/document/sticker) no payload, e a ingestão precisa saber qual é.\nexport function extractMediaDescriptor(\n message: MessageRow,\n): { sourceMediaId: string; mimeType: string; filename?: string } | undefined {\n const payload = (message.payload ?? {}) as Record<string, unknown>\n for (const key of ['image', 'audio', 'video', 'document', 'sticker'] as const) {\n const media = payload[key] as { id?: string; mime_type?: string; filename?: string } | undefined\n if (media?.id) {\n return {\n sourceMediaId: media.id,\n mimeType: media.mime_type ?? 'application/octet-stream',\n filename: media.filename,\n }\n }\n }\n return undefined\n}\n","import {\n AudioNotIngestedError,\n MessageNotAudioError,\n TranscriptionDisabledError,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { MetaWhatsAppHooks, ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport type { MessageRepository } from '../repositories/MessageRepository'\nimport type { MessageRow } from '../schema/schema'\nimport type { TranscriptionPolicyResolver } from './resolveTranscriptionPolicy'\nimport {\n TRANSCRIPTION_STATUS,\n isAudioMimeType,\n isRetriableTranscriptionError,\n isUnsupportedTranscriptionError,\n transcriptionRetryAfterSeconds,\n type AudioTranscriber,\n type TranscriptionStatus,\n} from '../transcription.types'\n\n/**\n * Storage com leitura garantida. `getObject` é opcional no contrato, então quem monta este use-case\n * precisa provar que o método existe — sem os bytes não há o que transcrever, e um use-case que\n * sempre falha é pior do que a ausência ser visível no tipo.\n */\ntype ReadableObjectStorage = ObjectStorageInterface & {\n getObject: NonNullable<ObjectStorageInterface['getObject']>\n}\n\nexport type TranscribeAudioParams = {\n companyId: string\n messageId: string\n /**\n * Refaz mesmo com transcrição já salva. Serve ao \"transcrever de novo\" depois de trocar de engine\n * — sem isto, um resultado ruim do engine antigo ficaria congelado para sempre.\n */\n force?: boolean\n}\n\nexport type TranscribeAudioResult = {\n status: TranscriptionStatus\n text: string | null\n language: string | null\n engine: string | null\n /** `true` quando devolveu o que já estava salvo, sem gastar cota. */\n alreadyTranscribed: boolean\n}\n\nexport type TranscribeAudioDependencies = {\n messageRepository: MessageRepository\n objectStorage: ReadableObjectStorage\n transcriber: AudioTranscriber\n /**\n * Política por empresa. Ausente, a transcrição sob demanda não consulta configuração nenhuma e\n * atende sempre — que é o comportamento de quem controla o liga/desliga só por ambiente.\n */\n resolvePolicy?: TranscriptionPolicyResolver\n /** ISO 639-1 do produto. Informar corta a detecção do Whisper e evita pt-BR curto virar espanhol. */\n languageHint?: string\n hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>\n}\n\n/**\n * Transcreve o áudio de UMA mensagem já persistida e ingerida.\n *\n * Serve aos dois modos: é o que o painel chama no botão \"transcrever\" (`onDemand`) e é o que o host\n * chama ao retomar um `pending` reenfileirado. O modo `auto` não passa por aqui — ele transcreve\n * dentro da ingestão, onde o buffer já está em memória e não custa um segundo download.\n *\n * Idempotente por `transcription_status`: chamar de novo num `'done'` devolve o que está salvo em\n * vez de gastar cota transcrevendo o mesmo áudio.\n */\nexport class TranscribeAudioUseCase {\n constructor(private readonly dependencies: TranscribeAudioDependencies) {}\n\n async execute(params: TranscribeAudioParams): Promise<TranscribeAudioResult> {\n const message = await this.dependencies.messageRepository.findById(params.companyId, params.messageId)\n if (!message) throw new Error(`Mensagem ${params.messageId} não encontrada para transcrição`)\n\n /**\n * Devolver o que já está salvo vem ANTES da checagem de política, e de propósito: o texto já\n * existe, já aparece na conversa, e desligar o recurso depois não deve apagar nem esconder o que\n * foi transcrito enquanto estava ligado. A política governa gastar cota, não ler o passado.\n */\n if (message.transcriptionStatus === TRANSCRIPTION_STATUS.DONE && !params.force) {\n return {\n status: TRANSCRIPTION_STATUS.DONE,\n text: message.transcriptionText,\n language: message.transcriptionLanguage,\n engine: message.transcriptionEngine,\n alreadyTranscribed: true,\n }\n }\n\n // Daqui para baixo gasta cota, então a política manda.\n if (this.dependencies.resolvePolicy) {\n const policy = await this.dependencies.resolvePolicy(params.companyId)\n if (!policy.isEnabled) throw new TranscriptionDisabledError()\n }\n\n const audio = extractAudioReference(message)\n const buffer = await this.dependencies.objectStorage.getObject(audio.uploadId)\n\n return this.transcribeBuffer({ ...params, buffer, mimeType: audio.mimeType, uploadId: audio.uploadId, message })\n }\n\n private async transcribeBuffer(context: {\n companyId: string\n messageId: string\n buffer: Buffer\n mimeType: string\n uploadId: string\n message: MessageRow\n }): Promise<TranscribeAudioResult> {\n try {\n const result = await this.dependencies.transcriber.transcribe({\n buffer: context.buffer,\n mimeType: context.mimeType,\n ...(this.dependencies.languageHint ? { languageHint: this.dependencies.languageHint } : {}),\n })\n\n await this.dependencies.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.messageId,\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n })\n\n return {\n status: TRANSCRIPTION_STATUS.DONE,\n text: result.text,\n language: result.language ?? null,\n engine: result.engine,\n alreadyTranscribed: false,\n }\n } catch (error) {\n await this.persistFailure(context, error)\n throw error\n }\n }\n\n /**\n * Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:\n * reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.\n */\n private async persistFailure(\n context: { companyId: string; messageId: string; uploadId: string; message: MessageRow },\n error: unknown,\n ): Promise<void> {\n const status = resolveFailureStatus(error)\n\n await this.dependencies.messageRepository.saveTranscription({\n companyId: context.companyId,\n messageId: context.messageId,\n status,\n })\n\n if (status !== TRANSCRIPTION_STATUS.PENDING) return\n\n const retryAfterSeconds = transcriptionRetryAfterSeconds(error)\n await this.dependencies.hooks?.onTranscriptionDeferred?.({\n companyId: context.companyId,\n messageId: context.messageId,\n whatsappNumber: context.message.whatsappNumber,\n uploadId: context.uploadId,\n ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),\n reason: retryAfterSeconds !== undefined ? 'rate-limited' : 'transient-failure',\n error,\n })\n }\n}\n\nexport function resolveFailureStatus(error: unknown): TranscriptionStatus {\n if (isUnsupportedTranscriptionError(error)) return TRANSCRIPTION_STATUS.UNSUPPORTED\n return isRetriableTranscriptionError(error) ? TRANSCRIPTION_STATUS.PENDING : TRANSCRIPTION_STATUS.FAILED\n}\n\n/**\n * Onde o áudio está e em que formato.\n *\n * Lê do `payload`, que é onde a ingestão gravou `uploadId` e `mimeType` — a tabela de documentos\n * também tem o dado, mas exigiria uma consulta a mais por mensagem e não é a fonte que a ingestão\n * atualiza primeiro.\n */\nfunction extractAudioReference(message: MessageRow): { uploadId: string; mimeType: string } {\n const payload = (message.payload ?? {}) as Record<string, unknown>\n const audio = payload['audio'] as { mime_type?: string } | undefined\n const mimeType = typeof payload['mimeType'] === 'string' ? payload['mimeType'] : audio?.mime_type\n\n // `type` do host é rótulo livre (varchar(32)), então a checagem real é o mime — é ele que diz se\n // existe áudio para transcrever, independente de como o produto nomeou a mensagem.\n if (!isAudioMimeType(mimeType) && !audio) {\n throw new MessageNotAudioError(message.id, message.type)\n }\n\n const uploadId = payload['uploadId']\n // Sem uploadId a ingestão ainda não terminou. O áudio existe na Meta, mas a URL de lá expira e\n // não é este use-case que a busca — quem copia é a ingestão, e ela é assíncrona.\n if (typeof uploadId !== 'string' || uploadId.length === 0) {\n throw new AudioNotIngestedError(message.id)\n }\n\n return { uploadId, mimeType: mimeType ?? 'audio/ogg' }\n}\n","/**\n * Vocabulário de transcrição de áudio do módulo.\n *\n * Fica em arquivo próprio porque o schema e os dois use-cases (ingestão automática e sob demanda)\n * precisam dos mesmos tipos, e pendurá-los em qualquer um dos três faria os outros dois importarem\n * de dentro de uma camada que não é a deles.\n */\n\nimport type { TranscriptionMode } from '@adatechnology/meta-whatsapp-contracts'\n\nexport const TRANSCRIPTION_STATUS = {\n /** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */\n PENDING: 'pending',\n /** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */\n DONE: 'done',\n /** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */\n FAILED: 'failed',\n /** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */\n UNSUPPORTED: 'unsupported',\n} as const\n\nexport type TranscriptionStatus = (typeof TRANSCRIPTION_STATUS)[keyof typeof TRANSCRIPTION_STATUS]\n\n/**\n * Quando transcrever.\n *\n * `auto` transcreve durante a ingestão da mídia, onde o buffer do áudio JÁ está em memória — não\n * custa um segundo download do storage. `onDemand` só transcreve quando o atendente pede, o que\n * troca latência na interface por não gastar cota com áudio que ninguém vai ler.\n *\n * O tipo vem do contrato (o painel escolhe, a API transporta, o módulo obedece) e `satisfies`\n * garante em tempo de compilação que estes valores continuam sendo exatamente os de lá — sem isso,\n * um modo novo no contrato passaria despercebido aqui.\n */\nexport const TRANSCRIPTION_MODE = {\n AUTO: 'auto',\n ON_DEMAND: 'onDemand',\n} as const satisfies Record<string, TranscriptionMode>\n\nexport type { TranscriptionMode }\n\n/**\n * O contrato mínimo de `@adatechnology/audio-transcription-provider`, declarado aqui em vez de\n * importado — mesma decisão do `MessageModerator` em `LogMessage.use-case.ts`.\n *\n * Assim o módulo não ganha dependência de pacote por um recurso opcional, e transcrição não fica\n * amarrada a WhatsApp: o que atravessa esta fronteira é um buffer de áudio e um mime.\n */\nexport type AudioTranscriber = {\n readonly name: string\n transcribe: (input: {\n buffer: Buffer\n mimeType: string\n languageHint?: string\n }) => Promise<{ text: string; language?: string; durationSeconds?: number; engine: string }>\n}\n\n/**\n * Recorte do erro do provider que o módulo precisa ler para escolher entre `'pending'` e\n * `'failed'`. Estrutural, não `instanceof`: o provider é opcional e pode nem estar instalado, e um\n * `instanceof` contra classe ausente não compila.\n */\nexport function isRetriableTranscriptionError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return true\n const isRetriable = (error as { isRetriable?: unknown }).isRetriable\n // Ausente conta como retriável: erro sem carimbo é quase sempre rede, e insistir é barato.\n return typeof isRetriable === 'boolean' ? isRetriable : true\n}\n\nexport function isUnsupportedTranscriptionError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as { name?: unknown }).name === 'TranscriptionUnsupportedError'\n )\n}\n\nexport function transcriptionRetryAfterSeconds(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const retryAfter = (error as { retryAfterSeconds?: unknown }).retryAfterSeconds\n return typeof retryAfter === 'number' ? retryAfter : undefined\n}\n\n/** Mime de áudio? Só áudio é transcrito — vídeo, imagem e documento passam sem tocar no engine. */\nexport function isAudioMimeType(mimeType: string | undefined | null): boolean {\n return typeof mimeType === 'string' && mimeType.trim().toLowerCase().startsWith('audio/')\n}\n","/**\n * Copyright (c) 2026 Ada Technology. MIT License.\n *\n * Separa a entrega do webhook em duas metades: o que precisa acontecer antes de responder à Meta\n * (persistir a mensagem, que é escrita local e rápida) e o que pode esperar alguns milissegundos\n * (os hooks do host, que costumam falar com rede — motor de fluxo, IA, integrações).\n *\n * A segunda metade rodava dentro da requisição do webhook. Um deploy no meio de uma conversa\n * matava a chamada em voo, e o cliente ficava sem resposta sem que nada registrasse a perda. Com\n * uma fila no host, derrubar o processo vira atraso: o job espera e é retomado.\n */\n\nimport type {\n ConversationSession,\n MetaWhatsAppHooks,\n RealtimeNotifierInterface,\n WhatsAppMessage,\n WhatsAppStatus,\n} from '@adatechnology/meta-whatsapp-contracts'\nimport type { SessionRepository } from '../repositories/SessionRepository'\nimport type { SessionRow } from '../schema/schema'\n\nexport type InboundMediaDescriptor = {\n sourceMediaId: string\n mimeType: string\n filename?: string\n}\n\nexport type InboundMessageEffectsJob = {\n kind: 'message'\n companyId: string\n message: WhatsAppMessage\n savedMessageId: string\n media?: InboundMediaDescriptor\n /**\n * Nome de perfil de quem mandou. Vai no JOB e não é relido depois porque o job pode passar por\n * fila: o payload da Meta não existe mais quando o worker roda.\n *\n * Opcional, e job antigo sem o campo continua válido — é o que permite subir o módulo sem drenar\n * a fila antes.\n */\n profileName?: string\n receivedAt: number\n}\n\nexport type InboundStatusEffectsJob = {\n kind: 'status'\n companyId: string\n status: WhatsAppStatus\n whatsappNumber: string\n receivedAt: number\n}\n\nexport type InboundDispatchJob = InboundMessageEffectsJob | InboundStatusEffectsJob\n\n/**\n * Porta de fila. O módulo não escolhe a tecnologia — BullMQ, SQS, o que o host já tiver — mas\n * exige dela duas garantias, e sem as duas o padrão não entrega o que promete:\n *\n * 1. **Durabilidade**: o job sobrevive à morte do processo que o enfileirou. Uma fila em memória\n * reintroduz exatamente a perda que este desenho existe para evitar.\n * 2. **Retentativa com backoff**: o destino dos hooks (n8n, IA, API de terceiro) também cai em\n * deploy. Sem retry, o job falha uma vez e a conversa morre do mesmo jeito.\n *\n * `jobId` é estável e derivado da mensagem: reentrega da Meta e re-enfileiramento produzem o mesmo\n * id, e a fila descarta o segundo em vez de rodar o efeito duas vezes.\n */\nexport interface InboundDispatchQueueInterface {\n enqueue(job: InboundDispatchJob, options: { jobId: string }): Promise<void>\n}\n\nexport function buildInboundJobId(job: InboundDispatchJob): string {\n return job.kind === 'message'\n ? `wa-inbound-message:${job.message.id}`\n : `wa-inbound-status:${job.status.id}:${job.status.status}`\n}\n\nexport function toSessionContract(row: SessionRow): ConversationSession {\n return {\n id: row.id,\n companyId: row.companyId,\n whatsappNumber: row.whatsappNumber,\n currentState: row.currentState,\n flowKey: row.flowKey,\n currentNodeId: row.currentNodeId,\n context: row.context,\n mode: row.mode as ConversationSession['mode'],\n assignedUserId: row.assignedUserId,\n humanRequestedAt: row.humanRequestedAt?.toISOString() ?? null,\n lastInboundAt: row.lastInboundAt?.toISOString() ?? null,\n lastAgentReadAt: row.lastAgentReadAt?.toISOString() ?? null,\n lastActivity: row.lastActivity.toISOString(),\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n }\n}\n\n/**\n * A resposta que o motor de fluxo deve consumir: para interativo é o ID da opção (estável), não\n * o título (que muda quando alguém edita o texto do botão no editor).\n */\nexport function extractAnswer(message: WhatsAppMessage): string | undefined {\n const interactive = message.interactive\n if (interactive?.button_reply) return interactive.button_reply.id\n if (interactive?.list_reply) return interactive.list_reply.id\n return message.text?.body\n}\n\n/**\n * Os efeitos de host de uma entrega já persistida. Vive fora do `ReceiveWebhookUseCase` porque\n * roda nos dois lados: inline, quando o host não configurou fila, e dentro do worker, quando\n * configurou. Um só corpo de regra para os dois caminhos — duas cópias divergiriam.\n */\nexport class InboundEffectsDispatcher {\n constructor(\n private readonly params: {\n sessionRepository: SessionRepository\n hooks?: MetaWhatsAppHooks\n realtime?: RealtimeNotifierInterface\n },\n ) {}\n\n async run(job: InboundDispatchJob): Promise<void> {\n if (job.kind === 'message') return this.runMessageEffects(job)\n return this.runStatusEffects(job)\n }\n\n async runMessageEffects(job: InboundMessageEffectsJob): Promise<void> {\n const { companyId, message, savedMessageId, media } = job\n\n // Antes de qualquer retorno antecipado: os `return` abaixo (atendimento humano, sessão ausente)\n // são sobre o FLUXO DO BOT, e mídia precisa ser copiada da Meta de qualquer forma. Justamente em\n // atendimento humano é que o cliente manda documento para o atendente — deixar a ingestão\n // depois desse `return` perderia esses arquivos, e a URL da Meta expira sem segunda chance.\n if (media) {\n await this.params.hooks?.onMediaReceived?.({\n companyId,\n messageId: savedMessageId,\n whatsappNumber: message.from,\n sourceMediaId: media.sourceMediaId,\n mimeType: media.mimeType,\n ...(media.filename ? { filename: media.filename } : {}),\n })\n }\n\n const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from)\n if (!sessionRow) return\n\n // Conversa em atendimento humano não é processada pelo bot — o atendente responde.\n if (sessionRow.mode === 'human') return\n\n const outcome = await this.params.hooks?.onMessageReceived?.(\n message,\n toSessionContract(sessionRow),\n job.profileName === undefined ? {} : { profileName: job.profileName },\n )\n // 'handled' = o host já respondeu e assumiu a mensagem; o módulo não segue com o fluxo.\n if (outcome?.outcome === 'handled') return\n\n // 'continue' (ou sem hook): quem dirige o motor de fluxo é o host via runFlowStep — o\n // webhook só entrega o sinal. Expor a resposta extraída evita que cada host reimplemente\n // a lógica de \"qual é a resposta do cliente\" para interativo vs texto.\n void extractAnswer(message)\n }\n\n async runStatusEffects(job: InboundStatusEffectsJob): Promise<void> {\n const sessionRow = await this.params.sessionRepository.getContext(job.companyId, job.whatsappNumber)\n await this.params.hooks?.onStatusUpdate?.(job.status, sessionRow ? toSessionContract(sessionRow) : null)\n }\n}\n\n/**\n * Ponto de entrada do worker do host: recebe o job que a fila devolveu e roda os efeitos.\n *\n * Deixe a exceção propagar. É ela que faz a fila contar a tentativa e reagendar com backoff;\n * capturar aqui para logar transforma falha recuperável em mensagem perdida em silêncio.\n */\nexport class ProcessInboundDispatchUseCase {\n constructor(private readonly dispatcher: InboundEffectsDispatcher) {}\n\n async execute(job: InboundDispatchJob): Promise<void> {\n await this.dispatcher.run(job)\n }\n}\n","import type { SettingsRepository } from '../repositories/SettingsRepository'\nimport { TRANSCRIPTION_MODE, type TranscriptionMode } from '../transcription.types'\n\n/**\n * Política efetiva de transcrição de uma empresa.\n *\n * A separação que este arquivo existe para manter: **ambiente decide se é POSSÍVEL, settings decide\n * se é para FAZER.** A capacidade (engine, chave, storage que sabe reler) é injetada pelo host e é\n * por deploy — chave de API não vai para tabela de configuração de tenant. Já \"transcrever ou não\" e\n * \"automático ou sob demanda\" são decisão de operação de cada empresa, e pedir deploy para mudar\n * isso é o que transforma um interruptor em ticket.\n */\nexport type TranscriptionPolicy = {\n readonly isEnabled: boolean\n readonly mode: TranscriptionMode\n}\n\nexport type TranscriptionPolicyDefaults = {\n /** O que vale quando o painel não decidiu. Tipicamente vem do ambiente do host. */\n readonly isEnabled: boolean\n readonly mode: TranscriptionMode\n}\n\nexport type ResolveTranscriptionPolicyDependencies = {\n readonly settingsRepository: SettingsRepository\n readonly defaults: TranscriptionPolicyDefaults\n}\n\n/**\n * Resolve a política por empresa, com o padrão do host como base.\n *\n * Uma consulta a `settings` por áudio transcrito. Não é cacheado de propósito: é uma leitura por\n * chave primária, acontece uma vez por nota de voz (não por mensagem), e cachear introduziria a\n * pergunta \"por quanto tempo o operador continua vendo o interruptor antigo depois de mexer nele\" —\n * custo real, para economizar um índice único.\n */\nexport function createTranscriptionPolicyResolver(dependencies: ResolveTranscriptionPolicyDependencies) {\n return async function resolveTranscriptionPolicy(companyId: string): Promise<TranscriptionPolicy> {\n const settings = await dependencies.settingsRepository.get(companyId)\n\n return {\n // `??` e não `||`: `false` gravado é decisão explícita de desligar, e `||` a trocaria pelo\n // padrão do host — desligar no painel não faria nada num deploy com transcrição ligada.\n isEnabled: settings.transcriptionEnabled ?? dependencies.defaults.isEnabled,\n mode: normalizeMode(settings.transcriptionMode) ?? dependencies.defaults.mode,\n }\n }\n}\n\nexport type TranscriptionPolicyResolver = ReturnType<typeof createTranscriptionPolicyResolver>\n\n/**\n * A coluna é `varchar`, então o banco aceita qualquer string — um valor legado ou digitado à mão não\n * pode virar um modo que ninguém trata. Desconhecido cai no padrão do host.\n */\nfunction normalizeMode(value: string | null): TranscriptionMode | undefined {\n if (value === TRANSCRIPTION_MODE.AUTO) return TRANSCRIPTION_MODE.AUTO\n if (value === TRANSCRIPTION_MODE.ON_DEMAND) return TRANSCRIPTION_MODE.ON_DEMAND\n return undefined\n}\n","import type { ObjectStorageInterface } from '@adatechnology/meta-whatsapp-contracts'\nimport { toPreviewMediaId } from '../channel/previewMedia'\n\n/**\n * Guarda um arquivo gravado no simulador e devolve o id que o webhook vai referenciar.\n *\n * Existe como use-case para o host só precisar da ROTA: receber o corpo, chamar isto, devolver o\n * `mediaId`. A parte que erra — onde gravar, com que chave, como marcar o id para o adaptador\n * reconhecer depois — fica aqui, num lugar só, e não em cada produto.\n *\n * O SDK para exatamente na porta do HTTP: registrar endpoint é do host, e um pacote que abrisse rota\n * no servidor de quem o instala decidiria caminho, autenticação e versionamento no lugar dele.\n */\nexport type StorePreviewMediaParams = {\n companyId: string\n /** Bytes do arquivo. Quem converte de base64 é a rota — o use-case não conhece transporte. */\n buffer: Buffer\n mimeType: string\n filename?: string\n}\n\nexport type StorePreviewMediaResult = {\n /** Já com o prefixo: é isto que o simulador manda no webhook. */\n mediaId: string\n uploadId: string\n}\n\nexport class StorePreviewMediaUseCase {\n constructor(\n private readonly objectStorage: ObjectStorageInterface,\n /**\n * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque\n * um teste precisa de chave previsível.\n */\n private readonly generateKeySuffix: () => string,\n ) {}\n\n async execute(params: StorePreviewMediaParams): Promise<StorePreviewMediaResult> {\n /**\n * Namespace próprio (`preview/`) e não junto da mídia real.\n *\n * Arquivo de simulação e arquivo de cliente têm ciclo de vida diferente: a retenção varre a\n * biblioteca da conversa, e misturar os dois faria a limpeza pensar duas vezes sobre o que é\n * dado de verdade. Separado, apagar tudo que é teste é apagar um prefixo.\n */\n const key = `meta-whatsapp/${params.companyId}/preview/${this.generateKeySuffix()}`\n\n const { uploadId } = await this.objectStorage.upload({\n buffer: params.buffer,\n mimeType: params.mimeType,\n key,\n })\n\n return { mediaId: toPreviewMediaId(uploadId), uploadId }\n }\n}\n","import { join } from 'node:path'\nimport type { MetaWhatsAppDatabase } from './database.types'\n\n// Migrations append-only com journal próprio — nunca colide com as migrations do host, que roda\n// as delas por um caminho totalmente separado (ver rules/packages/pluggable-module.md §3.2).\n//\n// O journal vive em `drizzle.meta_whatsapp_migrations` (schema `drizzle`, o padrão do\n// drizzle-kit; o nome DA TABELA é que isola o módulo — o host usa `__drizzle_migrations` ali do\n// lado, sem conflito). Deliberadamente NÃO fica dentro de `meta_whatsapp`: a primeira migration\n// do módulo é justamente quem cria esse schema (CREATE SCHEMA), então apontar o journal para lá\n// gera uma corrida entre o migrator garantindo o schema da tabela de controle e a migration\n// tentando criá-lo de novo — o erro \"schema meta_whatsapp already exists\".\n//\n// Para resetar um banco de teste é preciso derrubar os dois: o schema meta_whatsapp E o journal\n// em drizzle.meta_whatsapp_migrations. Só dropar o schema faz o migrator pular tudo na próxima\n// execução, deixando o banco sem as tabelas e sem erro aparente.\nexport const META_WHATSAPP_MIGRATIONS_TABLE = 'meta_whatsapp_migrations'\n\n// Pasta das migrations embarcadas, para o host que prefere chamar o próprio migrator.\nexport function metaWhatsAppMigrationsFolder(): string {\n return join(__dirname, 'migrations')\n}\n\nexport type RunMetaWhatsAppMigrationsParams = {\n db: MetaWhatsAppDatabase\n // O `migrate` do driver do host — `drizzle-orm/node-postgres/migrator`,\n // `drizzle-orm/bun-sql/migrator`, etc. O módulo não escolhe driver pelo host.\n migrate: (db: never, config: { migrationsFolder: string; migrationsTable?: string }) => Promise<void>\n}\n\nexport async function runMetaWhatsAppMigrations(params: RunMetaWhatsAppMigrationsParams): Promise<void> {\n await params.migrate(params.db as never, {\n migrationsFolder: metaWhatsAppMigrationsFolder(),\n migrationsTable: META_WHATSAPP_MIGRATIONS_TABLE,\n })\n}\n","import { randomUUID } from 'node:crypto'\nimport type { RealtimeNotifierInterface } from '@adatechnology/meta-whatsapp-contracts'\n\nexport type SseListener = (event: string, payload: Record<string, unknown>) => void\n\n// Porta mínima de cache para o ticket de conexão SSE — qualquer TTL cache do host serve\n// (Redis, in-memory); o módulo não assume Redis especificamente.\nexport interface TicketStoreInterface {\n set(key: string, value: string, ttlSeconds: number): Promise<void>\n get(key: string): Promise<string | null>\n delete(key: string): Promise<void>\n}\n\n// Publica localmente e, se um relay for injetado (ex.: Redis pub/sub), propaga entre processos\n// — várias instâncias do host atrás de um load balancer recebem o mesmo evento (T3.4).\nexport interface RealtimeRelay {\n publish(channel: string, message: string): Promise<void>\n subscribe(channel: string, onMessage: (message: string) => void): Promise<() => void>\n}\n\nconst TICKET_TTL_SECONDS = 60\n\n// T3.4 — hub de tempo real atrás da porta RealtimeNotifierInterface (contracts). O host pode\n// trocar por WebSocket ou desligar sem o resto do módulo saber — ver providers.ts.\ntype RelayEnvelope = {\n originId: string\n event: string\n payload: Record<string, unknown>\n}\n\nexport class SseHub implements RealtimeNotifierInterface {\n private readonly localListeners = new Map<string, Set<SseListener>>()\n // Uma assinatura de relay por CANAL, compartilhada por todos os listeners locais dele — uma\n // por listener faria o relay entregar N cópias num canal com N conexões abertas.\n private readonly relaySubscriptions = new Map<string, Promise<() => void>>()\n // Identifica esta instância do processo para descartar o próprio eco (pub/sub entrega a\n // mensagem a todos os inscritos, inclusive quem publicou).\n private readonly instanceId = randomUUID()\n\n constructor(private readonly relay?: RealtimeRelay) {}\n\n emit(channel: string, event: string, payload: Record<string, unknown>): void {\n this.dispatchLocal(channel, event, payload)\n if (this.relay) {\n const envelope: RelayEnvelope = { originId: this.instanceId, event, payload }\n this.relay.publish(channel, JSON.stringify(envelope)).catch(() => undefined)\n }\n }\n\n private dispatchLocal(channel: string, event: string, payload: Record<string, unknown>): void {\n const listeners = this.localListeners.get(channel)\n if (!listeners) return\n for (const listener of listeners) listener(event, payload)\n }\n\n async subscribe(channel: string, listener: SseListener): Promise<() => void> {\n if (!this.localListeners.has(channel)) this.localListeners.set(channel, new Set())\n this.localListeners.get(channel)!.add(listener)\n\n if (this.relay && !this.relaySubscriptions.has(channel)) {\n this.relaySubscriptions.set(\n channel,\n this.relay.subscribe(channel, (message) => {\n try {\n const envelope = JSON.parse(message) as RelayEnvelope\n // Eco do que nós mesmos publicamos: dispatchLocal já entregou em emit().\n if (envelope.originId === this.instanceId) return\n this.dispatchLocal(channel, envelope.event, envelope.payload)\n } catch {\n // mensagem de relay malformada — ignora, não derruba a conexão\n }\n }),\n )\n }\n\n return () => {\n const listeners = this.localListeners.get(channel)\n listeners?.delete(listener)\n // Último listener do canal saiu: solta a assinatura do relay e a chave do Map, senão\n // canais efêmeros (conv:<numero>) se acumulam pelo uptime inteiro do processo.\n if (listeners && listeners.size === 0) {\n this.localListeners.delete(channel)\n const pendingUnsubscribe = this.relaySubscriptions.get(channel)\n this.relaySubscriptions.delete(channel)\n pendingUnsubscribe?.then((unsubscribe) => unsubscribe()).catch(() => undefined)\n }\n }\n }\n}\n\n// EventSource do browser não manda headers customizados — o ticket vai na query string da URL\n// de conexão e expira em 60s, curto o bastante para não valer a pena capturar e reusar depois.\nexport async function issueSseTicket(\n store: TicketStoreInterface,\n companyId: string,\n whatsappNumber: string,\n): Promise<string> {\n const ticket = randomUUID()\n await store.set(`sse:ticket:${ticket}`, JSON.stringify({ companyId, whatsappNumber }), TICKET_TTL_SECONDS)\n return ticket\n}\n\nexport async function redeemSseTicket(\n store: TicketStoreInterface,\n ticket: string,\n): Promise<{ companyId: string; whatsappNumber: string } | null> {\n const raw = await store.get(`sse:ticket:${ticket}`)\n if (!raw) return null\n await store.delete(`sse:ticket:${ticket}`)\n return JSON.parse(raw) as { companyId: string; whatsappNumber: string }\n}\n"],"mappings":";;;;AACA,OAAOA,UAAU;AACjB,SAASC,qBAAqB;AAE9B,IAAMC,cAAc,6BAAMC,cAAc,YAAYC,GAAG,GAAnC;AACpB,IAAMC,aAAa,6BAAMC,KAAKC,QAAQL,YAAAA,CAAAA,GAAnB;AAEZ,IAAMM,YAA4BH,2BAAAA;;;ACPzC,SAASI,+BAA+B;AAExC,SAASC,wBAAwB;;;ACFjC,SAASC,KAAKC,MAAMC,IAAIC,OAAAA,YAAW;;;ACAnC,SAASC,WAAW;AACpB,SACEC,UACAC,MACAC,SACAC,OACAC,WACAC,MACAC,SACAC,SACAC,OACAC,mBACK;AAMA,IAAMC,qBAAqBV,SAAS,eAAA;AAEpC,IAAMW,WAAWD,mBAAmBE,MACzC,YACA;EACEC,IAAIZ,KAAK,IAAA,EAAMa,WAAU,EAAGC,cAAa;;;EAGzCC,WAAWf,KAAK,YAAA,EAAcgB,QAAO;EACrCC,gBAAgBhB,QAAQ,mBAAmB;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;;EAElEG,cAAclB,QAAQ,iBAAiB;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO,EAAGI,QAAQ,OAAA;;;;EAIzEC,SAASpB,QAAQ,YAAY;IAAEiB,QAAQ;EAAG,CAAA;EAC1CI,eAAerB,QAAQ,mBAAmB;IAAEiB,QAAQ;EAAG,CAAA;EACvDK,SAASrB,MAAM,SAAA,EAAWsB,MAAK,EAA4BR,QAAO,EAAGI,QAAQ,CAAC,CAAA;EAC9EK,MAAMxB,QAAQ,QAAQ;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO,EAAGI,QAAQ,KAAA;EACxDM,gBAAgB1B,KAAK,kBAAA;EACrB2B,kBAAkBxB,UAAU,sBAAsB;IAAEyB,cAAc;EAAK,CAAA;EACvEC,eAAe1B,UAAU,mBAAmB;IAAEyB,cAAc;EAAK,CAAA;EACjEE,iBAAiB3B,UAAU,sBAAsB;IAAEyB,cAAc;EAAK,CAAA;EACtEG,cAAc5B,UAAU,iBAAiB;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;EACrFC,WAAW9B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;EAC/EE,WAAW/B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AACjF,GACA,CAACrB,UAAU;;EAETH,YAAY,6BAAA,EAA+B2B,GAAGxB,MAAMI,WAAWJ,MAAMM,cAAc;EACnFV,MAAM,2BAAA,EAA6B4B,GAAGxB,MAAMI,WAAWJ,MAAMc,IAAI;;EAEjElB,MAAM,gCAAA,EAAkC4B,GAAGxB,MAAMI,WAAWJ,MAAMU,SAASV,MAAMW,aAAa;CAC/F;AAGI,IAAMc,WAAW3B,mBAAmBE,MACzC,YACA;EACEC,IAAIZ,KAAK,IAAA,EAAMa,WAAU,EAAGC,cAAa;EACzCC,WAAWf,KAAK,YAAA,EAAcgB,QAAO;EACrCqB,WAAWrC,KAAK,YAAA,EACbgB,QAAO,EACPsB,WAAW,MAAM5B,SAASE,IAAI;IAAE2B,UAAU;EAAU,CAAA;EACvDtB,gBAAgBhB,QAAQ,mBAAmB;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EAClEwB,WAAWvC,QAAQ,aAAa;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EACvDyB,QAAQxC,QAAQ,UAAU;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EACjD0B,aAAa1C,KAAK,eAAA;;EAElB2C;;;IAGE1C,QAAQ,QAAQ;MAAEiB,QAAQ;IAAG,CAAA,EAAGF,QAAO,EAAGI,QAAQ,MAAA;;EACpDwB,SAASxC,KAAK,SAAA;;;EAGdyC,SAAS3C,MAAM,SAAA,EAAWsB,MAAK;EAC/BsB,aAAa7C,QAAQ,iBAAiB;IAAEiB,QAAQ;EAAI,CAAA;EACpD6B,QAAQ9C,QAAQ,UAAU;IAAEiB,QAAQ;EAAG,CAAA;EACvC8B,QAAQ7C,UAAU,WAAW;IAAEyB,cAAc;EAAK,CAAA;;;;;EAKlDqB,mBAAmB3C,QAAQ,oBAAA;EAC3B4C,iBAAiBhD,MAAM,kBAAA,EAAoBsB,MAAK;;;;;;;;;;;EAWhD2B,qBAAqBlD,QAAQ,wBAAwB;IAAEiB,QAAQ;EAAG,CAAA,EAAGM,MAAK;EAC1E4B,mBAAmBhD,KAAK,oBAAA;EACxBiD,uBAAuBpD,QAAQ,0BAA0B;IAAEiB,QAAQ;EAAG,CAAA;;EAEtEoC,qBAAqBrD,QAAQ,wBAAwB;IAAEiB,QAAQ;EAAG,CAAA;EAClEe,WAAW9B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AACjF,GACA,CAACrB,UAAU;EACTJ,MAAM,8BAAA,EAAgC4B,GAAGxB,MAAM0B,WAAW1B,MAAMsB,SAAS;EACzE1B,MAAM,qCAAA,EAAuC4B,GAAGxB,MAAMI,WAAWJ,MAAMM,gBAAgBN,MAAMsB,SAAS;;;;;;EAMtGzB,YAAY,oCAAA,EACT2B,GAAGxB,MAAMI,WAAWJ,MAAMmC,WAAW,EACrCS,MAAMzD,MAAMa,MAAMmC,WAAW,cAAc;;;EAG9CvC,MAAM,iCAAA,EACH4B,GAAGxB,MAAMI,WAAWJ,MAAMsB,SAAS,EACnCsB,MAAMzD,MAAMa,MAAMsC,iBAAiB,EAAE;;;;EAIxC1C,MAAM,oCAAA,EACH4B,GAAGxB,MAAMI,WAAWJ,MAAMsB,SAAS,EACnCsB,MAAMzD,MAAMa,MAAMwC,mBAAmB,cAAc;CACvD;AAeI,IAAMK,YAAY/C,mBAAmBE,MAC1C,aACA;EACEC,IAAIZ,KAAK,IAAA,EAAMa,WAAU,EAAGC,cAAa;EACzCC,WAAWf,KAAK,YAAA,EAAcgB,QAAO;EACrCqB,WAAWrC,KAAK,YAAA,EACbgB,QAAO,EACPsB,WAAW,MAAM5B,SAASE,IAAI;IAAE2B,UAAU;EAAU,CAAA;;;EAGvDkB,WAAWzD,KAAK,YAAA,EAAcsC,WAAW,MAAMF,SAASxB,IAAI;IAAE2B,UAAU;EAAW,CAAA;EACnFmB,UAAUzD,QAAQ,aAAa;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACvD2C,UAAU1D,QAAQ,YAAY;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACtD4C,UAAU3D,QAAQ,aAAa;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACvD6C,WAAWxD,QAAQ,YAAA,EAAcW,QAAO;;;EAGxC8C,QAAQ7D,QAAQ,UAAU;IAAEiB,QAAQ;EAAG,CAAA;EACvC6C,QAAQ9D,QAAQ,UAAU;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EACjDgD,UAAU7D,UAAU,aAAa;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AAC/E,GACA,CAACrB,UAAU;EACTJ,MAAM,8BAAA,EAAgC4B,GAAGxB,MAAM0B,WAAW1B,MAAMqD,QAAQ;;EAExEzD,MAAM,8BAAA,EAAgC4B,GAAGxB,MAAMI,WAAWJ,MAAMqD,QAAQ;;;EAGxExD,YAAY,8BAAA,EAAgC2B,GAAGxB,MAAMI,WAAWJ,MAAM+C,QAAQ;CAC/E;AAMI,IAAMO,aAAaxD,mBAAmBE,MAC3C,eACA;EACEC,IAAIZ,KAAK,IAAA,EAAMa,WAAU,EAAGC,cAAa;EACzCC,WAAWf,KAAK,YAAA,EAAcgB,QAAO;EACrCkD,KAAKjE,QAAQ,OAAO;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EAC3CmD,OAAOlE,QAAQ,SAAS;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EAChDoD,aAAanE,QAAQ,iBAAiB;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EAC7DqD,OAAOnE,MAAM,OAAA,EAASsB,MAAK,EAA4BR,QAAO,EAAGI,QAAQ,CAAC,CAAA;;;EAG1EkD,SAASjE,QAAQ,SAAA,EAAWW,QAAO,EAAGI,QAAQ,CAAA;EAC9CmD,YAAYjE,QAAQ,cAAA,EAAgBU,QAAO,EAAGI,QAAQ,KAAA;EACtDoD,iBAAiBvE,QAAQ,qBAAqB;IAAEiB,QAAQ;EAAG,CAAA;EAC3De,WAAW9B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;EAC/EE,WAAW/B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AACjF,GACA,CAACrB,UAAU;EAACH,YAAY,6BAAA,EAA+B2B,GAAGxB,MAAMI,WAAWJ,MAAMuD,GAAG;CAAE;AAgBjF,IAAMO,YAAYhE,mBAAmBE,MAC1C,cACA;EACEC,IAAIZ,KAAK,IAAA,EAAMa,WAAU,EAAGC,cAAa;EACzCC,WAAWf,KAAK,YAAA,EAAcgB,QAAO;EACrCK,SAASpB,QAAQ,YAAY;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EACpD0D,QAAQzE,QAAQ,WAAW;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO;EAClD0C,UAAUzD,QAAQ,aAAa;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACvD2C,UAAU1D,QAAQ,YAAY;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACtD4C,UAAU3D,QAAQ,aAAa;IAAEiB,QAAQ;EAAI,CAAA,EAAGF,QAAO;EACvD6C,WAAWxD,QAAQ,YAAA,EAAcW,QAAO;;;EAGxC2D,SAASvE,KAAK,SAAA;;;EAGdwE,WAAWvE,QAAQ,YAAA,EAAcW,QAAO,EAAGI,QAAQ,CAAA;;;EAGnDyD,QAAQvE,QAAQ,QAAA,EAAUU,QAAO,EAAGI,QAAQ,IAAA;;;;;;;;EAQ5C0D,cAAc5E,MAAM,gBAAA,EAAkBsB,MAAK,EAA2BR,QAAO,EAAGI,QAAQ,CAAC,CAAA;EACzFa,WAAW9B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;EAC/EE,WAAW/B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AACjF,GACA,CAACrB,UAAU;EACTJ,MAAM,qBAAA,EAAuB4B,GAAGxB,MAAMI,WAAWJ,MAAMU,SAASV,MAAM+D,QAAQ/D,MAAMiE,SAAS;;;EAG7FpE,YAAY,4BAAA,EAA8B2B,GAAGxB,MAAMI,WAAWJ,MAAMU,SAASV,MAAM+D,QAAQ/D,MAAM+C,QAAQ;CAC1G;AAMI,IAAMqB,WAAWtE,mBAAmBE,MAAM,YAAY;EAC3DI,WAAWf,KAAK,YAAA,EAAca,WAAU;EACxCmE,cAAc/E,QAAQ,iBAAiB;IAAEiB,QAAQ;EAAI,CAAA;EACrD+D,kBAAkBhF,QAAQ,qBAAqB;IAAEiB,QAAQ;EAAG,CAAA,EAAGF,QAAO,EAAGI,QAAQ,OAAA;;EAEjF8D,mBAAmBhF,MAAM,oBAAA,EAAsBsB,MAAK,EAAaR,QAAO,EAAGI,QAAQ,CAAA,CAAE;EACrF+D,gBAAgB/E,KAAK,iBAAA;EACrBgF,iBAAiBhF,KAAK,kBAAA;;;;;;EAMtBiF,sBAAsB/E,QAAQ,uBAAA;EAC9BgF,mBAAmBrF,QAAQ,sBAAsB;IAAEiB,QAAQ;EAAG,CAAA,EAAGM,MAAK;EACtES,WAAW9B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;EAC/EE,WAAW/B,UAAU,cAAc;IAAEyB,cAAc;EAAK,CAAA,EAAGZ,QAAO,EAAGgB,WAAU;AACjF,CAAA;;;ADvPA,IAAMuD,gBAAgB;AAUf,IAAMC,gCAAgC;EAC3CC,aAAaC;4BACaC,QAAAA;2BACDC,QAAAA,kCAA0CA,QAAAA;;;EAGnEC,eAAeH;8BACaC,QAAAA;2BACHC,QAAAA,kCAA0CA,QAAAA;;;;;EAKnEE,QAAQJ;gCACsBC,QAAAA;2BACLC,QAAAA;2BACAA,QAAAA;;aAEdA,QAAAA,iDAAyDA,QAAAA;;AAEtE;AAKO,SAASG,oBAAoBC,OAA8B;AAChE,SAAON,OAAME,SAASK,OAAO,OAAOC,KAAKC,UAAUH,KAAAA,CAAAA;AACrD;AAFgBD;AAMT,IAAMK,oBAAN,MAAMA;EA1Db,OA0DaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;EAExD,MAAMC,WAAWC,WAAmBC,gBAAyD;AAC3F,UAAM,CAACC,GAAAA,IAAO,MAAM,KAAKJ,GACtBK,OAAM,EACNC,KAAKf,QAAAA,EACLgB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA,EACzEO,MAAM,CAAA;AACT,WAAON;EACT;EAEA,MAAMO,YAAYT,WAAmBC,gBAAwBS,YAA+C;AAC1G,UAAMC,WAAW,MAAM,KAAKZ,WAAWC,WAAWC,cAAAA;AAClD,QAAIU,SAAU,QAAOA;AAErB,UAAM,CAACC,OAAAA,IAAW,MAAM,KAAKd,GAC1Be,OAAOxB,QAAAA,EACPyB,OAAO;MAAEd;MAAWC;MAAgBc,cAAcL;IAAW,CAAA,EAC7DM,mBAAmB;MAClBC,QAAQ;QAAC5B,SAASW;QAAWX,SAASY;;MACtCiB,KAAK;QAAEC,cAAchC;MAAW;IAClC,CAAA,EACCiC,UAAS;AACZ,WAAOR;EACT;;;;EAKA,MAAMS,SACJrB,WACAC,gBACAqB,OACA5B,SACe;AACf,UAAM,KAAKI,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MACHH,cAAcO;MACd,GAAI5B,YAAY8B,SAAY;QAAE9B;MAAQ,IAAI,CAAC;MAC3CyB,cAAchC;MACdsC,WAAWtC;IACb,CAAA,EACCkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;;;;;EAMA,MAAMyB,aACJ1B,WACAC,gBACAR,OACe;AACf,UAAM,KAAKK,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MACHxB,SAASF,oBAAoBC,KAAAA;MAC7B0B,cAAchC;MACdsC,WAAWtC;IACb,CAAA,EACCkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;;;EAIA,MAAM0B,YACJ3B,WACAC,gBACsC;AACtC,UAAM,CAACC,GAAAA,IAAO,MAAM,KAAKJ,GACtBK,OAAO;MAAET,SAASL,SAASK;IAAQ,CAAA,EACnCU,KAAKf,QAAAA,EACLgB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA,EACzEO,MAAM,CAAA;AACT,WAAON,KAAKR;EACd;;;EAIA,MAAMkC,gBACJ5B,WACAC,gBACA4B,SACAC,eACe;AACf,UAAM,KAAKhC,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MAAEW;MAASC;MAAeX,cAAchC;MAAYsC,WAAWtC;IAAW,CAAA,EAC9EkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;;;;EAKA,MAAM8B,aAAa/B,WAAmBC,gBAAuC;AAC3E,UAAM,KAAKH,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MAAEc,eAAe7C;MAAYgC,cAAchC;MAAYsC,WAAWtC;IAAW,CAAA,EACjFkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;;;EAIA,MAAMgC,sBAAsBjC,WAAmBC,gBAAqD;AAClG,UAAMiC,UAAU,MAAM,KAAKnC,WAAWC,WAAWC,cAAAA;AACjD,QAAI,CAACiC,SAASF,cAAe,QAAOR;AACpC,YAAQW,KAAKC,IAAG,IAAKF,QAAQF,cAAcK,QAAO,MAAO,MAAO,KAAK;EACvE;;;;EAKA,MAAMC,QACJtC,WACAC,gBACAsC,MACAC,gBACAC,oBAAoB,OACL;AACf,UAAM,KAAK3C,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MACHqB;MACAC,gBAAgBA,kBAAkB;MAClC,GAAIC,oBAAoB;QAAEC,kBAAkB;MAAK,IAAI,CAAC;MACtDjB,WAAWtC;IACb,CAAA,EACCkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;;;;;;EAOA,MAAM0C,SAAS3C,WAAmBC,gBAAwB2C,aAAoC;AAC5F,UAAM,KAAKN,QAAQtC,WAAWC,gBAAgB,SAAS2C,aAAa,IAAA;EACtE;;;;;EAMA,MAAMC,QAAQ7C,WAAmBC,gBAAuC;AACtE,UAAM,KAAKqC,QAAQtC,WAAWC,gBAAgB,OAAO,MAAM,IAAA;EAC7D;;;;;;;;EASA,MAAM6C,eAAe9C,WAAmBC,gBAAuC;AAC7E,UAAM,KAAKH,GACRiD,OAAO1D,QAAAA,EACPgB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;EAEA,MAAM+C,aAAahD,WAAmBC,gBAAuC;AAC3E,UAAM,KAAKH,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MAAEwB,kBAAkBvD;MAAYsC,WAAWtC;IAAW,CAAA,EAC1DkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;EAEA,MAAMgD,SAASjD,WAAmBC,gBAAuC;AACvE,UAAM,KAAKH,GACRyB,OAAOlC,QAAAA,EACP6B,IAAI;MAAEgC,iBAAiB/D;IAAW,CAAA,EAClCkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASY,gBAAgBA,cAAAA,CAAAA,CAAAA;EAC9E;EAEA,MAAMkD,YAAYnD,WAAmBoD,QAAiC;AACpE,UAAMC,SAAS,MAAM,KAAKvD,GACvByB,OAAOlC,QAAAA,EACP6B,IAAI;MAAEgC,iBAAiB/D;IAAW,CAAA,EAClCkB,MAAMC,IAAIC,GAAGlB,SAASW,WAAWA,SAAAA,GAAYO,GAAGlB,SAASmD,gBAAgBY,MAAAA,CAAAA,CAAAA;AAC5E,WAAOE,OAAQD,OAA4CE,YAAY,CAAA;EACzE;EAEA,MAAMC,qBAAqBxD,WAAmByD,SAAmE;AAC/G,UAAMjD,QAAQiD,QAAQjD,SAASxB;AAC/B,UAAM0E,WAAWD,QAAQE,QAAQ,KAAK,KAAKnD;AAE3C,UAAMoD,aAAa;MAACrD,GAAGlB,SAASW,WAAWA,SAAAA;;AAC3C,QAAIyD,QAAQI,aAAcD,YAAWE,KAAK3E,OAAME,SAASqD,gBAAgB,cAAc;AACvF,QAAIe,QAAQM,OAAQH,YAAWE,KAAK3E,OAAME,SAASY,cAAc,UAAU,MAAMwD,QAAQM,SAAS,GAAA,EAAK;AAEvG,UAAMC,OAAO,MAAM,KAAKlE,GACrBK,OAAO;MACN8D,IAAI5E,SAAS4E;MACbhE,gBAAgBZ,SAASY;MACzBsC,MAAMlD,SAASkD;MACfC,gBAAgBnD,SAASmD;MACzBzB,cAAc1B,SAAS0B;MACvBI,cAAc9B,SAAS8B;MACvBa,eAAe3C,SAAS2C;MACxBU,kBAAkBrD,SAASqD;;;;;MAK3B,GAAGzD;IACL,CAAA,EACCmB,KAAKf,QAAAA,EACLgB,MAAMC,IAAAA,GAAOsD,UAAAA,CAAAA,EACbM,QAAQC,KAAK9E,SAAS8B,YAAY,CAAA,EAClCX,MAAMA,KAAAA,EACNkD,OAAOA,MAAAA;AAEV,WAAOM,KAAKI,IAAI,CAAClE,SAAS;MACxB+D,IAAI/D,IAAI+D;MACRhE,gBAAgBC,IAAID;MACpB,GAAIC,IAAIhB,gBAAgB,OAAO;QAAEA,aAAagB,IAAIhB;MAAY,IAAI,CAAC;MACnE,GAAIgB,IAAIZ,kBAAkB,OAAO;QAAEA,eAAeY,IAAIZ;MAAkC,IAAI,CAAC;MAC7F+E,QAAQnE,IAAIiB,aAAamD,YAAW;MACpCtC,eAAe9B,IAAI8B,eAAesC,YAAAA,KAAiB;MACnD/B,MAAMrC,IAAIqC;MACVC,gBAAgBtC,IAAIsC;MACpBqB,cAAc3D,IAAIwC,qBAAqB;MACvCnD,QAAQ+D,OAAOpD,IAAIX,MAAM;MACzBwB,cAAcb,IAAIa;IACpB,EAAA;EACF;;EAGA,MAAMwD,mBAAmBvE,WAAmBC,gBAAwB;AAClE,UAAMiC,UAAU,MAAM,KAAKnC,WAAWC,WAAWC,cAAAA;AACjD,QAAI,CAACiC,QAAS,QAAO;AAErB,UAAM8B,OAAO,MAAM,KAAKlE,GACrBK,OAAM,EACNC,KAAKhB,QAAAA,EACLiB,MAAMC,IAAIC,GAAGnB,SAASY,WAAWA,SAAAA,GAAYO,GAAGnB,SAASoF,WAAWtC,QAAQ+B,EAAE,CAAA,CAAA,EAC9EC,QAAQ9E,SAASqF,SAAS;AAE7B,WAAO;MAAEvC;MAAS9C,UAAU4E;IAAK;EACnC;AACF;;;AE7SA,SAASU,OAAAA,MAAKC,MAAAA,KAAIC,UAAU;AAiD5B,IAAMC,iBAAgB;AAEf,IAAMC,oBAAN,MAAMA;EAnDb,OAmDaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;;;;;;EAOxD,MAAMC,cAAcC,QAA8D;AAChF,UAAMC,SAAwB;MAC5BC,WAAWF,OAAOE;MAClBC,WAAWH,OAAOG;MAClBC,gBAAgBJ,OAAOI;MACvBC,WAAWL,OAAOK;MAClBC,QAAQN,OAAOM;MACfC,aAAaP,OAAOO,eAAe;MACnCC,MAAMR,OAAOQ;MACbC,SAAST,OAAOS,WAAW;MAC3BC,SAASV,OAAOU,WAAW;MAC3BC,aAAaX,OAAOW,eAAe;MACnCC,QAAQZ,OAAOY,UAAU;MACzBC,mBAAmBb,OAAOa,qBAAqB;MAC/CC,iBAAiBd,OAAOc,mBAAmB;IAC7C;AAEA,UAAM,CAACC,OAAAA,IAAW,MAAM,KAAKjB,GAAGkB,OAAOC,QAAAA,EAAUhB,OAAOA,MAAAA,EAAQiB,oBAAmB,EAAGC,UAAS;AAC/F,WAAOJ;EACT;EAEA,MAAMK,oBACJlB,WACAS,aACAC,QACiC;AACjC,UAAM,CAACS,OAAAA,IAAW,MAAM,KAAKvB,GAC1BwB,OAAOL,QAAAA,EACPM,IAAI;MAAEX;MAAQ,GAAIA,WAAW,SAAS;QAAEY,QAAQ,oBAAIC,KAAAA;MAAO,IAAI,CAAC;IAAG,CAAA,EACnEC,MAAMC,KAAIC,IAAGX,SAASf,WAAWA,SAAAA,GAAY0B,IAAGX,SAASN,aAAaA,WAAAA,CAAAA,CAAAA,EACtEQ,UAAS;AACZ,WAAOE;EACT;;;;;;;;;;;EAYA,MAAMQ,+BAA+B7B,QAA+E;AAClH,UAAM,CAACqB,OAAAA,IAAW,MAAM,KAAKvB,GAC1BwB,OAAOL,QAAAA,EACPM,IAAI;MACHO,qBAAqB9B,OAAOY;MAC5B,GAAIZ,OAAO+B,SAASC,SAAY;QAAEC,mBAAmBjC,OAAO+B;MAAK,IAAI,CAAC;MACtE,GAAI/B,OAAOkC,aAAaF,SAAY;QAAEG,uBAAuBnC,OAAOkC;MAAS,IAAI,CAAC;MAClF,GAAIlC,OAAOoC,WAAWJ,SAAY;QAAEK,qBAAqBrC,OAAOoC;MAAO,IAAI,CAAC;IAC9E,CAAA,EACCV,MAAMC,KAAIC,IAAGX,SAASf,WAAWF,OAAOE,SAAS,GAAG0B,IAAGX,SAASN,aAAaX,OAAOW,WAAW,CAAA,CAAA,EAC/FQ,UAAS;AACZ,WAAOE;EACT;EAEA,MAAMiB,SAASpC,WAAmBqC,WAAoD;AACpF,UAAM,CAACC,KAAAA,IAAS,MAAM,KAAK1C,GACxB2C,OAAM,EACNC,KAAKzB,QAAAA,EACLS,MAAMC,KAAIC,IAAGX,SAASf,WAAWA,SAAAA,GAAY0B,IAAGX,SAAS0B,IAAIJ,SAAAA,CAAAA,CAAAA,EAC7DK,MAAM,CAAA;AACT,WAAOJ;EACT;;;;;;;;;EAUA,MAAMK,kBAAkB7C,QAAkE;AACxF,UAAM,CAACqB,OAAAA,IAAW,MAAM,KAAKvB,GAC1BwB,OAAOL,QAAAA,EACPM,IAAI;MACHO,qBAAqB9B,OAAOY;MAC5B,GAAIZ,OAAO+B,SAASC,SAAY;QAAEC,mBAAmBjC,OAAO+B;MAAK,IAAI,CAAC;MACtE,GAAI/B,OAAOkC,aAAaF,SAAY;QAAEG,uBAAuBnC,OAAOkC;MAAS,IAAI,CAAC;MAClF,GAAIlC,OAAOoC,WAAWJ,SAAY;QAAEK,qBAAqBrC,OAAOoC;MAAO,IAAI,CAAC;IAC9E,CAAA,EACCV,MAAMC,KAAIC,IAAGX,SAASf,WAAWF,OAAOE,SAAS,GAAG0B,IAAGX,SAAS0B,IAAI3C,OAAOuC,SAAS,CAAA,CAAA,EACpFpB,UAAS;AACZ,WAAOE;EACT;EAEA,MAAMyB,mBAAmB9C,QAAmD;AAC1E,UAAM+C,aAAa;MAACnB,IAAGX,SAASf,WAAWF,OAAOE,SAAS;MAAG0B,IAAGX,SAASd,WAAWH,OAAOG,SAAS;;AACrG,QAAIH,OAAOgD,OAAQD,YAAWE,KAAKC,GAAGjC,SAASkC,WAAW,IAAI1B,KAAKzB,OAAOgD,MAAM,CAAA,CAAA;AAEhF,WAAO,KAAKlD,GACT2C,OAAM,EACNC,KAAKzB,QAAAA,EACLS,MAAMC,KAAAA,GAAOoB,UAAAA,CAAAA,EACbK,QAAQnC,SAASkC,SAAS,EAC1BP,MAAM5C,OAAO4C,SAAShD,cAAAA;EAC3B;AACF;;;AC/JA,SAASyD,OAAAA,MAAKC,MAAAA,KAAIC,WAAWC,OAAAA,YAAW;AAExC,SAASC,4BAA4B;AAU9B,IAAMC,wBAAN,cAAoCC,MAAAA;EAZ3C,OAY2CA;;;;EACzC,YACEC,KACgBC,mBAChB;AACA,UAAM,mBAAmBD,GAAAA,kCAAkCC,iBAAAA,EAAmB,GAAA,KAF9DA,oBAAAA;AAGhB,SAAKC,OAAO;EACd;AACF;AAKA,SAASC,gBAAgBC,KAAiB;AACxC,QAAMC,SAASC,qBAAqBC,UAAUH,IAAII,KAAK;AACvD,MAAI,CAACH,OAAOI,QAAS,OAAM,IAAIX,sBAAsBM,IAAIJ,KAAKK,OAAOK,MAAMC,OAAO;AAElF,SAAO;IACLX,KAAKI,IAAIJ;IACTY,OAAOR,IAAIQ;IACXC,aAAaT,IAAIS;IACjBC,SAASV,IAAIU;IACbN,OAAOH,OAAOU;EAChB;AACF;AAXSZ;AAaF,IAAMa,sBAAN,cAAkCjB,MAAAA;EAtCzC,OAsCyCA;;;EACvC,YAAYC,KAAa;AACvB,UAAM,UAAUA,GAAAA,uEAAkE;AAClF,SAAKE,OAAO;EACd;AACF;AAIO,IAAMe,sBAAN,MAAMA;EA/Cb,OA+CaA;;;;;;;EAGX,YACmBC,IACAC,OACjB;SAFiBD,KAAAA;SACAC,QAAAA;EAChB;EAEH,MAAMC,IAAIC,WAAmBrB,KAAiD;AAC5E,UAAMsB,SAAS,MAAM,KAAKH,OAAOI,KAAKF,WAAWrB,GAAAA;AACjD,QAAIsB,OAAQ,QAAOA;AAEnB,UAAM,CAAClB,GAAAA,IAAO,MAAM,KAAKc,GACtBM,OAAM,EACNC,KAAKC,UAAAA,EACLC,MAAMC,KAAIC,IAAGH,WAAWL,WAAWA,SAAAA,GAAYQ,IAAGH,WAAW1B,KAAKA,GAAAA,CAAAA,CAAAA,EAClE8B,MAAM,CAAA;AACT,QAAI,CAAC1B,IAAK,QAAO2B;AAEjB,UAAMC,QAAQ7B,gBAAgBC,GAAAA;AAC9B,UAAM,KAAKe,OAAOc,MAAMZ,WAAWW,KAAAA;AACnC,WAAOA;EACT;EAEA,MAAME,KAAKb,WAAgD;AACzD,UAAMc,OAAO,MAAM,KAAKjB,GAAGM,OAAM,EAAGC,KAAKC,UAAAA,EAAYC,MAAME,IAAGH,WAAWL,WAAWA,SAAAA,CAAAA;AACpF,WAAOc,KAAKC,IAAI,CAAChC,SAAS;MACxBJ,KAAKI,IAAIJ;MACTY,OAAOR,IAAIQ;MACXyB,WAAWC,OAAOC,KAAKnC,IAAII,KAAK,EAA6BgC;MAC7DC,YAAYrC,IAAIqC;MAChBC,iBAAiBtC,IAAIsC,mBAAmBX;MACxCY,WAAWvC,IAAIuC,UAAUC,YAAW;IACtC,EAAA;EACF;;;EAIQC,iBAAiB7C,KAAaQ,OAAsB;AAC1D,UAAMH,SAASC,qBAAqBC,UAAUC,KAAAA;AAC9C,QAAI,CAACH,OAAOI,QAAS,OAAM,IAAIX,sBAAsBE,KAAKK,OAAOK,MAAMC,OAAO;EAChF;EAEA,MAAMmC,OACJzB,WACAW,OACwB;AACxB,SAAKa,iBAAiBb,MAAMhC,KAAKgC,MAAMxB,KAAK;AAE5C,UAAM,CAACuC,OAAAA,IAAW,MAAM,KAAK7B,GAC1B8B,OAAOtB,UAAAA,EACPuB,OAAO;MACN5B;MACArB,KAAKgC,MAAMhC;MACXY,OAAOoB,MAAMpB;MACbC,aAAamB,MAAMnB;MACnBL,OAAOwB,MAAMxB;MACbiC,YAAYT,MAAMS,cAAc;MAChCC,iBAAiBV,MAAMU;IACzB,CAAA,EACCQ,UAAS;AAEZ,UAAMC,eAAehD,gBAAgB4C,OAAAA;AACrC,UAAM,KAAK5B,OAAOiC,WAAW/B,WAAW8B,aAAanD,GAAG;AACxD,WAAOmD;EACT;;;EAIA,MAAME,KAAKhC,WAAmBW,OAAsBsB,iBAAiD;AACnG,SAAKT,iBAAiBb,MAAMhC,KAAKgC,MAAMxB,KAAK;AAE5C,UAAM2B,OAAO,MAAM,KAAKjB,GACrBqC,OAAO7B,UAAAA,EACP8B,IAAI;MACH5C,OAAOoB,MAAMpB;MACbC,aAAamB,MAAMnB;MACnBL,OAAOwB,MAAMxB;MACbM,SAASwC,kBAAkB;MAC3BX,WAAW,oBAAIc,KAAAA;IACjB,CAAA,EACC9B,MACCC,KACEC,IAAGH,WAAWL,WAAWA,SAAAA,GACzBQ,IAAGH,WAAW1B,KAAKgC,MAAMhC,GAAG,GAC5B6B,IAAGH,WAAWZ,SAASwC,eAAAA,CAAAA,CAAAA,EAG1BJ,UAAS;AAEZ,QAAIf,KAAKK,WAAW,EAAG,OAAM,IAAIxB,oBAAoBgB,MAAMhC,GAAG;AAK9D,UAAM,KAAKmB,OAAOiC,WAAW/B,WAAWW,MAAMhC,GAAG;AACjD,WAAOG,gBAAgBgC,KAAK,CAAA,CAAE;EAChC;EAEA,MAAMuB,OAAOrC,WAAmBrB,KAA4B;AAC1D,UAAM,KAAKkB,GAAGwC,OAAOhC,UAAAA,EAAYC,MAAMC,KAAIC,IAAGH,WAAWL,WAAWA,SAAAA,GAAYQ,IAAGH,WAAW1B,KAAKA,GAAAA,CAAAA,CAAAA;AACnG,UAAM,KAAKmB,OAAOiC,WAAW/B,WAAWrB,GAAAA;EAC1C;;;;;EAMA,MAAM2D,qBAAqBtC,WAAgD;AACzE,UAAMc,OAAO,MAAM,KAAKjB,GACrBM,OAAO;MACNoC,SAASC,SAASD;MAClBE,QAAQD,SAASE;MACjBC,OAAOC;IACT,CAAA,EACCxC,KAAKoC,QAAAA,EACLlC,MACCC,KACEC,IAAGgC,SAASxC,WAAWA,SAAAA,GACvBQ,IAAGgC,SAASK,MAAM,KAAA,GAClBC,UAAUN,SAASD,OAAO,GAC1BO,UAAUN,SAASE,aAAa,CAAA,CAAA,EAGnCK,QAAQP,SAASD,SAASC,SAASE,aAAa;AAEnD,WAAO5B,KAAKC,IAAI,CAAChC,SAAS;MAAEwD,SAASxD,IAAIwD;MAAUE,QAAQ1D,IAAI0D;MAASE,OAAO5D,IAAI4D;IAAM,EAAA;EAC3F;AACF;;;AC3KA,IAAMK,aAAa;AAEZ,IAAMC,uCAAuC;AAc7C,IAAMC,iBAAN,MAAMA;EAlBb,OAkBaA;;;;;EACX,YACmBC,UACAC,aAAqBH,sCACtC;SAFiBE,WAAAA;SACAC,aAAAA;EAChB;;;;EAKKC,OAAOC,WAAmBC,SAAyB;AACzD,WAAO,GAAGP,UAAAA,IAAcM,SAAAA,IAAaC,OAAAA;EACvC;EAEA,MAAMC,KAAKF,WAAmBC,SAAqD;AACjF,QAAI;AACF,YAAME,SAAS,MAAM,KAAKN,SAASO,IAAI,KAAKL,OAAOC,WAAWC,OAAAA,CAAAA;AAC9D,UAAI,CAACE,OAAQ,QAAOE;AACpB,aAAOC,KAAKC,MAAMJ,MAAAA;IACpB,QAAQ;AAEN,aAAOE;IACT;EACF;EAEA,MAAMG,MAAMR,WAAmBS,OAAqC;AAClE,QAAI;AACF,YAAM,KAAKZ,SAASa,IAAI,KAAKX,OAAOC,WAAWS,MAAME,GAAG,GAAGL,KAAKM,UAAUH,KAAAA,GAAQ,KAAKX,UAAU;IACnG,QAAQ;IAER;EACF;EAEA,MAAMe,WAAWb,WAAmBC,SAAgC;AAClE,QAAI;AACF,YAAM,KAAKJ,SAASiB,OAAO,KAAKf,OAAOC,WAAWC,OAAAA,CAAAA;IACpD,QAAQ;IAGR;EACF;AACF;;;AC5DA,SAASc,MAAAA,WAAU;AAKnB,SAASC,mBAAmBC,KAAgB;AAC1C,SAAO;IACLC,cAAcD,IAAIC,gBAAgB;IAClCC,kBAAkBF,IAAIE;IACtBC,mBAAmBH,IAAIG;IACvBC,gBAAgBJ,IAAII,kBAAkB;IACtCC,iBAAiBL,IAAIK,mBAAmB;;;IAGxCC,sBAAsBN,IAAIM,wBAAwB;IAClDC,mBAAmBP,IAAIO,qBAAqB;EAC9C;AACF;AAZSR;AAcT,IAAMS,iBAAmC;EACvCP,cAAc;EACdC,kBAAkB;EAClBC,mBAAmB,CAAA;EACnBC,gBAAgB;EAChBC,iBAAiB;EACjBC,sBAAsB;EACtBC,mBAAmB;AACrB;AAGO,IAAME,qBAAN,MAAMA;EA9Bb,OA8BaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;;;;EAKxD,MAAMC,IAAIC,WAA8C;AACtD,UAAM,CAACZ,GAAAA,IAAO,MAAM,KAAKU,GAAGG,OAAM,EAAGC,KAAKC,QAAAA,EAAUC,MAAMC,IAAGF,SAASH,WAAWA,SAAAA,CAAAA,EAAYM,MAAM,CAAA;AACnG,WAAOlB,MAAMD,mBAAmBC,GAAAA,IAAOQ;EACzC;EAEA,MAAMW,KAAKP,WAAmBQ,QAA8D;AAC1F,UAAMC,UAAU,MAAM,KAAKV,IAAIC,SAAAA;AAC/B,UAAMU,SAAS;MAAE,GAAGD;MAAS,GAAGD;IAAO;AAEvC,UAAM,CAACpB,GAAAA,IAAO,MAAM,KAAKU,GACtBa,OAAOR,QAAAA,EACPS,OAAO;MACNZ;MACAX,cAAcqB,OAAOrB,gBAAgB;MACrCC,kBAAkBoB,OAAOpB;MACzBC,mBAAmBmB,OAAOnB;MAC1BC,gBAAgBkB,OAAOlB,kBAAkB;MACzCC,iBAAiBiB,OAAOjB,mBAAmB;;;;MAI3CC,sBAAsBgB,OAAOhB;MAC7BC,mBAAmBe,OAAOf;IAC5B,CAAA,EACCkB,mBAAmB;MAClBC,QAAQX,SAASH;MACjBe,KAAK;QACH1B,cAAcqB,OAAOrB,gBAAgB;QACrCC,kBAAkBoB,OAAOpB;QACzBC,mBAAmBmB,OAAOnB;QAC1BC,gBAAgBkB,OAAOlB,kBAAkB;QACzCC,iBAAiBiB,OAAOjB,mBAAmB;QAC3CC,sBAAsBgB,OAAOhB;QAC7BC,mBAAmBe,OAAOf;QAC1BqB,WAAW,oBAAIC,KAAAA;MACjB;IACF,CAAA,EACCC,UAAS;AAEZ,WAAO/B,mBAAmBC,GAAAA;EAC5B;;;;EAKA,MAAM+B,yBAAyBnB,WAAmBoB,SAAqD;AACrG,UAAM,EAAE7B,kBAAiB,IAAK,MAAM,KAAKQ,IAAIC,SAAAA;AAC7C,WAAOT,kBAAkB8B,IAAI,CAACC,UAAAA;AAC5B,YAAMC,MAAMD,MAAME,QAAQ,YAAY,EAAA;AACtC,YAAMC,QAAQL,QAAQG,GAAAA;AACtB,aAAOE,UAAUC,UAAaD,UAAU,OAAO,KAAKE,OAAOF,KAAAA;IAC7D,CAAA;EACF;AACF;;;AClEO,IAAMG,oBAAN,MAAMA;EAHb,OAGaA;;;;;;;EACX,YACmBC,mBACAC,mBACAC,UACAC,WACjB;SAJiBH,oBAAAA;SACAC,oBAAAA;SACAC,WAAAA;SACAC,YAAAA;EAChB;EAEH,MAAMC,QAAQC,QAA2D;AACvE,UAAMC,UAAU,MAAM,KAAKN,kBAAkBO,YAAYF,OAAOG,WAAWH,OAAOI,gBAAgBJ,OAAOK,UAAU;AAEnH,UAAMC,QAAQ,MAAM,KAAKV,kBAAkBW,cAAc;MACvD,GAAGP;MACHQ,WAAWP,QAAQQ;MACnB,GAAG,KAAKC,aAAaV,MAAAA;IACvB,CAAA;AAEA,QAAI,CAACM,MAAO,QAAOK;AAEnB,QAAIX,OAAOY,cAAc,WAAW;AAClC,YAAM,KAAKjB,kBAAkBkB,aAAab,OAAOG,WAAWH,OAAOI,cAAc;IACnF;AAEA,SAAKP,UAAUiB,KAAK,QAAQd,OAAOI,cAAc,IAAI,WAAW;MAC9DQ,WAAWZ,OAAOY;MAClBG,QAAQf,OAAOe;IACjB,CAAA;AACA,SAAKlB,UAAUiB,KAAK,UAAU,gBAAgB,CAAC,CAAA;AAE/C,WAAOR;EACT;;;EAIQI,aAAaV,QAA8F;AACjH,QAAI,CAAC,KAAKF,aAAaE,OAAOY,cAAc,UAAW,QAAO,CAAC;AAE/D,UAAMI,QAAOhB,OAAOiB,SAASC,KAAAA;AAC7B,QAAI,CAACF,MAAM,QAAO,CAAC;AAEnB,UAAMG,UAAU,KAAKrB,UAAUsB,QAAQJ,KAAAA;AAEvC,WAAO;MACLK,mBAAmBF,QAAQG;MAC3BC,iBAAiBJ,QAAQG,cAAc;WAAIH,QAAQK;UAAgB;IACrE;EACF;AACF;;;ACtEA,SACEC,0BAIK;AAMP,IAAMC,wBAAwB;AAwCvB,IAAMC,qBAAN,MAAMA;EAnDb,OAmDaA;;;;;;;;EACX,YACmBC,SACAC,mBACAC,YACAC,eAGAC,oBACjB;SAPiBJ,UAAAA;SACAC,oBAAAA;SACAC,aAAAA;SACAC,gBAAAA;SAGAC,qBAAAA;EAChB;EAEH,MAAcC,mBAAmBC,WAAmBC,gBAAuC;AACzF,UAAMC,QAAQ,MAAM,KAAKP,kBAAkBQ,sBAAsBH,WAAWC,cAAAA;AAE5E,QAAIC,UAAUE,OAAW,OAAM,IAAIC,mBAAAA;AACnC,QAAIH,SAASV,sBAAuB,OAAM,IAAIa,mBAAmBH,KAAAA;EACnE;EAEA,MAAMI,SAASC,QAAyD;AACtE,UAAM,KAAKR,mBAAmBQ,OAAOP,WAAWO,OAAON,cAAc;AAErE,UAAM,EAAEO,kBAAiB,IAAK,MAAM,KAAKd,QAAQY,SAASC,OAAON,gBAAgBM,OAAOE,IAAI;AAE5F,WAAO,KAAKb,WAAWc,QAAQ;MAC7BV,WAAWO,OAAOP;MAClBC,gBAAgBM,OAAON;MACvBU,WAAW;MACXC,QAAQL,OAAOK;MACfC,aAAaN,OAAOM,eAAe;MACnCC,MAAM;MACNC,SAASR,OAAOE;MAChBO,aAAaR;MACbS,QAAQ;MACRC,YAAYX,OAAOW;IACrB,CAAA;EACF;EAEA,MAAMC,UAAUZ,QAA0D;AACxE,UAAM,KAAKR,mBAAmBQ,OAAOP,WAAWO,OAAON,cAAc;AAErE,UAAM,EAAEO,kBAAiB,IAAK,MAAM,KAAKd,QAAQyB,UAAU;MACzDC,IAAIb,OAAON;MACXoB,QAAQd,OAAOc;MACfC,UAAUf,OAAOe;MACjBC,UAAUhB,OAAOgB;MACjBC,SAASjB,OAAOiB;IAClB,CAAA;AAKA,UAAMC,WAAW,KAAK5B,iBAEhB,MAAM,KAAKA,cAAc6B,OAAO;MAC9BL,QAAQd,OAAOc;MACfC,UAAUf,OAAOe;MACjBK,KAAK,iBAAiBpB,OAAOP,SAAS,IAAI4B,KAAKC,IAAG,CAAA,IAAMtB,OAAOgB,QAAQ;IACzE,CAAA,GACAE,WACFrB;AAEJ,UAAM0B,QAAQ,MAAM,KAAKlC,WAAWc,QAAQ;MAC1CV,WAAWO,OAAOP;MAClBC,gBAAgBM,OAAON;MACvBU,WAAW;MACXC,QAAQL,OAAOK;MACfC,aAAaN,OAAOM,eAAe;MACnCC,MAAMiB,aAAaxB,OAAOe,QAAQ;MAClCP,SAASR,OAAOiB,WAAWjB,OAAOgB;MAClCS,SAAS;QAAET,UAAUhB,OAAOgB;QAAUD,UAAUf,OAAOe;QAAU,GAAIG,WAAW;UAAEA;QAAS,IAAI,CAAC;MAAG;MACnGT,aAAaR;MACbS,QAAQ;MACRC,YAAYX,OAAOW;IACrB,CAAA;AAQA,QAAIY,SAASL,UAAU;AACrB,YAAM,KAAK3B,oBAAoBmC,KAAK;QAClCjC,WAAWO,OAAOP;QAClBkC,WAAWJ,MAAMI;QACjBC,WAAWL,MAAMM;QACjBX;QACAF,UAAUhB,OAAOgB;QACjBD,UAAUf,OAAOe;QACjBe,WAAW9B,OAAOc,OAAOiB;QACzBC,QAAQhC,OAAOK;MACjB,CAAA;IACF;AAEA,WAAOkB;EACT;;;EAIA,MAAMU,aAAajC,QAA6D;AAC9E,UAAM,EAAEC,kBAAiB,IAAK,MAAM,KAAKd,QAAQ8C,aAAa;MAC5DpB,IAAIb,OAAON;MACXwC,cAAclC,OAAOkC;MACrBC,cAAcnC,OAAOmC,gBAAgB;MACrCC,gBAAgBpC,OAAOoC;IACzB,CAAA;AAEA,WAAO,KAAK/C,WAAWc,QAAQ;MAC7BV,WAAWO,OAAOP;MAClBC,gBAAgBM,OAAON;MACvBU,WAAW;MACXC,QAAQL,OAAOK;MACfC,aAAaN,OAAOM,eAAe;MACnCC,MAAM;MACNC,SAASR,OAAOkC;MAChBT,SAAS;QAAES,cAAclC,OAAOkC;QAAcE,gBAAgBpC,OAAOoC,kBAAkB,CAAA;MAAG;MAC1F3B,aAAaR;MACbS,QAAQ;MACRC,YAAYX,OAAOW;IACrB,CAAA;EACF;AACF;AAEA,SAASa,aAAaT,UAAgB;AACpC,MAAIA,SAASsB,WAAW,QAAA,EAAW,QAAO;AAC1C,MAAItB,SAASsB,WAAW,QAAA,EAAW,QAAO;AAC1C,MAAItB,SAASsB,WAAW,QAAA,EAAW,QAAO;AAC1C,SAAO;AACT;AALSb;;;ACnKF,IAAMc,8BAAN,MAAMA;EAFb,OAEaA;;;;;EACX,YACmBC,mBACAC,UACjB;SAFiBD,oBAAAA;SACAC,WAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAmD;AAC/D,UAAM,KAAKH,kBAAkBI,SAASD,OAAOE,WAAWF,OAAOG,gBAAgBH,OAAOI,WAAW;AACjG,SAAKN,UAAUO,KAAK,QAAQL,OAAOG,cAAc,IAAI,gBAAgB;MACnEG,MAAM;MACNC,gBAAgBP,OAAOI;IACzB,CAAA;AACA,SAAKN,UAAUO,KAAK,UAAU,gBAAgB,CAAC,CAAA;EACjD;AACF;;;AChBO,IAAMG,6BAAN,MAAMA;EADb,OACaA;;;;;EACX,YACmBC,mBACAC,UACjB;SAFiBD,oBAAAA;SACAC,WAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAkD;AAC9D,UAAM,KAAKH,kBAAkBI,QAAQD,OAAOE,WAAWF,OAAOG,cAAc;AAC5E,SAAKL,UAAUM,KAAK,QAAQJ,OAAOG,cAAc,IAAI,gBAAgB;MAAEE,MAAM;MAAOC,gBAAgB;IAAK,CAAA;AACzG,SAAKR,UAAUM,KAAK,UAAU,gBAAgB,CAAC,CAAA;EACjD;AACF;;;ACXO,IAAMG,2BAAN,MAAMA;EADb,OACaA;;;;EACX,YAA6BC,mBAAsC;SAAtCA,oBAAAA;EAAuC;EAEpE,MAAMC,QAAQC,QAAiE;AAC7E,WAAO,KAAKF,kBAAkBG,qBAAqBD,OAAOE,WAAWF,OAAOG,WAAW,CAAC,CAAA;EAC1F;AACF;;;ACbA,SAASC,4BAA4B;AAY9B,IAAMC,sBAAN,MAAMA;EAZb,OAYaA;;;;;EACX,YACmBC,mBACAC,mBACjB;SAFiBD,oBAAAA;SACAC,oBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAmD;AAC/D,UAAMC,UAAU,MAAM,KAAKJ,kBAAkBK,WAAWF,OAAOG,WAAWH,OAAOI,cAAc;AAC/F,QAAI,CAACH,QAAS,OAAM,IAAII,qBAAqBL,OAAOI,cAAc;AAElE,UAAME,aAAqC;MACzCH,WAAWH,OAAOG;MAClBI,WAAWN,QAAQO;MACnBC,OAAOT,OAAOS;MACdC,QAAQV,OAAOU;IACjB;AACA,WAAO,KAAKZ,kBAAkBa,mBAAmBL,UAAAA;EACnD;AACF;;;AChCA,SAASM,wBAAAA,6BAA4B;AAyC9B,IAAMC,mCAAN,MAAMA;EAzCb,OAyCaA;;;;;EACX,YACmBC,mBACAC,oBACjB;SAFiBD,oBAAAA;SACAC,qBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAA6E;AACzF,UAAMC,UAAU,MAAM,KAAKJ,kBAAkBK,WAAWF,OAAOG,WAAWH,OAAOI,cAAc;AAC/F,QAAI,CAACH,QAAS,OAAM,IAAII,sBAAqBL,OAAOI,cAAc;AAElE,UAAM,EAAEE,MAAMC,MAAK,IAAK,MAAM,KAAKT,mBAAmBU,mBAAmB;MACvEL,WAAWH,OAAOG;MAClBM,WAAWR,QAAQS;MACnB,GAAIV,OAAOW,SAAS;QAAEA,QAAQX,OAAOW;MAAO,IAAI,CAAC;MACjD,GAAIX,OAAOY,WAAWZ,OAAOY,QAAQC,SAAS,IAAI;QAAED,SAASZ,OAAOY;MAAQ,IAAI,CAAC;MACjF,GAAIZ,OAAOc,gBAAgB;QAAEA,eAAed,OAAOc;MAAc,IAAI,CAAC;MACtE,GAAId,OAAOe,OAAO;QAAEA,MAAMf,OAAOe;MAAK,IAAI,CAAC;MAC3C,GAAIf,OAAOgB,QAAQ;QAAEA,OAAOhB,OAAOgB;MAAM,IAAI,CAAC;IAChD,CAAA;AAEA,WAAO;MACLC,WAAWX,KAAKY,IAAI,CAACC,SAAS;QAC5BT,IAAIS,IAAIC;QACRC,UAAUF,IAAIE;QACdC,UAAUH,IAAIG;QACdC,WAAWJ,IAAII;QACfC,QAAQL,IAAIK;QACZC,UAAUN,IAAIM,SAASC,YAAW;MACpC,EAAA;MACAnB;IACF;EACF;AACF;;;ACjCO,IAAMoB,8BAAN,MAAMA;EARb,OAQaA;;;;EACX,YAA6BC,oBAAwC;SAAxCA,qBAAAA;EAAyC;EAEtE,MAAMC,QAAQC,QAAmE;AAC/E,UAAM,EAAEC,MAAMC,MAAK,IAAK,MAAM,KAAKJ,mBAAmBK,cAAc;MAClEC,WAAWJ,OAAOI;MAClB,GAAIJ,OAAOK,SAAS;QAAEA,QAAQL,OAAOK;MAAO,IAAI,CAAC;MACjD,GAAIL,OAAOM,WAAWN,OAAOM,QAAQC,SAAS,IAAI;QAAED,SAASN,OAAOM;MAAQ,IAAI,CAAC;MACjF,GAAIN,OAAOQ,gBAAgB;QAAEA,eAAeR,OAAOQ;MAAc,IAAI,CAAC;MACtE,GAAIR,OAAOS,OAAO;QAAEA,MAAMT,OAAOS;MAAK,IAAI,CAAC;MAC3C,GAAIT,OAAOU,QAAQ;QAAEA,OAAOV,OAAOU;MAAM,IAAI,CAAC;IAChD,CAAA;AAEA,WAAO;MACLC,WAAWV,KAAKW,IAAI,CAACC,SAAS;QAC5BC,IAAID,IAAIE;QACRC,gBAAgBH,IAAII;QACpBC,UAAUL,IAAIK;QACdC,UAAUN,IAAIM;QACdC,WAAWP,IAAIO;QACfC,QAAQR,IAAIQ;QACZC,UAAUT,IAAIS,SAASC,YAAW;MACpC,EAAA;MACArB;IACF;EACF;AACF;;;AClEA,SAASsB,wBAAAA,6BAAyD;AA0B3D,IAAMC,4BAAN,MAAMA;EA1Bb,OA0BaA;;;;;;EACX,YACmBC,mBACAC,oBACAC,eACjB;SAHiBF,oBAAAA;SACAC,qBAAAA;SACAC,gBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAqE;AACjF,UAAMC,UAAU,MAAM,KAAKL,kBAAkBM,WAAWF,OAAOG,WAAWH,OAAOI,cAAc;AAC/F,QAAI,CAACH,QAAS,OAAM,IAAII,sBAAqBL,OAAOI,cAAc;AAElE,UAAME,YAAY,MAAM,KAAKT,mBAAmBU,uBAAuBP,OAAOG,WAAWF,QAAQO,EAAE;AAInG,QAAIF,UAAUG,SAAS,KAAK,CAAC,KAAKX,eAAeY,QAAQ;AACvD,YAAM,IAAIC,MAAM,mGAAA;IAClB;AAEA,UAAMC,gBAA0B,CAAA;AAChC,QAAIC,iBAAiB;AAErB,eAAWC,YAAYR,WAAW;AAChC,UAAI;AACF,cAAM,KAAKR,eAAeY,SAASI,QAAAA;AACnCD;MACF,QAAQ;AACND,sBAAcG,KAAKD,QAAAA;MACrB;IACF;AAEA,QAAIF,cAAcH,SAAS,EAAG,QAAO;MAAEI;MAAgBD;IAAc;AAGrE,UAAM,KAAKhB,kBAAkBoB,eAAehB,OAAOG,WAAWH,OAAOI,cAAc;AAEnF,WAAO;MAAES;MAAgBD,eAAe,CAAA;IAAG;EAC7C;AACF;;;AC9CA,IAAMK,qBAAqB;AAC3B,IAAMC,eAAe;AACrB,IAAMC,uBAAuB,KAAK,KAAK;AAShC,IAAMC,+BAAN,MAAMA;EAXb,OAWaA;;;;;EACX,YACmBC,oBACAC,eACjB;SAFiBD,qBAAAA;SACAC,gBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAA2E;AACvF,QAAI,CAAC,KAAKF,eAAeG,QAAQ;AAC/B,YAAM,IAAIC,MAAM,mFAAA;IAClB;AAEA,UAAMC,YAAYH,OAAOI,OAAO,oBAAIC,KAAAA;AACpC,UAAMC,YAAY,IAAID,KAAKF,UAAUI,QAAO,IAAKP,OAAOQ,gBAAgBd,eAAeC,oBAAAA;AAEvF,UAAMc,UAAU,MAAM,KAAKZ,mBAAmBa,YAC5CV,OAAOW,WACPL,WACAN,OAAOY,aAAanB,kBAAAA;AAGtB,UAAMoB,SAAmB,CAAA;AACzB,QAAIC,SAAS;AAEb,eAAWC,YAAYN,SAAS;AAC9B,UAAI;AACF,cAAM,KAAKX,cAAcG,OAAOc,SAASC,QAAQ;MACnD,QAAQ;AAGNH,eAAOI,KAAKF,SAASC,QAAQ;AAC7B;MACF;AAEA,YAAM,KAAKnB,mBAAmBqB,WAAWlB,OAAOW,WAAWI,SAASI,EAAE;AACtEL;IACF;AAEA,WAAO;MAAEA;MAAQD;IAAO;EAC1B;AACF;;;ACpEA,SAASO,OAAAA,MAAKC,KAAKC,OAAOC,QAAAA,OAAMC,MAAAA,KAAIC,OAAOC,SAASC,MAAAA,KAAIC,UAAoB;AA+D5E,IAAMC,iBAAgB;AAaf,SAASC,sBAAsBC,QAA0B;AAC9D,QAAMC,OAAOD,QAAQE,KAAAA;AACrB,MAAI,CAACD,KAAM,QAAOE;AAElB,QAAMC,SAASH,KAAKI,QAAQ,OAAO,EAAA;AACnC,QAAMC,aAAaC,MAAMC,UAAUC,UAAU,IAAIR,IAAAA,GAAO;AACxD,MAAI,CAACG,OAAQ,QAAOE;AAEpB,SAAOI,GAAGJ,YAAYC,MAAMI,SAASC,gBAAgB,IAAIR,MAAAA,GAAS,CAAA;AACpE;AATgBL;AAWT,IAAMc,qBAAN,MAAMA;EAvFb,OAuFaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;;;;;;;;EASxD,MAAMC,KAAKC,QAA8D;AACvE,UAAMC,SAAyB;MAC7BC,WAAWF,OAAOE;MAClBC,WAAWH,OAAOG;MAClBC,WAAWJ,OAAOI,aAAa;MAC/BC,UAAUL,OAAOK;MACjBZ,UAAUO,OAAOP;MACjBa,UAAUN,OAAOM;MACjBC,WAAWP,OAAOO;MAClBC,QAAQR,OAAOQ,UAAU;MACzBC,QAAQT,OAAOS;IACjB;AAEA,UAAM,CAACC,OAAAA,IAAW,MAAM,KAAKZ,GAAGa,OAAOnB,SAAAA,EAAWS,OAAOA,MAAAA,EAAQW,oBAAmB,EAAGC,UAAS;AAChG,WAAOH;EACT;EAEA,MAAMI,mBAAmBd,QAA2D;AAClF,UAAMe,UAAU;MAACC,IAAGxB,UAAUU,WAAWF,OAAOE,SAAS;MAAGc,IAAGxB,UAAUW,WAAWH,OAAOG,SAAS;;AAGpG,QAAIH,OAAOhB,OAAQ+B,SAAQE,KAAK1B,MAAMC,UAAUC,UAAU,IAAIO,OAAOhB,MAAM,GAAG,CAAA;AAG9E,QAAIgB,OAAOkB,WAAWlB,OAAOkB,QAAQC,SAAS,GAAG;AAC/CJ,cAAQE,KAAKG,QAAQ5B,UAAUiB,QAAQ;WAAIT,OAAOkB;OAAQ,CAAA;IAC5D;AAEA,UAAMG,QAAQC,KAAAA,GAAOP,OAAAA;AACrB,UAAMQ,QAAQvB,OAAOuB,SAASzC;AAC9B,UAAM0C,OAAOxB,OAAOwB,QAAQxB,OAAOwB,OAAO,IAAIxB,OAAOwB,OAAO;AAC5D,UAAMC,UAAUzB,OAAO0B,kBAAkB,QAAQC,IAAInC,UAAUoC,QAAQ,IAAIC,MAAKrC,UAAUoC,QAAQ;AAIlG,UAAM,CAACE,MAAMC,OAAAA,IAAW,MAAMC,QAAQC,IAAI;MACxC,KAAKnC,GACFoC,OAAM,EACNC,KAAK3C,SAAAA,EACL6B,MAAMA,KAAAA,EACNI,QAAQA,OAAAA,EACRF,MAAMA,KAAAA,EACNa,QAAQZ,OAAO,KAAKD,KAAAA;MACvB,KAAKzB,GAAGoC,OAAO;QAAEG,OAAOC,MAAAA;MAAQ,CAAA,EAAGH,KAAK3C,SAAAA,EAAW6B,MAAMA,KAAAA;KAC1D;AAED,WAAO;MAAES;MAAMS,OAAOR,QAAQ,CAAA,GAAIM,SAAS;IAAE;EAC/C;;;;;;;;;;;;EAaA,MAAMG,cAAcxC,QAAyE;AAC3F,UAAMe,UAAU;MAACC,IAAGxB,UAAUU,WAAWF,OAAOE,SAAS;;AACzD,UAAMlB,SAASD,sBAAsBiB,OAAOhB,MAAM;AAClD,QAAIA,OAAQ+B,SAAQE,KAAKjC,MAAAA;AACzB,QAAIgB,OAAOkB,WAAWlB,OAAOkB,QAAQC,SAAS,GAAG;AAC/CJ,cAAQE,KAAKG,QAAQ5B,UAAUiB,QAAQ;WAAIT,OAAOkB;OAAQ,CAAA;IAC5D;AAEA,UAAMG,QAAQC,KAAAA,GAAOP,OAAAA;AACrB,UAAMQ,QAAQvB,OAAOuB,SAASzC;AAC9B,UAAM0C,OAAOxB,OAAOwB,QAAQxB,OAAOwB,OAAO,IAAIxB,OAAOwB,OAAO;AAC5D,UAAMC,UAAUzB,OAAO0B,kBAAkB,QAAQC,IAAInC,UAAUoC,QAAQ,IAAIC,MAAKrC,UAAUoC,QAAQ;AAElG,UAAM,CAACE,MAAMC,OAAAA,IAAW,MAAMC,QAAQC,IAAI;MACxC,KAAKnC,GACFoC,OAAO;QACNO,IAAIjD,UAAUiD;QACdpC,UAAUb,UAAUa;QACpBZ,UAAUD,UAAUC;QACpBa,UAAUd,UAAUc;QACpBC,WAAWf,UAAUe;QACrBE,QAAQjB,UAAUiB;QAClBmB,UAAUpC,UAAUoC;;;QAGpBhC,gBAAgBD,SAASC;MAC3B,CAAA,EACCuC,KAAK3C,SAAAA,EACLkD,UAAU/C,UAAUqB,IAAGxB,UAAUW,WAAWR,SAAS8C,EAAE,CAAA,EACvDpB,MAAMA,KAAAA,EACNI,QAAQA,OAAAA,EACRF,MAAMA,KAAAA,EACNa,QAAQZ,OAAO,KAAKD,KAAAA;;;;MAIvB,KAAKzB,GACFoC,OAAO;QAAEG,OAAOC,MAAAA;MAAQ,CAAA,EACxBH,KAAK3C,SAAAA,EACLkD,UAAU/C,UAAUqB,IAAGxB,UAAUW,WAAWR,SAAS8C,EAAE,CAAA,EACvDpB,MAAMA,KAAAA;KACV;AAED,WAAO;MAAES;MAAMS,OAAOR,QAAQ,CAAA,GAAIM,SAAS;IAAE;EAC/C;;;;;EAMA,MAAMM,eAAezC,WAAmBG,UAAoD;AAC1F,UAAM,CAACuC,GAAAA,IAAO,MAAM,KAAK9C,GACtBoC,OAAM,EACNC,KAAK3C,SAAAA,EACL6B,MAAMC,KAAIN,IAAGxB,UAAUU,WAAWA,SAAAA,GAAYc,IAAGxB,UAAUa,UAAUA,QAAAA,CAAAA,CAAAA,EACrEkB,MAAM,CAAA;AAET,WAAOqB;EACT;;;;;;;EAQA,MAAMC,uBAAuB3C,WAAmBC,WAAsC;AACpF,UAAM2B,OAAO,MAAM,KAAKhC,GACrBoC,OAAO;MAAE7B,UAAUb,UAAUa;IAAS,CAAA,EACtC8B,KAAK3C,SAAAA,EACL6B,MAAMC,KAAIN,IAAGxB,UAAUU,WAAWA,SAAAA,GAAYc,IAAGxB,UAAUW,WAAWA,SAAAA,CAAAA,CAAAA;AAEzE,WAAO2B,KAAKgB,IAAI,CAACF,QAAQA,IAAIvC,QAAQ;EACvC;;EAGA,MAAM0C,YAAY7C,WAAmB8C,WAAiBzB,QAAQzC,gBAAuC;AACnG,WAAO,KAAKgB,GACToC,OAAM,EACNC,KAAK3C,SAAAA,EACL6B,MAAMC,KAAIN,IAAGxB,UAAUU,WAAWA,SAAAA,GAAY+C,IAAGzD,UAAUoC,UAAUoB,SAAAA,CAAAA,CAAAA,EACrEvB,QAAQjC,UAAUoC,QAAQ,EAC1BL,MAAMA,KAAAA;EACX;EAEA,MAAM2B,WAAWhD,WAAmBuC,IAA2B;AAC7D,UAAM,KAAK3C,GAAGqD,OAAO3D,SAAAA,EAAW6B,MAAMC,KAAIN,IAAGxB,UAAUU,WAAWA,SAAAA,GAAYc,IAAGxB,UAAUiD,IAAIA,EAAAA,CAAAA,CAAAA;EACjG;AACF;;;ACpPA,SAASW,wBAAAA,6BAA4B;AAc9B,IAAMC,4BAAN,MAAMA;EAdb,OAcaA;;;;EACX,YAA6BC,mBAAsC;SAAtCA,oBAAAA;EAAuC;EAEpE,MAAMC,QAAQC,QAAqE;AACjF,UAAMC,SAAS,MAAM,KAAKH,kBAAkBI,mBAAmBF,OAAOG,WAAWH,OAAOI,cAAc;AACtG,QAAI,CAACH,OAAQ,OAAM,IAAII,sBAAqBL,OAAOI,cAAc;AACjE,WAAOH;EACT;AACF;;;AChBO,IAAMK,sBAAN,MAAMA;EAJb,OAIaA;;;;EACX,YAA6BC,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAAgF;AAC5F,WAAO,KAAKF,oBAAoBG,IAAID,OAAOE,WAAWF,OAAOG,GAAG;EAClE;AACF;AAEO,IAAMC,wBAAN,MAAMA;EAXb,OAWaA;;;;EACX,YAA6BN,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAA4D;AACxE,WAAO,KAAKF,oBAAoBO,KAAKL,OAAOE,SAAS;EACvD;AACF;AAYO,IAAMI,yBAAN,MAAMA;EA5Bb,OA4BaA;;;;EACX,YAA6BR,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAAuD;AACnE,WAAO,KAAKF,oBAAoBS,OAAOP,OAAOE,WAAWF,MAAAA;EAC3D;AACF;AAYO,IAAMQ,uBAAN,MAAMA;EA7Cb,OA6CaA;;;;EACX,YAA6BV,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAAqD;AACjE,WAAO,KAAKF,oBAAoBW,KAAKT,OAAOE,WAAWF,OAAOU,OAAOV,OAAOW,eAAe;EAC7F;AACF;AAEO,IAAMC,yBAAN,MAAMA;EApDb,OAoDaA;;;;EACX,YAA6Bd,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAA2D;AACvE,WAAO,KAAKF,oBAAoBe,OAAOb,OAAOE,WAAWF,OAAOG,GAAG;EACrE;AACF;AAEO,IAAMW,8BAAN,MAAMA;EA3Db,OA2DaA;;;;EACX,YAA6BhB,qBAA0C;SAA1CA,sBAAAA;EAA2C;EACxE,MAAMC,QAAQC,QAA+B;AAC3C,WAAO,KAAKF,oBAAoBiB,qBAAqBf,OAAOE,SAAS;EACvE;AACF;;;AC1DA,SAASc,mBAAmBC,oBAAoB;AA+BhD,IAAMC,oBAAoB;AAE1B,SAASC,kBAAkBC,MAAoBC,SAAgC;AAC7E,MAAI,CAACD,KAAKE,uBAAuB,CAACF,KAAKG,qBAAqB,CAACH,KAAKI,eAAgB,QAAO;AACzF,QAAMC,SAASJ,QAAQD,KAAKE,mBAAmB;AAC/C,QAAMI,WAAWN,KAAKI;AAEtB,UAAQJ,KAAKG,mBAAiB;IAC5B,KAAK;AACH,aAAOI,OAAOF,MAAAA,MAAYC;IAC5B,KAAK;AACH,aAAOC,OAAOF,MAAAA,MAAYC;IAC5B,KAAK;AACH,aAAO,OAAOD,WAAW,YAAYA,OAAOG,SAASF,QAAAA;IACvD,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK,MAAM;AACT,YAAMG,YAAYC,OAAOL,MAAAA;AACzB,YAAMM,cAAcD,OAAOJ,QAAAA;AAC3B,UAAII,OAAOE,MAAMH,SAAAA,KAAcC,OAAOE,MAAMD,WAAAA,EAAc,QAAO;AACjE,UAAIX,KAAKG,sBAAsB,IAAK,QAAOM,YAAYE;AACvD,UAAIX,KAAKG,sBAAsB,KAAM,QAAOM,aAAaE;AACzD,UAAIX,KAAKG,sBAAsB,IAAK,QAAOM,YAAYE;AACvD,aAAOF,aAAaE;IACtB;IACA;AACE,aAAO;EACX;AACF;AA3BSZ;AA6BT,SAASc,YAAYb,MAAoBc,UAA4B;AACnE,MAAI,CAACd,KAAKe,KAAM,QAAOC;AACvB,MAAI,OAAOhB,KAAKe,SAAS,SAAU,QAAOf,KAAKe;AAC/C,MAAID,YAAYd,KAAKe,KAAKE,SAASH,QAAAA,EAAW,QAAOd,KAAKe,KAAKE,SAASH,QAAAA;AACxE,SAAOd,KAAKe,KAAKG;AACnB;AALSL;AAWF,IAAMM,kBAAN,MAAMA;EAzEb,OAyEaA;;;EACMC,iBAAiB,oBAAIC,IAAAA;EAEtCC,mBAAmBC,MAAsBC,SAAkC;AACzE,SAAKJ,eAAeK,IAAIF,MAAMC,OAAAA;EAChC;EAEA,MAAME,KAAKC,OAA+C;AACxD,UAAM3B,OAAO2B,MAAMC,MAAMC,MAAMF,MAAMG,aAAa;AAClD,QAAI,CAAC9B,KAAM,QAAO;MAAEuB,MAAM;MAAYtB,SAAS0B,MAAM1B;IAAQ;AAE7D,QAAID,KAAK+B,SAAS,aAAa;AAC7B,YAAMC,SAASjC,kBAAkBC,MAAM2B,MAAM1B,OAAO,IAAI,SAAS;AACjE,aAAO,KAAKgC,OAAON,OAAOd,YAAYb,MAAMgC,MAAAA,CAAAA;IAC9C;AAEA,QAAIhC,KAAK+B,SAAS,UAAU;AAC1B,YAAMP,UAAUxB,KAAKkC,aAAa,KAAKd,eAAee,IAAInC,KAAKkC,UAAU,IAAIlB;AAC7E,UAAI,CAACQ,QAAS,QAAO;QAAED,MAAM;QAAYtB,SAAS0B,MAAM1B;MAAQ;AAIhE,YAAMmC,SAAU,MAAMZ,QAAQ;QAC5BxB;QACAqC,SAASV,MAAMU;QACfC,SAASX,MAAMW;QACfrC,SAAS0B,MAAM1B;MACjB,CAAA;AAEA,YAAMsC,qBAAqBH,QAAQnC,UAAU;QAAE,GAAG0B,MAAM1B;QAAS,GAAGmC,OAAOnC;MAAQ,IAAI0B,MAAM1B;AAC7F,YAAMuC,cAAaJ,QAAQrB,QAAQF,YAAYb,MAAMgB,MAAAA;AACrD,aAAO,KAAKiB,OAAO;QAAE,GAAGN;QAAO1B,SAASsC;MAAmB,GAAGC,WAAAA;IAChE;AAEA,UAAMC,WAAWzC,KAAK+B,SAAS,UAAU/B,KAAK0C,iBAAiB;AAG/D,QAAIf,MAAMgB,eAAe3B,QAAW;AAClC,aAAO;QAAEO,MAAM;QAAmBqB,QAAQjB,MAAMG;QAAe7B,SAAS0B,MAAM1B;MAAQ;IACxF;AAEA,UAAM4C,iBAAiB7C,KAAK8C,aAAa;MAAE,GAAGnB,MAAM1B;MAAS,CAACD,KAAK8C,UAAU,GAAGnB,MAAMgB;IAAW,IAAIhB,MAAM1B;AAC3G,UAAMa,WAAW2B,WAAWd,MAAMgB,aAAa3B;AAC/C,UAAMwB,aAAa3B,YAAYb,MAAMc,QAAAA;AAErC,WAAO,KAAKmB,OAAO;MAAE,GAAGN;MAAO1B,SAAS4C;IAAe,GAAGL,UAAAA;EAC5D;EAEQP,OAAON,OAAsBa,YAAgD;AACnF,QAAI,CAACA,WAAY,QAAO;MAAEjB,MAAM;MAAYtB,SAAS0B,MAAM1B;IAAQ;AACnE,QAAI8C,kBAAkBP,UAAAA,EACpB,QAAO;MAAEjB,MAAM;MAAcyB,SAASC,aAAaT,UAAAA;MAAavC,SAAS0B,MAAM1B;IAAQ;AACzF,QAAI,CAAC0B,MAAMC,MAAMC,MAAMW,UAAAA,EAAa,QAAO;MAAEjB,MAAM;MAAYtB,SAAS0B,MAAM1B;IAAQ;AACtF,WAAO;MAAEsB,MAAM;MAAYqB,QAAQJ;MAAYvC,SAAS0B,MAAM1B;IAAQ;EACxE;;;;;;;;;;EAWA,MAAMiD,IAAIvB,OAAsBwB,SAAyD;AACvF,UAAMC,WAAWD,SAASC,YAAYtD;AACtC,UAAMuD,UAAoB;MAAC1B,MAAMG;;AAEjC,QAAIwB,UAAyB3B;AAC7B,QAAI4B,QAAQ;AAEZ,WAAOA,QAAQH,UAAU;AACvB,YAAMhB,SAAS,MAAM,KAAKV,KAAK4B,OAAAA;AAC/BC;AAEA,UAAInB,OAAOb,SAAS,WAAY,QAAO;QAAE,GAAGa;QAAQiB;QAASE;MAAM;AAEnEF,cAAQG,KAAKpB,OAAOQ,MAAM;AAI1BU,gBAAU;QAAE,GAAGA;QAASxB,eAAeM,OAAOQ;QAAQ3C,SAASmC,OAAOnC;QAAS0C,YAAY3B;MAAU;IACvG;AAEA,WAAO;MAAEO,MAAM;MAAsBtB,SAASqD,QAAQrD;MAASoD;MAASE;IAAM;EAChF;AACF;;;AC1GA,SAASE,cAAaC,UAAgB;AACpC,MAAIA,SAASC,WAAW,QAAA,EAAW,QAAO;AAC1C,MAAID,SAASC,WAAW,QAAA,EAAW,QAAO;AAC1C,MAAID,SAASC,WAAW,QAAA,EAAW,QAAO;AAC1C,SAAO;AACT;AALSF,OAAAA,eAAAA;AA+BT,eAAeG,eAAeC,QAM7B;AACC,QAAM,EAAEC,YAAYC,SAASC,IAAIC,MAAK,IAAKJ;AAC3C,QAAMK,SAAS;IACbF;IACAN,UAAUI,WAAWJ;IACrBS,UAAUL,WAAWK;IACrBC,SAASN,WAAWM,WAAWC;EACjC;AACA,QAAMC,WAAWL,QAAQ;IAAEM,aAAaT,WAAWU;IAAIC,WAAWR,MAAMQ;EAAU,IAAIJ;AAEtF,MAAIJ,SAASK,UAAU;AACrB,UAAMI,eAAe,MAAMT,MAAMU,MAAMC,IAAIN,QAAAA;AAC3C,QAAII,cAAc;AAChB,UAAI;AACF,eAAO,MAAMX,QAAQc,UAAU;UAAE,GAAGX;UAAQY,SAASJ;QAAa,CAAA;MACpE,QAAQ;AACN,cAAMT,MAAMU,MAAMI,MAAMT,QAAAA;MAC1B;IACF;EACF;AAEA,QAAMU,SAAS,MAAMnB,OAAOoB,cAAcC,UAAUpB,WAAWqB,QAAQ;AACvE,QAAMC,SAAS,MAAMrB,QAAQc,UAAU;IAAE,GAAGX;IAAQc;EAAO,CAAA;AAG3D,MAAIf,SAASK,YAAYc,OAAON,QAAS,OAAMb,MAAMU,MAAMU,IAAI;IAAE,GAAGf;IAAUQ,SAASM,OAAON;EAAQ,CAAA;AAEtG,SAAOM;AACT;AAlCexB;AAoCR,SAAS0B,sBAAsBzB,QAAmC;AACvE,SAAO,OAAO,EAAE0B,MAAMC,SAASzB,QAAO,MAAE;AAEtC,QAAI,CAACyB,QAAQC,QAAS;AAEtB,UAAMC,WAAW;MAAEC,WAAWH,QAAQG;MAAWF,SAASD,QAAQC;MAASG,QAAQL,KAAKf;IAAG;AAC3F,UAAMqB,cAAc,MAAMhC,OAAOiC,oBAAoBC,WAAWL,QAAAA;AAIhE,eAAW5B,cAAc+B,aAAa;AACpC,UAAI;AACF,cAAM,EAAEG,kBAAiB,IAAK,MAAMpC,eAAe;UACjDE;UACAC;UACAC,IAAIwB,QAAQS;UACZhB,eAAepB,OAAOoB;UACtB,GAAIpB,OAAOqC,eAAe;YAAEjC,OAAOJ,OAAOqC;UAAa,IAAI,CAAC;QAC9D,CAAA;AAQA,cAAMrC,OAAOsC,WAAWC,QAAQ;UAC9BT,WAAWH,QAAQG;UACnBM,gBAAgBT,QAAQS;UACxBI,WAAW;UACXC,QAAQ;UACRC,aAAa;UACbC,MAAM/C,cAAaK,WAAWJ,QAAQ;UACtC+C,SAAS3C,WAAWM,WAAWN,WAAWK;UAC1CuC,SAAS;YACPvC,UAAUL,WAAWK;YACrBT,UAAUI,WAAWJ;YACrByB,UAAUrB,WAAWqB;YACrBZ,aAAaT,WAAWU;UAC1B;UACAmC,aAAaX;UACbY,QAAQ;UACRC,YAAYhD,OAAOgD;QACrB,CAAA;MACF,SAASC,OAAO;AACdjD,eAAOkD,UAAUD,OAAO;UAAErB,SAASC,SAASD;UAASG,QAAQL,KAAKf;UAAIW,UAAUrB,WAAWqB;QAAS,CAAA;MACtG;IACF;EACF;AACF;AAjDgBG;;;ACzHT,IAAM0B,qBAAqB;EAChCC,OAAO;EACPC,UAAU;AACZ;AA6BO,SAASC,4BAA4BC,QAAyC;AACnF,SAAO,OAAO,EAAEC,MAAMC,SAASC,QAAO,MAAE;AAEtC,QAAI,CAACA,QAAQC,gBAAiB;AAE9B,UAAMC,eAAgBJ,KAAKI,gBAAgB,CAAC;AAE5C,QAAI;AACF,YAAMC,YAAY,MAAMC,sBAAsB;QAC5CC,SAASR,OAAOQ;QAChBC,WAAWT,OAAOS;QAClB,GAAIJ,aAAaK,SAAS;UAAEA,QAAQL,aAAaK;QAAO,IAAI,CAAC;MAC/D,CAAA;AAIA,UAAIJ,UAAUK,WAAW,EAAG;AAE5B,YAAMC,WAAWP,aAAaO,YAAY;AAE1C,YAAM,EAAEC,kBAAiB,IAAK,MAAMV,QAAQC,gBAAgB;QAC1DU,IAAIZ,QAAQa;QACZC,YAAYX,aAAaW,cAAc;QACvCC,MAAML;QACN,GAAIP,aAAaa,aAAa;UAAEA,YAAYb,aAAaa;QAAW,IAAI,CAAC;QACzEC,UAAU;UACR;YACEC,OAAOf,aAAagB,gBAAgB;YACpCC,aAAahB,UAAUiB,IAAI,CAACC,YAAYA,QAAQC,UAAU;UAC5D;;MAEJ,CAAA;AAOA,YAAMzB,OAAO0B,WAAWC,QAAQ;QAC9BC,WAAW1B,QAAQ0B;QACnBb,gBAAgBb,QAAQa;QACxBc,WAAW;QACXC,QAAQ;QACRC,aAAa;QACbC,MAAM;QACNC,SAASrB;QACTsB,SAAS;UAAEC,MAAM;UAAgBC,cAAc9B,UAAUK;QAAO;QAChE0B,aAAaxB;QACbyB,QAAQ;QACRC,YAAYvC,OAAOuC;MACrB,CAAA;IACF,SAASC,OAAO;AACdxC,aAAOyC,UAAUD,OAAO;QAAEE,SAASxC,QAAQwC,WAAW;QAAIC,QAAQ1C,KAAK2C;MAAG,CAAA;IAC5E;EACF;AACF;AAvDgB7C;AAyDhB,eAAeQ,sBAAsBP,QAIpC;AACC,QAAM6C,WAAW,MAAM7C,OAAOQ,QAAQsC,aAAa;IACjDrC,WAAWT,OAAOS;IAClB,GAAIT,OAAOU,SAAS;MAAEA,QAAQV,OAAOU;IAAO,IAAI,CAAC;EACnD,CAAA;AAEA,SAAOmC,SAASE,OAAO,CAACvB,YAAYA,QAAQwB,iBAAiB,UAAA,EAAYC,MAAM,GAAGrD,mBAAmBC,KAAK;AAC5G;AAXeU;;;AClGf,SAAS2C,OAAAA,MAAKC,OAAAA,MAAKC,MAAAA,KAAIC,OAAAA,YAAW;AA8B3B,IAAMC,sBAAN,MAAMA;EA9Bb,OA8BaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;EAEhDC,eAAeC,UAA6B;AAClD,WAAOC,KACLC,IAAGC,UAAUC,WAAWJ,SAASI,SAAS,GAC1CF,IAAGC,UAAUE,SAASL,SAASK,OAAO,GACtCH,IAAGC,UAAUG,QAAQN,SAASM,MAAM,CAAA;EAExC;;;;EAKA,MAAMC,WAAWP,UAAsD;AACrE,WAAO,KAAKF,GACTU,OAAM,EACNC,KAAKN,SAAAA,EACLO,MAAMT,KAAI,KAAKF,eAAeC,QAAAA,GAAWE,IAAGC,UAAUQ,QAAQ,IAAA,CAAA,CAAA,EAC9DC,QAAQC,KAAIV,UAAUW,SAAS,GAAGD,KAAIV,UAAUY,SAAS,CAAA;EAC9D;;;EAIA,MAAMC,QAAQhB,UAAsD;AAClE,WAAO,KAAKF,GACTU,OAAM,EACNC,KAAKN,SAAAA,EACLO,MAAM,KAAKX,eAAeC,QAAAA,CAAAA,EAC1BY,QAAQC,KAAIV,UAAUW,SAAS,GAAGD,KAAIV,UAAUY,SAAS,CAAA;EAC9D;;;;;;;;EASA,MAAME,OAAOC,QAAsD;AACjE,UAAM,CAACC,GAAAA,IAAO,MAAM,KAAKrB,GACtBsB,OAAOjB,SAAAA,EACPkB,OAAO;MACNjB,WAAWc,OAAOd;MAClBC,SAASa,OAAOb;MAChBC,QAAQY,OAAOZ;MACfgB,UAAUJ,OAAOI;MACjBC,UAAUL,OAAOK;MACjBC,UAAUN,OAAOM;MACjBC,WAAWP,OAAOO;MAClBC,SAASR,OAAOQ,WAAW;MAC3BZ,WAAWI,OAAOJ,aAAa;IACjC,CAAA,EACCa,mBAAmB;MAClBC,QAAQ;QAACzB,UAAUC;QAAWD,UAAUE;QAASF,UAAUG;QAAQH,UAAUmB;;MAC7EO,KAAK;QACHH,SAASR,OAAOQ,WAAW;QAC3BZ,WAAWI,OAAOJ,aAAa;QAC/BH,QAAQ;QACRmB,WAAWC;MACb;IACF,CAAA,EACCC,UAAS;AAEZ,WAAOb;EACT;EAEA,MAAMc,OAAOf,QAAkE;AAC7E,UAAM,CAACC,GAAAA,IAAO,MAAM,KAAKrB,GACtBmC,OAAO9B,SAAAA,EACP0B,IAAI;MACH,GAAIX,OAAOQ,YAAYQ,SAAY;QAAER,SAASR,OAAOQ;MAAQ,IAAI,CAAC;MAClE,GAAIR,OAAOJ,cAAcoB,SAAY;QAAEpB,WAAWI,OAAOJ;MAAU,IAAI,CAAC;MACxE,GAAII,OAAOP,WAAWuB,SAAY;QAAEvB,QAAQO,OAAOP;MAAO,IAAI,CAAC;MAC/DmB,WAAWC;IACb,CAAA,EACCrB,MAAMT,KAAIC,IAAGC,UAAUC,WAAWc,OAAOd,SAAS,GAAGF,IAAGC,UAAUgC,IAAIjB,OAAOiB,EAAE,CAAA,CAAA,EAC/EH,UAAS;AAEZ,WAAOb;EACT;;;;;;EAOA,MAAMiB,OAAOlB,QAA0D;AACrE,UAAM,KAAKpB,GAAGuC,OAAOlC,SAAAA,EAAWO,MAAMT,KAAIC,IAAGC,UAAUC,WAAWc,OAAOd,SAAS,GAAGF,IAAGC,UAAUgC,IAAIjB,OAAOiB,EAAE,CAAA,CAAA;EACjH;;EAGA,MAAMG,mBAAmBpB,QAA0F;AACjH,UAAMqB,YACJrB,OAAOsB,gBAAgBC,WAAW,IAAIP,SAAYH,OAAM5B,UAAUG,MAAM,WAAWY,OAAOsB,eAAe;AAE3G,UAAM,KAAK1C,GACRuC,OAAOlC,SAAAA,EACPO,MAAMT,KAAIC,IAAGC,UAAUC,WAAWc,OAAOd,SAAS,GAAGF,IAAGC,UAAUE,SAASa,OAAOb,OAAO,GAAGkC,SAAAA,CAAAA;EACjG;AACF;;;AClIA,SAASG,MAAAA,KAAIC,OAAAA,YAAW;AAgBjB,IAAMC,wBAAN,MAAMA;EAhBb,OAgBaA;;;;EACX,YAA6BC,IAA0B;SAA1BA,KAAAA;EAA2B;EAExD,MAAMC,IAAIC,QAAiF;AACzF,UAAMC,OAAO,MAAM,KAAKH,GACrBI,OAAO;MAAEC,KAAKC,UAAUC;IAAa,CAAA,EACrCC,KAAKF,SAAAA,EACLG,MAAMC,IAAGJ,UAAUK,IAAIT,OAAOU,WAAW,CAAA,EACzCC,MAAM,CAAA;AAET,WAAOV,KAAK,CAAA,GAAIE,MAAMH,OAAOY,SAAS;EACxC;;;;;;;;EASA,MAAMC,IAAIb,QAAoF;AAC5F,UAAM,KAAKF,GACRgB,OAAOV,SAAAA,EACPS,IAAI;MACHR,cAAcU,iBAAgBX,UAAUC,YAAY,WAAWL,OAAOY,SAAS,qBAAqBZ,OAAOgB,OAAO;IACpH,CAAA,EACCT,MAAMC,IAAGJ,UAAUK,IAAIT,OAAOU,WAAW,CAAA;EAC9C;;EAGA,MAAMO,MAAMjB,QAAmE;AAC7E,UAAM,KAAKF,GACRgB,OAAOV,SAAAA,EACPS,IAAI;MAAER,cAAcU,OAAMX,UAAUC,YAAY,MAAML,OAAOY,SAAS;IAAS,CAAA,EAC/EL,MAAMC,IAAGJ,UAAUK,IAAIT,OAAOU,WAAW,CAAA;EAC9C;AACF;;;ACpDA,SAASQ,8BAA8BC,kCAAkC;AAEzE,SACEC,sBAAAA,2BAIK;;;ACCP,SACEC,yBACAC,kBACAC,8BACK;;;ADsBA,IAAMC,yBAAN,MAAMA;EAlCb,OAkCaA;;;;;EACX,YACmBC,WAKAC,cACjB;SANiBD,WAAAA;SAKAC,eAAAA;EAChB;EAEH,MAAcC,gBAAyBC,WAAqD;AAC1F,QAAI;AACF,aAAO,MAAMA,UAAAA;IACf,SAASC,OAAO;AACd,UAAIA,iBAAiBC,2BAA4B,OAAM,IAAIC,oBAAAA;AAC3D,YAAMF;IACR;EACF;EAEA,MAAMG,SAASC,IAAYC,MAA6D;AACtF,UAAMC,SAAS,MAAM,KAAKR,gBAAgB,MAAM,KAAKF,SAASO,SAASC,IAAIC,IAAAA,CAAAA;AAC3E,WAAO;MAAEE,mBAAmBD,OAAOE;IAAY;EACjD;EAEA,MAAMC,UAAUC,QAGb;AACD,UAAMJ,SAAS,MAAM,KAAKR,gBAAgB,MAAM,KAAKF,SAASa,UAAUC,MAAAA,CAAAA;AACxE,WAAO;MAAEH,mBAAmBD,OAAOE;MAAaG,SAASL,OAAOK;IAAQ;EAC1E;EAEA,MAAMC,aAAaF,QAK+B;AAChD,UAAMJ,SAAS,MAAM,KAAKR,gBAAgB,MAAM,KAAKF,SAASgB,aAAaF,MAAAA,CAAAA;AAC3E,WAAO;MAAEH,mBAAmBD,OAAOE;IAAY;EACjD;EAEA,MAAMK,oBAAoBH,QAKwB;AAChD,UAAMJ,SAAS,MAAM,KAAKR,gBAAgB,MACxC,KAAKF,SAASiB,oBAAoB;MAChCT,IAAIM,OAAON;MACXU,UAAUJ,OAAOL;MACjBU,YAAYL,OAAOM;MACnBC,UAAU;QAAC;UAAEC,OAAOR,OAAOM;UAAaG,MAAMT,OAAOS;QAAK;;IAC5D,CAAA,CAAA;AAEF,WAAO;MAAEZ,mBAAmBD,OAAOE;IAAY;EACjD;EAEA,MAAMY,uBAAuBV,QAIqB;AAChD,UAAMJ,SAAS,MAAM,KAAKR,gBAAgB,MACxC,KAAKF,SAASwB,uBAAuB;MACnChB,IAAIM,OAAON;MACXU,UAAUJ,OAAOL;MACjBgB,SAASX,OAAOW;IAClB,CAAA,CAAA;AAEF,WAAO;MAAEd,mBAAmBD,OAAOE;IAAY;EACjD;EAEA,MAAMc,gBAAgBZ,QAM4B;AAChD,UAAMJ,SAAS,MAAM,KAAKR,gBAAgB,MACxC,KAAKF,SAAS2B,uBAAuB;MACnCnB,IAAIM,OAAON;MACXoB,YAAYd,OAAOc;MACnBV,UAAUJ,OAAOL;MACjB,GAAIK,OAAOe,aAAa;QAAEA,YAAYf,OAAOe;MAAW,IAAI,CAAC;MAC7DR,UAAUP,OAAOO;IACnB,CAAA,CAAA;AAEF,WAAO;MAAEV,mBAAmBD,OAAOE;IAAY;EACjD;;;;;;;EAQA,MAAMkB,mBAAmBf,SAA8D;AACrF,UAAMgB,WAAW,KAAK9B,cAAc+B,YAAYC,uBAAuBlB,OAAAA,IAAWmB;AAElF,QAAIH,UAAU;AACZ,YAAMI,SAAS,MAAM,KAAKlC,aAAcmC,cAAcC,UAAUN,QAAAA;AAChE,aAAO;QACLO,MAAMH,OAAOI,SAAS,QAAA;QACtBC,UAAU,KAAKvC,aAAcwC,mBAAmB;MAClD;IACF;AAEA,WAAO,KAAKvC,gBAAgB,MAAM,KAAKF,SAAS8B,mBAAmBf,OAAAA,CAAAA;EACrE;AACF;;;AElJA,SACE2B,8BACAC,2BACAC,oCACAC,wCACAC,+BAOK;;;ACLP,SACEC,2BACAC,2BACAC,yBACAC,yBACAC,+BACK;AACP,SAASC,oCAAoC;AAiB7C,IAAMC,0BAA0B;AAEzB,SAASC,uBAAuBC,QAKtC;AACC,MAAI,CAACC,wBAAwBD,MAAAA,EAAS,OAAM,IAAIE,6BAAAA;AAChD,SAAOF,OAAOG;AAChB;AARgBJ;AAUT,SAASK,uBAAuBJ,QAItC;AACC,MAAI,CAACK,wBAAwBL,MAAAA,EAAS,OAAM,IAAIE,6BAAAA;AAClD;AANgBE;AAQhB,SAASE,YAAYC,iBAAuB;AAC1C,SAAOC,wBAAwB;IAC7BC,WAAWX;IACXS;EACF,CAAA;AACF;AALSD;AAWT,eAAsBI,qBAAqBV,QAI1C;AACC,SAAOA,OAAOW,WAAWC,YACvBN,YAAYN,OAAOO,eAAe,GAClCP,OAAOa,cAAcC,yBAAAA;AAEzB;AATsBJ;AAYtB,eAAsBK,uBAAuBf,QAI5C;AACC,QAAMA,OAAOW,WAAWK,UAAUV,YAAYN,OAAOO,eAAe,GAAGP,OAAOa,cAAcI,yBAAAA;AAC9F;AANsBF;;;AC1EtB,SAASG,MAAAA,KAAIC,OAAAA,YAAW;;;ACAxB,SACEC,uBACAC,sBACAC,kCACK;;;ACMA,IAAMC,uBAAuB;;EAElCC,SAAS;;EAETC,MAAM;;EAENC,QAAQ;;EAERC,aAAa;AACf;AAeO,IAAMC,qBAAqB;EAChCC,MAAM;EACNC,WAAW;AACb;AAyBO,SAASC,8BAA8BC,OAAc;AAC1D,MAAI,OAAOA,UAAU,YAAYA,UAAU,KAAM,QAAO;AACxD,QAAMC,cAAeD,MAAoCC;AAEzD,SAAO,OAAOA,gBAAgB,YAAYA,cAAc;AAC1D;AALgBF;AAOT,SAASG,gCAAgCF,OAAc;AAC5D,SACE,OAAOA,UAAU,YACjBA,UAAU,QACTA,MAA6BG,SAAS;AAE3C;AANgBD;AAQT,SAASE,+BAA+BJ,OAAc;AAC3D,MAAI,OAAOA,UAAU,YAAYA,UAAU,KAAM,QAAOK;AACxD,QAAMC,aAAcN,MAA0CO;AAC9D,SAAO,OAAOD,eAAe,WAAWA,aAAaD;AACvD;AAJgBD;AAOT,SAASI,gBAAgBC,UAAmC;AACjE,SAAO,OAAOA,aAAa,YAAYA,SAASC,KAAI,EAAGC,YAAW,EAAGC,WAAW,QAAA;AAClF;AAFgBJ;;;ADbT,IAAMK,yBAAN,MAAMA;EAvEb,OAuEaA;;;;EACX,YAA6BC,cAA2C;SAA3CA,eAAAA;EAA4C;EAEzE,MAAMC,QAAQC,QAA+D;AAC3E,UAAMC,UAAU,MAAM,KAAKH,aAAaI,kBAAkBC,SAASH,OAAOI,WAAWJ,OAAOK,SAAS;AACrG,QAAI,CAACJ,QAAS,OAAM,IAAIK,MAAM,YAAYN,OAAOK,SAAS,2CAAkC;AAO5F,QAAIJ,QAAQM,wBAAwBC,qBAAqBC,QAAQ,CAACT,OAAOU,OAAO;AAC9E,aAAO;QACLC,QAAQH,qBAAqBC;QAC7BG,MAAMX,QAAQY;QACdC,UAAUb,QAAQc;QAClBC,QAAQf,QAAQgB;QAChBC,oBAAoB;MACtB;IACF;AAGA,QAAI,KAAKpB,aAAaqB,eAAe;AACnC,YAAMC,SAAS,MAAM,KAAKtB,aAAaqB,cAAcnB,OAAOI,SAAS;AACrE,UAAI,CAACgB,OAAOC,UAAW,OAAM,IAAIC,2BAAAA;IACnC;AAEA,UAAMC,QAAQC,sBAAsBvB,OAAAA;AACpC,UAAMwB,SAAS,MAAM,KAAK3B,aAAa4B,cAAcC,UAAUJ,MAAMK,QAAQ;AAE7E,WAAO,KAAKC,iBAAiB;MAAE,GAAG7B;MAAQyB;MAAQK,UAAUP,MAAMO;MAAUF,UAAUL,MAAMK;MAAU3B;IAAQ,CAAA;EAChH;EAEA,MAAc4B,iBAAiBE,SAOI;AACjC,QAAI;AACF,YAAMC,SAAS,MAAM,KAAKlC,aAAamC,YAAYC,WAAW;QAC5DT,QAAQM,QAAQN;QAChBK,UAAUC,QAAQD;QAClB,GAAI,KAAKhC,aAAaqC,eAAe;UAAEA,cAAc,KAAKrC,aAAaqC;QAAa,IAAI,CAAC;MAC3F,CAAA;AAEA,YAAM,KAAKrC,aAAaI,kBAAkBkC,kBAAkB;QAC1DhC,WAAW2B,QAAQ3B;QACnBC,WAAW0B,QAAQ1B;QACnBM,QAAQH,qBAAqBC;QAC7BG,MAAMoB,OAAOpB;QACbE,UAAUkB,OAAOlB,YAAY;QAC7BE,QAAQgB,OAAOhB;MACjB,CAAA;AAEA,aAAO;QACLL,QAAQH,qBAAqBC;QAC7BG,MAAMoB,OAAOpB;QACbE,UAAUkB,OAAOlB,YAAY;QAC7BE,QAAQgB,OAAOhB;QACfE,oBAAoB;MACtB;IACF,SAASmB,OAAO;AACd,YAAM,KAAKC,eAAeP,SAASM,KAAAA;AACnC,YAAMA;IACR;EACF;;;;;EAMA,MAAcC,eACZP,SACAM,OACe;AACf,UAAM1B,SAAS4B,qBAAqBF,KAAAA;AAEpC,UAAM,KAAKvC,aAAaI,kBAAkBkC,kBAAkB;MAC1DhC,WAAW2B,QAAQ3B;MACnBC,WAAW0B,QAAQ1B;MACnBM;IACF,CAAA;AAEA,QAAIA,WAAWH,qBAAqBgC,QAAS;AAE7C,UAAMC,oBAAoBC,+BAA+BL,KAAAA;AACzD,UAAM,KAAKvC,aAAa6C,OAAOC,0BAA0B;MACvDxC,WAAW2B,QAAQ3B;MACnBC,WAAW0B,QAAQ1B;MACnBwC,gBAAgBd,QAAQ9B,QAAQ4C;MAChCjB,UAAUG,QAAQH;MAClB,GAAIa,sBAAsBK,SAAY;QAAEL;MAAkB,IAAI,CAAC;MAC/DM,QAAQN,sBAAsBK,SAAY,iBAAiB;MAC3DT;IACF,CAAA;EACF;AACF;AAEO,SAASE,qBAAqBF,OAAc;AACjD,MAAIW,gCAAgCX,KAAAA,EAAQ,QAAO7B,qBAAqByC;AACxE,SAAOC,8BAA8Bb,KAAAA,IAAS7B,qBAAqBgC,UAAUhC,qBAAqB2C;AACpG;AAHgBZ;AAYhB,SAASf,sBAAsBvB,SAAmB;AAChD,QAAMmD,UAAWnD,QAAQmD,WAAW,CAAC;AACrC,QAAM7B,QAAQ6B,QAAQ,OAAA;AACtB,QAAMtB,WAAW,OAAOsB,QAAQ,UAAA,MAAgB,WAAWA,QAAQ,UAAA,IAAc7B,OAAO8B;AAIxF,MAAI,CAACC,gBAAgBxB,QAAAA,KAAa,CAACP,OAAO;AACxC,UAAM,IAAIgC,qBAAqBtD,QAAQuD,IAAIvD,QAAQwD,IAAI;EACzD;AAEA,QAAM7B,WAAWwB,QAAQ,UAAA;AAGzB,MAAI,OAAOxB,aAAa,YAAYA,SAAS8B,WAAW,GAAG;AACzD,UAAM,IAAIC,sBAAsB1D,QAAQuD,EAAE;EAC5C;AAEA,SAAO;IAAE5B;IAAUE,UAAUA,YAAY;EAAY;AACvD;AAnBSN;;;ADlHF,IAAMoC,4BAAN,MAAMA;EAvEb,OAuEaA;;;;;;;;EACX,YACmBC,IACAC,SACAC,eAGAC,oBAGAC,eACjB;SATiBJ,KAAAA;SACAC,UAAAA;SACAC,gBAAAA;SAGAC,qBAAAA;SAGAC,gBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAqE;AACjF,UAAM,CAACC,OAAAA,IAAW,MAAM,KAAKP,GAC1BQ,OAAM,EACNC,KAAKC,QAAAA,EACLC,MAAMC,KAAIC,IAAGH,SAASI,WAAWR,OAAOQ,SAAS,GAAGD,IAAGH,SAASK,IAAIT,OAAOU,SAAS,CAAA,CAAA,EACpFC,MAAM,CAAA;AAET,QAAI,CAACV,QAAS,OAAM,IAAIW,MAAM,YAAYZ,OAAOU,SAAS,iDAAwC;AAElG,UAAMG,UAAWZ,QAAQY,WAAW,CAAC;AAQrC,QAAIA,QAAQ,UAAA,KAAeA,QAAQ,eAAA,MAAqBb,OAAOc,eAAe;AAC5E,aAAO;QAAEC,UAAUC,OAAOH,QAAQ,UAAA,CAAW;QAAGI,iBAAiB;MAAK;IACxE;AAEA,UAAM,EAAEC,MAAMC,SAAQ,IAAK,MAAM,KAAKxB,QAAQyB,mBAAmBpB,OAAOc,aAAa;AACrF,UAAMO,SAASC,OAAOnB,KAAKe,MAAM,QAAA;AAEjC,UAAM,EAAEH,SAAQ,IAAK,MAAM,KAAKnB,cAAc2B,OAAO;MACnDF;MACAF,UAAUA,YAAYnB,OAAOmB;MAC7BK,KAAK,iBAAiBxB,OAAOQ,SAAS,YAAYR,OAAOc,aAAa;IACxE,CAAA;AAGA,UAAMW,iBAA0C;MAC9C,GAAGZ;MACHE;MACAD,eAAed,OAAOc;MACtBK,UAAUA,YAAYnB,OAAOmB;;;;MAI7BO,WAAWL,OAAOM;MAClB,GAAI3B,OAAO4B,WAAW;QAAEA,UAAU5B,OAAO4B;MAAS,IAAI,CAAC;IACzD;AAEA,UAAM,KAAKlC,GACRmC,OAAOzB,QAAAA,EACP0B,IAAI;MAAEjB,SAASY;IAAe,CAAA,EAC9BpB,MAAMC,KAAIC,IAAGH,SAASI,WAAWR,OAAOQ,SAAS,GAAGD,IAAGH,SAASK,IAAIT,OAAOU,SAAS,CAAA,CAAA;AAKvF,UAAM,KAAKb,oBAAoBkC,KAAK;MAClCvB,WAAWR,OAAOQ;MAClBwB,WAAW/B,QAAQ+B;MACnBtB,WAAWT,QAAQQ;MACnBM;;MAEAa,UAAU5B,OAAO4B,YAAY,GAAG5B,OAAOc,aAAa;MACpDK,UAAUA,YAAYnB,OAAOmB;MAC7BO,WAAWL,OAAOM;MAClBM,QAAQhC,QAAQiC;IAClB,CAAA;AAEA,UAAMpC,gBAAgB,MAAM,KAAKqC,iBAAiB;MAChD3B,WAAWR,OAAOQ;MAClBP;MACAc;MACAM;MACAF,UAAUA,YAAYnB,OAAOmB;IAC/B,CAAA;AAEA,WAAO;MAAEJ;MAAUE,iBAAiB;MAAO,GAAInB,gBAAgB;QAAEA;MAAc,IAAI,CAAC;IAAG;EACzF;;;;;;;;;;EAWA,MAAcqC,iBAAiBC,SAM0B;AACvD,UAAMtC,gBAAgB,KAAKA;AAC3B,QAAI,CAACA,cAAe,QAAOuC;AAG3B,QAAI,CAACC,gBAAgBF,QAAQjB,QAAQ,EAAG,QAAOkB;AAE/C,UAAME,SAAS,MAAMzC,cAAc0C,cAAcJ,QAAQ5B,SAAS;AAClE,QAAI,CAAC+B,OAAOE,aAAaF,OAAOG,SAASC,mBAAmBC,KAAM,QAAOP;AAkBzE,UAAMQ,UAAU,MAAM/C,cAAcgD,kBAAkBC,SAASX,QAAQ5B,WAAW4B,QAAQnC,QAAQQ,EAAE;AACpG,QAAIoC,SAASG,wBAAwBC,qBAAqBC,KAAM,QAAOb;AAEvE,QAAI;AACF,YAAMc,SAAS,MAAMrD,cAAcsD,YAAYC,WAAW;QACxDhC,QAAQe,QAAQf;QAChBF,UAAUiB,QAAQjB;QAClB,GAAIrB,cAAcwD,eAAe;UAAEA,cAAcxD,cAAcwD;QAAa,IAAI,CAAC;MACnF,CAAA;AAEA,YAAMxD,cAAcgD,kBAAkBS,kBAAkB;QACtD/C,WAAW4B,QAAQ5B;QACnBE,WAAW0B,QAAQnC,QAAQQ;QAC3B+C,QAAQP,qBAAqBC;QAC7BO,MAAMN,OAAOM;QACbC,UAAUP,OAAOO,YAAY;QAC7BC,QAAQR,OAAOQ;MACjB,CAAA;AAEA,aAAO;QAAEH,QAAQP,qBAAqBC;MAAK;IAC7C,SAASU,OAAO;AACd,aAAO,KAAKC,2BAA2BzB,SAASwB,KAAAA;IAClD;EACF;EAEA,MAAcC,2BACZzB,SACAwB,OAC0C;AAC1C,UAAMJ,SAASM,qBAAqBF,KAAAA;AAEpC,UAAM,KAAK9D,eAAegD,kBAAkBS,kBAAkB;MAC5D/C,WAAW4B,QAAQ5B;MACnBE,WAAW0B,QAAQnC,QAAQQ;MAC3B+C;IACF,CAAA;AAEA,QAAIA,WAAWP,qBAAqBc,SAAS;AAC3C,YAAMC,oBAAoBC,+BAA+BL,KAAAA;AACzD,YAAM,KAAK9D,eAAeoE,OAAOC,0BAA0B;QACzD3D,WAAW4B,QAAQ5B;QACnBE,WAAW0B,QAAQnC,QAAQQ;QAC3B2D,gBAAgBhC,QAAQnC,QAAQmE;QAChCrD,UAAUqB,QAAQrB;QAClB,GAAIiD,sBAAsB3B,SAAY;UAAE2B;QAAkB,IAAI,CAAC;QAC/DK,QAAQL,sBAAsB3B,SAAY,iBAAiB;QAC3DuB;MACF,CAAA;IACF;AAEA,WAAO;MAAEJ;IAAO;EAClB;AACF;AAIO,SAASc,uBACdrE,SAAmB;AAEnB,QAAMY,UAAWZ,QAAQY,WAAW,CAAC;AACrC,aAAWW,OAAO;IAAC;IAAS;IAAS;IAAS;IAAY;KAAqB;AAC7E,UAAM+C,QAAQ1D,QAAQW,GAAAA;AACtB,QAAI+C,OAAO9D,IAAI;AACb,aAAO;QACLK,eAAeyD,MAAM9D;QACrBU,UAAUoD,MAAMC,aAAa;QAC7B5C,UAAU2C,MAAM3C;MAClB;IACF;EACF;AACA,SAAOS;AACT;AAfgBiC;;;AGxLT,SAASG,kBAAkBC,KAAuB;AACvD,SAAOA,IAAIC,SAAS,YAChB,sBAAsBD,IAAIE,QAAQC,EAAE,KACpC,qBAAqBH,IAAII,OAAOD,EAAE,IAAIH,IAAII,OAAOA,MAAM;AAC7D;AAJgBL;AAMT,SAASM,kBAAkBC,KAAe;AAC/C,SAAO;IACLH,IAAIG,IAAIH;IACRI,WAAWD,IAAIC;IACfC,gBAAgBF,IAAIE;IACpBC,cAAcH,IAAIG;IAClBC,SAASJ,IAAII;IACbC,eAAeL,IAAIK;IACnBC,SAASN,IAAIM;IACbC,MAAMP,IAAIO;IACVC,gBAAgBR,IAAIQ;IACpBC,kBAAkBT,IAAIS,kBAAkBC,YAAAA,KAAiB;IACzDC,eAAeX,IAAIW,eAAeD,YAAAA,KAAiB;IACnDE,iBAAiBZ,IAAIY,iBAAiBF,YAAAA,KAAiB;IACvDG,cAAcb,IAAIa,aAAaH,YAAW;IAC1CI,WAAWd,IAAIc,UAAUJ,YAAW;IACpCK,WAAWf,IAAIe,UAAUL,YAAW;EACtC;AACF;AAlBgBX;AAwBT,SAASiB,cAAcpB,SAAwB;AACpD,QAAMqB,cAAcrB,QAAQqB;AAC5B,MAAIA,aAAaC,aAAc,QAAOD,YAAYC,aAAarB;AAC/D,MAAIoB,aAAaE,WAAY,QAAOF,YAAYE,WAAWtB;AAC3D,SAAOD,QAAQwB,MAAMC;AACvB;AALgBL;AAYT,IAAMM,2BAAN,MAAMA;EAjHb,OAiHaA;;;;EACX,YACmBC,QAKjB;SALiBA,SAAAA;EAKhB;EAEH,MAAMC,IAAI9B,KAAwC;AAChD,QAAIA,IAAIC,SAAS,UAAW,QAAO,KAAK8B,kBAAkB/B,GAAAA;AAC1D,WAAO,KAAKgC,iBAAiBhC,GAAAA;EAC/B;EAEA,MAAM+B,kBAAkB/B,KAA8C;AACpE,UAAM,EAAEO,WAAWL,SAAS+B,gBAAgBC,MAAK,IAAKlC;AAMtD,QAAIkC,OAAO;AACT,YAAM,KAAKL,OAAOM,OAAOC,kBAAkB;QACzC7B;QACA8B,WAAWJ;QACXzB,gBAAgBN,QAAQoC;QACxBC,eAAeL,MAAMK;QACrBC,UAAUN,MAAMM;QAChB,GAAIN,MAAMO,WAAW;UAAEA,UAAUP,MAAMO;QAAS,IAAI,CAAC;MACvD,CAAA;IACF;AAEA,UAAMC,aAAa,MAAM,KAAKb,OAAOc,kBAAkBC,WAAWrC,WAAWL,QAAQoC,IAAI;AACzF,QAAI,CAACI,WAAY;AAGjB,QAAIA,WAAW7B,SAAS,QAAS;AAEjC,UAAMgC,UAAU,MAAM,KAAKhB,OAAOM,OAAOW,oBACvC5C,SACAG,kBAAkBqC,UAAAA,GAClB1C,IAAI+C,gBAAgBC,SAAY,CAAC,IAAI;MAAED,aAAa/C,IAAI+C;IAAY,CAAA;AAGtE,QAAIF,SAASA,YAAY,UAAW;AAKpC,SAAKvB,cAAcpB,OAAAA;EACrB;EAEA,MAAM8B,iBAAiBhC,KAA6C;AAClE,UAAM0C,aAAa,MAAM,KAAKb,OAAOc,kBAAkBC,WAAW5C,IAAIO,WAAWP,IAAIQ,cAAc;AACnG,UAAM,KAAKqB,OAAOM,OAAOc,iBAAiBjD,IAAII,QAAQsC,aAAarC,kBAAkBqC,UAAAA,IAAc,IAAA;EACrG;AACF;AAQO,IAAMQ,gCAAN,MAAMA;EAjLb,OAiLaA;;;;EACX,YAA6BC,YAAsC;SAAtCA,aAAAA;EAAuC;EAEpE,MAAMC,QAAQpD,KAAwC;AACpD,UAAM,KAAKmD,WAAWrB,IAAI9B,GAAAA;EAC5B;AACF;;;ALnIA,IAAMqD,eAAwD;EAC5DC,mBAAmB;EACnBC,mBAAmB;EACnBC,sBAAsB;EACtBC,wBAAwB;EACxBC,iBAAiB;AACnB;AAIA,SAASC,eAAeC,SAAwB;AAC9C,MAAIA,QAAQC,MAAMC,KAAM,QAAOF,QAAQC,KAAKC;AAC5C,QAAMC,cAAcH,QAAQG;AAC5B,MAAIA,aAAaC,aAAc,QAAOD,YAAYC,aAAaC,SAASF,YAAYC,aAAaE;AACjG,MAAIH,aAAaI,WAAY,QAAOJ,YAAYI,WAAWF,SAASF,YAAYI,WAAWD;AAC3F,MAAIN,QAAQQ,OAAO;AACjB,UAAMC,QAAQT,QAAQQ,MAAME,cAAcC,OAAO,CAACC,KAAKC,SAASD,MAAMC,KAAKC,UAAU,CAAA;AACrF,WAAO,WAAWL,KAAAA;EACpB;AACA,MAAIT,QAAQe,UAAU;AACpB,UAAMC,QAAQhB,QAAQe,SAASE,QAAQjB,QAAQe,SAASG;AACxD,WAAOF,QAAQ,gCAAmBA,KAAAA,KAAU;EAC9C;AACA,SAAOhB,QAAQmB,OAAOC,WAAWpB,QAAQqB,UAAUD,WAAW;AAChE;AAdSrB;AAgBT,SAASuB,eAAetB,SAAwB;AAC9C,QAAMuB,UAAmC,CAAC;AAC1C,MAAIvB,QAAQG,YAAaoB,SAAQ,aAAA,IAAiBvB,QAAQG;AAC1D,MAAIH,QAAQQ,MAAOe,SAAQ,OAAA,IAAWvB,QAAQQ;AAC9C,MAAIR,QAAQmB,MAAOI,SAAQ,OAAA,IAAWvB,QAAQmB;AAC9C,MAAInB,QAAQwB,MAAOD,SAAQ,OAAA,IAAWvB,QAAQwB;AAC9C,MAAIxB,QAAQyB,MAAOF,SAAQ,OAAA,IAAWvB,QAAQyB;AAC9C,MAAIzB,QAAQqB,SAAUE,SAAQ,UAAA,IAAcvB,QAAQqB;AACpD,MAAIrB,QAAQ0B,QAASH,SAAQ,SAAA,IAAavB,QAAQ0B;AAClD,MAAI1B,QAAQe,SAAUQ,SAAQ,UAAA,IAAcvB,QAAQe;AACpD,MAAIf,QAAQ2B,SAASC,iBAAkBL,SAAQ,iBAAA,IAAqBvB,QAAQ2B,QAAQC;AACpF,SAAOC,OAAOC,KAAKP,OAAAA,EAASQ,SAAS,IAAIR,UAAU;AACrD;AAZSD;AAcT,IAAMU,uBAA0C;EAC9CC,wBAAwBC;EACxBD,wBAAwBE;;AAG1B,SAASC,oBAAoBC,OAAyB;AACpD,SAAOA,UAAUC,UAAaN,qBAAqBO,SAASF,KAAAA;AAC9D;AAFSD;AAOF,IAAMI,wBAAN,MAAMA;EAxGb,OAwGaA;;;;EACX,YACmBC,QAgBjB;SAhBiBA,SAAAA;AAiBjB,SAAKC,aAAa,IAAIC,yBAAyB;MAC7CC,mBAAmBH,OAAOG;MAC1B,GAAIH,OAAOI,QAAQ;QAAEA,OAAOJ,OAAOI;MAAM,IAAI,CAAC;MAC9C,GAAIJ,OAAOK,WAAW;QAAEA,UAAUL,OAAOK;MAAS,IAAI,CAAC;IACzD,CAAA;EACF;EAEiBJ;EAEjB,MAAMK,QAAQC,OAA4D;AACxEC,2BAAuB;MACrBC,SAASF,MAAME;MACfC,iBAAiBH,MAAMG;MACvBC,WAAW,KAAKX,OAAOW;IACzB,CAAA;AAEA,UAAMC,UAAU,MAAMC,qBAAqB;MACzCC,YAAY,KAAKd,OAAOc;MACxBJ,iBAAiBH,MAAMG;IACzB,CAAA;AACA,QAAI,CAACE,QAAS,QAAO;MAAEG,WAAW;MAAM,GAAG/D;IAAa;AAExD,UAAMgE,UAAU,OAAOT,MAAME,YAAY,WAAWF,MAAME,UAAUF,MAAME,QAAQQ,SAAS,MAAA;AAI3F,UAAMC,SAASC,6BAA6BC,UAAUC,KAAKC,MAAMN,OAAAA,CAAAA;AACjE,QAAI,CAACE,OAAOK,SAAS;AACnB,YAAM,KAAKvB,OAAOI,OAAOoB,0BAA0B;QACjD5B,OAAOC;QACP4B,QAAQ;QACRC,OAAOV;MACT,CAAA;AACA,aAAO;QAAED,WAAW;QAAO,GAAG/D;QAAcK,iBAAiB;MAAE;IACjE;AACA,UAAMyB,UAAUoC,OAAOS;AAEvB,QAAI1E,oBAAoB;AACxB,QAAIC,oBAAoB;AACxB,QAAIC,uBAAuB;AAC3B,QAAIC,yBAAyB;AAC7B,QAAIC,kBAAkB;AAEtB,eAAWuE,SAAS9C,QAAQ8C,OAAO;AACjC,iBAAWC,UAAUD,MAAME,SAAS;AAIlC,YAAInC,oBAAoBkC,OAAOjC,KAAK,GAAG;AACrC,gBAAMmC,UAAU,MAAM,KAAKC,mBAAmBH,MAAAA;AAC9C,cAAIE,QAAS3E;cACRC;AACL;QACF;AAOA,cAAM4E,eAAeJ,OAAOH,MAAMQ,UAAUC;AAC5C,YAAIF,gBAAgBA,iBAAiB,KAAKjC,OAAOoC,eAAe;AAC9DjF;AACA;QACF;AAEA,mBAAWI,WAAWsE,OAAOH,MAAMW,YAAY,CAAA,GAAI;AAGjD,gBAAMC,cAAcC,0BAA0B;YAAEC,UAAUX,OAAOH,MAAMc;YAAUC,MAAMlF,QAAQkF;UAAK,CAAA;AACpG,gBAAM,KAAKC,cAAcnC,MAAMoC,WAAWpF,SAAS+E,WAAAA;AACnDrF;QACF;AACA,mBAAW2F,UAAUf,OAAOH,MAAMmB,YAAY,CAAA,GAAI;AAChD,gBAAM,KAAKC,aAAavC,MAAMoC,WAAWC,MAAAA;AACzC1F;QACF;AAIA,cAAM6F,2BACHlB,OAAOH,MAAMW,UAAU/C,UAAU,KAAK,MACtCuC,OAAOH,MAAMsB,gBAAgB1D,UAAU,KAAK,MAC5CuC,OAAOH,MAAMmB,UAAUvD,UAAU,KAAK;AACzC,YAAI,CAACyD,yBAAyB;AAC5B1F;AACA,gBAAM,KAAK2C,OAAOI,OAAOoB,0BAA0B;YACjD5B,OAAOiC,OAAOjC;YACd6B,QAAQ;YACRC,OAAOG,OAAOH;UAChB,CAAA;QACF;MACF;IACF;AAIA,UAAMuB,uBAAuB;MAC3BnC,YAAY,KAAKd,OAAOc;MACxBJ,iBAAiBH,MAAMG;IACzB,CAAA;AAEA,WAAO;MACLK,WAAW;MACX9D;MACAC;MACAC;MACAC;MACAC;IACF;EACF;;;EAIA,MAAc2E,mBAAmBH,QAAiD;AAChF,QAAIA,OAAOjC,UAAUJ,wBAAwBC,wBAAwB;AACnE,YAAMyD,SAASC,mCAAmC/B,UAAUS,OAAOH,KAAK;AACxE,UAAI,CAACwB,OAAO3B,SAAS;AACnB,cAAM,KAAKvB,OAAOI,OAAOoB,0BAA0B;UACjD5B,OAAOiC,OAAOjC;UACd6B,QAAQ;UACRC,OAAOG,OAAOH;QAChB,CAAA;AACA,eAAO;MACT;AACA,YAAM,KAAK1B,OAAOI,OAAOgD,yBAAyBF,OAAOvB,IAAI;AAC7D,aAAO;IACT;AAEA,UAAM0B,UAAUC,uCAAuClC,UAAUS,OAAOH,KAAK;AAC7E,QAAI,CAAC2B,QAAQ9B,SAAS;AACpB,YAAM,KAAKvB,OAAOI,OAAOoB,0BAA0B;QACjD5B,OAAOiC,OAAOjC;QACd6B,QAAQ;QACRC,OAAOG,OAAOH;MAChB,CAAA;AACA,aAAO;IACT;AACA,UAAM,KAAK1B,OAAOI,OAAOmD,6BAA6BF,QAAQ1B,IAAI;AAClE,WAAO;EACT;EAEA,MAAce,cAAcC,WAAmBpF,SAA0B+E,aAAqC;AAG5G,UAAMkB,QAAQ,MAAM,KAAKxD,OAAOyD,WAAWnD,QAAQ;MACjDqC;MACAe,gBAAgBnG,QAAQkF;MACxBkB,WAAW;MACXC,QAAQ;MACRC,MAAMtG,QAAQsG;MACdC,SAASxG,eAAeC,OAAAA;MACxBuB,SAASD,eAAetB,OAAAA;MACxBwG,aAAaxG,QAAQM;MACrB+E,QAAQ;MACRoB,YAAY,KAAKhE,OAAOgE;IAC1B,CAAA;AAGA,QAAI,CAACR,MAAO;AAEZ,UAAMS,QAAQC,uBAAuBV,KAAAA;AACrC,UAAM,KAAKW,SAAS;MAClBC,MAAM;MACNzB;MACApF;MACA8G,gBAAgBb,MAAM3F;MACtB,GAAIoG,QAAQ;QAAEA;MAAM,IAAI,CAAC;MACzB,GAAI3B,cAAc;QAAEA;MAAY,IAAI,CAAC;MACrCgC,YAAYC,KAAKC,IAAG;IACtB,CAAA;EACF;EAEA,MAAc1B,aAAaH,WAAmBC,QAAuC;AACnF,UAAM6B,UAAU,MAAM,KAAKzE,OAAO0E,kBAAkBC,oBAClDhC,WACAC,OAAO/E,IACP+E,OAAOA,MAAM;AAEf,QAAI,CAAC6B,QAAS;AAEd,SAAKzE,OAAOK,UAAUuE,KAAK,QAAQH,QAAQf,cAAc,IAAI,kBAAkB;MAC7EK,aAAanB,OAAO/E;MACpB+E,QAAQA,OAAOA;IACjB,CAAA;AAEA,UAAM,KAAKuB,SAAS;MAClBC,MAAM;MACNzB;MACAC;MACAc,gBAAgBe,QAAQf;MACxBY,YAAYC,KAAKC,IAAG;IACtB,CAAA;EACF;;;EAIA,MAAcL,SAASU,KAAwC;AAC7D,QAAI,KAAK7E,OAAO8E,cAAc;AAC5B,YAAM,KAAK9E,OAAO8E,aAAaC,QAAQF,KAAK;QAAEG,OAAOC,kBAAkBJ,GAAAA;MAAK,CAAA;AAC5E;IACF;AACA,UAAM,KAAK5E,WAAWiF,IAAIL,GAAAA;EAC5B;AACF;;;AMnSO,SAASM,kCAAkCC,cAAoD;AACpG,SAAO,sCAAeC,2BAA2BC,WAAiB;AAChE,UAAMC,YAAW,MAAMH,aAAaI,mBAAmBC,IAAIH,SAAAA;AAE3D,WAAO;;;MAGLI,WAAWH,UAASI,wBAAwBP,aAAaQ,SAASF;MAClEG,MAAMC,cAAcP,UAASQ,iBAAiB,KAAKX,aAAaQ,SAASC;IAC3E;EACF,GATO;AAUT;AAXgBV;AAmBhB,SAASW,cAAcE,OAAoB;AACzC,MAAIA,UAAUC,mBAAmBC,KAAM,QAAOD,mBAAmBC;AACjE,MAAIF,UAAUC,mBAAmBE,UAAW,QAAOF,mBAAmBE;AACtE,SAAOC;AACT;AAJSN;;;AC5BF,IAAMO,2BAAN,MAAMA;EA1Bb,OA0BaA;;;;;EACX,YACmBC,eAKAC,mBACjB;SANiBD,gBAAAA;SAKAC,oBAAAA;EAChB;EAEH,MAAMC,QAAQC,QAAmE;AAQ/E,UAAMC,MAAM,iBAAiBD,OAAOE,SAAS,YAAY,KAAKJ,kBAAiB,CAAA;AAE/E,UAAM,EAAEK,SAAQ,IAAK,MAAM,KAAKN,cAAcO,OAAO;MACnDC,QAAQL,OAAOK;MACfC,UAAUN,OAAOM;MACjBL;IACF,CAAA;AAEA,WAAO;MAAEM,SAASC,iBAAiBL,QAAAA;MAAWA;IAAS;EACzD;AACF;;;AlCsGO,SAASM,yBAAyBC,QAAsC;AAC7E,QAAM,EAAEC,IAAIC,QAAQC,YAAYC,YAAY,CAAC,GAAGC,MAAK,IAAKL;AAC1D,QAAMM,aAAaN,OAAOM,cAAc;AACxC,QAAMC,oBAAoBP,OAAOQ,UAAUC,cAAc;AAEzD,QAAMC,kBAAkB,IAAIC,wBAAwB;IAClDC,aAAaV,OAAOU;IACpBC,eAAeX,OAAOW;IACtBC,QAAQZ,OAAOY;IACfC,YAAYb,OAAOa;IACnBC,SAASd,OAAOc;EAClB,CAAA;AAKA,QAAMC,sBACJjB,OAAOQ,UAAUU,gBAAgBd,UAAUe,eAAeC,YACtD;IACEC,WAAW;IACXF,eAAef,UAAUe;EAC3B,IACAG;AAEN,QAAMC,UAAmC,IAAIC,uBAAuBd,iBAAiBO,mBAAAA;AAErF,QAAMQ,oBAAoB,IAAIC,kBAAkBzB,EAAAA;AAChD,QAAM0B,oBAAoB,IAAIC,kBAAkB3B,EAAAA;AAChD,QAAM4B,qBAAqB,IAAIC,mBAAmB7B,EAAAA;AAClD,QAAM8B,wBAAwB/B,OAAOQ,UAAUwB,kBAAkB;AACjE,QAAMA,iBACJD,yBAAyB3B,UAAU6B,QAC/B,IAAIC,eACF9B,UAAU6B,OACV,OAAOF,0BAA0B,WAC5BA,sBAAsBI,cAAcC,uCACrCA,oCAAAA,IAENd;AAEN,QAAMe,sBAAsB,IAAIC,oBAAoBrC,IAAI+B,cAAAA;AACxD,QAAMO,qBAAqB,IAAIC,mBAAmBvC,EAAAA;AAClD,QAAMwC,sBAAsB,IAAIC,oBAAoBzC,EAAAA;AAEpD,QAAM0C,aAAa,IAAIC,kBACrBnB,mBACAE,mBACAvB,UAAUyC,UACVzC,UAAU0C,SAAS;AAErB,QAAMC,cAAc,IAAIC,mBACtBzB,SACAE,mBACAkB,YACAvC,UAAUe,eACVoB,kBAAAA;AAGF,QAAMU,iBAAiB,IAAIC,sBAAsB;IAC/CC,WAAWjD,OAAOiD;IAClBtC,eAAeX,OAAOW;IACtBV;IACAsB;IACAE;IACAgB;IACArC;IACAD;IACAwC,UAAUzC,UAAUyC;EACtB,CAAA;AAIA,QAAMO,kBAAkB7C,oBAAoB,IAAI8C,gBAAAA,IAAoB/B;AAKpE,MAAI8B,mBAAmBhD,UAAUe,eAAeC,WAAW;AACzDgC,oBAAgBE,mBACdC,iBAAiBC,YACjBC,sBAAsB;MACpBhB;MACAtB,eAAef,UAAUe;MACzBwB;MACArC;;;;;MAKAoD,cAAc;QAAEC,OAAO,IAAIC,sBAAsB3D,EAAAA;QAAK4D,WAAW3D,OAAOW;MAAc;MACtFiD,SAASzD,OAAO0D;IAClB,CAAA,CAAA;EAEJ;AAIA,MAAIX,mBAAmBhD,UAAU4D,WAAW9D,OAAO+D,WAAW;AAC5Db,oBAAgBE,mBACdC,iBAAiBW,mBACjBC,4BAA4B;MAC1BH,SAAS5D,UAAU4D;MACnBC,WAAW/D,OAAO+D;MAClBtB;MACArC;MACAwD,SAASzD,OAAO+D;IAClB,CAAA,CAAA;EAEJ;AAEA,QAAMC,oBAAoBjE,UAAUkE,eAAeC,QAAQC,mBAAmBC;AAM9E,QAAMC,6BAA6BtE,UAAUkE,gBACzCK,kCAAkC;IAChC9C;IACA+C,UAAU;MACRvD,WAAWjB,UAAUkE,cAAcO,sBAAsB;MACzDN,MAAMF;IACR;EACF,CAAA,IACA/C;AAIJ,QAAMwD,qBAAqB1E,UAAUe,gBACjC,IAAI4D,0BACF9E,IACAsB,SACAnB,UAAUe,eACVoB,oBACAnC,UAAUkE,iBAAiBI,6BACvB;IACEM,aAAa5E,UAAUkE,cAAcU;IACrCC,eAAeP;IACf/C;IACA,GAAIvB,UAAUkE,cAAcY,eAAe;MAAEA,cAAc9E,UAAUkE,cAAcY;IAAa,IAAI,CAAC;IACrG,GAAI7E,QAAQ;MAAEA;IAAM,IAAI,CAAC;EAC3B,IACAiB,MAAAA,IAENA;AAWJ,QAAM6D,kBACJ/E,UAAUkE,iBAAiBlE,UAAUe,eAAeC,YAChD,IAAIgE,uBAAuB;IACzBzD;IACAR,eAAef,UAAUe;IACzB6D,aAAa5E,UAAUkE,cAAcU;IACrC,GAAIN,6BAA6B;MAAEO,eAAeP;IAA2B,IAAI,CAAC;IAClF,GAAItE,UAAUkE,cAAcY,eAAe;MAAEA,cAAc9E,UAAUkE,cAAcY;IAAa,IAAI,CAAC;IACrG,GAAI7E,QAAQ;MAAEA;IAAM,IAAI,CAAC;EAC3B,CAAA,IACAiB;AAQN,QAAM+D,gBAAgB,IAAIC,iCAAiC7D,mBAAmBc,kBAAAA;AAE9E,SAAO;IACLhB;;IAEAuD;IACAS,eAAe;MACbC,KAAK7C;MACL8C,MAAM1C;MACN2C,UAAU,IAAIC,4BAA4BlE,mBAAmBrB,UAAUyC,QAAQ;MAC/E+C,SAAS,IAAIC,2BAA2BpE,mBAAmBrB,UAAUyC,QAAQ;MAC7EiD,MAAM,IAAIC,yBAAyBtE,iBAAAA;MACnCuE,cAAc,IAAIC,oBAAoBxE,mBAAmBE,iBAAAA;MACzD0D;;MAEAa,sBAAsB,IAAIC,4BAA4B5D,kBAAAA;;;MAGtD6D,QAAQ,IAAIC,0BAA0B5E,mBAAmBc,oBAAoBnC,UAAUe,aAAa;MACpGmF,uBAAuB,IAAIC,6BAA6BhE,oBAAoBnC,UAAUe,aAAa;MACnGqF,QAAQ,IAAIC,0BAA0BhF,iBAAAA;;;MAGtC0D;MACAuB,YAAYjF;MACZE;MACAY;IACF;;;;;;IAMA+B,eACElE,UAAUkE,iBAAiBI,6BACvB;MAAEiC,aAAatC;MAAmBY,eAAeP;IAA2B,IAC5EpD;IACNsF,UAAU/E;;;;;IAKVX,cAAcD,sBACV,IAAI4F,yBACFzG,UAAUe,eACV,MAAM,GAAG2F,KAAKC,IAAG,CAAA,IAAMC,KAAKC,OAAM,EAAGC,SAAS,EAAA,EAAIC,MAAM,CAAA,CAAA,EAAI,IAE9D7F;IACJ8F,SAAS;MACPC,SAASpE;;MAETqE,iBAAiB,wBAACC,UAChBC,uBAAuB;QAAE,GAAGD;QAAOE,eAAevH,OAAOwH;MAAmB,CAAA,GAD7D;IAEnB;;IAEAC,OAAOvE,kBACH;MACEwE,aAAaxE;MACbE,oBAAoB,wBAACuE,MAAsBC,YACzC1E,gBAAgBE,mBAAmBuE,MAAMC,OAAAA,GADvB;MAEpBC,KAAK,IAAIC,oBAAoB3F,mBAAAA;MAC7ByD,MAAM,IAAImC,sBAAsB5F,mBAAAA;MAChC6F,QAAQ,IAAIC,uBAAuB9F,mBAAAA;MACnC+F,MAAM,IAAIC,qBAAqBhG,mBAAAA;MAC/B+D,QAAQ,IAAIkC,uBAAuBjG,mBAAAA;MACnCkG,eAAe,IAAIC,4BAA4BnG,mBAAAA;MAC/CqE,YAAYrE;;;;MAIZoG,iBAAiBhG;IACnB,IACAnB;IACJ0C,SAAS5D,UAAU4D;EACrB;AACF;AAxPgBjE;;;AmC7JhB,SAAS2I,YAAY;AAgBd,IAAMC,iCAAiC;AAGvC,SAASC,+BAAAA;AACd,SAAOC,KAAKC,WAAW,YAAA;AACzB;AAFgBF;AAWhB,eAAsBG,0BAA0BC,QAAuC;AACrF,QAAMA,OAAOC,QAAQD,OAAOE,IAAa;IACvCC,kBAAkBP,6BAAAA;IAClBQ,iBAAiBT;EACnB,CAAA;AACF;AALsBI;;;AC9BtB,SAASM,kBAAkB;AAoB3B,IAAMC,qBAAqB;AAUpB,IAAMC,SAAN,MAAMA;EA9Bb,OA8BaA;;;;EACMC,iBAAiB,oBAAIC,IAAAA;;;EAGrBC,qBAAqB,oBAAID,IAAAA;;;EAGzBE,aAAaC,WAAAA;EAE9B,YAA6BC,OAAuB;SAAvBA,QAAAA;EAAwB;EAErDC,KAAKC,SAAiBC,OAAeC,SAAwC;AAC3E,SAAKC,cAAcH,SAASC,OAAOC,OAAAA;AACnC,QAAI,KAAKJ,OAAO;AACd,YAAMM,WAA0B;QAAEC,UAAU,KAAKT;QAAYK;QAAOC;MAAQ;AAC5E,WAAKJ,MAAMQ,QAAQN,SAASO,KAAKC,UAAUJ,QAAAA,CAAAA,EAAWK,MAAM,MAAMC,MAAAA;IACpE;EACF;EAEQP,cAAcH,SAAiBC,OAAeC,SAAwC;AAC5F,UAAMS,YAAY,KAAKlB,eAAemB,IAAIZ,OAAAA;AAC1C,QAAI,CAACW,UAAW;AAChB,eAAWE,YAAYF,UAAWE,UAASZ,OAAOC,OAAAA;EACpD;EAEA,MAAMY,UAAUd,SAAiBa,UAA4C;AAC3E,QAAI,CAAC,KAAKpB,eAAesB,IAAIf,OAAAA,EAAU,MAAKP,eAAeuB,IAAIhB,SAAS,oBAAIiB,IAAAA,CAAAA;AAC5E,SAAKxB,eAAemB,IAAIZ,OAAAA,EAAUkB,IAAIL,QAAAA;AAEtC,QAAI,KAAKf,SAAS,CAAC,KAAKH,mBAAmBoB,IAAIf,OAAAA,GAAU;AACvD,WAAKL,mBAAmBqB,IACtBhB,SACA,KAAKF,MAAMgB,UAAUd,SAAS,CAACmB,YAAAA;AAC7B,YAAI;AACF,gBAAMf,WAAWG,KAAKa,MAAMD,OAAAA;AAE5B,cAAIf,SAASC,aAAa,KAAKT,WAAY;AAC3C,eAAKO,cAAcH,SAASI,SAASH,OAAOG,SAASF,OAAO;QAC9D,QAAQ;QAER;MACF,CAAA,CAAA;IAEJ;AAEA,WAAO,MAAA;AACL,YAAMS,YAAY,KAAKlB,eAAemB,IAAIZ,OAAAA;AAC1CW,iBAAWU,OAAOR,QAAAA;AAGlB,UAAIF,aAAaA,UAAUW,SAAS,GAAG;AACrC,aAAK7B,eAAe4B,OAAOrB,OAAAA;AAC3B,cAAMuB,qBAAqB,KAAK5B,mBAAmBiB,IAAIZ,OAAAA;AACvD,aAAKL,mBAAmB0B,OAAOrB,OAAAA;AAC/BuB,4BAAoBC,KAAK,CAACC,gBAAgBA,YAAAA,CAAAA,EAAehB,MAAM,MAAMC,MAAAA;MACvE;IACF;EACF;AACF;AAIA,eAAsBgB,eACpBC,OACAC,WACAC,gBAAsB;AAEtB,QAAMC,SAASjC,WAAAA;AACf,QAAM8B,MAAMX,IAAI,cAAcc,MAAAA,IAAUvB,KAAKC,UAAU;IAAEoB;IAAWC;EAAe,CAAA,GAAItC,kBAAAA;AACvF,SAAOuC;AACT;AARsBJ;AAUtB,eAAsBK,gBACpBJ,OACAG,QAAc;AAEd,QAAME,MAAM,MAAML,MAAMf,IAAI,cAAckB,MAAAA,EAAQ;AAClD,MAAI,CAACE,IAAK,QAAO;AACjB,QAAML,MAAMN,OAAO,cAAcS,MAAAA,EAAQ;AACzC,SAAOvB,KAAKa,MAAMY,GAAAA;AACpB;AARsBD;","names":["path","fileURLToPath","getFilename","fileURLToPath","url","getDirname","path","dirname","__dirname","WhatsAppMessageProvider","FLOW_ACTION_KIND","and","desc","eq","sql","sql","pgSchema","uuid","varchar","jsonb","timestamp","text","integer","boolean","index","uniqueIndex","metaWhatsAppSchema","sessions","table","id","primaryKey","defaultRandom","companyId","notNull","whatsappNumber","length","currentState","default","flowKey","currentNodeId","context","$type","mode","assignedUserId","humanRequestedAt","withTimezone","lastInboundAt","lastAgentReadAt","lastActivity","defaultNow","createdAt","updatedAt","on","messages","sessionId","references","onDelete","direction","sender","agentUserId","type","content","payload","waMessageId","status","readAt","moderationFlagged","moderationTerms","transcriptionStatus","transcriptionText","transcriptionLanguage","transcriptionEngine","where","documents","messageId","uploadId","filename","mimeType","sizeBytes","sha256","source","linkedAt","flowGraphs","key","label","startNodeId","nodes","version","showInMenu","menuOptionLabel","flowMedia","nodeId","caption","sortOrder","active","metaMediaIds","settings","templateName","templateLanguage","templateVariables","welcomeMessage","farewellMessage","transcriptionEnabled","transcriptionMode","DEFAULT_LIMIT","conversationSummaryProjection","lastContent","sql","messages","sessions","lastDirection","unread","sessionContextPatch","patch","context","JSON","stringify","SessionRepository","db","getContext","companyId","whatsappNumber","row","select","from","where","and","eq","limit","getOrCreate","startState","existing","created","insert","values","currentState","onConflictDoUpdate","target","set","lastActivity","returning","setState","state","update","undefined","updatedAt","patchContext","readContext","setFlowPosition","flowKey","currentNodeId","touchInbound","lastInboundAt","hoursSinceLastInbound","session","Date","now","getTime","setMode","mode","assignedUserId","clearHumanRequest","humanRequestedAt","takeover","agentUserId","release","deleteByNumber","delete","requestHuman","markRead","lastAgentReadAt","markAllRead","userId","result","Number","rowCount","listByContextFilters","filters","offset","page","conditions","waitingHuman","push","search","rows","id","orderBy","desc","map","lastAt","toISOString","exportConversation","sessionId","createdAt","and","eq","lt","DEFAULT_LIMIT","MessageRepository","db","insertMessage","params","values","companyId","sessionId","whatsappNumber","direction","sender","agentUserId","type","content","payload","waMessageId","status","moderationFlagged","moderationTerms","created","insert","messages","onConflictDoNothing","returning","updateMessageStatus","updated","update","set","readAt","Date","where","and","eq","saveTranscriptionByWaMessageId","transcriptionStatus","text","undefined","transcriptionText","language","transcriptionLanguage","engine","transcriptionEngine","findById","messageId","found","select","from","id","limit","saveTranscription","listByConversation","conditions","before","push","lt","createdAt","orderBy","and","eq","isNotNull","sql","flowGraphNodesSchema","InvalidFlowGraphError","Error","key","validationMessage","name","toContractGraph","row","parsed","flowGraphNodesSchema","safeParse","nodes","success","error","message","label","startNodeId","version","data","OptimisticLockError","FlowGraphRepository","db","cache","get","companyId","cached","read","select","from","flowGraphs","where","and","eq","limit","undefined","graph","write","list","rows","map","nodeCount","Object","keys","length","showInMenu","menuOptionLabel","updatedAt","toISOString","assertValidNodes","create","created","insert","values","returning","createdGraph","invalidate","save","expectedVersion","update","set","Date","delete","getLiveFlowPositions","flowKey","sessions","nodeId","currentNodeId","count","sql","mode","isNotNull","groupBy","KEY_PREFIX","DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS","FlowGraphCache","provider","ttlSeconds","keyFor","companyId","flowKey","read","cached","get","undefined","JSON","parse","write","graph","set","key","stringify","invalidate","delete","eq","toContractSettings","row","templateName","templateLanguage","templateVariables","welcomeMessage","farewellMessage","transcriptionEnabled","transcriptionMode","EMPTY_SETTINGS","SettingsRepository","db","get","companyId","select","from","settings","where","eq","limit","save","update","current","merged","insert","values","onConflictDoUpdate","target","set","updatedAt","Date","returning","resolveTemplateVariables","context","map","token","key","replace","value","undefined","String","LogMessageUseCase","sessionRepository","messageRepository","realtime","moderator","execute","params","session","getOrCreate","companyId","whatsappNumber","startState","saved","insertMessage","sessionId","id","moderationOf","undefined","direction","touchInbound","emit","sender","text","content","trim","verdict","inspect","moderationFlagged","isOffensive","moderationTerms","matchedTerms","WindowExpiredError","WHATSAPP_WINDOW_HOURS","SendMessageUseCase","channel","sessionRepository","logMessage","objectStorage","documentRepository","assertWithinWindow","companyId","whatsappNumber","hours","hoursSinceLastInbound","undefined","WindowExpiredError","sendText","params","externalMessageId","body","execute","direction","sender","agentUserId","type","content","waMessageId","status","startState","sendMedia","to","buffer","mimeType","filename","caption","uploadId","upload","key","Date","now","saved","mediaTypeFor","payload","link","sessionId","messageId","id","sizeBytes","length","source","sendTemplate","templateName","languageCode","bodyParameters","startsWith","TakeoverConversationUseCase","sessionRepository","realtime","execute","params","takeover","companyId","whatsappNumber","agentUserId","emit","mode","assignedUserId","ReleaseConversationUseCase","sessionRepository","realtime","execute","params","release","companyId","whatsappNumber","emit","mode","assignedUserId","ListConversationsUseCase","sessionRepository","execute","params","listByContextFilters","companyId","filters","SessionNotFoundError","ListMessagesUseCase","sessionRepository","messageRepository","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","repoParams","sessionId","id","limit","before","listByConversation","SessionNotFoundError","ListConversationDocumentsUseCase","sessionRepository","documentRepository","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","rows","total","listByConversation","sessionId","id","search","sources","length","sortDirection","page","limit","documents","map","row","uploadId","filename","mimeType","sizeBytes","source","linkedAt","toISOString","ListCompanyDocumentsUseCase","documentRepository","execute","params","rows","total","listByCompany","companyId","search","sources","length","sortDirection","page","limit","documents","map","row","id","uploadId","conversationId","whatsappNumber","filename","mimeType","sizeBytes","source","linkedAt","toISOString","SessionNotFoundError","DeleteConversationUseCase","sessionRepository","documentRepository","objectStorage","execute","params","session","getContext","companyId","whatsappNumber","SessionNotFoundError","uploadIds","listUploadIdsBySession","id","length","delete","Error","failedObjects","deletedObjects","uploadId","push","deleteByNumber","DEFAULT_BATCH_SIZE","HOURS_IN_DAY","MILLISECONDS_IN_HOUR","PurgeExpiredDocumentsUseCase","documentRepository","objectStorage","execute","params","delete","Error","reference","now","Date","olderThan","getTime","retentionDays","expired","listExpired","companyId","batchSize","failed","purged","document","uploadId","push","deleteById","id","and","asc","count","desc","eq","ilike","inArray","lt","or","DEFAULT_LIMIT","companyDocumentSearch","search","term","trim","undefined","digits","replace","byFilename","ilike","documents","filename","or","sessions","whatsappNumber","DocumentRepository","db","link","params","values","companyId","sessionId","messageId","uploadId","mimeType","sizeBytes","sha256","source","created","insert","onConflictDoNothing","returning","listByConversation","filters","eq","push","sources","length","inArray","where","and","limit","page","orderBy","sortDirection","asc","linkedAt","desc","rows","counted","Promise","all","select","from","offset","value","count","total","listByCompany","id","innerJoin","findByUploadId","row","listUploadIdsBySession","map","listExpired","olderThan","lt","deleteById","delete","SessionNotFoundError","ExportConversationUseCase","sessionRepository","execute","params","result","exportConversation","companyId","whatsappNumber","SessionNotFoundError","GetFlowGraphUseCase","flowGraphRepository","execute","params","get","companyId","key","ListFlowGraphsUseCase","list","CreateFlowGraphUseCase","create","SaveFlowGraphUseCase","save","graph","expectedVersion","DeleteFlowGraphUseCase","delete","GetLiveFlowPositionsUseCase","getLiveFlowPositions","isCrossFlowTarget","crossFlowKey","DEFAULT_MAX_STEPS","evaluateCondition","node","context","conditionContextKey","conditionOperator","conditionValue","actual","expected","String","includes","actualNum","Number","expectedNum","isNaN","resolveNext","answerId","next","undefined","byAnswer","default","FlowInterpreter","actionHandlers","Map","registerFlowAction","kind","handler","set","step","input","graph","nodes","currentNodeId","type","branch","moveTo","actionKind","get","result","session","channel","contextAfterAction","nextNodeId","isChoice","questionType","userAnswer","nodeId","updatedContext","contextKey","isCrossFlowTarget","flowKey","crossFlowKey","run","options","maxSteps","visited","current","steps","push","mediaTypeFor","mimeType","startsWith","sendAttachment","params","attachment","channel","to","cache","common","filename","caption","undefined","cacheKey","flowMediaId","id","senderKey","knownMediaId","store","get","sendMedia","mediaId","clear","buffer","objectStorage","getObject","uploadId","result","set","createSendMediaAction","node","session","flowKey","location","companyId","nodeId","attachments","flowMediaRepository","listActive","externalMessageId","whatsappNumber","mediaIdCache","logMessage","execute","direction","sender","agentUserId","type","content","payload","waMessageId","status","startState","error","onError","PRODUCT_LIST_LIMIT","ITEMS","SECTIONS","createSendProductListAction","params","node","session","channel","sendProductList","actionParams","available","listAvailableProducts","catalog","catalogId","search","length","bodyText","externalMessageId","to","whatsappNumber","headerText","body","footerText","sections","title","sectionTitle","retailerIds","map","product","retailerId","logMessage","execute","companyId","direction","sender","agentUserId","type","content","payload","kind","productCount","waMessageId","status","startState","error","onError","flowKey","nodeId","id","products","listProducts","filter","availability","slice","and","asc","eq","sql","FlowMediaRepository","db","locationFilter","location","and","eq","flowMedia","companyId","flowKey","nodeId","listActive","select","from","where","active","orderBy","asc","sortOrder","createdAt","listAll","attach","params","row","insert","values","uploadId","filename","mimeType","sizeBytes","caption","onConflictDoUpdate","target","set","updatedAt","sql","returning","update","undefined","id","detach","delete","detachRemovedNodes","condition","existingNodeIds","length","eq","sql","FlowMediaIdRepository","db","get","params","rows","select","ids","flowMedia","metaMediaIds","from","where","eq","id","flowMediaId","limit","senderKey","set","update","sql","mediaId","clear","WhatsAppWindowExpiredError","ProviderWindowExpiredError","WindowExpiredError","PREVIEW_MEDIA_ID_PREFIX","toPreviewMediaId","resolvePreviewUploadId","WhatsAppChannelAdapter","messages","previewMedia","translateErrors","operation","error","ProviderWindowExpiredError","WindowExpiredError","sendText","to","body","result","externalMessageId","waMessageId","sendMedia","params","mediaId","sendTemplate","sendInteractiveList","bodyText","buttonText","buttonLabel","sections","title","rows","sendInteractiveButtons","buttons","sendProductList","sendProductListMessage","headerText","footerText","fetchMediaAsBase64","uploadId","isEnabled","resolvePreviewUploadId","undefined","buffer","objectStorage","getObject","data","toString","mimeType","defaultMimeType","whatsAppWebhookPayloadSchema","resolveContactProfileName","whatsAppTemplateStatusUpdateSchema","whatsAppPhoneNumberQualityUpdateSchema","WHATSAPP_WEBHOOK_FIELDS","WEBHOOK_CLAIM_TTL_SECONDS","WEBHOOK_NONCE_TTL_SECONDS","buildWebhookDeliveryKey","isValidWebhookChallenge","isValidWebhookSignature","InvalidWebhookSignatureError","WEBHOOK_NONCE_NAMESPACE","verifyWebhookChallenge","params","isValidWebhookChallenge","InvalidWebhookSignatureError","challenge","verifyWebhookSignature","isValidWebhookSignature","deliveryKey","signatureHeader","buildWebhookDeliveryKey","namespace","claimWebhookDelivery","nonceStore","setIfAbsent","ttlSeconds","WEBHOOK_CLAIM_TTL_SECONDS","confirmWebhookDelivery","confirm","WEBHOOK_NONCE_TTL_SECONDS","eq","and","AudioNotIngestedError","MessageNotAudioError","TranscriptionDisabledError","TRANSCRIPTION_STATUS","PENDING","DONE","FAILED","UNSUPPORTED","TRANSCRIPTION_MODE","AUTO","ON_DEMAND","isRetriableTranscriptionError","error","isRetriable","isUnsupportedTranscriptionError","name","transcriptionRetryAfterSeconds","undefined","retryAfter","retryAfterSeconds","isAudioMimeType","mimeType","trim","toLowerCase","startsWith","TranscribeAudioUseCase","dependencies","execute","params","message","messageRepository","findById","companyId","messageId","Error","transcriptionStatus","TRANSCRIPTION_STATUS","DONE","force","status","text","transcriptionText","language","transcriptionLanguage","engine","transcriptionEngine","alreadyTranscribed","resolvePolicy","policy","isEnabled","TranscriptionDisabledError","audio","extractAudioReference","buffer","objectStorage","getObject","uploadId","transcribeBuffer","mimeType","context","result","transcriber","transcribe","languageHint","saveTranscription","error","persistFailure","resolveFailureStatus","PENDING","retryAfterSeconds","transcriptionRetryAfterSeconds","hooks","onTranscriptionDeferred","whatsappNumber","undefined","reason","isUnsupportedTranscriptionError","UNSUPPORTED","isRetriableTranscriptionError","FAILED","payload","mime_type","isAudioMimeType","MessageNotAudioError","id","type","length","AudioNotIngestedError","IngestInboundMediaUseCase","db","channel","objectStorage","documentRepository","transcription","execute","params","message","select","from","messages","where","and","eq","companyId","id","messageId","limit","Error","payload","sourceMediaId","uploadId","String","alreadyIngested","data","mimeType","fetchMediaAsBase64","buffer","Buffer","upload","key","updatedPayload","sizeBytes","length","filename","update","set","link","sessionId","source","sender","transcribeIfAuto","context","undefined","isAudioMimeType","policy","resolvePolicy","isEnabled","mode","TRANSCRIPTION_MODE","AUTO","current","messageRepository","findById","transcriptionStatus","TRANSCRIPTION_STATUS","DONE","result","transcriber","transcribe","languageHint","saveTranscription","status","text","language","engine","error","recordTranscriptionFailure","resolveFailureStatus","PENDING","retryAfterSeconds","transcriptionRetryAfterSeconds","hooks","onTranscriptionDeferred","whatsappNumber","reason","extractMediaDescriptor","media","mime_type","buildInboundJobId","job","kind","message","id","status","toSessionContract","row","companyId","whatsappNumber","currentState","flowKey","currentNodeId","context","mode","assignedUserId","humanRequestedAt","toISOString","lastInboundAt","lastAgentReadAt","lastActivity","createdAt","updatedAt","extractAnswer","interactive","button_reply","list_reply","text","body","InboundEffectsDispatcher","params","run","runMessageEffects","runStatusEffects","savedMessageId","media","hooks","onMediaReceived","messageId","from","sourceMediaId","mimeType","filename","sessionRow","sessionRepository","getContext","outcome","onMessageReceived","profileName","undefined","onStatusUpdate","ProcessInboundDispatchUseCase","dispatcher","execute","EMPTY_RESULT","messagesProcessed","statusesProcessed","ignoredForeignNumber","accountEventsProcessed","unhandledEvents","extractContent","message","text","body","interactive","button_reply","title","id","list_reply","order","items","product_items","reduce","sum","item","quantity","location","label","name","address","image","caption","document","extractPayload","payload","audio","video","sticker","context","referred_product","Object","keys","length","ACCOUNT_EVENT_FIELDS","WHATSAPP_WEBHOOK_FIELDS","TEMPLATE_STATUS_UPDATE","PHONE_NUMBER_QUALITY_UPDATE","isAccountEventField","field","undefined","includes","ReceiveWebhookUseCase","params","dispatcher","InboundEffectsDispatcher","sessionRepository","hooks","realtime","execute","input","verifyWebhookSignature","rawBody","signatureHeader","appSecret","claimed","claimWebhookDelivery","nonceStore","duplicate","rawText","toString","parsed","whatsAppWebhookPayloadSchema","safeParse","JSON","parse","success","onUnhandledWebhookEvent","reason","value","data","entry","change","changes","handled","handleAccountEvent","targetNumber","metadata","phone_number_id","phoneNumberId","messages","profileName","resolveContactProfileName","contacts","from","handleMessage","companyId","status","statuses","handleStatus","carriedConversationData","message_echoes","confirmWebhookDelivery","update","whatsAppTemplateStatusUpdateSchema","onTemplateStatusUpdate","quality","whatsAppPhoneNumberQualityUpdateSchema","onPhoneNumberQualityUpdate","saved","logMessage","whatsappNumber","direction","sender","type","content","waMessageId","startState","media","extractMediaDescriptor","dispatch","kind","savedMessageId","receivedAt","Date","now","updated","messageRepository","updateMessageStatus","emit","job","inboundQueue","enqueue","jobId","buildInboundJobId","run","createTranscriptionPolicyResolver","dependencies","resolveTranscriptionPolicy","companyId","settings","settingsRepository","get","isEnabled","transcriptionEnabled","defaults","mode","normalizeMode","transcriptionMode","value","TRANSCRIPTION_MODE","AUTO","ON_DEMAND","undefined","StorePreviewMediaUseCase","objectStorage","generateKeySuffix","execute","params","key","companyId","uploadId","upload","buffer","mimeType","mediaId","toPreviewMediaId","createMetaWhatsAppModule","params","db","config","nonceStore","providers","hooks","startState","flowEngineEnabled","features","flowEngine","messageProvider","WhatsAppMessageProvider","accessToken","phoneNumberId","wabaId","apiVersion","baseUrl","previewMediaSupport","previewMedia","objectStorage","getObject","isEnabled","undefined","channel","WhatsAppChannelAdapter","sessionRepository","SessionRepository","messageRepository","MessageRepository","settingsRepository","SettingsRepository","flowGraphCacheFeature","flowGraphCache","cache","FlowGraphCache","ttlSeconds","DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS","flowGraphRepository","FlowGraphRepository","documentRepository","DocumentRepository","flowMediaRepository","FlowMediaRepository","logMessage","LogMessageUseCase","realtime","moderator","sendMessage","SendMessageUseCase","receiveWebhook","ReceiveWebhookUseCase","appSecret","flowInterpreter","FlowInterpreter","registerFlowAction","FLOW_ACTION_KIND","SEND_MEDIA","createSendMediaAction","mediaIdCache","store","FlowMediaIdRepository","senderKey","onError","onFlowMediaError","catalog","catalogId","SEND_PRODUCT_LIST","createSendProductListAction","onFlowProductListError","transcriptionMode","transcription","mode","TRANSCRIPTION_MODE","ON_DEMAND","resolveTranscriptionPolicy","createTranscriptionPolicyResolver","defaults","isEnabledByDefault","ingestInboundMedia","IngestInboundMediaUseCase","transcriber","resolvePolicy","languageHint","transcribeAudio","TranscribeAudioUseCase","listDocuments","ListConversationDocumentsUseCase","conversations","log","send","takeover","TakeoverConversationUseCase","release","ReleaseConversationUseCase","list","ListConversationsUseCase","listMessages","ListMessagesUseCase","listCompanyDocuments","ListCompanyDocumentsUseCase","delete","DeleteConversationUseCase","purgeExpiredDocuments","PurgeExpiredDocumentsUseCase","export","ExportConversationUseCase","repository","defaultMode","settings","StorePreviewMediaUseCase","Date","now","Math","random","toString","slice","webhook","receive","verifyChallenge","query","verifyWebhookChallenge","expectedToken","webhookVerifyToken","flows","interpreter","kind","handler","get","GetFlowGraphUseCase","ListFlowGraphsUseCase","create","CreateFlowGraphUseCase","save","SaveFlowGraphUseCase","DeleteFlowGraphUseCase","livePositions","GetLiveFlowPositionsUseCase","mediaRepository","join","META_WHATSAPP_MIGRATIONS_TABLE","metaWhatsAppMigrationsFolder","join","__dirname","runMetaWhatsAppMigrations","params","migrate","db","migrationsFolder","migrationsTable","randomUUID","TICKET_TTL_SECONDS","SseHub","localListeners","Map","relaySubscriptions","instanceId","randomUUID","relay","emit","channel","event","payload","dispatchLocal","envelope","originId","publish","JSON","stringify","catch","undefined","listeners","get","listener","subscribe","has","set","Set","add","message","parse","delete","size","pendingUnsubscribe","then","unsubscribe","issueSseTicket","store","companyId","whatsappNumber","ticket","redeemSseTicket","raw"]}
|