Banco de Dados
Documentação técnica de banco de dados
Projeto:ljvemasearomas· Stack: Next.js 15 / Prisma ORM / MySQL 8.0
Última atualização: referência gerada a partir doprisma/schema.prisma
Sumário
- Visão Geral
- Diagrama de Entidades Principais
- Referência dos Modelos
- Estratégia de Migrações
- Seeds
- Índices e Performance
- Soft Deletes e Auditoria
- Padrões de Nomenclatura
- Configuração Docker
- Troubleshooting
1. Visão Geral
Tecnologia
| Camada | Tecnologia | Versão |
|---|---|---|
| ORM | Prisma | 6.x |
| SGBD | MySQL | 8.0 |
| Driver | mysql2 | latest |
| Charset | utf8mb4 | — |
| Collation | utf8mb4_unicode_ci | — |
O banco possui 28 modelos organizados em 7 domínios funcionais: Usuários, Catálogo, Pedidos & Pagamentos, Variantes, Avaliações, Estoque e Sistema.
Ambientes
| Ambiente | SGBD | Banco | Observação |
|---|---|---|---|
| Produção | MySQL 8.0 (Hostinger) | DATABASE_URL (env) | prisma migrate deploy aplica migrações versionadas |
| Desenvolvimento | MySQL 8.0 via Docker | ljvemasearomas | prisma db push sincroniza schema sem histórico |
| Testes | SQLite | file:./test.db | Criado e destruído a cada jest run via db push --force-reset |
Variáveis de Ambiente
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x", "linux-musl"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
| Variável | Uso |
|---|---|
DATABASE_URL | String de conexão principal — MySQL em todos os ambientes |
SHADOW_DATABASE_URL | Banco shadow para prisma migrate dev (MySQL exige banco extra) |
Formato da connection string:
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/ljvemasearomas"
SHADOW_DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/ljvemasearomas_shadow"
2. Diagrama de Entidades Principais
O diagrama abaixo usa notação simplificada ERD (||--o{ = um para muitos, ||--|| = um para um, }o--o{ = muitos para muitos via tabela pivot).
3. Referência dos Modelos
3.1 Usuários e Autenticação
users
Tabela central de autenticação e identidade. Unifica clientes e administradores pelo campo role.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid() | Identificador único UUID v4 |
name | VARCHAR(191) | não | — | Nome completo do usuário |
email | VARCHAR(191) | não | — | E-mail único — usado como login |
password | VARCHAR(191) | não | — | Hash bcrypt (custo 10) |
phone | VARCHAR(191) | sim | NULL | Telefone opcional |
role | ENUM | não | CUSTOMER | ADMIN ou CUSTOMER |
isActive | TINYINT(1) | não | 1 | false = conta suspensa (soft delete) |
createdAt | DATETIME(3) | não | now() | Data de cadastro |
updatedAt | DATETIME(3) | não | auto | Última modificação |
Índices: email (unique), role, (role, createdAt), isActive
Relações:
| Relação | Cardinalidade | Tabela destino | Cascade |
|---|---|---|---|
addresses | 1:N | addresses | onDelete: Cascade |
orders | 1:N | orders | onDelete: Restrict |
cartItems | 1:N | cart_items | onDelete: Cascade |
wishlist | 1:N | wishlists | onDelete: Cascade |
preferences | 1:1 | user_preferences | onDelete: Cascade |
reviews | 1:N | reviews | onDelete: Cascade |
addresses
Endereços de entrega associados a um usuário. Um usuário pode ter múltiplos endereços, com um marcado como padrão.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
userId | VARCHAR(36) | não | — | FK → users.id (Cascade) |
label | VARCHAR(191) | não | "Casa" | Rótulo amigável |
street | VARCHAR(191) | não | — | Logradouro |
number | VARCHAR(191) | não | — | Número |
complement | VARCHAR(191) | sim | NULL | Apto, bloco, etc. |
district | VARCHAR(191) | não | — | Bairro |
city | VARCHAR(191) | não | — | Cidade |
state | VARCHAR(191) | não | — | UF (2 letras, ex: "SP") |
zipCode | VARCHAR(191) | não | — | CEP sem formatação |
isDefault | TINYINT(1) | não | 0 | Endereço padrão do usuário |
O endereço de um pedido é capturado por referência viva (
addressIdemorders). O snapshot dos dados de entrega fica na NF-e, quando emitida.
user_preferences
Preferências de notificação de cada usuário. Criadas automaticamente no primeiro acesso ou via configurações de perfil.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
userId | VARCHAR(36) | não | — | FK → users.id (unique, Cascade) |
emailOrders | TINYINT(1) | não | 1 | Notificar por e-mail sobre pedidos |
emailPromos | TINYINT(1) | não | 1 | Receber promoções por e-mail |
emailNewsletter | TINYINT(1) | não | 1 | Receber newsletter |
api_keys
Chaves de API para integração com sistemas externos (ERPs, webhooks de estoque, etc.).
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome descritivo (ex: "ERP Omie") |
keyHash | VARCHAR(191) | não | — | SHA-256 da chave raw — nunca armazenada em texto |
keyPrefix | VARCHAR(191) | não | — | Primeiros 16 chars para exibição na UI |
scopes | VARCHAR(191) | não | "products:read,stock:write,orders:read,..." | CSV de permissões |
active | TINYINT(1) | não | 1 | Chave habilitada |
lastUsedAt | DATETIME(3) | sim | NULL | Último uso (atualizado a cada request autenticado) |
expiresAt | DATETIME(3) | sim | NULL | Validade opcional |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Segurança: A chave raw é gerada e exibida apenas uma vez no momento da criação. O sistema armazena somente o hash SHA-256, tornando impossível recuperar a chave original.
3.2 Catálogo
categories
Categorias hierárquicas de produtos (nível único). Controlam a navegação da loja.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome exibido na loja |
slug | VARCHAR(191) | não | — | URL amigável único (ex: "velas-aromaticas") |
description | VARCHAR(191) | sim | NULL | Texto descritivo |
imageUrl | VARCHAR(191) | sim | NULL | Imagem da categoria |
active | TINYINT(1) | não | 1 | Visível na loja |
position | INT | não | 0 | Ordem de exibição no menu |
createdAt | DATETIME(3) | não | now() | — |
Índice: slug (unique)
products
Entidade central do catálogo. Contém preços base, estoque consolidado e metadados SEO.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome do produto |
slug | VARCHAR(191) | não | — | URL única do produto |
description | TEXT | não | — | Descrição longa (@db.Text) |
price | DECIMAL(10,2) | não | — | Preço de venda base (sem variante) |
comparePrice | DECIMAL(10,2) | sim | NULL | Preço "de" para exibição de desconto |
stock | INT | não | 0 | Estoque total (ou do produto sem variante) |
sku | VARCHAR(191) | sim | NULL | Código único de estoque |
weight | DECIMAL(8,3) | sim | NULL | Peso em kg (para cálculo de frete) |
active | TINYINT(1) | não | 1 | Visível na loja (soft delete) |
featured | TINYINT(1) | não | 0 | Destaque na homepage |
categoryId | VARCHAR(36) | sim | NULL | FK → categories.id (onDelete: SetNull) |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: slug (unique), sku (unique), (active), (active, featured), (active, createdAt)
Relações:
| Relação | Tabela destino | Cascade |
|---|---|---|
category | categories | SetNull |
images | product_images | Cascade |
variants | product_variants | Cascade |
cartItems | cart_items | Cascade |
orderItems | order_items | Restrict |
wishlist | wishlists | Cascade |
reviews | reviews | Cascade |
stockMovements | stock_movements | Cascade |
product_images
Galeria de imagens de um produto. A imagem marcada como isPrimary é usada em listagens e thumbnails.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
productId | VARCHAR(36) | não | — | FK → products.id (Cascade) |
url | VARCHAR(191) | não | — | URL da imagem (Hostinger Files / CDN) |
alt | VARCHAR(191) | sim | NULL | Texto alternativo (acessibilidade) |
isPrimary | TINYINT(1) | não | 0 | Imagem principal do produto |
order | INT | não | 0 | Posição na galeria |
shipping_rates
Tabela de fretes configuráveis pelo admin. Suporta regras por estado ou taxa universal.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Ex: "PAC", "SEDEX", "Econômico" |
description | VARCHAR(191) | sim | NULL | Descrição exibida no checkout |
states | VARCHAR(191) | não | "*" | CSV de UFs ("SP,RJ") ou "*" para todos |
price | DECIMAL(10,2) | não | — | Valor do frete em BRL |
estimatedDays | VARCHAR(191) | não | — | Prazo legível (ex: "3-5 dias úteis") |
active | TINYINT(1) | não | 1 | Opção disponível no checkout |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
3.3 Carrinho, Pedidos e Pagamentos
cart_items
Itens do carrinho de compras. Suporta tanto usuários autenticados quanto sessões anônimas.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
userId | VARCHAR(36) | não | — | FK → users.id (Cascade) |
productId | VARCHAR(36) | não | — | FK → products.id (Cascade) |
variantId | VARCHAR(36) | sim | NULL | FK → product_variants.id (SetNull) |
quantity | INT | não | 1 | Quantidade adicionada |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índice: (userId, productId)
Nota: Unicidade
(userId, productId, variantId)não é aplicada em nível de banco de dados, pois MySQL trataNULLcomo distinto em constraints unique. A deduplicação de itens no carrinho é gerenciada na camada de aplicação (Zustand + API route).
orders
Pedidos realizados. Contém snapshot financeiro (subtotal, frete, desconto, total) e rastreamento de NF-e.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
orderNumber | VARCHAR(191) | não | — | Número legível único (ex: "LJVE-A1B2C3D4") |
userId | VARCHAR(36) | não | — | FK → users.id (Restrict) |
addressId | VARCHAR(36) | não | — | FK → addresses.id |
status | ENUM | não | PENDING | Ver enum OrderStatus abaixo |
subtotal | DECIMAL(10,2) | não | — | Soma dos itens |
shipping | DECIMAL(10,2) | não | 0 | Custo de frete |
discount | DECIMAL(10,2) | não | 0 | Valor de desconto (cupom) |
total | DECIMAL(10,2) | não | — | subtotal + shipping - discount |
notes | VARCHAR(191) | sim | NULL | Observações do cliente |
trackingCode | VARCHAR(191) | sim | NULL | Código de rastreio dos Correios |
couponId | VARCHAR(36) | sim | NULL | FK → coupons.id (SetNull) |
nfeStatus | ENUM | não | NOT_ISSUED | Ver enum NFeStatus abaixo |
nfeChave | VARCHAR(191) | sim | NULL | Chave de acesso NF-e (44 dígitos) |
nfeProtocolo | VARCHAR(191) | sim | NULL | Protocolo de autorização SEFAZ |
nfeEmitidaEm | DATETIME(3) | sim | NULL | Data/hora de emissão |
nfeCanceladaEm | DATETIME(3) | sim | NULL | Data/hora de cancelamento |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: orderNumber (unique), (userId, createdAt), (status, createdAt), (status), (createdAt)
Enum OrderStatus:
PENDING → Aguardando pagamento
CONFIRMED → Pagamento confirmado
PROCESSING → Em preparação
SHIPPED → Enviado (trackingCode preenchido)
DELIVERED → Entregue
CANCELLED → Cancelado
REFUNDED → Estornado
Enum NFeStatus:
NOT_ISSUED → NF-e ainda não emitida
ISSUED → NF-e emitida e autorizada pela SEFAZ
CANCELLED → NF-e cancelada
ERROR → Falha na emissão
order_items
Itens individuais de um pedido. Contém snapshots dos dados do produto no momento da compra para garantir integridade histórica.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
orderId | VARCHAR(36) | não | — | FK → orders.id (Cascade) |
productId | VARCHAR(36) | não | — | FK → products.id (Restrict) |
variantId | VARCHAR(36) | sim | NULL | FK → product_variants.id (SetNull) |
variantLabel | VARCHAR(191) | sim | NULL | Snapshot descritivo (ex: "200ml · Lavanda") |
productNameSnapshot | VARCHAR(191) | sim | NULL | Nome do produto no momento da compra |
productSkuSnapshot | VARCHAR(191) | sim | NULL | SKU do produto no momento da compra |
quantity | INT | não | — | Quantidade comprada |
price | DECIMAL(10,2) | não | — | Preço unitário no momento da compra |
total | DECIMAL(10,2) | não | — | price × quantity |
Os campos
productNameSnapshoteproductSkuSnapshotgarantem que alterações futuras no cadastro de produtos não afetem o histórico de pedidos.
payments
Registro de pagamento de um pedido. Relação 1:1 com orders.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
orderId | VARCHAR(36) | não | — | FK → orders.id (unique) |
method | ENUM | não | — | Ver enum PaymentMethod abaixo |
status | ENUM | não | PENDING | Ver enum PaymentStatus abaixo |
amount | DECIMAL(10,2) | não | — | Valor total cobrado |
transactionId | VARCHAR(191) | sim | NULL | ID da transação no gateway (Stripe / PagSeguro) |
pixQrCode | TEXT | sim | NULL | QR Code PIX em base64 (imagem) |
pixQrCodeText | TEXT | sim | NULL | Código PIX copia-e-cola |
expiresAt | DATETIME(3) | sim | NULL | Validade do PIX ou boleto |
paidAt | DATETIME(3) | sim | NULL | Timestamp da confirmação do pagamento |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índice: (status)
Enum PaymentMethod: STRIPE_CARD · CARD · PIX
Enum PaymentStatus: PENDING · PROCESSING · PAID · FAILED · REFUNDED · CANCELLED
order_events
Histórico de eventos de um pedido — mudanças de status, notas manuais do admin. Imutável por design (append-only).
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
orderId | VARCHAR(36) | não | — | FK → orders.id (Cascade) |
type | ENUM | não | STATUS_CHANGE | STATUS_CHANGE ou NOTE |
fromStatus | ENUM | sim | NULL | Status anterior (OrderStatus) |
toStatus | ENUM | sim | NULL | Status novo (NULL para eventos do tipo NOTE) |
note | VARCHAR(191) | sim | NULL | Observação textual |
adminId | VARCHAR(36) | sim | NULL | UUID do admin que realizou a ação |
createdAt | DATETIME(3) | não | now() | — |
coupons
Cupons de desconto para uso no checkout. Suporta desconto percentual e valor fixo.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
code | VARCHAR(191) | não | — | Código único (ex: "NATAL20") |
type | ENUM | não | — | PERCENT ou FIXED |
value | DECIMAL(10,2) | não | — | Percentual (ex: 20.00) ou valor (ex: 15.00) |
minOrderValue | DECIMAL(10,2) | sim | NULL | Valor mínimo do pedido para aplicar |
maxUses | INT | sim | NULL | Limite de usos totais (NULL = ilimitado) |
usedCount | INT | não | 0 | Contador de usos (incrementado atomicamente) |
active | TINYINT(1) | não | 1 | Cupom habilitado |
expiresAt | DATETIME(3) | sim | NULL | Validade (NULL = sem expiração) |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: code (unique), (active), (active, expiresAt)
kit_requests
Solicitações de kits personalizados (orçamentos para eventos corporativos, casamentos, etc.).
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
eventType | VARCHAR(191) | não | — | Tipo do evento (casamento, corporativo, etc.) |
name | VARCHAR(191) | não | — | Nome do solicitante |
email | VARCHAR(191) | não | — | E-mail de contato |
phone | VARCHAR(191) | não | — | Telefone |
quantity | INT | não | — | Quantidade de kits |
products | TEXT | não | — | JSON com produtos de interesse |
neededBy | VARCHAR(191) | sim | NULL | Data limite de entrega |
personalization | TEXT | sim | NULL | Detalhes de personalização solicitados |
notes | TEXT | sim | NULL | Observações adicionais |
status | ENUM | não | PENDING | Ver enum KitRequestStatus abaixo |
emailSent | TINYINT(1) | não | 0 | E-mail de confirmação enviado ao solicitante |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: (status), (createdAt)
Enum KitRequestStatus: PENDING · IN_CONTACT · QUOTED · CONFIRMED · COMPLETED · CANCELLED
3.4 Variantes de Produto
O sistema de variantes utiliza um modelo EAV (Entity-Attribute-Value) simplificado que permite configurar atributos dinâmicos (tamanho, fragrância, intensidade) sem alterar o schema.
Arquitetura de variantes:
VariantType VariantOption ProductVariant
┌────────────┐ 1:N ┌──────────────┐ N:M ┌──────────────────┐
│ "Tamanho" │──────▶│ "100ml" │◀─────▶│ Variante A │
│ "Fragrância│ │ "200ml" │ │ sku: VEL-LAV-200 │
└────────────┘ │ "Lavanda" │ │ price: 48.90 │
│ "Baunilha" │ │ stock: 50 │
└──────────────┘ └──────────────────┘
│
ProductVariantValue
┌──────────────────────────┐
│ "200ml" + "Lavanda" │
└──────────────────────────┘
variant_types
Tipos de atributo globais (ex: Tamanho, Fragrância, Intensidade). Gerenciados em /admin/variantes.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome exibido (ex: "Tamanho") |
slug | VARCHAR(191) | não | — | Identificador URL (ex: "tamanho") |
active | TINYINT(1) | não | 1 | Tipo habilitado |
order | INT | não | 0 | Ordem de exibição na PDP |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: name (unique), slug (unique)
variant_options
Valores concretos para cada tipo (ex: "100ml", "200ml" para o tipo "Tamanho").
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
variantTypeId | VARCHAR(36) | não | — | FK → variant_types.id (Cascade) |
value | VARCHAR(191) | não | — | Valor da opção (ex: "Lavanda") |
active | TINYINT(1) | não | 1 | Opção disponível |
order | INT | não | 0 | Ordem de exibição |
Unique: (variantTypeId, value) — evita opções duplicadas no mesmo tipo.
product_variants
Cada variante representa um SKU específico de um produto (combinação única de atributos com preço e estoque próprios).
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
productId | VARCHAR(36) | não | — | FK → products.id (Cascade) |
price | DECIMAL(10,2) | não | — | Preço desta variante (pode diferir do produto base) |
comparePrice | DECIMAL(10,2) | sim | NULL | Preço "de" para desconto |
stock | INT | não | 0 | Estoque desta variante |
sku | VARCHAR(191) | sim | NULL | SKU único (ex: "VEL-LAV-200-LAV") |
active | TINYINT(1) | não | 1 | Variante disponível para compra |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: sku (unique), (productId, active)
product_variant_values
Tabela pivot que associa uma variante de produto às suas opções de atributos. Permite combinações N:M entre variantes e opções.
| Campo | Tipo | Nulável | Descrição |
|---|---|---|---|
id | VARCHAR(36) | não | — |
productVariantId | VARCHAR(36) | não | FK → product_variants.id (Cascade) |
variantOptionId | VARCHAR(36) | não | FK → variant_options.id (Cascade) |
Unique: (productVariantId, variantOptionId) — evita duplicação da mesma opção numa variante.
3.5 Avaliações, Wishlist e Depoimentos
wishlists
Lista de desejos do usuário. Uma entrada por produto por usuário.
| Campo | Tipo | Nulável | Descrição |
|---|---|---|---|
id | VARCHAR(36) | não | — |
userId | VARCHAR(36) | não | FK → users.id (Cascade) |
productId | VARCHAR(36) | não | FK → products.id (Cascade) |
createdAt | DATETIME(3) | não | Data de adição à wishlist |
Unique: (userId, productId)
reviews
Avaliações de clientes verificados. Uma avaliação por produto por usuário. Requer aprovação do admin antes de ser exibida publicamente.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
productId | VARCHAR(36) | não | — | FK → products.id (Cascade) |
userId | VARCHAR(36) | não | — | FK → users.id (Cascade) |
rating | INT | não | — | Nota de 1 a 5 |
title | VARCHAR(191) | sim | NULL | Título da avaliação |
body | TEXT | não | — | Texto completo da avaliação |
approved | TINYINT(1) | não | 0 | Aprovada para exibição pública |
createdAt | DATETIME(3) | não | now() | — |
Unique: (userId, productId) — um cliente, uma avaliação por produto.
Índice: (productId, approved) — otimiza queries de avaliações aprovadas por produto.
testimonials
Depoimentos curados de clientes para exibição na homepage. Gerenciados manualmente pelo admin.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome do cliente |
location | VARCHAR(191) | não | — | Cidade/estado (ex: "São Paulo, SP") |
rating | INT | não | 5 | Nota de 1 a 5 |
text | TEXT | não | — | Texto do depoimento |
productName | VARCHAR(191) | sim | NULL | Produto citado no depoimento |
avatar | VARCHAR(191) | não | — | Iniciais para avatar (ex: "AC") |
active | TINYINT(1) | não | 1 | Visível na homepage |
order | INT | não | 0 | Posição no carrossel |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índice: (active, order) — query de homepage busca depoimentos ativos ordenados.
3.6 Estoque
stock_movements
Log imutável de todas as movimentações de estoque. Permite reconstituir o histórico completo e auditar divergências.
| Campo | Tipo | Nulável | Descrição |
|---|---|---|---|
id | VARCHAR(36) | não | — |
productId | VARCHAR(36) | não | FK → products.id (Cascade) |
variantId | VARCHAR(36) | sim | FK → product_variants.id (SetNull) |
delta | INT | não | Positivo = entrada · Negativo = saída |
reason | ENUM | não | Ver enum StockMovementReason abaixo |
source | ENUM | não | Ver enum StockMovementSource abaixo |
orderId | VARCHAR(36) | sim | Pedido relacionado (quando reason = PURCHASE) |
note | VARCHAR(191) | sim | Observação textual do operador |
adminId | VARCHAR(36) | sim | UUID do admin responsável (movimentos manuais) |
createdAt | DATETIME(3) | não | — |
Índices: (productId, createdAt), (orderId)
Enum StockMovementReason:
| Valor | Descrição |
|---|---|
PURCHASE | Saída por venda (pedido confirmado) |
ADJUSTMENT | Ajuste manual pelo administrador |
RETURN | Devolução de mercadoria |
INITIAL | Estoque inicial ao cadastrar o produto |
CORRECTION | Correção de erro de lançamento |
Enum StockMovementSource:
| Valor | Descrição |
|---|---|
ORDER | Disparado automaticamente por pedido |
ADMIN | Realizado manualmente pelo painel admin |
API | Originado via API key externa (ERP/integração) |
SYSTEM | Processo interno automatizado |
3.7 Integrações e Sistema
notifications
Notificações in-app do painel administrativo. Não são vinculadas a usuários específicos — são alertas globais para o administrador.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
type | ENUM | não | — | Tipo da notificação (ver abaixo) |
title | VARCHAR(191) | não | — | Título resumido |
body | VARCHAR(191) | não | — | Mensagem completa |
link | VARCHAR(191) | sim | NULL | Href para navegação ao clicar |
read | TINYINT(1) | não | 0 | Notificação lida |
createdAt | DATETIME(3) | não | now() | — |
Índice: (read, createdAt) — badge de notificações não lidas.
Enum NotificationType: NEW_ORDER · LOW_STOCK · NEW_REVIEW · KIT_REQUEST · PAYMENT_FAILED · INFO
subscribers
Lista de assinantes da newsletter da loja.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
email | VARCHAR(191) | não | — | E-mail único |
name | VARCHAR(191) | sim | NULL | Nome opcional |
source | VARCHAR(191) | não | "website" | Origem: "website", "checkout", "admin" |
confirmedAt | DATETIME(3) | sim | NULL | Data de confirmação double opt-in |
unsubscribedAt | DATETIME(3) | sim | NULL | Data de descadastramento |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Índices: email (unique), (createdAt), (confirmedAt)
Double opt-in:
confirmedAt IS NULLindica assinante pendente. A loja só envia e-mails marketing a assinantes confirmados.
site_settings
Armazenamento chave-valor de conteúdo editável da loja. Cada entrada representa uma seção da homepage configurável pelo admin via editor visual.
| Campo | Tipo | Nulável | Descrição |
|---|---|---|---|
key | VARCHAR(191) | não | Chave primária — identifica a seção (ex: "hero", "benefits") |
value | JSON | não | JSON estruturado conforme tipo da seção (ver lib/site-settings.ts) |
updatedAt | DATETIME(3) | não | Última modificação pelo admin |
Chaves conhecidas: "hero" · "benefits" · "brand_story" e demais seções da homepage.
Diferente de um singleton, este modelo é um key-value store flexível que permite adicionar novas seções editáveis sem alterar o schema.
webhook_endpoints
Endpoints de webhook outbound para integração com sistemas externos (ERPs, plataformas de estoque, etc.).
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
name | VARCHAR(191) | não | — | Nome descritivo do endpoint |
url | VARCHAR(191) | não | — | URL do receptor |
events | VARCHAR(191) | não | "stock.updated" | Eventos CSV assinados pelo endpoint |
secret | VARCHAR(191) | não | — | Segredo HMAC-SHA256 para assinar o payload |
active | TINYINT(1) | não | 1 | Endpoint habilitado |
lastError | TEXT | sim | NULL | Último erro de entrega |
lastSentAt | DATETIME(3) | sim | NULL | Última entrega com sucesso |
createdAt | DATETIME(3) | não | now() | — |
updatedAt | DATETIME(3) | não | auto | — |
Eventos suportados: stock.updated · stock.low · order.placed · order.status_changed
webhook_deliveries
Log de todas as tentativas de entrega de webhook. Permite auditoria e reenvio manual em caso de falhas.
| Campo | Tipo | Nulável | Padrão | Descrição |
|---|---|---|---|---|
id | VARCHAR(36) | não | uuid | — |
endpointId | VARCHAR(36) | não | — | FK → webhook_endpoints.id (Cascade) |
event | VARCHAR(191) | não | — | Tipo do evento disparado |
payload | TEXT | não | — | Body JSON enviado |
responseCode | INT | sim | NULL | HTTP status code da resposta |
responseBody | TEXT | sim | NULL | Body da resposta (para debug) |
success | TINYINT(1) | não | 0 | true se HTTP 2xx |
durationMs | INT | sim | NULL | Latência da requisição em milissegundos |
createdAt | DATETIME(3) | não | now() | — |
Índice: (endpointId, createdAt) — histórico de entregas por endpoint.
4. Estratégia de Migrações
Fluxo por ambiente
┌─────────────────────────────────────────────────────────────────┐
│ DESENVOLVIMENTO │
│ │
│ Editar schema.prisma │
│ │ │
│ ▼ │
│ npx prisma db push ← sincroniza schema diretamente │
│ │ sem criar arquivos de migração │
│ ▼ │
│ (banco ljvemasearomas atualizado no Docker) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PRODUÇÃO │
│ │
│ Editar schema.prisma │
│ │ │
│ ▼ │
│ npx prisma migrate dev --name <descricao> │
│ │ ← gera arquivo em prisma/migrations/ │
│ │ ← requer SHADOW_DATABASE_URL configurado │
│ ▼ │
│ git commit + push │
│ │ │
│ ▼ │
│ npx prisma migrate deploy ← aplicado no CI/CD │
│ │ ou no servidor de produção │
│ ▼ │
│ (banco Hostinger atualizado com auditoria completa) │
└─────────────────────────────────────────────────────────────────┘
Comandos de referência
| Comando | Ambiente | Quando usar |
|---|---|---|
npx prisma db push | Dev | Iteração rápida — sem histórico de migração |
npx prisma migrate dev --name <desc> | Dev | Quando a mudança precisa ser versionada para produção |
npx prisma migrate deploy | Prod | Aplica todas as migrações pendentes |
npx prisma migrate status | Qualquer | Verifica quais migrações foram aplicadas |
npx prisma migrate reset | Dev | Destrutivo — recria banco e roda seed |
npx prisma db push --force-reset | Testes | Recria banco SQLite limpo antes de cada test run |
npx prisma studio | Dev | Interface visual para inspecionar dados |
npx prisma generate | Qualquer | Regenera o Prisma Client após mudar schema |
Shadow Database
O MySQL não permite que o Prisma calcule drift automaticamente num banco em uso. Para isso, é necessário um banco shadow dedicado onde o Prisma aplica migrações em modo trial:
Durante `prisma migrate dev`:
1. Prisma aplica todas as migrações existentes no shadow DB
2. Aplica o schema atual (db push) no shadow DB
3. Calcula o diff entre os dois estados
4. Gera arquivo SQL de migração com as diferenças
5. Aplica no banco principal de dev
6. Registra em _prisma_migrations
Em desenvolvimento local (Docker), o shadow DB é criado automaticamente pelo docker/mysql-init.sql como ljvemasearomas_shadow.
push vs migrate dev — Guia de decisão
Usar `db push` quando:
✓ Iterando no schema em desenvolvimento local
✓ Experimentos que podem ser descartados
✓ Primeira configuração do banco local
✓ Testes automatizados (SQLite)
Usar `migrate dev` quando:
✓ A mudança vai para produção
✓ Precisa de revisão de código do SQL gerado
✓ Há dados em produção que precisam ser migrados
✓ Múltiplos desenvolvedores precisam sincronizar o schema
5. Seeds
O arquivo prisma/seed.js popula o banco com dados mínimos para desenvolvimento e testes.
Dados criados pelo seed
| Entidade | Quantidade | Detalhes |
|---|---|---|
| Usuários | 2 | 1 admin + 1 cliente de teste |
| Categorias | 4 | Velas Aromáticas, Difusores, Kits Presentes, Sachês |
| Produtos | 9 | Distribuídos entre as 4 categorias, 3 destacados |
| VariantTypes | 2 | Tamanho (slug: tamanho), Fragrância (slug: fragrancia) |
| VariantOptions | 4 | 100ml, 200ml, Lavanda, Baunilha |
| ProductVariants | 4 | Combinações do produto Lavanda 200g |
Credenciais de teste
| Perfil | Senha | Role | |
|---|---|---|---|
| Admin | admin@ljvemasearomas.com.br | admin123 | ADMIN |
| Cliente | cliente@teste.com | cliente123 | CUSTOMER |
Atenção: Estas credenciais são apenas para desenvolvimento local. Nunca use o seed em produção.
Como executar o seed
# Método 1: via script npm (recomendado)
npm run db:seed
# Método 2: direto
npx prisma db seed
# Método 3: reset completo + seed
npx prisma migrate reset
# → confirme com 'y' quando solicitado
Como estender o seed
Para adicionar dados ao seed, edite prisma/seed.js e use o padrão upsert para garantir idempotência (o seed pode ser executado múltiplas vezes sem duplicar dados):
const admin = await prisma.user.upsert({
where: { email: "admin@ljvemasearomas.com.br" },
update: { password: senhaHash },
create: {
name: "Administrador",
email: "admin@ljvemasearomas.com.br",
password: senhaHash,
role: "ADMIN",
},
});
6. Índices e Performance
Mapa completo de índices
Tabela users
| Índice | Tipo | Justificativa |
|---|---|---|
email | UNIQUE | Login por e-mail — busca O(log n) obrigatória em toda autenticação |
(role) | INDEX | Filtrar admins vs clientes no painel |
(role, createdAt) | INDEX | Paginação de usuários por role ordenada por data |
(isActive) | INDEX | Filtrar contas suspensas rapidamente |
Tabela products
| Índice | Tipo | Justificativa |
|---|---|---|
slug | UNIQUE | Lookup de página de produto — cada acesso à PDP usa este índice |
sku | UNIQUE | Busca por SKU em integrações de estoque e importações |
(active) | INDEX | Todas as listagens da loja filtram active = true |
(active, featured) | INDEX | Homepage: produtos em destaque ativos |
(active, createdAt) | INDEX | Ordenação "Mais recentes" na listagem da loja |
Tabela orders
| Índice | Tipo | Justificativa |
|---|---|---|
orderNumber | UNIQUE | Busca de pedido por número — usada em e-mails e suporte |
(userId, createdAt) | INDEX | Histórico de pedidos de um cliente paginado por data |
(status, createdAt) | INDEX | Dashboard admin: pedidos por status + ordenação temporal |
(status) | INDEX | Contagem de pedidos por status (badges do painel) |
(createdAt) | INDEX | Relatórios por período (faturamento mensal, etc.) |
Tabela coupons
| Índice | Tipo | Justificativa |
|---|---|---|
code | UNIQUE | Validação de cupom no checkout — busca pelo código |
(active) | INDEX | Listagem de cupons ativos no admin |
(active, expiresAt) | INDEX | Filtragem de cupons válidos (ativos e não expirados) |
Tabela reviews
| Índice | Tipo | Justificativa |
|---|---|---|
(userId, productId) | UNIQUE | Impede múltiplas avaliações do mesmo cliente no mesmo produto |
(productId, approved) | INDEX | Carregar avaliações aprovadas de um produto na PDP |
Tabela product_variants
| Índice | Tipo | Justificativa |
|---|---|---|
sku | UNIQUE | Identificação única de variante para integrações externas |
(productId, active) | INDEX | Carregar variantes ativas de um produto para o seletor de PDP |
Tabela stock_movements
| Índice | Tipo | Justificativa |
|---|---|---|
(productId, createdAt) | INDEX | Histórico de movimentações de um produto ordenado por data |
(orderId) | INDEX | Buscar movimentação associada a um pedido específico |
Tabela notifications
| Índice | Tipo | Justificativa |
|---|---|---|
(read, createdAt) | INDEX | Badge de não lidas + listagem ordenada no painel admin |
7. Soft Deletes e Auditoria
Soft Deletes
O projeto não remove registros físicos para entidades de negócio. Em vez disso, usa flags booleanas:
| Tabela | Campo | Efeito quando false |
|---|---|---|
users | isActive | Conta suspensa — login bloqueado, dados preservados |
products | active | Produto oculto na loja, permanece em pedidos históricos |
categories | active | Categoria oculta da navegação |
coupons | active | Cupom desabilitado para novos usos |
api_keys | active | Chave de API revogada — requests retornam 401 |
shipping_rates | active | Opção de frete removida do checkout |
testimonials | active | Depoimento oculto da homepage |
subscribers | unsubscribedAt IS NOT NULL | Descadastrado da newsletter |
A exclusão física (
DELETE) é reservada apenas para dados sem impacto histórico (ex: itens de carrinho, imagens de produto substituídas).
Auditoria via OrderEvent
Todo estado de pedido é rastreado via order_events. O log é append-only — nunca atualizado:
Exemplo de histórico para um pedido:
createdAt type fromStatus toStatus note
─────────────────── ───────────── ─────────── ────────── ─────────────────
2024-01-10 10:00:00 STATUS_CHANGE null PENDING —
2024-01-10 10:05:00 STATUS_CHANGE PENDING CONFIRMED Pagamento PIX confirmado
2024-01-10 14:30:00 STATUS_CHANGE CONFIRMED PROCESSING Separando para envio
2024-01-11 09:00:00 STATUS_CHANGE PROCESSING SHIPPED Rastreio: BR123456789
2024-01-11 09:00:00 NOTE — — Produto embalado com cuidado especial
2024-01-14 16:00:00 STATUS_CHANGE SHIPPED DELIVERED —
Auditoria via StockMovement
Cada alteração de estoque gera um registro em stock_movements, permitindo reconstruir o saldo em qualquer ponto no tempo:
Reconstrução do estoque de um produto:
SELECT
SUM(delta) AS estoque_atual
FROM stock_movements
WHERE productId = '<uuid>'
AND createdAt <= '<data_referencia>';
Exemplo de registros:
INITIAL +50 (cadastro do produto)
PURCHASE -3 (pedido LJVE-001)
PURCHASE -2 (pedido LJVE-002)
ADJUSTMENT +10 (reposição manual)
→ Estoque atual: 55
8. Padrões de Nomenclatura
Nomes de tabela
Todas as tabelas usam snake_case definido via @@map() no schema Prisma:
| Modelo Prisma | Tabela MySQL (@@map) |
|---|---|
User | users |
Address | addresses |
Category | categories |
Product | products |
ProductImage | product_images |
CartItem | cart_items |
Order | orders |
OrderItem | order_items |
Payment | payments |
Wishlist | wishlists |
UserPreferences | user_preferences |
ApiKey | api_keys |
ShippingRate | shipping_rates |
VariantType | variant_types |
VariantOption | variant_options |
ProductVariant | product_variants |
ProductVariantValue | product_variant_values |
Review | reviews |
Coupon | coupons |
OrderEvent | order_events |
KitRequest | kit_requests |
StockMovement | stock_movements |
Notification | notifications |
Subscriber | subscribers |
SiteSettings | site_settings |
WebhookEndpoint | webhook_endpoints |
WebhookDelivery | webhook_deliveries |
Testimonial | testimonials |
Identificadores
- Tipo:
VARCHAR(36)— UUID v4 gerado por@default(uuid()) - Motivação: Evita vazamento de sequência, permite geração client-side, seguro para exposição em URLs
Timestamps
| Campo | Tipo | @default | @updatedAt | Presença |
|---|---|---|---|---|
createdAt | DATETIME(3) | now() | não | Todos os modelos |
updatedAt | DATETIME(3) | — | @updatedAt | Modelos mutáveis |
Modelos append-only (
order_events,stock_movements,webhook_deliveries) têm apenascreatedAt.
Campos financeiros
Todos os valores monetários usam DECIMAL(10,2) via @db.Decimal(10,2) — nunca FLOAT para evitar erros de arredondamento:
-- ✅ Correto
price DECIMAL(10, 2) -- Ex: 48.90
-- ❌ Nunca usar para valores financeiros
price FLOAT -- Erros de arredondamento em somas
Campos de texto longo
Campos com conteúdo variável grande usam @db.Text para mapear para TEXT no MySQL (vs VARCHAR(191) padrão):
products.descriptionorder_items.variantLabel,productNameSnapshotpayments.pixQrCode,pixQrCodeTextkit_requests.products,personalization,noteswebhook_endpoints.lastErrorwebhook_deliveries.payload,responseBodyreviews.bodytestimonials.text
9. Configuração Docker
docker-compose.yml
O arquivo na raiz do projeto define dois serviços para desenvolvimento local:
version: '3.9'
services:
mysql:
image: mysql:8.0
container_name: ljvelas_mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: localdev
MYSQL_DATABASE: ljvemasearomas
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
ports:
- '3306:3306'
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql-init.sql:/docker-entrypoint-initdb.d/init.sql
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
healthcheck:
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-uroot', '-plocaldev']
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
phpmyadmin:
image: phpmyadmin:latest
container_name: ljvelas_phpmyadmin
depends_on:
mysql:
condition: service_healthy
environment:
PMA_HOST: mysql
MYSQL_ROOT_PASSWORD: localdev
ports:
- '8080:80'
volumes:
mysql_data:
name: ljvelas_mysql_data
Script de inicialização (docker/mysql-init.sql)
Executado automaticamente na primeira subida do container:
CREATE DATABASE IF NOT EXISTS `ljvemasearomas`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS `ljvemasearomas_shadow`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON `ljvemasearomas`.* TO 'root'@'%';
GRANT ALL PRIVILEGES ON `ljvemasearomas_shadow`.* TO 'root'@'%';
FLUSH PRIVILEGES;
Primeiros passos em ambiente local
# 1. Suba o MySQL e phpMyAdmin
docker-compose up -d
# 2. Aguarde o healthcheck (30s na primeira vez)
docker-compose ps
# mysql: healthy
# 3. Configure o .env.local
DATABASE_URL="mysql://root:localdev@localhost:3306/ljvemasearomas"
SHADOW_DATABASE_URL="mysql://root:localdev@localhost:3306/ljvemasearomas_shadow"
# 4. Sincronize o schema
npx prisma db push
# 5. Popule com dados de teste
npm run db:seed
# 6. (Opcional) Acesse o phpMyAdmin
# http://localhost:8080
# Servidor: mysql | Usuário: root | Senha: localdev
Gerenciamento do volume
# Verificar volume existente
docker volume inspect ljvelas_mysql_data
# Destruir dados e recriar do zero
docker-compose down -v
docker-compose up -d
npx prisma db push
npm run db:seed
10. Troubleshooting
Erros do Prisma Client
P1000 — Autenticação falhou
Error: P1000 - Authentication failed against database server
Causa: Credenciais inválidas na DATABASE_URL.
Solução: Verificar DATABASE_URL no .env.local. Para Docker local, usar root:localdev.
P1001 — Servidor inacessível
Error: P1001 - Can't reach database server at localhost:3306
Causa: Container MySQL não está rodando ou porta errada.
Solução:
docker-compose up -d mysql
docker-compose ps # verificar status "healthy"
P1010 — Banco inexistente
Error: P1010 - User was denied access on the database ljvemasearomas
Causa: Banco ljvemasearomas não foi criado (init.sql não executou).
Solução: Destruir e recriar o volume para forçar re-execução do init.sql:
docker-compose down -v
docker-compose up -d
P3014 — Shadow database error
Error: P3014 - Prisma Migrate could not create the shadow database.
Please make sure the database user has the `CREATE DATABASE` permission.
Causa: SHADOW_DATABASE_URL não configurada ou banco shadow não existe.
Solução para Docker: O banco shadow é criado pelo mysql-init.sql. Se não existir:
# Conectar ao MySQL e criar manualmente
docker exec -it ljvelas_mysql mysql -uroot -plocaldev
> CREATE DATABASE ljvemasearomas_shadow CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
> GRANT ALL PRIVILEGES ON ljvemasearomas_shadow.* TO 'root'@'%';
Drift detectado em prisma migrate dev
Error: P3006 - Migration "20240101_init" failed to apply cleanly
to the shadow database.
Drift detected: your database schema is not in sync with your migrations.
Causa: O banco de desenvolvimento foi modificado diretamente (via db push ou SQL manual) e não está mais alinhado com os arquivos de migração.
Solução:
# Opção 1: Resetar banco de dev (perde todos os dados locais)
npx prisma migrate reset
# Opção 2: Introspect e criar baseline
npx prisma migrate diff --from-migrations ./prisma/migrations \
--to-schema-datasource prisma/schema.prisma \
--script > prisma/migrations/manual_fix.sql
P2002 — Unique constraint violation
Error: P2002 - Unique constraint failed on the fields: (`email`)
Causa: Tentativa de inserir registro com valor único já existente.
Solução: Usar upsert em vez de create, ou verificar existência antes de inserir. Comum no seed ao executar sem upsert.
Prisma Client desatualizado
Error: @prisma/client did not initialize yet.
Run `npx prisma generate` to generate the Prisma Client.
Causa: Schema modificado mas Prisma Client não foi regenerado.
Solução:
npx prisma generate
# ou inclua em package.json como postinstall
Problema de charset com emoji/caracteres especiais
Error: Incorrect string value: '\xF0\x9F\x95\xAF' for column ...
Causa: Coluna com utf8 em vez de utf8mb4.
Solução: O Docker já configura utf8mb4 globalmente. Para banco Hostinger, verificar se o banco foi criado com utf8mb4_unicode_ci.
Checklist de verificação de ambiente
[ ] docker-compose up -d → containers rodando
[ ] docker-compose ps → status "healthy"
[ ] .env.local configurado → DATABASE_URL e SHADOW_DATABASE_URL
[ ] npx prisma db push → schema sincronizado
[ ] npm run db:seed → dados de seed aplicados
[ ] npx prisma studio → inspecionar dados visualmente
[ ] http://localhost:3000/admin → painel admin acessível
[ ] http://localhost:8080 → phpMyAdmin acessível
Documentação gerada com base no prisma/schema.prisma e arquivos de infraestrutura do projeto.