# Technical Implementation Blueprint: VetFlow OS
**AI Operations Platform for Veterinary Clinics**
*Versão de Engenharia: v1.0.0*

---

## 1. Visão Geral da Arquitetura de Software

O **VetFlow OS** é estruturado como um **Monólito Modular Event-Driven** com suporte nativo a **Multi-tenancy**, execução durável via **Temporal** e governança rígida sobre agentes de IA por meio de um **Agent Runtime** e **Policy Engine**.

```mermaid
flowchart TB
    subgraph Channels ["Experience Layer"]
        WA["WhatsApp Cloud API"]
        WEB["Next.js Web App / Dashboard"]
    end

    subgraph API ["NestJS Application Core"]
        GW["Webhook Gateway & API Controller"]
        Auth["Auth & Tenant RLS Context"]
        OutboxWriter["Transactional Outbox Writer"]
    end

    subgraph Storage ["Data & Event Persistence"]
        PG[("PostgreSQL\n(Multi-tenant + RLS + pgvector)")]
        Redis[("Redis\n(Cache & Distributed Locks)")]
    end

    subgraph TemporalCluster ["Workflow & Execution Layer"]
        TemporalServer["Temporal Cluster"]
        WorkflowWorker["Workflow Worker\n(Durable State Machines)"]
        ActivityWorker["Activity Worker\n(External I/O & Retries)"]
    end

    subgraph Intelligence ["Intelligence & Agent Runtime"]
        Orchestrator["Agent Orchestrator"]
        PolicyEngine["Policy Engine\n(Guardrails & RLS Validation)"]
        ToolRegistry["Tool Registry\n(Scoped Tools)"]
        AIGateway["AI Gateway\n(Claude 3.5 / GPT-4o / Fallback)"]
    end

    WA --> GW
    WEB --> GW
    GW --> Auth
    Auth --> OutboxWriter
    OutboxWriter --> PG
    
    PG -.->|CDC / Outbox Poller| TemporalServer
    TemporalServer --> WorkflowWorker
    WorkflowWorker --> ActivityWorker
    ActivityWorker --> Orchestrator
    
    Orchestrator --> PolicyEngine
    PolicyEngine --> ToolRegistry
    PolicyEngine --> AIGateway
    ToolRegistry --> PG
    ToolRegistry --> WA
```

---

## 2. Estrutura do Monorepo (`pnpm` + `Turborepo`)

```bash
vetflow/
├── apps/
│   ├── api/                    # NestJS REST & Webhook Gateway
│   ├── web/                    # Next.js 15 (App Router, Tailwind, Shadcn/ui)
│   └── worker/                 # Temporal Workers (Workflows & Activities)
├── packages/
│   ├── database/               # Drizzle ORM schemas, migrations, seeders, RLS helpers
│   ├── domain/                 # Entidades, Value Objects, Domain Events e regras puras
│   ├── contracts/              # Schemas Zod compartilhados e DTOs de API
│   ├── agent-runtime/          # Context Builder, Policy Engine, Tool Registry, LLM Gateway
│   ├── temporal-workflows/     # Definições de Workflows, Atividades e Sinais do Temporal
│   ├── integrations/           # WhatsApp Cloud API, Asaas, Provedores de Prontuário
│   ├── logger/                 # OpenTelemetry, Winston estruturado e Sentry wrapper
│   └── tsconfig/               # Configurações TypeScript base compartilhadas
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── docker-compose.yml          # PostgreSQL + Redis + Temporal dev server
```

---

## 3. Schema do Banco de Dados (Drizzle ORM / PostgreSQL)

O modelo de dados implementa isolamento estrito via `tenant_id` e políticas de Row-Level Security (RLS). Valores monetários são armazenados estritamente em **centavos inteiros** (`integer`).

### 3.1 DDL e Schemas Principais (`packages/database/src/schema.ts`)

```typescript
import { 
  pgTable, 
  uuid, 
  varchar, 
  text, 
  timestamp, 
  integer, 
  boolean, 
  jsonb, 
  index, 
  pgEnum 
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

// Enums
export const userRoleEnum = pgEnum('user_role', ['owner', 'admin', 'manager', 'receptionist', 'veterinarian']);
export const opportunityStatusEnum = pgEnum('opportunity_status', ['open', 'won', 'lost', 'expired']);
export const quoteStatusEnum = pgEnum('quote_status', ['draft', 'presented', 'accepted', 'rejected', 'expired']);
export const conversationStatusEnum = pgEnum('conversation_status', ['active', 'paused', 'escalated_to_human', 'closed']);
export const messageSenderEnum = pgEnum('message_sender', ['tutor', 'agent', 'human_operator', 'system']);
export const outboxStatusEnum = pgEnum('outbox_status', ['pending', 'processing', 'published', 'failed']);

// --- 1. Tenants & Clinics ---
export const tenants = pgTable('tenants', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  documentNumber: varchar('document_number', { length: 32 }).notNull().unique(), // CNPJ
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});

export const clinics = pgTable('clinics', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  name: varchar('name', { length: 255 }).notNull(),
  phone: varchar('phone', { length: 32 }).notNull(),
  settings: jsonb('settings').default({}).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_clinics_tenant').on(table.tenantId),
]);

// --- 2. Identity & RBAC ---
export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
  role: userRoleEnum('role').default('receptionist').notNull(),
  passwordHash: text('password_hash').notNull(),
  isActive: boolean('is_active').default(true).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_users_tenant').on(table.tenantId),
]);

// --- 3. CRM: Tutors & Patients ---
export const tutors = pgTable('tutors', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  clinicId: uuid('clinic_id').references(() => clinics.id, { onDelete: 'cascade' }).notNull(),
  name: varchar('name', { length: 255 }).notNull(),
  phone: varchar('phone', { length: 32 }).notNull(), // Formato E.164: +5511999999999
  cpf: varchar('cpf', { length: 14 }),
  optInCommercial: boolean('opt_in_commercial').default(true).notNull(),
  optInTransactional: boolean('opt_in_transactional').default(true).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_tutors_tenant_phone').on(table.tenantId, table.phone),
]);

export const patients = pgTable('patients', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  tutorId: uuid('tutorId').references(() => tutors.id, { onDelete: 'cascade' }).notNull(),
  name: varchar('name', { length: 255 }).notNull(),
  species: varchar('species', { length: 64 }).notNull(), // 'canine', 'feline', etc.
  breed: varchar('breed', { length: 128 }),
  birthDate: timestamp('birth_date', { withTimezone: true }),
  isDeceased: boolean('is_deceased').default(false).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_patients_tutor').on(table.tutorId),
]);

// --- 4. Commercial Domain: Opportunities & Quotes ---
export const opportunities = pgTable('opportunities', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  clinicId: uuid('clinic_id').references(() => clinics.id, { onDelete: 'cascade' }).notNull(),
  tutorId: uuid('tutor_id').references(() => tutors.id, { onDelete: 'cascade' }).notNull(),
  patientId: uuid('patient_id').references(() => patients.id, { onDelete: 'set null' }),
  title: varchar('title', { length: 255 }).notNull(), // Ex: "Cirurgia Tártaro + Consulta Especialista"
  status: opportunityStatusEnum('status').default('open').notNull(),
  estimatedValueInCents: integer('estimated_value_in_cents').default(0).notNull(),
  recoveredValueInCents: integer('recovered_value_in_cents').default(0).notNull(),
  attributedToAgent: boolean('attributed_to_agent').default(false).notNull(),
  lostReason: text('lost_reason'),
  closedAt: timestamp('closed_at', { withTimezone: true }),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_opportunities_tenant_status').on(table.tenantId, table.status),
  index('idx_opportunities_tutor').on(table.tutorId),
]);

export const quotes = pgTable('quotes', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  opportunityId: uuid('opportunity_id').references(() => opportunities.id, { onDelete: 'cascade' }).notNull(),
  code: varchar('code', { length: 64 }).notNull(), // Ex: "ORC-2026-0042"
  totalAmountInCents: integer('total_amount_in_cents').notNull(),
  status: quoteStatusEnum('status').default('presented').notNull(),
  presentedAt: timestamp('presented_at', { withTimezone: true }).defaultNow().notNull(),
  validUntil: timestamp('valid_until', { withTimezone: true }).notNull(),
  notes: text('notes'),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_quotes_opportunity').on(table.opportunityId),
]);

export const quoteItems = pgTable('quote_items', {
  id: uuid('id').defaultRandom().primaryKey(),
  quoteId: uuid('quote_id').references(() => quotes.id, { onDelete: 'cascade' }).notNull(),
  description: varchar('description', { length: 255 }).notNull(),
  quantity: integer('quantity').default(1).notNull(),
  unitPriceInCents: integer('unit_price_in_cents').notNull(),
  totalPriceInCents: integer('total_price_in_cents').notNull(),
});

// --- 5. Communication & Omnichannel ---
export const conversations = pgTable('conversations', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  clinicId: uuid('clinic_id').references(() => clinics.id, { onDelete: 'cascade' }).notNull(),
  tutorId: uuid('tutor_id').references(() => tutors.id, { onDelete: 'cascade' }).notNull(),
  externalThreadId: varchar('external_thread_id', { length: 128 }).notNull(), // WhatsApp Phone / WaID
  activeAgent: varchar('active_agent', { length: 64 }).default('revenue').notNull(),
  status: conversationStatusEnum('status').default('active').notNull(),
  lastInteractionAt: timestamp('last_interaction_at', { withTimezone: true }).defaultNow().notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_conversations_tenant_tutor').on(table.tenantId, table.tutorId),
  index('idx_conversations_external').on(table.externalThreadId),
]);

export const messages = pgTable('messages', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(),
  conversationId: uuid('conversation_id').references(() => conversations.id, { onDelete: 'cascade' }).notNull(),
  sender: messageSenderEnum('sender').notNull(),
  senderId: varchar('sender_id', { length: 128 }), // ID do agente ou user_id
  content: text('content').notNull(),
  externalMessageId: varchar('external_message_id', { length: 128 }).unique(), // wamid.xxx
  metadata: jsonb('metadata').default({}).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
  index('idx_messages_conversation').on(table.conversationId),
]);

// --- 6. Transactional Outbox (Event Persistence) ---
export const outboxEvents = pgTable('outbox_events', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull(),
  aggregateType: varchar('aggregate_type', { length: 64 }).notNull(), // 'opportunity', 'quote', etc.
  aggregateId: uuid('aggregate_id').notNull(),
  eventType: varchar('event_type', { length: 128 }).notNull(), // 'quote.presented'
  payload: jsonb('payload').notNull(),
  status: outboxStatusEnum('status').default('pending').notNull(),
  retryCount: integer('retry_count').default(0).notNull(),
  lastError: text('last_error'),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  processedAt: timestamp('processed_at', { withTimezone: true }),
}, (table) => [
  index('idx_outbox_pending').on(table.status, table.createdAt),
]);

// --- 7. Audit & AI Tool Execution Log ---
export const agentRuns = pgTable('agent_runs', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull(),
  conversationId: uuid('conversation_id').references(() => conversations.id).notNull(),
  agentName: varchar('agent_name', { length: 64 }).notNull(),
  model: varchar('model', { length: 128 }).notNull(),
  promptTokens: integer('prompt_tokens').notNull(),
  completionTokens: integer('completion_tokens').notNull(),
  latencyMs: integer('latency_ms').notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});

export const toolCalls = pgTable('tool_calls', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull(),
  agentRunId: uuid('agent_run_id').references(() => agentRuns.id).notNull(),
  toolName: varchar('tool_name', { length: 128 }).notNull(),
  inputPayload: jsonb('input_payload').notNull(),
  outputPayload: jsonb('output_payload').notNull(),
  isError: boolean('is_error').default(false).notNull(),
  durationMs: integer('duration_ms').notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
```

---

## 4. Transactional Outbox & Event Contract

Para garantir que nenhum evento de negócio seja perdido em falhas transitórias, todo comando (ex: apresentar orçamento) persiste a alteração na tabela de negócio e insere o evento correspondente na tabela `outbox_events` na mesma transação atômica.

### 4.1 Contrato de Evento TypeScript (`packages/domain/src/events/commercial.ts`)

```typescript
export interface BaseDomainEvent<T = unknown> {
  eventId: string;
  tenantId: string;
  aggregateType: string;
  aggregateId: string;
  eventType: string;
  occurredAt: string; // ISO 8601
  payload: T;
}

export interface QuotePresentedEventPayload {
  quoteId: string;
  opportunityId: string;
  clinicId: string;
  tutorId: string;
  tutorName: string;
  tutorPhone: string;
  patientId?: string;
  patientName?: string;
  totalAmountInCents: number;
  validUntil: string;
  presentedAt: string;
}

export type QuotePresentedEvent = BaseDomainEvent<QuotePresentedEventPayload>;
```

---

## 5. Agent Runtime & Policy Engine

O **Agent Runtime** nunca expõe queries diretas ao modelo. Toda interação passa pelo contrato `AgentTool` com validação de permissões pré-execução e verificação de políticas sanitárias/éticas.

### 5.1 Contrato da Ferramenta (`packages/agent-runtime/src/tool.interface.ts`)

```typescript
import { z } from 'zod';

export interface AgentContext {
  tenantId: string;
  clinicId: string;
  conversationId: string;
  tutorId: string;
  tutorPhone: string;
  agentName: string;
  allowedPermissions: string[];
}

export interface ToolResult<T = unknown> {
  success: boolean;
  data?: T;
  error?: string;
}

export interface AgentTool<TInput = any, TOutput = any> {
  name: string;
  description: string;
  schema: z.ZodType<TInput>;
  requiredPermission: string;
  execute(input: TInput, ctx: AgentContext): Promise<ToolResult<TOutput>>;
}
```

### 5.2 Ferramentas do Revenue Agent (`packages/agent-runtime/src/tools/revenue-tools.ts`)

```typescript
import { z } from 'zod';
import { AgentTool, AgentContext, ToolResult } from '../tool.interface';

// 1. Tool: Obter detalhes do orçamento
export const GetQuoteDetailsSchema = z.object({
  quoteId: z.string().uuid(),
});

export class GetQuoteDetailsTool implements AgentTool<z.infer<typeof GetQuoteDetailsSchema>> {
  name = 'get_quote_details';
  description = 'Recupera os itens, valor total e data de validade de um orçamento apresentado.';
  schema = GetQuoteDetailsSchema;
  requiredPermission = 'commercial:quotes:read';

  async execute(input: z.infer<typeof GetQuoteDetailsSchema>, ctx: AgentContext): Promise<ToolResult> {
    // Busca restrita por tenantId e quoteId via Drizzle
    return {
      success: true,
      data: {
        quoteId: input.quoteId,
        totalAmountFormatted: 'R$ 850,00',
        items: ['Consulta Especialista Cardiologia', 'Ecocardiograma com Doppler'],
        status: 'presented',
        validUntil: '2026-11-15'
      }
    };
  }
}

// 2. Tool: Enviar mensagem aprovada de follow-up
export const SendFollowupMessageSchema = z.object({
  conversationId: z.string().uuid(),
  templateKey: z.enum(['quote_reminder_gentle', 'quote_expiration_warning', 'ask_clinical_doubt']),
  customParameters: z.record(z.string()),
});

export class SendFollowupMessageTool implements AgentTool<z.infer<typeof SendFollowupMessageSchema>> {
  name = 'send_approved_followup';
  description = 'Dispara uma mensagem de acompanhamento comercial via WhatsApp a partir de um template aprovado.';
  schema = SendFollowupMessageSchema;
  requiredPermission = 'commercial:followup:write';

  async execute(input: z.infer<typeof SendFollowupMessageSchema>, ctx: AgentContext): Promise<ToolResult> {
    // Invoca WhatsApp Integration Gateway com verificação de opt-out
    return {
      success: true,
      data: { messageDispatched: true, template: input.templateKey }
    };
  }
}

// 3. Tool: Escalar para atendimento humano (Handoff)
export const RequestHumanHandoffSchema = z.object({
  reason: z.enum(['clinical_inquiry', 'pricing_objection', 'unhappy_customer', 'explicit_request']),
  priority: z.enum(['normal', 'urgent']),
  summaryForAttendant: z.string().min(5),
});

export class RequestHumanHandoffTool implements AgentTool<z.infer<typeof RequestHumanHandoffSchema>> {
  name = 'request_human_handoff';
  description = 'Pausa a automação do agente e transfere imediatamente a conversa para a equipe humana da clínica.';
  schema = RequestHumanHandoffSchema;
  requiredPermission = 'communication:handoff:execute';

  async execute(input: z.infer<typeof RequestHumanHandoffSchema>, ctx: AgentContext): Promise<ToolResult> {
    return {
      success: true,
      data: { handoffCompleted: true, assignedQueue: 'reception_front' }
    };
  }
}
```

### 5.3 Policy Engine & Guardrails (`packages/agent-runtime/src/policy-engine.ts`)

```typescript
export interface PolicyCheckResult {
  allowed: boolean;
  blockReason?: string;
}

export class PolicyEngine {
  /**
   * Avalia pré-condições absolutas antes que qualquer ferramenta do agente seja executada.
   */
  static evaluate(toolName: string, requiredPermission: string, ctx: AgentContext, tutorStatus: { isDeceased?: boolean; optInCommercial: boolean }): PolicyCheckResult {
    // Regra 1: Pet falecido impede qualquer mensagem comercial
    if (tutorStatus.isDeceased) {
      return { allowed: false, blockReason: 'PACIENTE_FALECIDO_BLOQUEIO_COMPREENSIVO' };
    }

    // Regra 2: Opt-out comercial estrito (LGPD)
    if (!tutorStatus.optInCommercial && toolName === 'send_approved_followup') {
      return { allowed: false, blockReason: 'TUTOR_OPT_OUT_COMERCIAL' };
    }

    // Regra 3: Permissão RBAC do Agent Context
    if (!ctx.allowedPermissions.includes(requiredPermission)) {
      return { allowed: false, blockReason: `PERMISSAO_INSUFICIENTE: ${requiredPermission}` };
    }

    return { allowed: true };
  }
}
```

---

## 6. Especificação do Workflow Temporal: Recuperação de Orçamentos

O **Revenue Recovery Workflow** lida com esperas prolongadas (24h a 72h), gerencia sinais de resposta do cliente e executa com garantia de idempotência.

### 6.1 Definição do Workflow (`packages/temporal-workflows/src/workflows/revenue-recovery.workflow.ts`)

```typescript
import { 
  defineSignal, 
  setHandler, 
  sleep, 
  condition, 
  proxyActivities 
} from '@temporalio/workflow';
import type * as activities from '../activities/revenue-activities';

const {
  checkOpportunityStatusActivity,
  sendFollowupActivity,
  markOpportunityExpiredActivity
} = proxyActivities<typeof activities>({
  startToCloseTimeout: '1 minute',
  retry: {
    initialInterval: '5s',
    backoffCoefficient: 2,
    maximumAttempts: 4,
  },
});

// Sinais recebidos de Webhooks do WhatsApp ou CRM
export const quoteAcceptedSignal = defineSignal('quoteAccepted');
export const tutorRepliedSignal = defineSignal<[string]>('tutorReplied');
export const optOutRequestedSignal = defineSignal('optOutRequested');

export interface RevenueRecoveryWorkflowInput {
  tenantId: string;
  opportunityId: string;
  quoteId: string;
  tutorPhone: string;
}

export async function revenueRecoveryWorkflow(input: RevenueRecoveryWorkflowInput): Promise<string> {
  let isWon = false;
  let isOptOut = false;
  let tutorResponseText: string | null = null;

  setHandler(quoteAcceptedSignal, () => {
    isWon = true;
  });

  setHandler(optOutRequestedSignal, () => {
    isOptOut = true;
  });

  setHandler(tutorRepliedSignal, (msg: string) => {
    tutorResponseText = msg;
  });

  // Etapa 1: Aguarda 24 horas antes do primeiro contato (respeitando horário comercial)
  await sleep('24 hours');

  if (isWon || isOptOut) {
    return 'Workflow finalizado precocemente por conversão ou cancelamento.';
  }

  // Verifica se o orçamento ainda está aberto no banco
  const statusCheck1 = await checkOpportunityStatusActivity({
    tenantId: input.tenantId,
    opportunityId: input.opportunityId,
  });

  if (statusCheck1 !== 'open') {
    return `Oportunidade já não se encontra aberta (Status: ${statusCheck1}).`;
  }

  // Dispara o Follow-up 1
  await sendFollowupActivity({
    tenantId: input.tenantId,
    opportunityId: input.opportunityId,
    quoteId: input.quoteId,
    step: 1,
  });

  // Etapa 2: Aguarda até 48 horas por uma resposta do tutor
  const gotResponseInStep1 = await condition(() => isWon || isOptOut || tutorResponseText !== null, '48 hours');

  if (isWon || isOptOut || gotResponseInStep1) {
    return 'Tutor respondeu ou converteu no Step 1.';
  }

  // Follow-up 2: Aviso de término de validade
  await sendFollowupActivity({
    tenantId: input.tenantId,
    opportunityId: input.opportunityId,
    quoteId: input.quoteId,
    step: 2,
  });

  // Etapa 3: Aguarda mais 48 horas
  const finalWait = await condition(() => isWon || isOptOut || tutorResponseText !== null, '48 hours');

  if (!finalWait && !isWon && !isOptOut) {
    await markOpportunityExpiredActivity({
      tenantId: input.tenantId,
      opportunityId: input.opportunityId,
    });
    return 'Oportunidade expirada sem resposta.';
  }

  return 'Workflow de recuperação concluído.';
}
```

---

## 7. Módulos do Backend NestJS (`apps/api`)

A aplicação backend modulariza o domínio e isola regras de negócio:

```
apps/api/src/
├── app.module.ts
├── common/
│   ├── decorators/tenant.decorator.ts
│   ├── guards/tenant-auth.guard.ts
│   └── interceptors/idempotency.interceptor.ts
├── modules/
│   ├── auth/                     # Autenticação JWT, Tenant Resolution, RBAC
│   ├── clinics/                  # Gestão de unidades e configurações
│   ├── crm/                      # Tutores e Pacientes
│   ├── commercial/               # Oportunidades, Orçamentos e Outbox
│   ├── communication/            # Webhook do WhatsApp e envio de mensagens
│   ├── agent/                    # Orquestrador de Agentes e LLM Gateway
│   └── analytics/                # Atribuição de receita e relatórios
```

### Exemplo de Controller com Validação de Idempotência:
```typescript
@Controller('api/v1/opportunities')
@UseGuards(TenantAuthGuard)
export class CommercialController {
  constructor(private readonly commercialService: CommercialService) {}

  @Post(':id/quotes')
  @UseInterceptors(IdempotencyInterceptor)
  async createQuote(
    @CurrentTenant() tenant: TenantContext,
    @Param('id') opportunityId: string,
    @Body() dto: CreateQuoteDto,
  ) {
    return this.commercialService.presentQuote(tenant.tenantId, opportunityId, dto);
  }
}
```

---

## 8. Plano de Execução Imediato no Workspace

| Passo | Ação Técnica | Entrega Concreta |
| :--- | :--- | :--- |
| **Passo 1** | Inicialização do Monorepo Turborepo + pnpm | `pnpm-workspace.yaml`, `package.json`, `turbo.json` configurados. |
| **Passo 2** | Pacote `@vetflow/database` com Drizzle | Conexão PostgreSQL, RLS policies e migrations geradas. |
| **Passo 3** | Setup do `@vetflow/contracts` & `@vetflow/domain` | Modelos Zod, tipos TypeScript e Transactional Outbox models. |
| **Passo 4** | Implementação do `@vetflow/agent-runtime` | Tool Registry, Policy Engine e suite de testes do Revenue Agent. |
| **Passo 5** | Workflows Temporal em `@vetflow/temporal-workflows` | Revenue Recovery Workflow e atividades integradas ao banco. |
| **Passo 6** | `apps/api` (NestJS) com Webhook WhatsApp | Recebimento, deduplicação de webhooks e trigger de workflows. |
| **Passo 7** | `apps/web` (Next.js) Dashboard Financeiro | Interface para visualização de orçamentos, recuperação e ROI. |
