Curso Lovable

Parte 1 · Pensar 10min

03 O que o sistema precisa lembrar (o banco)

Na mesma conversa, pedimos à IA o desenho do banco de dados a partir do PRD. É aqui que você entende o que o sistema guarda e como as informações se relacionam.

O que acontece

  1. O PRD diz o que o sistema faz. Agora precisamos definir o que ele guarda.
  2. Usamos o Prompt 2 na mesma conversa. A IA devolve o schema do banco acompanhado de uma explicação em português.
  3. Vemos as quatro coisas que o qualificador precisa guardar: as perguntas, os leads, as respostas e o diagnóstico, e como elas se ligam.
  4. Conferimos o schema contra o PRD: existe alguma tabela que o PRD não pede? Existe algum dado que o PRD mostra e ninguém guarda?
Na tela

A mesma conversa da aula anterior, o schema e a explicação em português.

Do Lovable

Ainda a IA de conversa. Esta é a primeira vez que o conceito de banco de dados aparece no curso.

Você sai com

O schema do banco e a noção de tabela e relação, sem precisar ler código.

Na tela

Um slide nesta aula.

Apresentar ▶
  1. 01O que o sistema precisa lembrar: as tabelas

Prompts

Prompt 2 — o schema

Na mesma conversa, logo depois do PRD.

Com base nesse PRD, crie o schema Prisma do banco de dados do sistema.

- Uma model para cada coisa que o sistema precisa guardar, incluindo
  o conteúdo pronto da seção 6 do PRD (por exemplo, as perguntas e
  suas opções vivem no banco, não no código).
- As relações entre as models explícitas.
- Cada campo com tipo, se é obrigatório ou não, e valor padrão
  quando fizer sentido.
- id, createdAt e updatedAt em toda model.
- Enum para todo campo que tem uma lista fixa de valores (status,
  tipo, temperatura, etc.).
- Nomes em inglês nas models e nos campos, como é o padrão.

O login é feito pela própria plataforma (Lovable Cloud). Não crie
tabela de usuário, de sessão nem de senha. Não pergunte sobre
autenticação, infraestrutura ou tecnologia: essas decisões já estão
tomadas.

Entregue o schema como um arquivo para download chamado
`schema.prisma`, só o código. Se não conseguir criar arquivo, entregue
num bloco de código.

Depois do arquivo, escreva em português, em poucas linhas, o que cada
model guarda e como elas se ligam.

Não pergunte nada. Se algo no PRD estiver ambíguo ou contraditório,
escolha a opção mais simples, siga com ela, e liste no fim o que você
decidiu. Não invente tabela para algo que o PRD não pede.

Depois que a IA responder: baixe o arquivo. A explicação em português, logo abaixo dele, é a parte que importa para quem não programa. Se ela não corresponder ao que você imaginava, o schema está errado, e não você. Ajuste na conversa.


Documentos

schema-qualificador.prisma

// Gerado pelo Prompt 2 na mesma conversa do PRD, sem correção.
// Sete tabelas: Question, QuestionOption, Lead, LeadAnswer, Diagnosis, DiagnosisStep, Settings.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------

/// Identidade estável de cada uma das 6 perguntas.
/// O texto é editável no painel; a chave nunca muda, porque a lógica de
/// pontuação e a leitura das respostas dependem dela.
enum QuestionKey {
  SELLING_MODE // 1. Como você vende hoje?
  REVENUE // 2. Faturamento médio dos últimos 3 meses
  ACQUISITION_CHANNEL // 3. De onde vêm os clientes
  MAIN_PROBLEM // 4. Maior problema hoje
  TIME_INVESTED // 5. Tempo por semana atraindo clientes
  URGENCY // 6. Quando quer resolver
}

/// Faixas fixas do PRD 7.3: 70-100 HOT, 40-69 WARM, 0-39 COLD.
enum Temperature {
  HOT
  WARM
  COLD
}

/// NO_DIAGNOSIS é o quarto rótulo do painel: o lead foi gravado, mas a IA
/// falhou ou estourou o tempo. Esses leads vão para o topo da lista.
enum LeadStatus {
  NO_DIAGNOSIS
  DIAGNOSED
}

enum ContactStatus {
  NOT_CONTACTED
  CONTACTED
}

// ---------------------------------------------------------------------------
// Conteúdo editável
// ---------------------------------------------------------------------------

model Question {
  id          String      @id @default(cuid())
  key         QuestionKey @unique
  position    Int         @unique // 1 a 6, fixo
  text        String // enunciado atual, editável no painel (máx. 120)
  defaultText String // texto de fábrica, usado em "Restaurar textos originais"
  maxPoints   Int // teto do bloco desta pergunta na tabela de nota
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt

  options QuestionOption[]
  answers LeadAnswer[]

  @@index([position])
}

model QuestionOption {
  id          String   @id @default(cuid())
  questionId  String
  code        String // identidade estável da opção (ex.: "ABOVE_50K")
  position    Int // ordem de exibição dentro da pergunta
  text        String // texto atual, editável no painel (máx. 60)
  defaultText String // texto de fábrica
  points      Int      @default(0) // pontos que esta opção soma na nota
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  question Question     @relation(fields: [questionId], references: [id], onDelete: Cascade)
  answers  LeadAnswer[]

  @@unique([questionId, code])
  @@unique([questionId, position])
}

// ---------------------------------------------------------------------------
// Leads
// ---------------------------------------------------------------------------

model Lead {
  id       String @id @default(cuid())
  name     String
  email    String
  company  String
  whatsapp String // só dígitos, 10 ou 11 com DDD

  /// Identificador do navegador que enviou. Usado só para o limite de
  /// 5 envios por hora (PRD 8.18). Não identifica pessoa.
  browserId String

  score       Int? // nulo enquanto não houver diagnóstico
  temperature Temperature? // nulo enquanto não houver diagnóstico

  status        LeadStatus    @default(NO_DIAGNOSIS)
  contactStatus ContactStatus @default(NOT_CONTACTED)
  contactedAt   DateTime?

  /// Segundo envio ou mais do mesmo e-mail (PRD 8.17).
  isRepeat Boolean @default(false)

  /// Lead de demonstração; some junto com os outros em "Remover exemplos".
  isExample Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  answers   LeadAnswer[]
  diagnosis Diagnosis?

  @@index([temperature, score, createdAt])
  @@index([status])
  @@index([contactStatus])
  @@index([email])
  @@index([browserId, createdAt])
  @@index([isExample])
}

/// Uma linha por pergunta respondida. Guarda o texto da pergunta e da opção
/// como estavam no momento do envio (PRD 8.5): editar as perguntas depois
/// não altera leads antigos.
model LeadAnswer {
  id           String   @id @default(cuid())
  leadId       String
  questionId   String
  optionId     String
  position     Int // 1 a 6, copiado da pergunta no envio
  questionText String
  optionText   String
  points       Int
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt

  lead     Lead           @relation(fields: [leadId], references: [id], onDelete: Cascade)
  question Question       @relation(fields: [questionId], references: [id], onDelete: Restrict)
  option   QuestionOption @relation(fields: [optionId], references: [id], onDelete: Restrict)

  @@unique([leadId, questionId])
  @@index([leadId, position])
}

// ---------------------------------------------------------------------------
// Saída da IA
// ---------------------------------------------------------------------------

/// Resultado da única chamada de IA. Gerar de novo substitui o registro
/// inteiro (PRD 8.9), por isso é 1-para-1 com o lead.
model Diagnosis {
  id     String @id @default(cuid())
  leadId String @unique

  // Parte pública (T5)
  title          String // máx. 70 caracteres
  whereYouAre    String @db.Text
  whatIsBlocking String @db.Text

  // Parte interna (T9) — nunca exibida ao lead
  internalContext String   @db.Text
  approachPoints  String[] // exatamente 3 frases
  firstMessage    String   @db.Text // máx. 400 caracteres
  likelyObjection String
  whatToAvoid     String

  /// Quantas vezes a IA foi executada para este lead.
  generationCount Int      @default(1)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  lead  Lead            @relation(fields: [leadId], references: [id], onDelete: Cascade)
  steps DiagnosisStep[]
}

/// Os três passos do bloco "Por onde começar".
model DiagnosisStep {
  id          String   @id @default(cuid())
  diagnosisId String
  position    Int // 1, 2, 3
  title       String // máx. 40 caracteres
  text        String   @db.Text
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  diagnosis Diagnosis @relation(fields: [diagnosisId], references: [id], onDelete: Cascade)

  @@unique([diagnosisId, position])
}

// ---------------------------------------------------------------------------
// Configuração
// ---------------------------------------------------------------------------

/// Linha única. Guarda o que o PRD trata como "definido na montagem":
/// textos da capa, WhatsApp do dono e o contexto de negócio do prompt.
model Settings {
  id        String  @id @default(cuid())
  singleton Boolean @unique @default(true)

  companyName     String
  ownerWhatsapp   String // destino do botão "Falar no WhatsApp"
  whatsappPrefill String // mensagem já escrita no link do lead

  publicTitle    String
  publicSubtitle String
  publicBenefits String[] // 3 itens abaixo do botão da capa
  publicFooter   String

  /// Descrição fixa de quem é o dono e o que ele vende, enviada à IA.
  aiBusinessContext String @db.Text

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}