Pular para o conteúdo principal

Guia de Contribuição

Obrigado pelo interesse em contribuir com o projeto LJ Velas & Aromas! Este guia foi elaborado para tornar o processo de contribuição claro, consistente e eficiente, independentemente do seu nível de experiência.


Sumário

  1. Bem-vindo
  2. Pré-requisitos
  3. Fluxo de trabalho Git
  4. Convenções de commits
  5. Estrutura de arquivos e convenções de código
  6. Padrões por camada
  7. Escrevendo testes
  8. Checklist de PR
  9. Atualizando o schema do banco
  10. Adicionando uma integração de terceiros
  11. Code review
  12. Dúvidas e suporte

1. Bem-vindo

Este é o repositório da LJ Velas & Aromas — uma loja de velas artesanais construída com Next.js 15 e App Router. O projeto abrange vitrine da loja, painel administrativo, integração com meios de pagamento, cálculo de frete e emissão de NF-e.

Filosofia do projeto

  • Qualidade acima de velocidade. Código correto e testado vale mais do que código rápido e frágil.
  • Testes são obrigatórios. Toda feature nova e todo bug corrigido devem ter cobertura de testes.
  • Consistência é respeito. Seguir as convenções existentes facilita a vida de todos que mantêm o projeto.
  • Segurança não é opcional. Preços, estoque e autenticação são sempre validados no servidor.
  • Sem surpresas no CI. O pipeline deve sempre passar antes de um PR ser mergeado.

Novo no projeto? Comece por docs/SETUP.md para configurar o ambiente de desenvolvimento local, depois volte aqui.


2. Pré-requisitos

Certifique-se de ter as ferramentas abaixo instaladas antes de começar. Para instruções de instalação detalhadas, consulte docs/SETUP.md.

FerramentaVersão mínimaObservação
Node.js20.x LTSVersão recomendada para o projeto
npm10.xIncluído com Node.js 20
Git2.xNecessário para versionamento
Docker + Compose24.x / 2.xRecomendado para o banco de dados MySQL
MySQL8.0+Alternativa ao Docker para ambiente local

Verifique suas versões antes de começar:

node -v # → v20.x.x ou superior
npm -v # → 10.x.x ou superior
git --version # → git version 2.x.x
docker -v # → Docker version 24.x.x (se estiver usando)

Dica: O Docker é o caminho mais rápido para subir o banco de dados em desenvolvimento. Um único npm run db:up inicia o MySQL 8.0 e o phpMyAdmin sem nenhuma configuração adicional.


3. Fluxo de trabalho Git

3.1 Clone o repositório

# Clone via HTTPS
git clone https://github.com/seu-usuario/ljvemasearomas.git
cd ljvemasearomas

# Instale as dependências (o postinstall já roda prisma generate)
npm install

3.2 Configure o ambiente

# Copie o arquivo de variáveis de ambiente
cp .env.example .env

# Edite o .env com suas configurações locais
# Consulte docs/SETUP.md para detalhes de cada variável

3.3 Crie uma branch a partir de main

A branch padrão do projeto é main. Nunca commite diretamente em main.

Use a seguinte nomenclatura para nomear sua branch:

TipoPrefixoExemplo
Nova featurefeat/feat/filtro-por-faixa-de-preco
Correção de bugfix/fix/500-checkout-estoque-zero
Documentaçãodocs/docs/api-endpoints-v1
Testestest/test/e2e-variantes-produto
Refatoraçãorefactor/refactor/checkout-extrair-steps
Manutençãochore/chore/atualizar-dependencias
# Garanta que você está em main e atualizado
git checkout main
git pull origin main

# Crie e acesse sua branch
git checkout -b feat/nome-da-feature

Use nomes de branch em kebab-case, descritivos e em português ou inglês. Evite nomes genéricos como fix/bug ou feat/mudancas.

3.4 Desenvolva com commits frequentes

Commite com frequência à medida que avança. Commits pequenos e atômicos são mais fáceis de revisar e reverter. Siga as convenções de commits descritas na seção seguinte.

git add .
git commit -m "feat(loja): adiciona filtro por faixa de preço no sidebar"

3.5 Garanta qualidade antes do push

Execute os checks localmente antes de enviar seu código. O CI vai rodar os mesmos checks e falhará se eles não passarem.

# 1. Lint — zero warnings tolerados
npm run lint

# 2. Type check — sem erros de tipo
npx tsc --noEmit

# 3. Testes unitários e de integração
npm test

# 4. (Opcional) Testes E2E — necessário app rodando
npm run test:e2e

3.6 Abra um Pull Request para main

git push origin feat/nome-da-feature

Em seguida, abra um Pull Request no GitHub apontando para main. Use um título claro e uma descrição que explique o quê foi feito e por quê. Consulte o Checklist de PR antes de submeter.

O pipeline de CI (.github/workflows/ci.yml e .github/workflows/tests.yml) será acionado automaticamente. Os testes Playwright E2E rodam apenas em PRs para main, após os testes Jest passarem.


4. Convenções de commits

O projeto adota Conventional Commits com prefixos que descrevem o tipo e a área da mudança. Mensagens de commit claras tornam o histórico legível e facilitam a geração de changelogs.

Formato

<tipo>(<área>): <descrição imperativa em minúsculas>

[corpo opcional — mais detalhes sobre a mudança]

[rodapé opcional — refs de issues, breaking changes]

Tabela de prefixos

PrefixoQuando usarExemplo
feat(área)Nova funcionalidadefeat(loja): adiciona filtro por faixa de preço
fix(área)Correção de bugfix(api): corrige 500 em POST /api/orders quando estoque é 0
test(área)Adição ou correção de testestest(e2e): adiciona spec de variantes de produto
docs(área)Documentaçãodocs(api): documenta endpoints da API v1
refactor(área)Refatoração sem mudança de comportamentorefactor(checkout): extrai lógica de cálculo para hook
choreManutenção, dependências, CIchore: atualiza dependências de desenvolvimento
style(área)Ajustes de formatação sem lógicastyle(header): ajusta espaçamento do menu mobile
perf(área)Melhoria de performanceperf(produto): adiciona cache no endpoint de produtos em destaque
sprint(15.x)Commits de sprint (padrão interno)sprint(15.3): implementa fluxo de rastreamento de pedidos

Boas práticas para mensagens de commit

  • Use o imperativo presente: "adiciona" e não "adicionado" ou "adicionando"
  • A primeira linha deve ter no máximo 72 caracteres
  • A área deve ser o módulo afetado: loja, admin, api, checkout, auth, frete, email, db, e2e, etc.
  • Se o commit resolve um issue, adicione Closes #123 no rodapé
# Exemplo com corpo e rodapé
git commit -m "fix(api): corrige 500 em POST /api/orders quando estoque é 0

Ao tentar criar um pedido com produto sem estoque, o servidor lançava
uma exceção não tratada. Agora retorna 422 com mensagem adequada.

Closes #47"

5. Estrutura de arquivos e convenções de código

5.1 Nomes de arquivo: kebab-case obrigatório

Todos os arquivos TypeScript/TSX do projeto usam kebab-case. Nunca use PascalCase ou camelCase para nomes de arquivo.

✅ product-card.tsx
✅ cart-drawer.tsx
✅ add-to-cart-button.tsx
✅ use-debounce.ts
✅ shipping-calculator.ts

❌ ProductCard.tsx
❌ cartDrawer.tsx
❌ AddToCartButton.tsx

5.2 Onde cada tipo de arquivo vive

TipoDiretórioExemplo de arquivo
Componentes da lojacomponents/store/product-card.tsx
Componentes do admincomponents/admin/orders-table.tsx
Design system / UIcomponents/ui/button.tsx, dialog.tsx
Páginas da lojaapp/(store)/app/(store)/page.tsx
Páginas do adminapp/(admin)/app/(admin)/dashboard/page.tsx
Route Handlersapp/api/app/api/products/route.ts
Utilitários e serviçoslib/lib/shipping.ts, lib/email.ts
Stores Zustandstore/store/cart.ts
Tipos globaistypes/types/index.ts
Testes unitários / lib__tests__/lib/__tests__/lib/shipping.test.ts
Testes de API__tests__/api/__tests__/api/orders/
Testes de componente__tests__/components/__tests__/components/store/
Testes E2Ee2e/e2e/purchase.spec.ts

5.3 Componentes

  • Um componente por arquivo — nunca exporte múltiplos componentes de um mesmo arquivo
  • Export default para o componente principal do arquivo
  • Named exports para tipos, helpers e constantes relacionados ao componente
// ✅ Padrão correto
export interface ProductCardProps {
product: ProductSummary
priority?: boolean
}

export default function ProductCard({ product, priority = false }: ProductCardProps) {
// ...
}

5.4 Ícones: sempre Lucide React

O projeto usa exclusivamente Lucide React para ícones. Nunca use emojis no JSX como substitutos de ícones funcionais.

// ✅ Correto
import { ShoppingCart, Heart, Search } from 'lucide-react'

function CartButton() {
return <ShoppingCart className="h-5 w-5" />
}

// ❌ Errado — nunca use emojis no JSX
function CartButton() {
return <span>🛒</span>
}

5.5 Comentários: em português

Os comentários de código seguem o português como padrão do projeto, alinhado com a documentação e as mensagens de commit.

// ✅ Correto — comentários em português
// Chave única por item — combina produto + variante quando houver
function cartKey(p: Pick<CartProduct, 'id' | 'variantId'>): string {
return p.variantId ? `${p.id}::${p.variantId}` : p.id
}

5.6 TypeScript: tipagem explícita

  • Interfaces e props sempre com tipos explícitos — nunca any
  • Retornos de função devem ter tipo declarado quando não for trivialmente inferível
  • Use unknown em vez de any quando o tipo for realmente desconhecido
  • Prefira interface a type para objetos (use type para unions, intersections e utilitários)
// ✅ Correto
export interface ProductFilters {
categorySlug?: string
minPrice?: number
maxPrice?: number
inStock?: boolean
}

async function fetchProducts(filters: ProductFilters): Promise<Product[]> {
// ...
}

// ❌ Errado — nunca use `any`
async function fetchProducts(filters: any): Promise<any> {
// ...
}

6. Padrões por camada

6.1 Route Handlers (app/api/)

Todo Route Handler deve seguir o padrão de autenticação, formato de resposta e tratamento de erros adotado no projeto:

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { requireAuth } from '@/lib/api-auth'
import { z } from 'zod'

// Schema Zod para validação do body
const createOrderSchema = z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
})).min(1),
})

export async function POST(req: NextRequest) {
// 1. Autenticação
const session = await requireAuth(req)
if (!session) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 })
}

// 2. Validação com Zod
const body = await req.json()
const parsed = createOrderSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Dados inválidos', details: parsed.error.flatten() },
{ status: 422 },
)
}

// 3. Lógica de negócio com tratamento de erro
try {
const order = await prisma.order.create({ data: { /* ... */ } })
return NextResponse.json(order, { status: 201 })
} catch (error) {
console.error('[POST /api/orders]', error)
return NextResponse.json({ error: 'Erro interno' }, { status: 500 })
}
}

Regras para Route Handlers:

  • Sempre validar o body com Zod antes de processar
  • Sempre re-validar preços e disponibilidade de estoque no servidor — nunca confiar nos dados do cliente
  • Retornar mensagens de erro sem vazar detalhes internos para o cliente
  • Logar erros com console.error incluindo o endpoint no prefixo: [POST /api/orders]

6.2 Componentes RSC vs RCC

O App Router permite dois tipos de componentes: React Server Components (RSC) e React Client Components (RCC). Prefira sempre RSC e adicione "use client" apenas quando estritamente necessário.

Use RSC (padrão) quando:

  • O componente apenas renderiza dados (busca no banco, lê arquivos)
  • Não há interação do usuário (eventos de clique, hover, formulários)
  • Não precisa de estado local (useState) ou efeitos (useEffect)
  • Não usa hooks de browser (useRouter, useSearchParams, etc.)

Use RCC ("use client") quando:

  • O componente precisa de estado local interativo
  • Usa event listeners (onClick, onChange)
  • Depende de APIs do browser (localStorage, window, document)
  • Usa hooks como useState, useEffect, useContext
  • Usa stores Zustand (useCartStore, useWishlistStore)
// ✅ RSC — busca dados diretamente, sem "use client"
// app/(store)/produtos/page.tsx
async function ProdutosPage() {
const products = await prisma.product.findMany({ where: { active: true } })
return <ProductGrid products={products} />
}

// ✅ RCC — necessário porque usa estado e eventos
// components/store/add-to-cart-button.tsx
'use client'

export default function AddToCartButton({ product }: Props) {
const addItem = useCartStore((s) => s.addItem)
return (
<button onClick={() => addItem(product)}>
<ShoppingCart className="h-4 w-4" /> Adicionar ao carrinho
</button>
)
}

Atenção: Marcar um componente com "use client" também torna todos os seus filhos componentes cliente. Por isso, mantenha os componentes cliente o mais "folha" possível na árvore.

6.3 Prisma: sempre via singleton

Nunca instancie PrismaClient diretamente. Use sempre o singleton exportado de @/lib/prisma:

// ✅ Correto — sempre importe do singleton
import { prisma } from '@/lib/prisma'

const products = await prisma.product.findMany({
include: { category: true, variants: true },
})

// ❌ Errado — nunca instancie diretamente
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() // causa múltiplas conexões em dev!

Boas práticas com Prisma:

  • Use include para carregar relações em uma única query — evite N+1 queries
  • Use select para retornar apenas os campos necessários
  • Em transações, use prisma.$transaction([...])
  • Após alterar prisma/schema.prisma, rode npx prisma generate para atualizar o client

6.4 Zustand: stores tipados com partialize

As stores Zustand ficam em store/ e devem ter tipagem explícita. Use persist com partialize para nunca salvar estado volátil (como se um drawer está aberto) no localStorage:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// ✅ Interface explícita para o estado e as ações
interface ExampleStore {
// Estado persistido
items: Item[]
// Estado volátil (não deve ser salvo)
isLoading: boolean

// Ações
addItem: (item: Item) => void
setLoading: (loading: boolean) => void
}

export const useExampleStore = create<ExampleStore>()(
persist(
(set) => ({
items: [],
isLoading: false,
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
setLoading: (loading) => set({ isLoading: loading }),
}),
{
name: 'ljvemasearomas-example',
// ✅ partialize exclui estado volátil da persistência
partialize: (state) => ({ items: state.items }),
},
),
)

6.5 Validação: Zod e React Hook Form

  • API Routes: sempre use Zod para validar o corpo das requisições (.safeParse())
  • Formulários: sempre use react-hook-form com zodResolver para validação client-side
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

// ✅ Schema Zod compartilhado entre frontend e backend
const addressSchema = z.object({
cep: z.string().regex(/^\d{8}$/, 'CEP inválido'),
street: z.string().min(3, 'Rua é obrigatória'),
number: z.string().min(1, 'Número é obrigatório'),
city: z.string().min(2, 'Cidade é obrigatória'),
state: z.string().length(2, 'UF deve ter 2 caracteres'),
})

type AddressFormData = z.infer<typeof addressSchema>

function AddressForm() {
const { register, handleSubmit, formState: { errors } } = useForm<AddressFormData>({
resolver: zodResolver(addressSchema),
})

const onSubmit = (data: AddressFormData) => { /* ... */ }

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('cep')} placeholder="CEP" />
{errors.cep && <span>{errors.cep.message}</span>}
{/* ... */}
</form>
)
}

7. Escrevendo testes

O projeto usa Jest + React Testing Library para testes unitários e de componentes, e Playwright para testes E2E. A cobertura de código é medida sobre lib/** e components/** com um threshold mínimo de 60% em branches, funções, linhas e statements.

7.1 Onde criar cada tipo de teste

TipoDiretórioSufixoExemplo
Utilitários / lib__tests__/lib/.test.ts__tests__/lib/shipping.test.ts
Route Handlers (integração)__tests__/api/.test.ts__tests__/api/orders/create.test.ts
Componentes RTL__tests__/components/.test.tsx__tests__/components/store/product-card.test.tsx
Fluxos E2Ee2e/.spec.tse2e/purchase.spec.ts

7.2 Scripts de teste

# Roda todos os testes Jest uma vez
npm test

# Modo watch — ideal durante o desenvolvimento
npm run test:watch

# Modo CI — com coverage e forceExit (usado no pipeline)
npm run test:ci

# Testes E2E com Playwright (requer app rodando)
npm run test:e2e

# Playwright com interface visual (debug interativo)
npm run test:e2e:ui

# Abre o relatório HTML do último run E2E
npm run test:e2e:report

7.3 Teste unitário de utilitário

import { formatPrice, calculateDiscount } from '@/lib/utils'

describe('formatPrice', () => {
it('formata valores em reais com duas casas decimais', () => {
expect(formatPrice(1990)).toBe('R$ 19,90')
expect(formatPrice(100000)).toBe('R$ 1.000,00')
})

it('retorna R$ 0,00 para valores zero ou negativos', () => {
expect(formatPrice(0)).toBe('R$ 0,00')
expect(formatPrice(-50)).toBe('R$ 0,00')
})
})

describe('calculateDiscount', () => {
it('aplica desconto percentual corretamente', () => {
expect(calculateDiscount(10000, 10)).toBe(9000)
})

it('não permite desconto maior que 100%', () => {
expect(calculateDiscount(10000, 150)).toBe(0)
})
})

7.4 Teste de Route Handler (integração com SQLite)

Os testes de API usam SQLite em memória (DATABASE_URL=file:./test.db) para isolar do banco de produção:

import { POST } from '@/app/api/products/route'
import { prisma } from '@/lib/prisma'

// Limpa o banco antes de cada teste para garantir isolamento
beforeEach(async () => {
await prisma.product.deleteMany()
})

afterAll(async () => {
await prisma.$disconnect()
})

describe('POST /api/products', () => {
it('cria produto com dados válidos e retorna 201', async () => {
const req = new Request('http://localhost/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Vela Lavanda', price: 3990, stock: 10 }),
})

const res = await POST(req as any)
const data = await res.json()

expect(res.status).toBe(201)
expect(data.name).toBe('Vela Lavanda')
expect(data.price).toBe(3990)
})

it('retorna 422 quando campos obrigatórios estão ausentes', async () => {
const req = new Request('http://localhost/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Produto sem preço' }),
})

const res = await POST(req as any)
expect(res.status).toBe(422)
})
})

7.5 Teste de componente (React Testing Library)

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ProductCard from '@/components/store/product-card'

const mockProduct = {
id: '1',
name: 'Vela Soja Lavanda',
slug: 'vela-soja-lavanda',
price: 3990,
image: '/products/lavanda.jpg',
}

describe('ProductCard', () => {
it('renderiza o nome e preço do produto', () => {
render(<ProductCard product={mockProduct} />)

expect(screen.getByText('Vela Soja Lavanda')).toBeInTheDocument()
expect(screen.getByText('R$ 39,90')).toBeInTheDocument()
})

it('exibe imagem com alt text acessível', () => {
render(<ProductCard product={mockProduct} />)

const img = screen.getByRole('img', { name: /vela soja lavanda/i })
expect(img).toBeInTheDocument()
})

it('navega para a página do produto ao clicar', async () => {
render(<ProductCard product={mockProduct} />)

const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', '/produtos/vela-soja-lavanda')
})
})

7.6 Teste E2E (Playwright)

import { test, expect } from '@playwright/test'

test.describe('Fluxo de compra', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/produtos/vela-soja-lavanda')
})

test('adiciona produto ao carrinho e verifica contador', async ({ page }) => {
await page.getByRole('button', { name: /adicionar ao carrinho/i }).click()

const cartBadge = page.getByTestId('cart-badge')
await expect(cartBadge).toHaveText('1')
})

test('aplica cupom de desconto no checkout', async ({ page }) => {
await page.getByRole('button', { name: /adicionar ao carrinho/i }).click()
await page.goto('/checkout')

await page.getByPlaceholder(/código do cupom/i).fill('PRIMEIRAVELA10')
await page.getByRole('button', { name: /aplicar/i }).click()

await expect(page.getByText(/desconto aplicado/i)).toBeVisible()
})
})

7.7 Regras de cobertura de testes

  • Toda feature nova deve ter ao menos um teste — unitário, de integração ou E2E
  • Todo bug corrigido deve ter um teste de regressão — garanta que o bug não volte
  • Testes de comportamento são mais valiosos do que testes de implementação — teste o que o usuário vê e faz, não os detalhes internos

8. Checklist de PR

Antes de submeter seu Pull Request, revise cada item abaixo:

Qualidade de código

  • npm run lint passa sem erros (o script local enforça --max-warnings=0)
  • npx tsc --noEmit passa sem erros de tipo
  • Nenhum any introduzido sem justificativa documentada em comentário
  • Nenhum console.log, console.debug ou código de debug commitado

Testes

  • npm test passa com todos os testes verdes
  • Testes novos ou atualizados para cobrir a mudança
  • Se corrigiu um bug: existe um teste de regressão que falha antes da correção e passa depois

Segurança e dados

  • Nenhum arquivo .env, .env.local ou segredo commitado
  • Nenhuma chave de API ou token hardcoded no código
  • Preços e estoque re-validados no servidor (nunca confiar no payload do cliente)
  • Inputs do usuário validados com Zod antes de qualquer operação de banco

Banco de dados (se aplicável)

  • Alterações no prisma/schema.prisma acompanhadas de arquivo de migração (npx prisma migrate dev)
  • prisma/seed.js atualizado se novos dados de seed forem necessários
  • docs/DATABASE.md atualizado com as mudanças de schema

Descrição do PR

  • Título do PR segue as convenções de commit (feat(área): descrição)
  • Descrição explica o quê foi feito e por quê
  • Issues relacionadas referenciadas no corpo (Closes #123, Refs #456)
  • Screenshots ou GIFs incluídos para mudanças visuais

Template de descrição de PR

## O que foi feito

Descreva brevemente as mudanças introduzidas neste PR.

## Por que foi feito

Contexto e motivação para a mudança.

## Como testar

Passo a passo para verificar o comportamento esperado.

## Screenshots (se aplicável)

Inclua capturas de tela para mudanças de UI.

## Issues relacionadas

Closes #

9. Atualizando o schema do banco

O banco de dados é gerenciado pelo Prisma ORM. Siga este fluxo ao fazer alterações no schema:

9.1 Fluxo em desenvolvimento

# 1. Edite o schema
# Arquivo: prisma/schema.prisma

# 2. Sincronize o banco local (sem gerar arquivo de migração)
npm run db:push

# 3. Regenere o Prisma Client
npx prisma generate

# 4. Teste sua mudança
npm test

db:push é ideal para exploração rápida em desenvolvimento. Não gera arquivos de migração rastreáveis — use apenas localmente.

9.2 Fluxo para mudanças que vão para produção

# 1. Edite prisma/schema.prisma

# 2. Gere um arquivo de migração versionado
npx prisma migrate dev --name descricao-da-mudanca
# Exemplo: npx prisma migrate dev --name add-product-weight-field

# Isso irá:
# - Criar um arquivo em prisma/migrations/TIMESTAMP_descricao-da-mudanca/migration.sql
# - Aplicar a migração ao banco local
# - Regenerar o Prisma Client automaticamente

# 3. Revise o SQL gerado antes de commitar
cat prisma/migrations/*/migration.sql

Nunca altere um arquivo de migração já commitado. Crie sempre uma nova migração para corrigir uma migração anterior.

9.3 Checklist de mudança de schema

  • prisma/schema.prisma editado com as novas definições
  • Arquivo de migração gerado com npx prisma migrate dev --name <descricao>
  • prisma/seed.js atualizado se a mudança introduz novos campos obrigatórios
  • docs/DATABASE.md atualizado com a documentação das novas tabelas/campos
  • Testes passando após a mudança (npm test)

9.4 Comandos Prisma de referência

ComandoDescrição
npm run db:pushSincroniza schema com o banco (sem migração — apenas dev)
npm run db:seedPopula o banco com dados de teste
npm run db:resetApaga tudo e recria com seed (destrói dados!)
npm run db:studioAbre o Prisma Studio em http://localhost:5555
npx prisma migrate dev --name <nome>Cria e aplica migração versionada
npx prisma generateRegenera o Prisma Client após alterar o schema
npx prisma migrate statusMostra status das migrações pendentes

10. Adicionando uma integração de terceiros

Ao integrar um novo serviço externo (gateway de pagamento, transportadora, serviço de e-mail, etc.), siga este processo:

10.1 Estrutura e localização

# Crie o wrapper/client em lib/
lib/nome-do-servico.ts

# Exemplos existentes no projeto:
lib/mercadopago.ts # Cliente do Mercado Pago
lib/email.ts # Nodemailer / Resend
lib/shipping.ts # Cálculo de frete (múltiplos provedores)
lib/nfe.ts # Serviço de NF-e

10.2 Variáveis de ambiente

Toda credencial de integração deve passar por variável de ambiente. Nunca hardcode tokens ou chaves no código.

# 1. Adicione ao .env.example com comentário explicativo
# Serviço XYZ — obtenha em https://servico.com/developers
SERVICO_XYZ_API_KEY="sua-chave-aqui"
SERVICO_XYZ_WEBHOOK_SECRET="seu-secret-aqui"

# 2. Adicione ao seu .env local com os valores reais
# (o .env nunca é commitado — está no .gitignore)

10.3 Documentação obrigatória

  • Documente as novas variáveis em docs/SETUP.md (seção de configuração do .env)
  • Se a integração afeta o deploy, documente em docs/DEPLOYMENT.md
  • Adicione um comentário JSDoc no wrapper explicando o propósito e limitações do serviço

10.4 Padrão de implementação

/**
* Cliente para o Serviço XYZ.
* Documentação: https://docs.servico.xyz/api
*
* Variáveis necessárias:
* - SERVICO_XYZ_API_KEY: chave de API obtida no painel do serviço
*/

const API_KEY = process.env.SERVICO_XYZ_API_KEY
const BASE_URL = 'https://api.servico.xyz/v1'

if (!API_KEY && process.env.NODE_ENV === 'production') {
throw new Error('SERVICO_XYZ_API_KEY não configurada')
}

export async function fetchFromServico<T>(endpoint: string): Promise<T> {
const res = await fetch(`${BASE_URL}${endpoint}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
})

// ✅ Fallback gracioso — nunca deixe uma integração travar o fluxo principal
if (!res.ok) {
console.error(`[servico-xyz] Erro ${res.status} em ${endpoint}`)
throw new Error(`Serviço indisponível: ${res.status}`)
}

return res.json() as Promise<T>
}

10.5 Testes com mock

Toda integração de terceiros deve ter testes usando mock para não depender do serviço externo em CI:

// Use jest.mock ou MSW para interceptar as chamadas HTTP
jest.mock('@/lib/nome-do-servico', () => ({
fetchFromServico: jest.fn().mockResolvedValue({ status: 'ok' }),
}))

import { fetchFromServico } from '@/lib/nome-do-servico'

describe('Integração ServicoXYZ', () => {
it('processa resposta de sucesso corretamente', async () => {
const result = await fetchFromServico('/endpoint')
expect(result).toEqual({ status: 'ok' })
})

it('lança erro quando serviço retorna falha', async () => {
(fetchFromServico as jest.Mock).mockRejectedValueOnce(
new Error('Serviço indisponível: 503')
)

await expect(fetchFromServico('/endpoint')).rejects.toThrow('503')
})
})

10.6 Checklist de nova integração

  • Wrapper criado em lib/nome-do-servico.ts
  • Variáveis de ambiente adicionadas em .env.example com comentários
  • Variáveis documentadas em docs/SETUP.md
  • Deploy documentado em docs/DEPLOYMENT.md (se aplicável)
  • Fallback ou tratamento de erro gracioso implementado
  • Testes com mock cobrindo cenários de sucesso e falha

11. Code review

O que os revisores verificam

O processo de code review tem como objetivo garantir qualidade, segurança e consistência. Os revisores verificarão:

Segurança

PontoO que verificar
Segredos expostosNenhum token, senha ou chave hardcoded
Validação server-sidePreços, estoque e permissões re-validados na API
Inputs sanitizadosTodos os inputs de usuário validados com Zod
Autenticação corretaRotas protegidas usam requireAuth ou requireAdminAuth
Dados sensíveis em logNenhum dado pessoal ou financeiro logado

Performance

PontoO que verificar
N+1 queriesUsar include do Prisma em vez de queries dentro de loops
use client desnecessárioComponentes que não precisam de interatividade são RSC
Imagens otimizadasUso de <Image> do Next.js com width, height e alt
Imports pesadosImports de bibliotecas grandes feitos de forma seletiva

Acessibilidade

PontoO que verificar
Labels em inputsTodo <input> tem <label> ou aria-label associado
Alt em imagensToda <Image> tem alt descritivo (vazio apenas para imagens decorativas)
Contraste adequadoTexto sobre fundo tem contraste mínimo de 4.5:1 (WCAG AA)
Elementos interativosBotões e links são focáveis via teclado
Roles semânticosElementos usam as tags HTML semanticamente corretas

Consistência

PontoO que verificar
Convenções de arquivokebab-case, diretório correto, export default
Padrões de componenteSegue a estrutura dos componentes existentes
Tratamento de estadoUsa Zustand corretamente, sem prop drilling excessivo
Mensagens ao usuárioUsa react-hot-toast para feedback de ações

Como responder a uma revisão

  • Endereçe cada comentário com uma ação: corrija, explique por que não concorda, ou pergunte se não entendeu
  • Commits de fixup após a revisão devem seguir o mesmo padrão: fix(área): endereça review — descrição
  • Uma vez que todos os comentários estejam resolvidos e os checks do CI estejam verdes, o PR está pronto para merge

12. Dúvidas e suporte

Onde pedir ajuda

  • Dúvidas sobre o projeto: Abra uma Discussion no GitHub
  • Bug encontrado: Abra um Issue seguindo o template abaixo
  • Dúvidas sobre setup: Consulte primeiro docs/SETUP.md — a maioria dos problemas comuns está documentada lá

Como reportar um bug

Ao abrir um issue de bug, inclua as seguintes informações para facilitar a reprodução e correção:

**Versão do Node.js:**
node -v → v20.x.x

**Descrição do bug:**
Descrição clara e concisa do comportamento incorreto.

**Passos para reproduzir:**
1. Acesse a página '...'
2. Clique em '...'
3. Preencha o campo '...' com '...'
4. Veja o erro

**Comportamento esperado:**
O que deveria acontecer.

**Comportamento atual:**
O que está acontecendo.

**Screenshots / Logs:**
Se aplicável, adicione capturas de tela ou a mensagem de erro do console.

**Ambiente:**
- OS: Windows / macOS / Linux
- Browser: Chrome 120, Firefox 121, etc.
- Versão do projeto: (hash do último commit)

Como sugerir uma melhoria

Para sugestões de nova funcionalidade ou melhoria, abra um issue com o prefixo [Feature Request] no título e descreva:

  • Motivação: qual problema essa melhoria resolve?
  • Proposta: como você imagina a solução?
  • Alternativas consideradas: quais outras abordagens foram descartadas e por quê?

Feito com 🕯️ pela equipe LJ Velas & Aromas.
Sua contribuição faz o projeto crescer — obrigado por dedicar seu tempo a torná-lo melhor!