Pular para o conteúdo principal

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 do prisma/schema.prisma


Sumário

  1. Visão Geral
  2. Diagrama de Entidades Principais
  3. Referência dos Modelos
  4. Estratégia de Migrações
  5. Seeds
  6. Índices e Performance
  7. Soft Deletes e Auditoria
  8. Padrões de Nomenclatura
  9. Configuração Docker
  10. Troubleshooting

1. Visão Geral

Tecnologia

CamadaTecnologiaVersão
ORMPrisma6.x
SGBDMySQL8.0
Drivermysql2latest
Charsetutf8mb4
Collationutf8mb4_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

AmbienteSGBDBancoObservação
ProduçãoMySQL 8.0 (Hostinger)DATABASE_URL (env)prisma migrate deploy aplica migrações versionadas
DesenvolvimentoMySQL 8.0 via Dockerljvemasearomasprisma db push sincroniza schema sem histórico
TestesSQLitefile:./test.dbCriado 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ávelUso
DATABASE_URLString de conexão principal — MySQL em todos os ambientes
SHADOW_DATABASE_URLBanco 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid()Identificador único UUID v4
nameVARCHAR(191)nãoNome completo do usuário
emailVARCHAR(191)nãoE-mail único — usado como login
passwordVARCHAR(191)nãoHash bcrypt (custo 10)
phoneVARCHAR(191)simNULLTelefone opcional
roleENUMnãoCUSTOMERADMIN ou CUSTOMER
isActiveTINYINT(1)não1false = conta suspensa (soft delete)
createdAtDATETIME(3)nãonow()Data de cadastro
updatedAtDATETIME(3)nãoautoÚltima modificação

Índices: email (unique), role, (role, createdAt), isActive

Relações:

RelaçãoCardinalidadeTabela destinoCascade
addresses1:NaddressesonDelete: Cascade
orders1:NordersonDelete: Restrict
cartItems1:Ncart_itemsonDelete: Cascade
wishlist1:NwishlistsonDelete: Cascade
preferences1:1user_preferencesonDelete: Cascade
reviews1:NreviewsonDelete: 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
userIdVARCHAR(36)nãoFK → users.id (Cascade)
labelVARCHAR(191)não"Casa"Rótulo amigável
streetVARCHAR(191)nãoLogradouro
numberVARCHAR(191)nãoNúmero
complementVARCHAR(191)simNULLApto, bloco, etc.
districtVARCHAR(191)nãoBairro
cityVARCHAR(191)nãoCidade
stateVARCHAR(191)nãoUF (2 letras, ex: "SP")
zipCodeVARCHAR(191)nãoCEP sem formatação
isDefaultTINYINT(1)não0Endereço padrão do usuário

O endereço de um pedido é capturado por referência viva (addressId em orders). 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
userIdVARCHAR(36)nãoFK → users.id (unique, Cascade)
emailOrdersTINYINT(1)não1Notificar por e-mail sobre pedidos
emailPromosTINYINT(1)não1Receber promoções por e-mail
emailNewsletterTINYINT(1)não1Receber newsletter

api_keys

Chaves de API para integração com sistemas externos (ERPs, webhooks de estoque, etc.).

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome descritivo (ex: "ERP Omie")
keyHashVARCHAR(191)nãoSHA-256 da chave raw — nunca armazenada em texto
keyPrefixVARCHAR(191)nãoPrimeiros 16 chars para exibição na UI
scopesVARCHAR(191)não"products:read,stock:write,orders:read,..."CSV de permissões
activeTINYINT(1)não1Chave habilitada
lastUsedAtDATETIME(3)simNULLÚltimo uso (atualizado a cada request autenticado)
expiresAtDATETIME(3)simNULLValidade opcional
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

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.


categories

Categorias hierárquicas de produtos (nível único). Controlam a navegação da loja.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome exibido na loja
slugVARCHAR(191)nãoURL amigável único (ex: "velas-aromaticas")
descriptionVARCHAR(191)simNULLTexto descritivo
imageUrlVARCHAR(191)simNULLImagem da categoria
activeTINYINT(1)não1Visível na loja
positionINTnão0Ordem de exibição no menu
createdAtDATETIME(3)nãonow()

Índice: slug (unique)


products

Entidade central do catálogo. Contém preços base, estoque consolidado e metadados SEO.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome do produto
slugVARCHAR(191)nãoURL única do produto
descriptionTEXTnãoDescrição longa (@db.Text)
priceDECIMAL(10,2)nãoPreço de venda base (sem variante)
comparePriceDECIMAL(10,2)simNULLPreço "de" para exibição de desconto
stockINTnão0Estoque total (ou do produto sem variante)
skuVARCHAR(191)simNULLCódigo único de estoque
weightDECIMAL(8,3)simNULLPeso em kg (para cálculo de frete)
activeTINYINT(1)não1Visível na loja (soft delete)
featuredTINYINT(1)não0Destaque na homepage
categoryIdVARCHAR(36)simNULLFK → categories.id (onDelete: SetNull)
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Índices: slug (unique), sku (unique), (active), (active, featured), (active, createdAt)

Relações:

RelaçãoTabela destinoCascade
categorycategoriesSetNull
imagesproduct_imagesCascade
variantsproduct_variantsCascade
cartItemscart_itemsCascade
orderItemsorder_itemsRestrict
wishlistwishlistsCascade
reviewsreviewsCascade
stockMovementsstock_movementsCascade

product_images

Galeria de imagens de um produto. A imagem marcada como isPrimary é usada em listagens e thumbnails.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
productIdVARCHAR(36)nãoFK → products.id (Cascade)
urlVARCHAR(191)nãoURL da imagem (Hostinger Files / CDN)
altVARCHAR(191)simNULLTexto alternativo (acessibilidade)
isPrimaryTINYINT(1)não0Imagem principal do produto
orderINTnão0Posição na galeria

shipping_rates

Tabela de fretes configuráveis pelo admin. Suporta regras por estado ou taxa universal.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoEx: "PAC", "SEDEX", "Econômico"
descriptionVARCHAR(191)simNULLDescrição exibida no checkout
statesVARCHAR(191)não"*"CSV de UFs ("SP,RJ") ou "*" para todos
priceDECIMAL(10,2)nãoValor do frete em BRL
estimatedDaysVARCHAR(191)nãoPrazo legível (ex: "3-5 dias úteis")
activeTINYINT(1)não1Opção disponível no checkout
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

3.3 Carrinho, Pedidos e Pagamentos

cart_items

Itens do carrinho de compras. Suporta tanto usuários autenticados quanto sessões anônimas.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
userIdVARCHAR(36)nãoFK → users.id (Cascade)
productIdVARCHAR(36)nãoFK → products.id (Cascade)
variantIdVARCHAR(36)simNULLFK → product_variants.id (SetNull)
quantityINTnão1Quantidade adicionada
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Índice: (userId, productId)

Nota: Unicidade (userId, productId, variantId) não é aplicada em nível de banco de dados, pois MySQL trata NULL como 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
orderNumberVARCHAR(191)nãoNúmero legível único (ex: "LJVE-A1B2C3D4")
userIdVARCHAR(36)nãoFK → users.id (Restrict)
addressIdVARCHAR(36)nãoFK → addresses.id
statusENUMnãoPENDINGVer enum OrderStatus abaixo
subtotalDECIMAL(10,2)nãoSoma dos itens
shippingDECIMAL(10,2)não0Custo de frete
discountDECIMAL(10,2)não0Valor de desconto (cupom)
totalDECIMAL(10,2)nãosubtotal + shipping - discount
notesVARCHAR(191)simNULLObservações do cliente
trackingCodeVARCHAR(191)simNULLCódigo de rastreio dos Correios
couponIdVARCHAR(36)simNULLFK → coupons.id (SetNull)
nfeStatusENUMnãoNOT_ISSUEDVer enum NFeStatus abaixo
nfeChaveVARCHAR(191)simNULLChave de acesso NF-e (44 dígitos)
nfeProtocoloVARCHAR(191)simNULLProtocolo de autorização SEFAZ
nfeEmitidaEmDATETIME(3)simNULLData/hora de emissão
nfeCanceladaEmDATETIME(3)simNULLData/hora de cancelamento
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Í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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
orderIdVARCHAR(36)nãoFK → orders.id (Cascade)
productIdVARCHAR(36)nãoFK → products.id (Restrict)
variantIdVARCHAR(36)simNULLFK → product_variants.id (SetNull)
variantLabelVARCHAR(191)simNULLSnapshot descritivo (ex: "200ml · Lavanda")
productNameSnapshotVARCHAR(191)simNULLNome do produto no momento da compra
productSkuSnapshotVARCHAR(191)simNULLSKU do produto no momento da compra
quantityINTnãoQuantidade comprada
priceDECIMAL(10,2)nãoPreço unitário no momento da compra
totalDECIMAL(10,2)nãoprice × quantity

Os campos productNameSnapshot e productSkuSnapshot garantem 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
orderIdVARCHAR(36)nãoFK → orders.id (unique)
methodENUMnãoVer enum PaymentMethod abaixo
statusENUMnãoPENDINGVer enum PaymentStatus abaixo
amountDECIMAL(10,2)nãoValor total cobrado
transactionIdVARCHAR(191)simNULLID da transação no gateway (Stripe / PagSeguro)
pixQrCodeTEXTsimNULLQR Code PIX em base64 (imagem)
pixQrCodeTextTEXTsimNULLCódigo PIX copia-e-cola
expiresAtDATETIME(3)simNULLValidade do PIX ou boleto
paidAtDATETIME(3)simNULLTimestamp da confirmação do pagamento
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Í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).

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
orderIdVARCHAR(36)nãoFK → orders.id (Cascade)
typeENUMnãoSTATUS_CHANGESTATUS_CHANGE ou NOTE
fromStatusENUMsimNULLStatus anterior (OrderStatus)
toStatusENUMsimNULLStatus novo (NULL para eventos do tipo NOTE)
noteVARCHAR(191)simNULLObservação textual
adminIdVARCHAR(36)simNULLUUID do admin que realizou a ação
createdAtDATETIME(3)nãonow()

coupons

Cupons de desconto para uso no checkout. Suporta desconto percentual e valor fixo.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
codeVARCHAR(191)nãoCódigo único (ex: "NATAL20")
typeENUMnãoPERCENT ou FIXED
valueDECIMAL(10,2)nãoPercentual (ex: 20.00) ou valor (ex: 15.00)
minOrderValueDECIMAL(10,2)simNULLValor mínimo do pedido para aplicar
maxUsesINTsimNULLLimite de usos totais (NULL = ilimitado)
usedCountINTnão0Contador de usos (incrementado atomicamente)
activeTINYINT(1)não1Cupom habilitado
expiresAtDATETIME(3)simNULLValidade (NULL = sem expiração)
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Índices: code (unique), (active), (active, expiresAt)


kit_requests

Solicitações de kits personalizados (orçamentos para eventos corporativos, casamentos, etc.).

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
eventTypeVARCHAR(191)nãoTipo do evento (casamento, corporativo, etc.)
nameVARCHAR(191)nãoNome do solicitante
emailVARCHAR(191)nãoE-mail de contato
phoneVARCHAR(191)nãoTelefone
quantityINTnãoQuantidade de kits
productsTEXTnãoJSON com produtos de interesse
neededByVARCHAR(191)simNULLData limite de entrega
personalizationTEXTsimNULLDetalhes de personalização solicitados
notesTEXTsimNULLObservações adicionais
statusENUMnãoPENDINGVer enum KitRequestStatus abaixo
emailSentTINYINT(1)não0E-mail de confirmação enviado ao solicitante
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Í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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome exibido (ex: "Tamanho")
slugVARCHAR(191)nãoIdentificador URL (ex: "tamanho")
activeTINYINT(1)não1Tipo habilitado
orderINTnão0Ordem de exibição na PDP
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Índices: name (unique), slug (unique)


variant_options

Valores concretos para cada tipo (ex: "100ml", "200ml" para o tipo "Tamanho").

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
variantTypeIdVARCHAR(36)nãoFK → variant_types.id (Cascade)
valueVARCHAR(191)nãoValor da opção (ex: "Lavanda")
activeTINYINT(1)não1Opção disponível
orderINTnão0Ordem 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).

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
productIdVARCHAR(36)nãoFK → products.id (Cascade)
priceDECIMAL(10,2)nãoPreço desta variante (pode diferir do produto base)
comparePriceDECIMAL(10,2)simNULLPreço "de" para desconto
stockINTnão0Estoque desta variante
skuVARCHAR(191)simNULLSKU único (ex: "VEL-LAV-200-LAV")
activeTINYINT(1)não1Variante disponível para compra
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Í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.

CampoTipoNulávelDescrição
idVARCHAR(36)não
productVariantIdVARCHAR(36)nãoFK → product_variants.id (Cascade)
variantOptionIdVARCHAR(36)nãoFK → 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.

CampoTipoNulávelDescrição
idVARCHAR(36)não
userIdVARCHAR(36)nãoFK → users.id (Cascade)
productIdVARCHAR(36)nãoFK → products.id (Cascade)
createdAtDATETIME(3)nãoData 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
productIdVARCHAR(36)nãoFK → products.id (Cascade)
userIdVARCHAR(36)nãoFK → users.id (Cascade)
ratingINTnãoNota de 1 a 5
titleVARCHAR(191)simNULLTítulo da avaliação
bodyTEXTnãoTexto completo da avaliação
approvedTINYINT(1)não0Aprovada para exibição pública
createdAtDATETIME(3)nãonow()

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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome do cliente
locationVARCHAR(191)nãoCidade/estado (ex: "São Paulo, SP")
ratingINTnão5Nota de 1 a 5
textTEXTnãoTexto do depoimento
productNameVARCHAR(191)simNULLProduto citado no depoimento
avatarVARCHAR(191)nãoIniciais para avatar (ex: "AC")
activeTINYINT(1)não1Visível na homepage
orderINTnão0Posição no carrossel
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Í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.

CampoTipoNulávelDescrição
idVARCHAR(36)não
productIdVARCHAR(36)nãoFK → products.id (Cascade)
variantIdVARCHAR(36)simFK → product_variants.id (SetNull)
deltaINTnãoPositivo = entrada · Negativo = saída
reasonENUMnãoVer enum StockMovementReason abaixo
sourceENUMnãoVer enum StockMovementSource abaixo
orderIdVARCHAR(36)simPedido relacionado (quando reason = PURCHASE)
noteVARCHAR(191)simObservação textual do operador
adminIdVARCHAR(36)simUUID do admin responsável (movimentos manuais)
createdAtDATETIME(3)não

Índices: (productId, createdAt), (orderId)

Enum StockMovementReason:

ValorDescrição
PURCHASESaída por venda (pedido confirmado)
ADJUSTMENTAjuste manual pelo administrador
RETURNDevolução de mercadoria
INITIALEstoque inicial ao cadastrar o produto
CORRECTIONCorreção de erro de lançamento

Enum StockMovementSource:

ValorDescrição
ORDERDisparado automaticamente por pedido
ADMINRealizado manualmente pelo painel admin
APIOriginado via API key externa (ERP/integração)
SYSTEMProcesso 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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
typeENUMnãoTipo da notificação (ver abaixo)
titleVARCHAR(191)nãoTítulo resumido
bodyVARCHAR(191)nãoMensagem completa
linkVARCHAR(191)simNULLHref para navegação ao clicar
readTINYINT(1)não0Notificação lida
createdAtDATETIME(3)nãonow()

Í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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
emailVARCHAR(191)nãoE-mail único
nameVARCHAR(191)simNULLNome opcional
sourceVARCHAR(191)não"website"Origem: "website", "checkout", "admin"
confirmedAtDATETIME(3)simNULLData de confirmação double opt-in
unsubscribedAtDATETIME(3)simNULLData de descadastramento
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

Índices: email (unique), (createdAt), (confirmedAt)

Double opt-in: confirmedAt IS NULL indica 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.

CampoTipoNulávelDescrição
keyVARCHAR(191)nãoChave primária — identifica a seção (ex: "hero", "benefits")
valueJSONnãoJSON estruturado conforme tipo da seção (ver lib/site-settings.ts)
updatedAtDATETIME(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.).

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
nameVARCHAR(191)nãoNome descritivo do endpoint
urlVARCHAR(191)nãoURL do receptor
eventsVARCHAR(191)não"stock.updated"Eventos CSV assinados pelo endpoint
secretVARCHAR(191)nãoSegredo HMAC-SHA256 para assinar o payload
activeTINYINT(1)não1Endpoint habilitado
lastErrorTEXTsimNULLÚltimo erro de entrega
lastSentAtDATETIME(3)simNULLÚltima entrega com sucesso
createdAtDATETIME(3)nãonow()
updatedAtDATETIME(3)nãoauto

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.

CampoTipoNulávelPadrãoDescrição
idVARCHAR(36)nãouuid
endpointIdVARCHAR(36)nãoFK → webhook_endpoints.id (Cascade)
eventVARCHAR(191)nãoTipo do evento disparado
payloadTEXTnãoBody JSON enviado
responseCodeINTsimNULLHTTP status code da resposta
responseBodyTEXTsimNULLBody da resposta (para debug)
successTINYINT(1)não0true se HTTP 2xx
durationMsINTsimNULLLatência da requisição em milissegundos
createdAtDATETIME(3)nãonow()

Í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

ComandoAmbienteQuando usar
npx prisma db pushDevIteração rápida — sem histórico de migração
npx prisma migrate dev --name <desc>DevQuando a mudança precisa ser versionada para produção
npx prisma migrate deployProdAplica todas as migrações pendentes
npx prisma migrate statusQualquerVerifica quais migrações foram aplicadas
npx prisma migrate resetDevDestrutivo — recria banco e roda seed
npx prisma db push --force-resetTestesRecria banco SQLite limpo antes de cada test run
npx prisma studioDevInterface visual para inspecionar dados
npx prisma generateQualquerRegenera 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

EntidadeQuantidadeDetalhes
Usuários21 admin + 1 cliente de teste
Categorias4Velas Aromáticas, Difusores, Kits Presentes, Sachês
Produtos9Distribuídos entre as 4 categorias, 3 destacados
VariantTypes2Tamanho (slug: tamanho), Fragrância (slug: fragrancia)
VariantOptions4100ml, 200ml, Lavanda, Baunilha
ProductVariants4Combinações do produto Lavanda 200g

Credenciais de teste

PerfilE-mailSenhaRole
Adminadmin@ljvemasearomas.com.bradmin123ADMIN
Clientecliente@teste.comcliente123CUSTOMER

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

ÍndiceTipoJustificativa
emailUNIQUELogin por e-mail — busca O(log n) obrigatória em toda autenticação
(role)INDEXFiltrar admins vs clientes no painel
(role, createdAt)INDEXPaginação de usuários por role ordenada por data
(isActive)INDEXFiltrar contas suspensas rapidamente

Tabela products

ÍndiceTipoJustificativa
slugUNIQUELookup de página de produto — cada acesso à PDP usa este índice
skuUNIQUEBusca por SKU em integrações de estoque e importações
(active)INDEXTodas as listagens da loja filtram active = true
(active, featured)INDEXHomepage: produtos em destaque ativos
(active, createdAt)INDEXOrdenação "Mais recentes" na listagem da loja

Tabela orders

ÍndiceTipoJustificativa
orderNumberUNIQUEBusca de pedido por número — usada em e-mails e suporte
(userId, createdAt)INDEXHistórico de pedidos de um cliente paginado por data
(status, createdAt)INDEXDashboard admin: pedidos por status + ordenação temporal
(status)INDEXContagem de pedidos por status (badges do painel)
(createdAt)INDEXRelatórios por período (faturamento mensal, etc.)

Tabela coupons

ÍndiceTipoJustificativa
codeUNIQUEValidação de cupom no checkout — busca pelo código
(active)INDEXListagem de cupons ativos no admin
(active, expiresAt)INDEXFiltragem de cupons válidos (ativos e não expirados)

Tabela reviews

ÍndiceTipoJustificativa
(userId, productId)UNIQUEImpede múltiplas avaliações do mesmo cliente no mesmo produto
(productId, approved)INDEXCarregar avaliações aprovadas de um produto na PDP

Tabela product_variants

ÍndiceTipoJustificativa
skuUNIQUEIdentificação única de variante para integrações externas
(productId, active)INDEXCarregar variantes ativas de um produto para o seletor de PDP

Tabela stock_movements

ÍndiceTipoJustificativa
(productId, createdAt)INDEXHistórico de movimentações de um produto ordenado por data
(orderId)INDEXBuscar movimentação associada a um pedido específico

Tabela notifications

ÍndiceTipoJustificativa
(read, createdAt)INDEXBadge 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:

TabelaCampoEfeito quando false
usersisActiveConta suspensa — login bloqueado, dados preservados
productsactiveProduto oculto na loja, permanece em pedidos históricos
categoriesactiveCategoria oculta da navegação
couponsactiveCupom desabilitado para novos usos
api_keysactiveChave de API revogada — requests retornam 401
shipping_ratesactiveOpção de frete removida do checkout
testimonialsactiveDepoimento oculto da homepage
subscribersunsubscribedAt IS NOT NULLDescadastrado 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 PrismaTabela MySQL (@@map)
Userusers
Addressaddresses
Categorycategories
Productproducts
ProductImageproduct_images
CartItemcart_items
Orderorders
OrderItemorder_items
Paymentpayments
Wishlistwishlists
UserPreferencesuser_preferences
ApiKeyapi_keys
ShippingRateshipping_rates
VariantTypevariant_types
VariantOptionvariant_options
ProductVariantproduct_variants
ProductVariantValueproduct_variant_values
Reviewreviews
Couponcoupons
OrderEventorder_events
KitRequestkit_requests
StockMovementstock_movements
Notificationnotifications
Subscribersubscribers
SiteSettingssite_settings
WebhookEndpointwebhook_endpoints
WebhookDeliverywebhook_deliveries
Testimonialtestimonials

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

CampoTipo@default@updatedAtPresença
createdAtDATETIME(3)now()nãoTodos os modelos
updatedAtDATETIME(3)@updatedAtModelos mutáveis

Modelos append-only (order_events, stock_movements, webhook_deliveries) têm apenas createdAt.

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.description
  • order_items.variantLabel, productNameSnapshot
  • payments.pixQrCode, pixQrCodeText
  • kit_requests.products, personalization, notes
  • webhook_endpoints.lastError
  • webhook_deliveries.payload, responseBody
  • reviews.body
  • testimonials.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.