← Playbooks
Lojas e sites 90 dias Avançado

Desenvolvimento de E-commerce Proprietário (Laravel)

Playbook completo para construir uma loja virtual própria em Laravel, do levantamento à produção: arquitetura de domínio, catálogo com variações, carrinho, checkout, Mercado Pago, frete Melhor Envio, antifraude, painel Filament, IA para descrições e atendimento, SEO técnico, testes e deploy. Fiel à stack usada em db e fm (Laravel, Filament, Tailwind, Alpine, R2).

Etapa 1 — Descoberta e arquitetura

Entender o negócio e desenhar a solução antes de codar.

Briefing inicial com o cliente

2h Básico

Conversa estruturada para entender objetivos, contexto e restrições.

O que é o briefing inicial

O briefing é a primeira conversa estruturada do projeto. Ele alinha expectativas, descobre restrições escondidas e define o que é sucesso.

Como conduzir

  1. Apresentação do projeto e do que será feito.
  2. Perguntas abertas sobre o problema que motivou o projeto.
  3. Perguntas fechadas sobre restrições (prazo, orçamento, marca, técnico).
  4. Resumo verbal: "ouvi X, Y e Z — confirma?".
  5. Próximos passos e prazos para resposta.

Critérios de qualidade

  • Foi gravado/transcrito (com autorização).
  • O cliente confirmou o resumo.
  • Foram identificados os principais riscos.

Checklist

  • Agendar reunião de briefing
  • Preparar lista de perguntas
  • Conduzir reunião com gravação/transcrição
  • Escrever resumo estruturado
  • Enviar para confirmação do cliente

Ferramentas

  • Bloco de notas

Entregável

Resumo escrito do briefing + ata da reunião.

Levantar os requisitos da loja

8h Intermediário

Transformar a visão do cliente em um backlog técnico claro e priorizado para o e-commerce.

Por que isto importa

Construir um e-commerce proprietário em Laravel é caro e demorado. O maior risco do projeto não é técnico, é de escopo: descobrir tarde que o cliente esperava parcelamento, retirada na loja ou um catálogo dez vezes maior do que o previsto. A descoberta de requisitos existe para eliminar essas surpresas antes de uma única migration ser escrita. Ela transforma a visão difusa do lojista em decisões concretas que vão moldar o domínio, a integração de pagamento, o frete e a infraestrutura. Uma descoberta bem-feita economiza semanas de retrabalho.

Mapear o escopo do catálogo

Comece entendendo o que será vendido. O tamanho e a complexidade do catálogo definem boa parte da arquitetura. Pergunte: quantos produtos hoje e quantos em um ano? Os produtos têm variações (tamanho, cor, voltagem)? Cada variação tem estoque e preço próprios? Há marcas, coleções e categorias hierárquicas?

Essa conversa antecipa decisões de modelagem. Se há variação com SKU próprio, você já sabe que precisará de Produto e ProdutoVariacao separados, com estoque na variação. Se o cliente fala em coleções sazonais, surge a entidade Colecao. Registre cada atributo relevante: precisa de cor como entidade própria, de avaliações de clientes, de lista de desejos. Não decida a modelagem agora, apenas colete o material bruto.

Pagamento, frete e omnichannel

Pagamento e entrega são onde os projetos mais derrapam. Para o pagamento, confirme com o cliente o gateway (o padrão da stack é Mercado Pago via mercadopago/dx-php) e os meios aceitos: PIX, cartão, parcelamento e em quantas parcelas. Pergunte sobre antifraude e sobre o tratamento de pedidos pendentes de pagamento, porque isso vira regra de negócio no CheckoutService.

Para o frete, o padrão é Melhor Envio. Defina se haverá cálculo dinâmico por CEP, frete grátis acima de um valor e quais transportadoras. Avalie desde já se a integração usará token legado ou OAuth2 com refresh token, pois muda a complexidade.

O ponto mais esquecido é o omnichannel. Pergunte explicitamente: existe loja física? O cliente pode retirar no balcão? Há mais de uma unidade? Se sim, nasce a entidade Unidade, que afeta estoque por local e opções de entrega no checkout. Descobrir isso na descoberta é barato; descobrir depois do checkout pronto é caro.

Requisitos não-funcionais

Funcionalidade não basta. Levante o que o sistema precisa suportar de carga e comportamento:

  • Volume esperado de pedidos por dia e picos de tráfego.
  • Sazonalidade (Black Friday, datas comemorativas) e o que acontece se a loja triplicar de acesso.
  • Expectativa de performance nas páginas de listagem e produto.
  • Necessidade de auditoria (quem alterou preço, estoque, status de pedido).
  • Política de imagens: peso, formato, quantidade por produto.

Esses requisitos justificam decisões como usar a fila database para processar webhooks de pagamento de forma assíncrona, armazenar imagens no Cloudflare R2 com conversão WebP, e habilitar o Activity Log da Spatie para auditoria. Sem registrá-los, essas escolhas parecem capricho técnico; com eles, viram requisito rastreável.

Traduzir em backlog técnico

A descoberta só tem valor se virar plano de trabalho. Pegue cada requisito coletado e transforme em itens de backlog que o time de desenvolvimento consiga estimar. Agrupe por épico: catálogo, carrinho, checkout, pagamento, frete, painel administrativo, conta do cliente.

Exemplo de tradução de requisito em tarefa técnica:

Requisito: "Cliente quer PIX e cartão em até 12x"
Backlog:
  - Integrar MercadoPagoService (PIX + cartão)
  - Model Pagamento com PagamentoStatus (enum)
  - Webhook assíncrono na fila database
  - Regra de parcelamento no CheckoutService

Priorize pelo que destrava o fluxo de compra ponta a ponta. Catálogo e checkout vêm antes de wishlist e cupom. Marque dependências, registre as decisões de pagamento e frete e valide o documento com o cliente antes de avançar. O entregável é um documento que qualquer pessoa do time consiga ler e entender o que será construído e por quê.

Critérios de qualidade

  • O documento descreve o catálogo com volume, variações e atributos confirmados pelo cliente.
  • Formas de pagamento e entrega estão decididas, incluindo parcelamento e frete grátis.
  • A existência (ou não) de ponto de retirada e multi-loja está explicitamente registrada.
  • Há requisitos não-funcionais quantificados (volume, picos, sazonalidade, auditoria).
  • Cada requisito tem ao menos um item correspondente no backlog técnico.
  • O cliente validou e aprovou o documento antes do início do desenvolvimento.

Checklist

  • Mapear o catálogo esperado (volume de produtos, variações, categorias)
  • Definir formas de pagamento e entrega com o cliente
  • Confirmar se há ponto de retirada ou multi-loja (omnichannel)
  • Registrar requisitos não-funcionais (volume, performance, sazonalidade)
  • Traduzir tudo em backlog técnico priorizado

Ferramentas

Entregável

Documento de requisitos com escopo, regras de negócio e backlog técnico priorizado.

Desenhar a arquitetura de domínio

12h Avançado

Modelar o domínio do e-commerce e definir a stack e os padrões de arquitetura.

Por que isto importa

A arquitetura de domínio é a decisão mais cara de reverter em um e-commerce. Um modelo mal desenhado vaza para o checkout, para o painel administrativo e para os relatórios fiscais, e cada correção depois de produção custa muito. Esta atividade transforma os requisitos levantados em um modelo de dados coeso, em padrões de código que o time vai seguir e em escolhas de stack justificadas. O objetivo não é prever todo o futuro, é construir um núcleo sólido que aceite crescimento sem virar uma teia de gambiarras.

Modelar as entidades centrais

O domínio em português organiza o vocabulário do projeto. As entidades centrais são:

  • Catálogo: Produto, ProdutoVariacao (o SKU real, com estoque e preço diferencial), ProdutoImagem, Categoria, Marca, Colecao, Cor.
  • Venda: Carrinho, CarrinhoItem, Cupom, Pedido, PedidoItem, PedidoHistorico.
  • Pagamento: Pagamento como model separado do pedido.
  • Cliente e logística: Cliente, Avaliacao, ListaDesejo, ClienteCredito, Unidade (ponto de retirada e multi-loja).

A separação entre Produto e ProdutoVariacao é deliberada. O produto é o conceito comercial; a variação é o que tem código de barras, estoque e preço. Quem controla estoque é a variação. O mesmo vale para manter Pagamento separado do Pedido: um pedido pode ter tentativas de pagamento, estornos e métodos diferentes ao longo do tempo, e amarrar tudo na tabela de pedidos engessa o fluxo financeiro.

Service Layer e enums de status

Regra de negócio não mora no controller nem no model. Ela mora em serviços dedicados, e isso é uma decisão de arquitetura, não estilo. Os serviços previstos são CarrinhoService, CheckoutService, MercadoPagoService, MelhorEnvioService, AntiFraudeService e os de IA (DescricaoIaService, ChatIaService).

A vantagem é testabilidade e clareza. O CheckoutService orquestra a validação de estoque, a aplicação de cupom, o cálculo de frete e a criação do pedido em uma transação. O controller só recebe a requisição e delega.

Os status do domínio são enums, não strings soltas:

enum PedidoStatus: string
{
    case Aguardando = 'aguardando';
    case Pago = 'pago';
    case Enviado = 'enviado';
    case Entregue = 'entregue';
    case Cancelado = 'cancelado';
}

O mesmo para PagamentoStatus. O enum centraliza os valores válidos, alimenta os badges do Filament e impede transições inválidas escritas à mão.

Escolher e justificar a stack

A stack de referência é madura e coerente, e a escolha precisa estar documentada para que o time saiba o porquê:

  • Laravel 11/12 com PHP 8.2 ou superior como base.
  • Filament 3.x como painel administrativo, evitando construir CRUD do zero.
  • Tailwind e Alpine.js no front, com Vite para os assets; Swiper e GSAP quando a vitrine exigir.
  • Cloudflare R2 via league/flysystem-aws-s3-v3 para imagens, com conversão WebP pela Intervention Image.
  • Spatie para auditoria (activitylog), RBAC do admin (permission), backup e health.
  • Fila database para processar webhooks de pagamento e frete de forma assíncrona.

A justificativa central é reaproveitamento: cada peça resolve um problema clássico de e-commerce sem invenção. R2 reduz custo de saída de dados; Filament acelera o admin; a fila database evita perder webhook sob pico.

Diagrama de dados e a decisão de endereço

Produza um diagrama de entidade-relacionamento antes de codar. Ele revela cardinalidades que o texto esconde: um Produto tem muitas ProdutoVariacao; uma variação pertence a uma cor; um Pedido tem muitos PedidoItem e um histórico em PedidoHistorico; um Pedido referencia uma Unidade quando a entrega é retirada.

Uma decisão recorrente é como guardar endereço. Há dois caminhos legítimos na stack:

  • Endereço em colunas próprias na tabela (CEP, rua, número, cidade, UF). Facilita consulta, filtro e relatório fiscal.
  • Endereço como JSON em uma coluna. Flexível para formatos variados e snapshots do pedido.

A recomendação é colunas quando o endereço participa de regras (cálculo de frete por UF, relatórios) e JSON quando é apenas um retrato imutável do pedido no momento da compra. Documente a escolha e seja consistente.

Critérios de qualidade

  • Todas as entidades centrais estão no diagrama com relacionamentos e cardinalidades.
  • Estoque e preço diferencial estão na variação, não no produto.
  • Pagamento é um model separado do Pedido, com histórico próprio.
  • Os serviços de negócio estão definidos com responsabilidade clara cada um.
  • Existem enums para PedidoStatus e PagamentoStatus com todos os estados válidos.
  • A escolha de stack e a estratégia de endereço (colunas ou JSON) estão justificadas por escrito.

Checklist

  • Modelar as entidades centrais do domínio
  • Definir Service Layer e enums de status
  • Escolher e justificar a stack
  • Decidir endereço em colunas ou JSON
  • Produzir o diagrama de dados

Ferramentas

Entregável

Documento de arquitetura com diagrama de dados, padrões e decisões justificadas.

Etapa 2 — Fundação do projeto

Setup do projeto e modelagem do catálogo.

Montar a fundação do projeto Laravel

6h Intermediário

Criar o projeto Laravel base com Filament, assets, storage, fila e testes prontos.

Por que isto importa

A fundação do projeto é onde se decide se o time vai trabalhar com tração ou com atrito durante meses. Um setup malfeito gera retrabalho silencioso: assets que não compilam, fila que não processa, ambiente que difere de produção. Esta atividade entrega um esqueleto Laravel que já nasce com painel administrativo, armazenamento de imagens, processamento assíncrono, auditoria e testes funcionando. Tudo o que vier depois, catálogo, checkout, pagamento, vai apoiar nesta base. Vale investir tempo aqui para não pagar juros depois.

Criar o projeto e o painel administrativo

Comece com o projeto limpo e o Filament instalado. O painel é a casa do lojista, então ele entra cedo:

composer create-project laravel/laravel loja
cd loja
composer require filament/filament:"^3.0"
php artisan filament:install --panels
php artisan make:filament-user

Confirme que o painel sobe em /admin e que o usuário criado consegue logar. Há ainda a opção de 2FA via aurorawebsoftware/filament-2fa, que pode ser adicionada quando a segurança do admin exigir. Defina já o idioma da aplicação para pt_BR, porque labels e mensagens nascem em português.

Vite, Tailwind e Alpine

O front público usa Tailwind e Alpine, compilados pelo Vite. Instale e configure os assets:

npm install -D tailwindcss postcss autoprefixer
npm install alpinejs
npx tailwindcss init -p

Registre o Alpine no resources/js/app.js, aponte o content do Tailwind para as views Blade e rode npm run build para validar a compilação. Quando a vitrine pedir carrosséis e animações, Swiper e GSAP entram no mesmo pipeline. Lembre-se de que em produção o npm run dev deixa um hot file órfão que quebra o site, então em servidor sempre use npm run build.

Armazenamento de imagens no R2

As imagens de produto não ficam no disco local. Elas vão para o Cloudflare R2, que é compatível com a API S3. Instale o driver do Flysystem e a Intervention Image para a conversão WebP:

composer require league/flysystem-aws-s3-v3 intervention/image

Configure um disco r2 em config/filesystems.php com as chaves do .env (endpoint, bucket, access key e secret). O fluxo de upload converte a imagem para WebP com a Intervention antes de enviar, reduzindo peso sem perder qualidade. Teste subindo um arquivo e conferindo a URL pública gerada.

O motivo de usar R2 em vez do disco local vai além de espaço. O servidor de aplicação fica enxuto, o deploy não carrega gigabytes de mídia, e o custo de saída de dados do R2 é muito menor que o de outros provedores compatíveis com S3. Em produção a loja pode escalar horizontalmente sem se preocupar com sincronizar imagens entre máquinas, porque todas leem do mesmo bucket. Defina desde já um padrão de nomenclatura para os caminhos, por exemplo produtos/{id}/{uuid}.webp, para evitar colisões e facilitar a limpeza de órfãos depois.

Fila database, Activity Log e ambiente

Webhooks de pagamento e frete precisam ser assíncronos para não travar a resposta HTTP. A stack usa a fila database:

php artisan make:queue-table
php artisan migrate

Defina QUEUE_CONNECTION=database no .env. Em produção a fila roda sob supervisor ou cron; em desenvolvimento, php artisan queue:work resolve. Em seguida habilite a auditoria com o Activity Log da Spatie:

composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider"
php artisan migrate

Ainda nesta fase, prepare o .env com as credenciais de banco, R2, Mercado Pago e Melhor Envio (mesmo que como placeholders), e crie o symlink de storage. As demais peças da Spatie, permission, backup e health, entram conforme a necessidade for surgindo. O spatie/laravel-backup merece atenção precoce, porque um e-commerce sem backup diário do banco é um risco que ninguém quer descobrir tarde; configure-o para rodar pela fila e enviar o dump para um destino externo.

Documente cada variável do .env em um .env.example versionado, deixando claro o que cada chave faz e onde obtê-la. Isso encurta o onboarding de quem entrar no projeto depois e evita que segredos vazem por engano para o repositório. Nunca commite o .env real. Em ambiente HestiaCP, lembre-se de que o document root costuma ser public_html, então valide que os assets e o symlink de storage apontam para o lugar certo antes de considerar o ambiente pronto.

Testes e CI básica

Nada da fundação está pronto enquanto php artisan test não passar. O projeto usa PHPUnit no estilo de classe (extends TestCase com métodos test_*), não Pest. Garanta que a suíte sobe contra um banco isolado de teste, nunca contra produção, e escreva um teste mínimo que confirme que a aplicação responde e que o painel exige autenticação.

Configure uma CI básica que rode em cada push: instalar dependências, copiar o .env de teste, rodar migrations e executar a suíte. Mesmo simples, ela protege a base contra regressões desde o primeiro commit do catálogo.

Critérios de qualidade

  • O painel Filament sobe em /admin e exige login.
  • npm run build compila Tailwind e Alpine sem erro e o site usa os assets buildados.
  • Um upload de imagem chega ao R2 já convertido em WebP.
  • A fila database está migrada e processa um job de teste.
  • O Activity Log está instalado e registra ao menos uma ação.
  • php artisan test roda verde contra banco isolado e a CI executa a suíte em cada push.

Checklist

  • Criar o projeto e instalar o Filament
  • Configurar Vite, Tailwind e Alpine
  • Configurar Flysystem com Cloudflare R2
  • Habilitar fila database e Activity Log
  • Garantir phpunit rodando e CI básica

Ferramentas

Entregável

Repositório Laravel funcional com Filament, R2, fila database, Activity Log e phpunit verde.

Modelar o catálogo (migrations e models)

10h Avançado

Implementar as migrations, models, relacionamentos e scopes do catálogo do e-commerce.

Por que isto importa

O catálogo é o coração do e-commerce. Toda a vitrine, a busca, o carrinho e o checkout consultam estas tabelas o tempo todo, então a modelagem precisa ser correta e performática desde o início. Esta atividade materializa o domínio desenhado na arquitetura em migrations, models, relacionamentos e scopes reais. Um catálogo bem modelado torna o resto do sistema simples; um catálogo torto contamina cada consulta com remendos. O foco aqui é fidelidade ao domínio em português e separação clara entre produto e variação.

Categorias, marcas e coleções

Comece pelas entidades de organização, porque produto depende delas. Categoria pode ser hierárquica (com parent_id), enquanto marca e coleção costumam ser planas:

Schema::create('categorias', function (Blueprint $table) {
    $table->id();
    $table->string('nome');
    $table->string('slug')->unique();
    $table->foreignId('parent_id')->nullable()->constrained('categorias');
    $table->boolean('ativo')->default(true);
    $table->timestamps();
});

Marca e Colecao seguem o mesmo padrão, com nome, slug e ativo. O slug único garante URLs limpas e indexáveis pelos buscadores, o que importa muito para um e-commerce que depende de tráfego orgânico. A coluna ativo aparece em quase tudo, porque o lojista despublica itens sem apagá-los; um produto fora de linha precisa sumir da vitrine sem quebrar os pedidos antigos que o referenciam. Por isso o catálogo evita exclusão física e prefere a flag de ativo, deixando o histórico intacto. Pense nos índices desde a migration: slug e as chaves estrangeiras de categoria e marca serão filtradas o tempo todo e merecem índice.

Produtos e variações com SKU

Aqui está a decisão central: o produto é o conceito comercial e a variação é o item vendável de verdade. Estoque e diferença de preço moram na variação.

Schema::create('produtos', function (Blueprint $table) {
    $table->id();
    $table->string('nome');
    $table->string('slug')->unique();
    $table->text('descricao')->nullable();
    $table->decimal('preco', 10, 2);
    $table->foreignId('categoria_id')->constrained('categorias');
    $table->foreignId('marca_id')->nullable()->constrained('marcas');
    $table->boolean('destaque')->default(false);
    $table->boolean('ativo')->default(true);
    $table->timestamps();
});

Schema::create('produto_variacoes', function (Blueprint $table) {
    $table->id();
    $table->foreignId('produto_id')->constrained('produtos')->cascadeOnDelete();
    $table->string('sku')->unique();
    $table->foreignId('cor_id')->nullable()->constrained('cores');
    $table->integer('estoque')->default(0);
    $table->decimal('preco_diferenca', 10, 2)->default(0);
    $table->boolean('ativo')->default(true);
    $table->timestamps();
});

O preco_diferenca permite que uma variação custe mais ou menos que o preço base, sem duplicar o produto. Um modelo de tênis pode ter o número 44 mais caro que o 38, e isso se resolve com um delta na variação em vez de criar dois produtos distintos. O sku único é o identificador real para estoque e logística, aquele que aparece na nota fiscal e na etiqueta. A coluna estoque na variação é a fonte de verdade que o carrinho e o checkout consultam antes de confirmar a compra; nunca confie em estoque guardado em outro lugar. O cascadeOnDelete na chave estrangeira garante que, ao remover de fato um produto, suas variações não fiquem órfãs no banco. Avalie ainda um índice composto em produto_id e ativo, porque a vitrine quase sempre busca as variações ativas de um produto.

Imagens com R2 e WebP

As imagens não ficam no banco, apenas seu caminho no R2. A tabela guarda o caminho, marca a principal e controla a ordem de exibição:

Schema::create('produto_imagens', function (Blueprint $table) {
    $table->id();
    $table->foreignId('produto_id')->constrained('produtos')->cascadeOnDelete();
    $table->string('caminho');
    $table->boolean('principal')->default(false);
    $table->unsignedInteger('ordem')->default(0);
    $table->timestamps();
});

No upload, a Intervention Image converte para WebP antes de enviar ao disco r2, e o caminho salvo é a chave no bucket. A flag principal define a capa do produto na vitrine, e ordem controla a galeria. Garanta que sempre haja exatamente uma imagem principal por produto.

Relacionamentos e scopes

Com as tabelas prontas, declare os relacionamentos Eloquent. Um Produto tem muitas ProdutoVariacao e muitas ProdutoImagem, pertence a uma Categoria e a uma Marca, e pode pertencer a muitas Colecao por tabela pivô. A ProdutoVariacao pertence a uma Cor.

Os scopes encapsulam as consultas que se repetem em toda a vitrine. Eles deixam o código de leitura limpo e consistente:

public function scopeAtivo(Builder $query): Builder
{
    return $query->where('ativo', true);
}

public function scopeDestaque(Builder $query): Builder
{
    return $query->where('destaque', true);
}

public function scopeComEstoque(Builder $query): Builder
{
    return $query->whereHas('variacoes', fn ($q) => $q->where('estoque', '>', 0));
}

Assim, listar produtos da home vira Produto::ativo()->destaque()->comEstoque()->get(), legível e reaproveitável. Crie factories para todas as entidades desde já, porque os testes do catálogo e do checkout vão depender delas. Adicione casts explícitos nos models (preço como decimal, flags como boolean) e mantenha fillable declarado.

Critérios de qualidade

  • Migrations de categorias, marcas, coleções, produtos, variações e imagens criadas e migradas.
  • Estoque e preco_diferenca residem na ProdutoVariacao, e cada variação tem sku único.
  • A tabela de imagens guarda o caminho no R2, a flag principal e a ordem.
  • Os relacionamentos Eloquent entre todos os models estão declarados e testados.
  • Existem os scopes scopeAtivo, scopeComEstoque e scopeDestaque funcionando.
  • Há factory para cada entidade do catálogo e os testes de model passam.

Checklist

  • Criar migrations de categorias, marcas e coleções
  • Criar migrations de produtos e variações com SKU e estoque
  • Criar migration de imagens com caminho R2 e WebP
  • Definir relacionamentos Eloquent entre os models
  • Implementar scopes de ativo, com estoque e destaque

Entregável

Migrations e models do catálogo com relacionamentos, scopes e factories.

Etapa 3 — Catálogo e vitrine

Vitrine pública e gestão de estoque.

Construir a vitrine pública

12h Intermediário

Entregar as páginas públicas da loja com catálogo navegável, filtros, galeria e SEO on-page.

Por que isto importa

A vitrine é a única parte da loja que o cliente realmente vê. Por melhor que esteja o backend, é a página de produto que converte e é o catálogo que faz a pessoa achar o que procura. Uma vitrine lenta, com filtros confusos ou imagens pesadas, derruba a taxa de conversão antes mesmo do checkout. Aqui o objetivo é construir as quatro páginas públicas essenciais (home, listagem por categoria, página de produto e busca) com foco em clareza, velocidade e descoberta orgânica via SEO.

Vale separar dois públicos que chegam por caminhos diferentes. Quem vem da busca orgânica costuma cair direto numa página de produto ou de categoria, e precisa entender em segundos o que é aquele item, quanto custa e se está disponível. Quem já conhece a marca entra pela home e quer navegar por coleções e novidades. As mesmas páginas atendem aos dois, mas a hierarquia visual e o SEO precisam pensar no visitante que nunca ouviu falar da loja, porque é ele quem ainda não foi conquistado.

Modelagem da consulta de catálogo

As páginas de listagem leem Produto com seus relacionamentos Categoria, Marca, Cor e ProdutoImagem. Use os scopes do domínio para nunca expor itens indisponíveis e evitar o problema de N+1 carregando os relacionamentos de uma vez.

$produtos = Produto::query()
    ->ativo()
    ->comEstoque()
    ->with(['imagens', 'marca', 'cores'])
    ->when($categoria, fn ($q) => $q->daCategoria($categoria))
    ->paginate(24);

A busca pode começar simples, com where('nome', 'ilike', "%{$termo}%") mais o nome da marca, e evoluir depois para um índice dedicado. O importante é que a primeira versão já respeite estoque e status, retornando resultados confiáveis.

Filtros e galeria com Alpine.js

Os filtros laterais (cor, marca, faixa de preço) não precisam de Livewire para a primeira versão. Mantenha o estado na própria URL via query string e deixe o Alpine.js cuidar apenas da interatividade visual, como abrir e fechar grupos de filtro no mobile. Isso preserva os filtros como links indexáveis e compartilháveis.

<div x-data="{ aberto: false }">
    <button @click="aberto = !aberto">Filtrar</button>
    <aside x-show="aberto" x-transition>
        <!-- grupos de filtro -->
    </aside>
</div>

A galeria da página de produto troca a imagem principal ao clicar nas miniaturas. Guarde o índice ativo no Alpine e renderize todas as ProdutoImagem ordenadas; a variação de cor selecionada pode trocar o conjunto exibido.

Imagens WebP responsivas

Imagem é o maior peso de uma loja. Gere variações WebP em alguns tamanhos com a Intervention Image no momento do upload, guarde no Cloudflare R2 e sirva com srcset para o navegador escolher a resolução adequada.

<img
    src="{{ $produto->imagemUrl('md') }}"
    srcset="{{ $produto->imagemUrl('sm') }} 400w, {{ $produto->imagemUrl('lg') }} 800w"
    sizes="(max-width: 640px) 50vw, 25vw"
    alt="{{ $produto->nome }}"
    loading="lazy"
    width="400" height="400">

O atributo loading="lazy" em tudo que está abaixo da dobra e os atributos width/height explícitos evitam o salto de layout (CLS) e ajudam diretamente no Core Web Vitals. A imagem principal da página de produto, ao contrário, deve carregar com prioridade alta e sem lazy, porque ela costuma ser o maior elemento visível (LCP); marque-a com fetchpriority="high" para que o navegador a baixe antes das miniaturas.

Não gere as variações de tamanho a cada requisição: faça isso uma vez no upload e guarde os arquivos derivados no R2. Servir imagem redimensionada em tempo real consome CPU e atrasa a resposta. Mantenha também um fallback para navegadores antigos, embora o WebP já tenha suporte praticamente universal hoje.

SEO on-page e breadcrumbs

Cada página precisa de um title exclusivo, uma meta description descritiva, um único H1 e um slug legível (por exemplo /tenis/corrida/modelo-alpha). A página de produto deve incluir dados estruturados de produto em JSON-LD, com nome, imagem, preço e disponibilidade derivada do estoque da variação. Breadcrumbs visíveis e marcados com Schema reforçam a hierarquia para o usuário e para o buscador, e devem espelhar exatamente o caminho Categoria pai e Categoria filha.

Performance e percepção de velocidade

Use cache de fragmentos para blocos estáveis da home (vitrines de coleção, banners), evite carregar coleções inteiras na memória e prefira paginação. Comprima e versiona os assets pelo Vite, e teste o resultado em conexão móvel real, não só no desktop. A meta é uma página de produto que pinte o conteúdo principal em menos de dois segundos e meio em 4G.

Percepção importa tanto quanto número. Reserve espaço para a imagem antes dela carregar, mostre o preço e o botão de comprar o mais cedo possível no HTML, e evite que um carrossel ou um banner empurre o conteúdo para baixo enquanto a página monta. A Colecao em destaque na home pode ser cacheada por algumas horas, já que muda pouco, enquanto preço e estoque da página de produto precisam estar sempre frescos. Quando precisar de interatividade mais pesada, prefira carregar o JavaScript de forma diferida para não bloquear a primeira pintura.

Critérios de qualidade

  • As quatro páginas (home, categoria, produto, busca) carregam apenas itens ativo e comEstoque, sem N+1.
  • Filtros mantêm o estado na URL, são compartilháveis e funcionam com o JavaScript desabilitado parcialmente.
  • Todas as imagens são servidas em WebP com srcset, loading="lazy" e dimensões explícitas.
  • Cada página tem title, meta description e H1 únicos, além de slug amigável.
  • A página de produto expõe JSON-LD de produto com preço e disponibilidade corretos.
  • Breadcrumbs refletem a hierarquia real de categorias em todas as páginas internas.

Checklist

  • Publicar home, listagem de categoria, página de produto e busca
  • Implementar filtros laterais (cor, marca, preço) reativos com Alpine.js
  • Gerar imagens WebP responsivas servidas pelo Cloudflare R2
  • Definir title, meta description, H1 único e slug amigável por página
  • Adicionar breadcrumbs navegáveis em todas as páginas internas

Entregável

Home, listagem por categoria, página de produto e busca funcionando, com filtros, galeria, imagens WebP responsivas e SEO on-page básico.

Implementar o estoque por variação

8h Avançado

Controlar estoque no nível da variação com movimentações, reserva, liberação e proteção contra venda sem saldo.

Por que isto importa

Vender o que não existe é o erro mais caro de um e-commerce: gera reembolso, atrito com o cliente e dano à reputação. O estoque precisa ser a fonte única de verdade sobre o que pode ser comprado, e ele vive no nível da variação, não do produto. Um tênis tem saldo por número e cor; uma camiseta, por tamanho. Aqui o objetivo é construir um controle de estoque por ProdutoVariacao que seja rastreável, reserve saldo no momento certo e não venda duas vezes a mesma última peça sob acesso simultâneo.

O estoque também é o que separa uma loja amadora de uma profissional na hora do pico. Numa promoção, dezenas de pessoas disputam as mesmas peças nos mesmos segundos. Se o controle for ingênuo, a loja vende além do que tem e depois precisa cancelar pedidos já pagos, o que é a pior experiência possível. Tratar estoque como um problema de concorrência desde o início, e não como um detalhe a ajustar depois, é o que evita esse tipo de incêndio.

Saldo no nível da variação

Toda a quantidade disponível mora no ProdutoVariacao, identificado por SKU único. O Produto é apenas o agregador editorial; ele nunca carrega saldo próprio. O scope comEstoque deve operar sobre as variações, e um produto só aparece como disponível se ao menos uma variação tiver saldo livre maior que zero.

public function scopeComEstoque(Builder $query): Builder
{
    return $query->whereHas('variacoes', fn ($q) =>
        $q->whereRaw('quantidade - quantidade_reservada > 0')
    );
}

Separar quantidade (físico) de quantidade_reservada permite saber a qualquer momento o saldo realmente vendável sem precisar varrer carrinhos abertos.

Movimentações como histórico imutável

Nunca altere o saldo com um update cego. Cada mudança gera uma linha em uma tabela de movimentações (entrada, saída, reserva, liberação, ajuste, devolução), com origem, quantidade e referência ao pedido ou carrinho. O saldo atual passa a ser auditável e reconstruível. Concentre essa lógica em um serviço.

class EstoqueService
{
    public function movimentar(ProdutoVariacao $v, int $delta, string $tipo, $origem = null): void
    {
        $v->movimentacoes()->create([
            'tipo' => $tipo,
            'quantidade' => $delta,
            'origem_type' => $origem?->getMorphClass(),
            'origem_id' => $origem?->getKey(),
        ]);
        $v->increment('quantidade', $delta);
    }
}

Reserva ao adicionar no carrinho

Quando o cliente coloca um item no carrinho, incremente quantidade_reservada em vez de debitar o saldo físico. Isso protege a peça durante a compra sem perdê-la caso o carrinho seja abandonado. A reserva precisa de prazo: grave o instante e libere automaticamente o que expirou, devolvendo a peça à vitrine. Um comando agendado na fila database varre carrinhos vencidos.

ProdutoVariacao::where('reserva_expira_em', '<', now())
    ->each(fn ($v) => app(EstoqueService::class)->liberarReservasExpiradas($v));

Alerta de estoque baixo

Cada variação tem um limite mínimo. Quando uma saída faz o saldo cruzar esse limite, registre o alerta em uma tabela própria (um stock_alerts) e notifique o gestor. Evite alertas duplicados verificando se já existe um alerta aberto para a mesma variação antes de criar outro. O alerta deve fechar sozinho quando uma entrada recompõe o saldo acima do mínimo, para que a tela de gestão mostre apenas o que realmente precisa de reposição agora.

Esse limite mínimo não é um número mágico: ele depende do giro do item e do prazo de reposição do fornecedor. Um produto que vende rápido e demora a chegar precisa de uma margem maior. Deixe o limite configurável por variação no painel Filament, com um valor padrão razoável, para que o gestor afine conforme aprende o comportamento de cada SKU.

Concorrência e bloqueio de venda

O ponto mais delicado é a última peça disputada por dois pedidos no mesmo instante. A solução é debitar dentro de uma transação com locking pessimista, relendo a linha travada antes de decidir.

DB::transaction(function () use ($variacaoId, $qtd) {
    $v = ProdutoVariacao::lockForUpdate()->find($variacaoId);
    if (($v->quantidade - $v->quantidade_reservada) < $qtd) {
        throw new EstoqueInsuficienteException();
    }
    $v->decrement('quantidade', $qtd);
});

O lockForUpdate faz a segunda requisição esperar a primeira terminar, garantindo que o saldo seja sempre verificado contra o valor já comprometido. A confirmação do pagamento converte a reserva em saída definitiva; o cancelamento devolve a reserva ao saldo livre.

Critérios de qualidade

  • O saldo vive exclusivamente no ProdutoVariacao por SKU, nunca no Produto.
  • Toda alteração de saldo deixa um registro de movimentação rastreável até a origem.
  • Adicionar ao carrinho reserva saldo, e reservas expiradas são liberadas automaticamente.
  • O scope comEstoque considera apenas o saldo livre (físico menos reservado).
  • Cruzar o limite mínimo gera alerta de estoque baixo sem duplicação.
  • O débito final ocorre em transação com lockForUpdate, impedindo venda sem saldo sob concorrência.

Checklist

  • Centralizar o saldo no ProdutoVariacao (SKU), não no Produto
  • Registrar toda alteração de saldo em uma tabela de movimentações
  • Reservar estoque ao adicionar no carrinho e liberar por expiração
  • Disparar alerta quando o saldo cruzar o limite mínimo
  • Proteger o débito de estoque com locking pessimista em transação

Ferramentas

Entregável

Estoque controlado por ProdutoVariacao com histórico de movimentações, alerta de estoque baixo, reserva no carrinho e bloqueio de venda sem saldo sob concorrência.

Etapa 4 — Carrinho e checkout

Da escolha do produto ao pedido criado.

Implementar o carrinho de compras

10h Avançado

Construir um carrinho que funcione para visitante e cliente logado, com cupom, recálculo, compartilhamento por link e expiração.

Por que isto importa

O carrinho é a antessala do checkout e onde o cliente decide se a compra vale a pena. Ele precisa sobreviver a três situações que parecem triviais e quebram lojas mal feitas: o visitante que ainda não tem conta, o cliente que fecha o navegador e volta dias depois, e o casal que monta a compra junto a partir de dois aparelhos. Aqui o objetivo é um carrinho persistente, com totais sempre corretos, cupom validado e capaz de ser compartilhado por link via um model CarrinhoCompartilhado.

Há ainda uma razão de negócio: o carrinho persistido é a base da recuperação de venda. Um carrinho que sobrevive ao fechamento do navegador permite lembrar o cliente do que ficou para trás e medir abandono com precisão. Tratar o carrinho como um registro de banco, e não como um punhado de dados voláteis na sessão, transforma um detalhe técnico numa alavanca de receita.

Identidade: sessão para visitante, banco para cliente

O carrinho existe sempre no banco, no model Carrinho com seus CarrinhoItem. A diferença é como ele é localizado. Para o visitante, a chave é um token guardado na sessão; para o cliente logado, é o cliente_id. O serviço resolve qual carrinho usar de forma transparente.

public function carrinhoAtual(): Carrinho
{
    if ($cliente = auth()->user()?->cliente) {
        return Carrinho::firstOrCreate(['cliente_id' => $cliente->id]);
    }

    $token = session()->get('carrinho_token')
        ?? tap(Str::uuid()->toString(), fn ($t) => session(['carrinho_token' => $t]));

    return Carrinho::firstOrCreate(['token_sessao' => $token]);
}

No momento do login, o carrinho de visitante é mesclado ao carrinho do cliente: itens iguais somam quantidade, itens novos são anexados e o carrinho de sessão é descartado. Isso evita a frustração clássica de perder o que se montou antes de entrar na conta.

O CarrinhoService como ponto único

Toda operação passa pelo CarrinhoService: adicionar item, alterar quantidade, remover, aplicar cupom e recalcular. Centralizar evita que o total seja somado em três lugares diferentes com regras divergentes. Adicionar um item valida o saldo da variação antes de aceitar.

public function adicionar(Carrinho $carrinho, ProdutoVariacao $v, int $qtd): void
{
    if (($v->quantidade - $v->quantidade_reservada) < $qtd) {
        throw new EstoqueInsuficienteException();
    }
    $item = $carrinho->itens()->firstOrNew(['produto_variacao_id' => $v->id]);
    $item->quantidade += $qtd;
    $item->preco_unitario = $v->produto->preco;
    $item->save();
    $this->recalcular($carrinho);
}

O preco_unitario é gravado no item no momento da inclusão, mas sempre revalidado no recálculo: nunca confie no preço enviado pelo navegador.

Cupom: validar antes de aplicar

Aplicar cupom é validar regras, não só multiplicar. O serviço verifica se o Cupom está ativo, dentro da validade, dentro do limite de usos, se o valor mínimo do carrinho foi atingido e se o cupom não conflita com restrições de categoria. Só então o desconto entra no recálculo. Guarde o cupom aplicado no próprio carrinho para reavaliá-lo a cada mudança, porque retirar um item pode invalidar o valor mínimo e o desconto deve cair junto.

Recálculo de totais

O recálculo é determinístico e roda após qualquer mutação. Some os itens (quantidade vezes preço atual), aplique o desconto do cupom, considere crédito do cliente (ClienteCredito) quando houver e produza subtotal, desconto e total. O frete não entra aqui: ele pertence ao checkout, pois depende do endereço. Mantenha os valores monetários em inteiros de centavos para fugir de erros de ponto flutuante.

O recálculo também é o momento de detectar problemas silenciosos. Um item pode ter ficado sem estoque desde que entrou no carrinho, ou o produto pode ter sido desativado, ou o preço pode ter mudado. Em vez de esconder isso até o checkout, sinalize na própria tela do carrinho: marque o item indisponível, atualize o preço para o vigente e avise o cliente. Surpresas só são bem-vindas quando são para menos, nunca na hora de pagar.

Compartilhamento por token e expiração

O carrinho compartilhável recebe um token público de leitura. Quem abre o link enxerga os itens e pode cloná-los para o próprio carrinho, sem alterar o original. No frontend, um botão Alpine.js copia a URL com o token. Carrinhos de visitante têm prazo de validade: um comando na fila database limpa periodicamente os abandonados, liberando as reservas de estoque associadas e mantendo a base enxuta.

Critérios de qualidade

  • O carrinho é localizado por token de sessão para visitante e por cliente_id para logado.
  • No login, o carrinho de visitante é mesclado ao do cliente sem perder itens.
  • Todas as mutações passam pelo CarrinhoService, com recálculo único e determinístico.
  • O preço do item é revalidado no servidor a cada recálculo, nunca confiado ao navegador.
  • O cupom é validado contra status, validade, usos e valor mínimo, e é reavaliado quando o carrinho muda.
  • Existe carrinho compartilhável por token e rotina de expiração que libera reservas de estoque.

Checklist

  • Identificar o carrinho por sessão (visitante) ou por cliente (logado)
  • Centralizar adicionar, remover e recalcular no CarrinhoService
  • Validar e aplicar cupom com recálculo dos totais
  • Mesclar o carrinho de visitante ao carrinho do cliente no login
  • Gerar carrinho compartilhável por token e definir expiração

Ferramentas

Entregável

CarrinhoService com carrinho de sessão para visitante, carrinho persistido para cliente logado, validação de cupom, recálculo de totais, compartilhamento por token e expiração.

Construir o checkout e o pedido

12h Avançado

Transformar o carrinho em pedido com endereço, transação atômica, numeração e histórico de status por enum.

Por que isto importa

O checkout é o momento em que o carrinho, que era um rascunho mutável, vira um compromisso financeiro imutável: o Pedido. Se algo falhar no meio (estoque debitado mas pedido não criado, item gravado com preço errado, dois pedidos com o mesmo número), o prejuízo é direto e a recuperação manual é dolorosa. Aqui o objetivo é um CheckoutService que converta o carrinho em pedido de forma atômica, com endereço capturado, numeração única, estoque debitado e histórico de status desde o primeiro instante.

É também a etapa de maior abandono de toda a loja. Cada campo a mais, cada recarregamento de página e cada incerteza sobre prazo ou frete custam conversão. Por isso o checkout deve ser curto, mostrar o resumo do pedido o tempo todo e nunca obrigar o cliente a recomeçar quando algo dá errado. A robustez do CheckoutService no backend é o que permite que a interface seja simples na frente: como a criação do pedido é atômica, a tela pode confiar que ou deu certo por inteiro, ou nada foi gravado.

Captura de endereço: colunas ou JSON

Existem dois caminhos válidos. O primeiro guarda o endereço em colunas dedicadas no pedido (logradouro, número, bairro, cidade, CEP), o que facilita relatórios e consultas por região. O segundo guarda como JSON, o que dá flexibilidade para formatos variados sem migração. A escolha depende de quanto você precisa consultar o endereço depois; para a maioria das lojas, colunas tornam frete e fiscal mais simples. Em qualquer caso, o endereço é fotografado no pedido: ele não pode mudar se o cliente editar o cadastro depois.

Retirada na loja pula o envio

Quando o cliente escolhe retirar em uma Unidade, o checkout não pede endereço de entrega nem calcula frete pelo Melhor Envio. O fluxo grava a unidade escolhida, zera o valor de frete e marca o pedido como retirada. Trate isso como uma bifurcação explícita logo no início do checkout, porque ela altera quais etapas o cliente vê.

if ($checkout->ehRetirada()) {
    $pedido->unidade_id = $checkout->unidadeId;
    $pedido->valor_frete = 0;
} else {
    $pedido->preencherEndereco($checkout->endereco);
    $pedido->valor_frete = $this->frete->cotar($checkout)->valor;
}

CheckoutService em transação atômica

O coração da operação é criar o Pedido e seus PedidoItem dentro de uma única transação, junto ao débito de estoque. Ou tudo é gravado, ou nada é. O preço de cada item é congelado a partir da variação, não do que veio do navegador.

return DB::transaction(function () use ($carrinho, $checkout) {
    $pedido = Pedido::create([
        'cliente_id' => $checkout->clienteId,
        'numero' => $this->gerarNumero(),
        'status' => PedidoStatus::AguardandoPagamento,
        'valor_total' => $carrinho->total,
    ]);

    foreach ($carrinho->itens as $item) {
        $pedido->itens()->create([
            'produto_variacao_id' => $item->produto_variacao_id,
            'quantidade' => $item->quantidade,
            'preco_unitario' => $item->produto_variacao->produto->preco,
        ]);
        app(EstoqueService::class)->debitar($item->produto_variacao, $item->quantidade);
    }

    $pedido->registrarStatus(PedidoStatus::AguardandoPagamento, 'Pedido criado');
    return $pedido;
});

Numeração padronizada

O número visível ao cliente segue o padrão DB-AAAAMMDD-0001, com prefixo da loja, data e sequencial diário. Gere o sequencial contando os pedidos do dia dentro da própria transação, sob lock, para não repetir número em criações simultâneas. O número é para humanos; a chave técnica continua sendo o id.

Esse formato tem valor prático além da estética. O prefixo identifica a loja em cenários multi-marca, a data agrupa pedidos por dia para o operacional e o sequencial dá uma noção imediata de volume. Quando o cliente liga para o suporte e fala o número, o atendente localiza o pedido sem ambiguidade. Mantenha a geração do número isolada num método do serviço, para que a regra fique num lugar só e possa evoluir sem espalhar concat de string pelo código.

Histórico de status com enum

O ciclo de vida do pedido é governado pelo enum PedidoStatus (aguardando pagamento, pago, em separação, enviado, entregue, cancelado), e cada transição grava uma linha em PedidoHistorico com status anterior, novo status, autor e observação. Isso dá uma trilha de auditoria completa e alimenta a tela de acompanhamento do cliente. A confirmação do Pagamento move o pedido para pago e converte as reservas de estoque em saída definitiva; o cancelamento devolve o saldo. Como o Pedido usa soft deletes, nada se perde de fato, e o Activity Log complementa o rastro de quem fez o quê.

Critérios de qualidade

  • O endereço é fotografado no pedido e não muda quando o cliente edita o cadastro.
  • A retirada na loja pula endereço e frete, gravando a Unidade escolhida.
  • Pedido, PedidoItem e débito de estoque acontecem em uma única transação atômica.
  • O preço de cada item é congelado a partir da variação no servidor, nunca do navegador.
  • A numeração segue DB-AAAAMMDD-0001 e é única mesmo sob criação concorrente.
  • Toda transição de status grava em PedidoHistorico usando o enum PedidoStatus.

Checklist

  • Capturar endereço de entrega e tratar retirada na loja
  • Criar Pedido e PedidoItem dentro de uma única transação
  • Gerar numeração padronizada do pedido (DB-AAAAMMDD-0001)
  • Registrar o status inicial em PedidoHistorico com o enum PedidoStatus
  • Debitar o estoque das variações ao confirmar o pedido

Ferramentas

Entregável

Fluxo de checkout completo com captura de endereço, CheckoutService criando Pedido e PedidoItem em transação, numeração padronizada e histórico de status via PedidoStatus.

Etapa 5 — Pagamentos, frete e segurança

Receber pagamento, calcular frete e blindar a operação.

Integrar o Mercado Pago

12h Avançado

Receber pagamentos por PIX, cartão e parcelamento com confirmação assíncrona via webhook.

Por que isto importa

O pagamento é o único ponto do e-commerce onde dinheiro de verdade troca de mãos. Um erro aqui não é um bug cosmético: é venda perdida, cobrança duplicada ou pedido liberado sem ter sido pago. A integração com o Mercado Pago precisa ser confiável, auditável e à prova de reprocessamento. O comprador brasileiro espera PIX instantâneo e parcelamento no cartão; entregar isso bem é o que separa uma loja amadora de uma profissional. Esta atividade entrega a espinha dorsal financeira da loja, com os padrões que provaram funcionar em operação real.

Configurar credenciais e ambiente

Use o pacote mercadopago/dx-php. A autenticação tem dois modelos: o access_token simples, que resolve a loja vendendo em conta própria, e o OAuth2 com client_id e client_secret, necessário quando a aplicação opera em nome de terceiros. Nunca deixe o token no código: tudo em config/services.php lendo do .env. Mantenha dois conjuntos de credenciais (sandbox e produção) e alterne por variável de ambiente.

// config/services.php
'mercadopago' => [
    'access_token' => env('MERCADOPAGO_ACCESS_TOKEN'),
    'environment' => env('MERCADOPAGO_ENV', 'sandbox'),
],

No construtor do service, registre o token uma única vez:

public function __construct()
{
    MercadoPagoConfig::setAccessToken(config('services.mercadopago.access_token'));
}

O model Pagamento separado do Pedido

Não guarde os dados financeiros direto no Pedido. Crie um model Pagamento separado, ligado por pedido_id. Isso permite múltiplas transações por pedido (parcelamento, split, tentativas que falharam e foram retentadas) sem poluir a tabela de pedidos. O Pagamento carrega metodo (enum MetodoPagamento), gateway, gateway_id, status (enum PagamentoStatus), valor, parcelas, e um JSON dados_gateway com a resposta crua. O PedidoStatus é derivado: o pedido só vira Pago quando existe um pagamento aprovado conciliado.

PIX, cartão e parcelamento

O PIX usa checkout transparente: você cria o pagamento com payment_method_id => 'pix' e devolve o qr_code e o qr_code_base64 para a tela. O cartão recebe um token gerado no navegador (o número do cartão nunca toca seu servidor, o que mantém o PCI fora do seu escopo) mais installments para o parcelamento em até 12x.

$pagamento = (new PaymentClient())->create([
    'transaction_amount' => (float) $pedido->total,
    'token' => $dados['token'],
    'installments' => (int) $dados['parcelas'],
    'payment_method_id' => $dados['payment_method_id'],
    'external_reference' => $pedido->numero_pedido,
    'notification_url' => route('webhook.mercadopago'),
]);

O external_reference é o elo entre o gateway e seu pedido: sempre o numero_pedido. Sem ele, o webhook fica órfão.

Webhook em fila e idempotência

O Mercado Pago confirma o pagamento de forma assíncrona via notificação HTTP. Nunca confie no retorno imediato da criação do pagamento para liberar o pedido; confie no webhook. O controller do webhook deve fazer o mínimo: validar a origem, despachar um Job para a fila database e responder 200 rápido. O Mercado Pago reenvia a mesma notificação várias vezes, então o processamento precisa ser idempotente.

A idempotência tem duas camadas. Primeiro, consulte o pagamento real na API (não confie no corpo do webhook, que pode ser forjado). Segundo, só mude o status se ele realmente mudou, e atualize o pedido de forma atômica:

$atualizado = Pedido::where('id', $pedido->id)
    ->where('status', PedidoStatus::Pendente)
    ->update(['status' => PedidoStatus::Pago, 'pago_em' => now()]);

if ($atualizado) {
    $this->decrementarEstoque($pedido);
    event(new PedidoPago($pedido));
}

Como o update condicionado ao status anterior só afeta uma linha uma vez, duas notificações concorrentes não decrementam o estoque duas vezes nem disparam o evento em dobro.

Conciliação e mapeamento de status

O gateway fala uma língua (approved, pending, in_process, rejected, cancelled, refunded, charged_back); seu domínio fala outra. Mantenha um mapa explícito traduzindo cada status do Mercado Pago para o seu PagamentoStatus interno, e do pagamento derive o PedidoStatus. Pagamento aprovado marca pago_em e libera estoque; cancelado ou estornado cancela o pedido. Esse mapeamento centralizado num único método é o que torna a conciliação previsível e testável.

Sandbox versus produção

Teste tudo em sandbox primeiro: o SDK devolve init_point e sandbox_init_point justamente para você escolher o ambiente. Use os cartões de teste do Mercado Pago para forçar aprovação, recusa e pendência, e simule o reenvio do webhook duas vezes para provar a idempotência. Só promova para produção depois de ver um pedido percorrer o ciclo inteiro: criação, notificação, conciliação e liberação de estoque.

Critérios de qualidade

  • O token e o client_secret vivem apenas no .env, nunca no código versionado.
  • O webhook responde em fila, é idempotente e consulta o pagamento real na API antes de conciliar.
  • Reenviar a mesma notificação duas vezes não duplica baixa de estoque nem o evento de pedido pago.
  • O status do Pedido é sempre derivado do Pagamento conciliado, nunca do retorno imediato da criação.
  • PIX (QR Code), cartão e parcelamento até 12x foram validados em sandbox com cartões de teste.
  • Existe teste automatizado cobrindo aprovação, recusa e reprocessamento do webhook.

Checklist

  • Instalar o SDK e configurar credenciais de sandbox e produção
  • Implementar MercadoPagoService com PIX, cartão e parcelamento
  • Criar o model Pagamento separado do Pedido
  • Receber o webhook em fila e torná-lo idempotente
  • Conciliar o status do pedido conforme o retorno do gateway
  • Validar o fluxo inteiro em sandbox antes de subir produção

Entregável

MercadoPagoService completo (PIX, cartão, parcelamento), model Pagamento, webhook idempotente em fila e status do pedido conciliado com o gateway.

Integrar o frete com o Melhor Envio

10h Avançado

Calcular frete em tempo real, gerar etiqueta e rastreio, com OAuth2 ou token legado.

Por que isto importa

Frete errado custa caro nos dois sentidos: cobrar a menos come a margem, cobrar a mais espanta o comprador no momento mais sensível, o checkout. O Melhor Envio resolve isso cotando várias transportadoras de uma vez e ainda emite a etiqueta com o código de rastreio. Esta atividade entrega o cálculo de frete em tempo real, a emissão da etiqueta e o fluxo de autenticação, cobrindo os dois modelos que a API aceita: o token legado por header e o OAuth2 completo com refresh_token.

Cálculo de frete em tempo real no checkout

O comprador informa o CEP e a loja precisa devolver as opções de frete em segundos. A cotação é uma chamada à API enviando origem (o CEP da loja), destino (o CEP do comprador) e as dimensões e peso dos produtos do carrinho. A resposta traz preço e prazo por transportadora. Cacheie a cotação por alguns minutos por combinação de CEP e carrinho para não bater na API a cada digitação, e trate timeout com elegância: se a API não responder, ofereça um valor de contingência em vez de travar o checkout.

$opcoes = Http::withToken($this->oauth->getValidAccessToken())
    ->acceptJson()
    ->withHeaders(['User-Agent' => $this->userAgent])
    ->post($this->baseUrl . '/api/v2/me/shipment/calculate', [
        'from' => ['postal_code' => $cepOrigem],
        'to' => ['postal_code' => $cepDestino],
        'products' => $produtos,
    ])->json();

OAuth2 com refresh token versus token legado

O caminho simples é um token fixo enviado no header Authorization. Funciona, mas expira e exige troca manual. O caminho robusto é o OAuth2 de verdade, encapsulado em um MelhorEnvioOAuthService, com client_id, client_secret, redirect_uri e refresh_token. O segredo de robustez é o método que devolve sempre um token válido: ele checa a expiração, renova via refresh_token quando falta pouco, e usa um lock atômico para evitar que dois processos renovem ao mesmo tempo.

public function getValidAccessToken(): ?string
{
    // token válido com margem de 5 min -> retorna
    // perto de expirar -> Cache::lock + refresh
    // sem token OAuth -> fallback no token legado do .env
}

O fallback é o que mantém a compatibilidade: se não houver token OAuth persistido, o service cai no token legado do .env. Assim o mesmo código serve aos dois modelos de autenticação.

Fluxo de conexão: connect, callback, disconnect

A loja conecta a conta do Melhor Envio pelo painel admin em três rotas. /melhor-envio/connect monta a URL de autorização com um state aleatório guardado na sessão (proteção CSRF) e redireciona ao Melhor Envio. /melhor-envio/callback recebe o code, valida o state com hash_equals, troca o code por access_token e refresh_token, e persiste os tokens criptografados com Crypt::encryptString. /melhor-envio/disconnect apaga todos os tokens. Tokens em texto plano no banco são um vazamento esperando para acontecer; criptografe sempre.

Geração de etiqueta e código de rastreio

Confirmado o pagamento, o operador gera a etiqueta. O fluxo no Melhor Envio tem etapas: inserir o frete no carrinho, fazer o checkout (pagar a etiqueta), gerar e por fim imprimir. O dado que mais importa salvar no Pedido é o frete_codigo_rastreio, que alimenta o acompanhamento do comprador e a comunicação automática. Guarde também o link da etiqueta para reimpressão. Registre cada passo em log: quando algo falha na geração, você precisa saber em qual etapa parou.

Retirada na loja via Unidade

Nem todo pedido precisa de frete. A retirada na loja usa o model Unidade: o comprador escolhe uma unidade física e o frete é zero. Isso muda o fluxo do pedido, que não gera etiqueta nem rastreio, apenas notifica a unidade de que há um pedido para separar. Trate a retirada como um método de entrega de primeira classe no checkout, lado a lado com as transportadoras, e não como exceção mal encaixada. O endereço de origem da cotação, aliás, deve ser sempre o da Unidade que despacha.

Critérios de qualidade

  • O cálculo de frete responde em tempo real no checkout, com cache por CEP e fallback em caso de timeout.
  • O OAuth2 renova o token sozinho via refresh_token, com lock atômico contra renovação concorrente.
  • Os tokens são persistidos criptografados, nunca em texto plano.
  • O fluxo connect/callback/disconnect valida o state com hash_equals antes de aceitar o code.
  • A etiqueta é gerada e o frete_codigo_rastreio fica salvo no Pedido.
  • A retirada na loja via Unidade funciona com frete zero e sem geração de etiqueta.

Checklist

  • Calcular o frete em tempo real no checkout
  • Implementar o fluxo OAuth2 (connect, callback, disconnect) com refresh token
  • Manter compatibilidade com o token legado por header
  • Gerar etiqueta e salvar o código de rastreio no Pedido
  • Implementar a retirada na loja via Unidade, sem frete
  • Validar tudo no ambiente sandbox do Melhor Envio

Entregável

MelhorEnvioService e MelhorEnvioOAuthService com cálculo de frete no checkout, fluxo connect/callback/disconnect, geração de etiqueta, código de rastreio e opção de retirada na loja.

Blindar pagamento e antifraude

8h Avançado

Pontuar risco de fraude e fechar as brechas de segurança do fluxo de pagamento.

Por que isto importa

Fraude e falha de segurança no checkout não são riscos hipotéticos: são chargebacks que o lojista paga, pedidos liberados sem pagamento e dados de clientes vazados. Diferente de um bug comum, esses problemas só aparecem quando alguém mal-intencionado já causou prejuízo. Esta atividade entrega a camada de defesa do fluxo financeiro em três frentes: um AntiFraudeService com regras explicáveis, campos de risco no Pedido e 2FA no admin.

Score de fraude com o AntiFraudeService

O AntiFraudeService pontua cada pedido de 0 a 100 somando o peso de regras de risco, e grava o resultado nos campos score_fraude, flag_fraude e analise_fraude (um JSON com as regras que dispararam). Acima de um limiar (50 é um bom ponto de partida), o pedido é marcado para revisão manual em vez de bloqueado, porque falso positivo também é venda perdida. As regras são heurísticas simples e explicáveis: CPF que já apareceu em cancelamentos recentes, mesmo IP com vários pedidos em 24h, valor muito acima do ticket médio do cliente, e-mail recém-criado, endereço de entrega divergente da cobrança e quantidade anormal do mesmo item.

public function aplicar(Pedido $pedido): Pedido
{
    $resultado = $this->analisar($pedido);
    $pedido->update([
        'score_fraude' => $resultado['score'],
        'flag_fraude' => $resultado['flag'],
        'analise_fraude' => $resultado,
    ]);
    return $pedido->refresh();
}

Guardar o detalhamento em analise_fraude é o que permite ao operador entender por que o pedido foi sinalizado, em vez de confiar cegamente num número.

Validação de assinatura e origem do webhook

O webhook do gateway é uma porta aberta para a internet: qualquer um pode enviar um POST fingindo ser o Mercado Pago para tentar marcar um pedido como pago. Duas defesas são obrigatórias. Primeiro, validar a assinatura da notificação (o header de assinatura do gateway) com hash_equals contra o segredo configurado. Segundo, nunca confiar no corpo do webhook: ele só carrega o ID do pagamento; o status verdadeiro vem de uma consulta de volta à API com seu token. Assim, mesmo um payload forjado não consegue mentir sobre o status.

Idempotência no processamento

O gateway reenvia a mesma notificação várias vezes, por desenho. Processar duas vezes significa baixar estoque em dobro ou disparar o e-mail de confirmação repetido. A idempotência se garante atualizando o pedido de forma condicional e atômica, só quando o status anterior ainda permite a transição:

$mudou = Pedido::where('id', $pedido->id)
    ->where('status', PedidoStatus::Pendente)
    ->update(['status' => PedidoStatus::Pago]);

if ($mudou) {
    // só executa uma vez, mesmo com notificações concorrentes
}

Quem perde a corrida recebe zero linhas afetadas e não faz nada.

2FA no admin

O painel administrativo dá acesso a pedidos, clientes e estornos: é o alvo mais valioso. O segundo fator vem do pacote aurorawebsoftware/filament-2fa, que adiciona TOTP ao login do Filament. Exija 2FA para todos os usuários do admin, não apenas opcionalmente. Senha vazada acontece; senha vazada com 2FA ativo ainda não dá acesso ao painel.

IDOR e mass assignment

IDOR (Insecure Direct Object Reference) é o cliente A acessar o pedido do cliente B só trocando o ID na URL. A defesa é nunca buscar um pedido apenas pelo ID: sempre escope pela posse, com Pedido::where('cliente_id', auth()->id())->findOrFail($id) ou uma Policy. Mass assignment é o atacante injetar campos que você não esperava no request, por exemplo status => pago ou total => 0. A defesa é o $fillable enxuto no model e nunca passar $request->all() direto para update: valide e selecione explicitamente os campos permitidos.

Rate limiting no checkout

O checkout e as rotas de pagamento são alvo de força bruta de cartão (testar números roubados) e de abuso de cotação de frete. Aplique throttle por IP e por sessão nas rotas sensíveis, com limites generosos para o usuário legítimo e apertados o bastante para barrar automação. Combine com o AntiFraudeService: muitas tentativas seguidas do mesmo IP já é, por si só, um sinal que entra no score.

Critérios de qualidade

  • Todo pedido recebe score_fraude, flag_fraude e analise_fraude preenchidos pelo AntiFraudeService.
  • O webhook valida a assinatura com hash_equals e confirma o status consultando a API, nunca o corpo recebido.
  • Reprocessar a mesma notificação não duplica baixa de estoque nem e-mails.
  • O acesso ao painel admin exige 2FA para todos os usuários.
  • Um cliente não consegue acessar o pedido de outro trocando o ID, e o $fillable bloqueia campos sensíveis.
  • As rotas de checkout e pagamento têm rate limiting por IP e sessão.

Checklist

  • Implementar o AntiFraudeService com score e flag no Pedido
  • Validar a assinatura e a origem do webhook do gateway
  • Garantir idempotência no processamento do webhook
  • Ativar 2FA no painel admin
  • Fechar IDOR e mass assignment nos pedidos
  • Aplicar rate limiting nas rotas de checkout

Entregável

AntiFraudeService gravando score no Pedido, webhook com assinatura validada e idempotente, 2FA no admin, proteção contra IDOR e mass assignment, e rate limiting no checkout.

Etapa 6 — Admin (Filament) e operação

Painel de gestão e comunicação transacional.

Montar o painel administrativo (Filament)

12h Intermediário

Entregar o admin da loja em Filament com Resources de catálogo e pedidos, dashboards, permissões e auditoria.

Por que isto importa

O painel administrativo é onde a loja vive depois do lançamento. Toda a operação diária acontece ali: cadastrar produto, despachar pedido, aplicar cupom, atender cliente. Se o admin é confuso ou incompleto, o lojista trabalha errado, perde venda e culpa o sistema. Por isso ele não é um detalhe ao final do projeto, é um entregável de primeira classe. Em uma loja proprietária em Laravel, o Filament resolve quase tudo de forma declarativa, deixando você focar nas regras de negócio em vez de reinventar tabelas, formulários e autenticação. O objetivo deste bloco é montar um admin que o dono da loja use sozinho, com segurança e rastreabilidade.

Estruturar os Resources de catálogo

Comece pelo coração do catálogo, o ProdutoResource. O formulário precisa de uma Section para dados principais (nome, SKU, preço, preço promocional, categoria) e outra para conteúdo. A descrição usa RichEditor, que gera HTML controlado, e as imagens usam FileUpload com image() e imageEditor(), salvando em disco com conversão para WebP.

Forms\Components\RichEditor::make('descricao')
    ->label('Descrição')
    ->columnSpanFull(),

Forms\Components\FileUpload::make('imagens')
    ->label('Imagens')
    ->image()
    ->multiple()
    ->reorderable()
    ->imageEditor(),

Replique o padrão para CategoriaResource, CorResource e TamanhoResource. A tabela do produto deve ser pesada em ações em lote (BulkAction) porque o lojista altera preço, categoria e destaque de vários itens de uma vez. Sempre aplique emptyStateHeading, descrição e CTA para guiar quem nunca usou o painel.

PedidoResource com timeline e ações

O pedido é o objeto mais delicado do admin, porque mexer nele errado afeta dinheiro e expedição. O PedidoResource precisa de uma visão clara do estado atual e do histórico. Use um enum StatusPedido para o ciclo (pendente, confirmado, em separação, enviado, entregue, cancelado) e renderize uma timeline com as transições já registradas pela auditoria.

As mudanças de status não devem acontecer por edição direta de campo, e sim por ações nomeadas com confirmação, deixando explícito o efeito de cada uma:

Tables\Actions\Action::make('marcar_enviado')
    ->label('Marcar como enviado')
    ->icon('heroicon-o-truck')
    ->requiresConfirmation()
    ->form([
        Forms\Components\TextInput::make('codigo_rastreio')
            ->label('Código de rastreio')
            ->required(),
    ])
    ->action(fn (Pedido $pedido, array $data) =>
        app(PedidoService::class)->marcarEnviado($pedido, $data['codigo_rastreio'])
    ),

Cada ação delega para um Service, mantendo a regra de negócio fora do Filament. Deixe explícito na interface que alterar status não dispara e-mail sozinho, evitando que o operador acredite ter notificado o cliente sem ter feito isso. O ClienteResource e o CupomResource seguem a mesma filosofia: formulário enxuto, tabela rica em filtros e ações que chamam serviços.

Permissões com spatie/laravel-permission

Nem todo mundo que entra no admin pode tudo. O estagiário de marketing não deveria cancelar pedido nem reembolsar. Use spatie/laravel-permission para definir papéis (administrador, operação, marketing, atendimento) e permissões granulares. No Filament, controle a visibilidade de Resources, ações e campos com can() e políticas.

public static function canViewAny(): bool
{
    return auth()->user()->can('ver_pedidos');
}

Defina as permissões em um seeder, atribua aos papéis e registre uma Policy por model. O ganho é duplo: segurança real e uma interface que mostra a cada perfil apenas o que lhe diz respeito, reduzindo erro humano e ruído visual.

Auditoria com Activity Log

Em e-commerce, a pergunta "quem mudou esse preço?" ou "quem cancelou o pedido?" aparece toda semana. Habilite spatie/laravel-activitylog nos models críticos (Produto, Pedido, Cupom, Cliente) com o trait LogsActivity, configurando quais atributos logar. Exponha o histórico em um ActivityResource somente leitura e use o mesmo log para alimentar a timeline do pedido. Assim a auditoria deixa de ser uma promessa e vira uma tela que o lojista consulta sozinho.

Dashboards e exportação

O lojista precisa enxergar a saúde da loja em segundos. Crie duas Pages customizadas com widgets de KPI: um dashboard de operação (pedidos pendentes, em separação, faturamento do dia, ticket médio) e um de marketing (cupons usados, novos clientes, produtos mais vendidos). Marque os widgets como lazy para não travar o carregamento.

protected static bool $isLazy = true;

protected function getStats(): array
{
    return [
        Stat::make('Pedidos pendentes', $this->pendentes())
            ->color('warning'),
    ];
}

Por fim, ofereça exportação Excel com Maatwebsite Excel nas listagens principais (pedidos, produtos, clientes), porque o contador e o financeiro sempre pedem a planilha. Uma Action de exportar na tabela, respeitando os filtros ativos, resolve esse pedido recorrente sem virar planilha manual.

Critérios de qualidade

  • O ProdutoResource tem RichEditor para descrição e upload de imagens com conversão funcionando.
  • O PedidoResource mostra timeline de status e muda o estado apenas por ações nomeadas que chamam Services.
  • Existem papéis e permissões com spatie/laravel-permission controlando Resources, ações e campos.
  • O Activity Log registra alterações nos models críticos e está visível em tela somente leitura.
  • Há dashboards de operação e marketing com widgets de KPI carregados de forma lazy.
  • As listagens principais exportam para Excel respeitando os filtros aplicados.

Checklist

  • Criar ProdutoResource com RichEditor e upload de imagens com conversão
  • Criar PedidoResource com timeline de status e ações operacionais
  • Configurar papéis e permissões com spatie/laravel-permission
  • Habilitar auditoria com spatie/laravel-activitylog nos models críticos
  • Montar dashboards de operação e marketing com widgets de KPI
  • Disponibilizar exportação Excel das listagens principais

Entregável

Painel administrativo funcional com CRUD de Produto, Pedido, Cliente e Cupom, dashboards de KPI, papéis e auditoria.

Implementar os e-mails transacionais

6h Intermediário

Entregar os e-mails do ciclo do pedido enviados de forma assíncrona, idempotente e testada.

Por que isto importa

O e-mail transacional é o cordão que mantém o cliente calmo depois de pagar. Sem ele, o comprador fica no escuro, abre chamado, pede reembolso por insegurança e some. Cada etapa do pedido tem um e-mail esperado: confirmação da compra, pagamento aprovado, pedido em separação e pedido enviado com rastreio. Esses e-mails reduzem ansiedade, diminuem o suporte e aumentam a recompra. O risco técnico não é escrever o texto, é entregar de forma confiável: sem travar o checkout, sem duplicar mensagem e sem sumir silenciosamente. Este bloco entrega os e-mails do ciclo do pedido com envio assíncrono, idempotência e testes, que é o que separa um envio amador de um confiável.

Mailables com Markdown

Cada e-mail é um Mailable dedicado, com template Markdown. O Markdown do Laravel já entrega um layout responsivo com botão, painel e tabela, então você foca na mensagem em vez de brigar com HTML de e-mail. Mantenha um Mailable por evento do ciclo: PedidoConfirmacaoMail, PagamentoAprovadoMail, PedidoEmSeparacaoMail e PedidoEnviadoMail.

class PedidoEnviadoMail extends Mailable implements ShouldQueue
{
    public function __construct(public Pedido $pedido) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: "Seu pedido #{$this->pedido->numero} saiu para entrega");
    }

    public function content(): Content
    {
        return new Content(markdown: 'emails.pedido-enviado');
    }
}

No template, mostre o número do pedido, os itens, o valor e, no caso do envio, o código de rastreio com link da transportadora. Escreva tudo em português com acentuação correta e tom da loja. Reaproveite um componente de cabeçalho com a marca para que todos os e-mails tenham identidade consistente.

Envio assíncrono via fila

Nenhum desses e-mails pode rodar dentro do request do checkout. Se o servidor SMTP estiver lento, o cliente esperaria o e-mail para ver a tela de obrigado. A solução é implementar ShouldQueue no Mailable e usar a fila database, simples de operar em um VPS sem Redis.

Mail::to($pedido->cliente->email)->queue(new PedidoEnviadoMail($pedido));

Configure QUEUE_CONNECTION=database, rode as migrations de fila e mantenha um worker ativo (queue:work via Supervisor). O ganho é direto: o checkout responde instantaneamente e o e-mail é processado em segundo plano. Se o envio falhar, a fila tenta de novo conforme a política de retry, em vez de perder a mensagem.

Idempotência de envio

O maior pecado de e-mail transacional é enviar duas vezes. Webhook de pagamento que chega duplicado, operador que clica de novo em "reenviar", job que roda novamente após falha parcial: tudo isso gera o mesmo e-mail repetido, e o cliente desconfia. A defesa é registrar cada notificação já enviada e checar antes de enfileirar.

Crie uma tabela pedido_notificacoes com pedido_id e tipo, e proteja o disparo:

public function notificar(Pedido $pedido, string $tipo, Mailable $email): void
{
    $jaEnviou = PedidoNotificacao::firstOrCreate([
        'pedido_id' => $pedido->id,
        'tipo' => $tipo,
    ])->wasRecentlyCreated === false;

    if ($jaEnviou) {
        return;
    }

    Mail::to($pedido->cliente->email)->queue($email);
}

Assim, mesmo que o evento dispare várias vezes, o cliente recebe cada notificação uma só vez. O registro ainda serve de auditoria: você sabe exatamente quais e-mails aquele pedido recebeu e quando.

Disparo a partir das transições

Os e-mails devem nascer das mudanças de status do pedido, não espalhados pelo código. Centralize as transições em um PedidoService que, ao confirmar pagamento ou registrar o envio, chama o serviço de notificação com o tipo certo. Um observer ou eventos de domínio também funcionam, desde que o ponto de disparo seja único e previsível. Mantenha claro que mudar o status no painel é o que aciona o e-mail correspondente, alinhando o comportamento real com a expectativa do operador.

Testar com Mail::fake()

E-mail é fácil de quebrar em silêncio, então teste. Com Mail::fake() você verifica que o e-mail certo foi enfileirado, para o destinatário certo, na transição certa, sem disparar nada de verdade.

public function test_envio_dispara_email_de_enviado(): void
{
    Mail::fake();

    $pedido = Pedido::factory()->confirmado()->create();
    app(PedidoService::class)->marcarEnviado($pedido, 'BR123456789BR');

    Mail::assertQueued(PedidoEnviadoMail::class, fn ($mail) =>
        $mail->hasTo($pedido->cliente->email)
    );
}

Escreva também um teste de idempotência que dispara a mesma transição duas vezes e usa Mail::assertQueuedCount(1). Esses testes congelam o comportamento e impedem que uma refatoração futura passe a enviar e-mail duplicado ou pare de enviar sem ninguém perceber.

Critérios de qualidade

  • Existe um Mailable Markdown por etapa do ciclo (confirmação, pagamento aprovado, separação, enviado).
  • Todos os Mailables implementam ShouldQueue e rodam na fila database com worker ativo.
  • O envio é idempotente: a mesma notificação nunca chega duas vezes ao cliente.
  • Os e-mails são disparados a partir das transições de status centralizadas em um Service.
  • O e-mail de envio inclui o código de rastreio e link da transportadora.
  • Há testes com Mail::fake() cobrindo o disparo correto e a não duplicação.

Checklist

  • Criar Mailables Markdown para confirmação, pagamento aprovado, separação e envio
  • Tornar os Mailables assíncronos com ShouldQueue na fila database
  • Garantir idempotência registrando cada notificação já enviada
  • Disparar os e-mails a partir das transições de status do pedido
  • Cobrir os envios com testes usando Mail::fake()

Entregável

Conjunto de Mailables Markdown do ciclo do pedido, enfileirados, idempotentes e cobertos por testes.

Etapa 7 — Conteúdo, IA e SEO on-store

IA para escala e SEO técnico da loja.

Adicionar IA para descrições e atendimento

8h Avançado

Integrar IA generativa para descrever produtos em lote e atender clientes por chat em tempo real.

Por que isto importa

IA em e-commerce não é enfeite, é alavanca de produtividade e conversão. Descrever quinhentos produtos à mão consome semanas do lojista; uma IA faz o rascunho em minutos e ele só revisa. No atendimento, um chat que responde na hora, conhece o catálogo e captura o contato do visitante recupera vendas que se perderiam no silêncio. Mas IA mal integrada é cara, lenta e frágil: chamada síncrona que trava a página, custo de token que explode, resposta que vem fora do formato e quebra o parser. Este bloco mostra como integrar dois usos comprovados em operação real, geração de descrição em lote e chat de atendimento, com a engenharia que os torna confiáveis em produção.

DescricaoIaService com Anthropic

Para descrições, o padrão usa a API da Anthropic via mozex/anthropic-php. O serviço recebe o contexto do produto (nome, categoria, cores, SKU), monta um prompt com tom configurável e pede a resposta em JSON estruturado, com descrição curta para a listagem e descrição completa em HTML para a página.

$response = $this->client->messages()->create([
    'model' => 'claude-haiku-4-5-20251001',
    'max_tokens' => 1024,
    'messages' => [
        ['role' => 'user', 'content' => $prompt],
    ],
]);

$dados = $this->parsearResposta($response->content[0]->text);

O segredo está no prompt e no parser. O prompt fixa a persona da loja, o tom e o formato JSON exato, instruindo a IA a não escrever nada fora do JSON. O parser remove cercas de código eventuais, decodifica e, se o JSON vier inválido, lança exceção em vez de salvar lixo no banco. Use um modelo barato como o Haiku para essa tarefa de volume, porque a qualidade necessária para um rascunho revisável não exige o modelo mais caro.

Geração em lote via fila

Gerar uma descrição é rápido; gerar quinhentas no request trava o navegador e estoura o tempo limite do PHP. A geração em lote precisa rodar na fila. No painel, uma ação em massa seleciona produtos sem descrição e despacha um job por produto (ou em pequenos blocos), respeitando o rate limit da API.

foreach ($produtos as $produto) {
    GerarDescricaoIaJob::dispatch($produto)
        ->onQueue('ia');
}

O job chama o DescricaoIaService, salva o resultado no produto e marca o item como processado. Use uma fila própria (ia) para não competir com e-mails, e aplique RateLimited ou Bus::throttle para não bater no teto da API. Assim o lojista clica em "gerar descrições", continua trabalhando, e os textos aparecem aos poucos sem nenhum travamento.

ChatIaService com OpenAI

O atendimento em tempo real usa a API da OpenAI via openai-php/laravel. O ChatIaService monta o histórico (system prompt + estado da coleta + últimas mensagens), chama o modelo e devolve a resposta. Com modelos da família gpt-5, os parâmetros mudam: usa-se max_completion_tokens e reasoning_effort em vez de max_tokens e temperature.

$payload = [
    'model' => $modelo,
    'messages' => $this->montarHistorico($chat),
    'response_format' => $this->jsonSchemaResposta(),
    'max_completion_tokens' => 800,
    'reasoning_effort' => 'low',
];

$resposta = OpenAI::chat()->create($payload);

O system prompt define a persona, o catálogo resumido (categorias, destaques, promoções) e a estratégia de coleta de dados. O response_format com json_schema em modo strict força a IA a devolver dois campos: a mensagem natural para o cliente e os dados que ela conseguiu capturar (nome, e-mail, interesse). Limite o histórico às últimas dez mensagens para conter o custo de token a cada turno.

Persistir a conversa

A conversa precisa virar dado, não evaporar. Use dois models: Chat, que representa a sessão com o visitante e guarda os dados capturados, e ChatMensagem, uma linha por mensagem com autor, conteúdo, modelo usado e tokens consumidos.

ChatMensagem::create([
    'chat_id' => $chat->id,
    'autor' => 'atendente',
    'is_ia' => true,
    'mensagem' => $textoResposta,
    'modelo_ia' => $modelo,
    'tokens_ia' => $tokens,
]);

Persistir tudo dá três benefícios: a IA monta o histórico para responder com contexto, o atendente humano assume a conversa pelo inbox do painel quando necessário, e os dados capturados alimentam o CRM da loja. Os dados sensíveis (nome, e-mail, WhatsApp) são validados antes de gravar e nunca sobrescrevem o que já existe.

Custo, limites e falhas

IA cobra por token e pode falhar, então trate as duas coisas como requisito. Para custo: limite o histórico, use modelos baratos onde a tarefa permite, cacheie partes estáticas do prompt (como o resumo de catálogo) e registre tokens_ia por mensagem para acompanhar o gasto. Para rate limit: aplique throttle nos jobs e um limitador por sessão de chat para conter abuso.

Para falhas, jamais deixe a IA derrubar o fluxo do usuário. Envolva toda chamada em try/catch, registre o erro com Log::error e devolva um fallback gracioso, como uma mensagem pedindo o contato para um humano responder.

} catch (\Throwable $e) {
    Log::error('Falha na resposta IA do chat', ['chat_id' => $chat->id, 'erro' => $e->getMessage()]);
    return null;
}

Esse cuidado mantém a loja funcionando mesmo quando a API da IA está fora do ar, fica lenta ou estoura cota. A IA agrega valor quando funciona, e nunca atrapalha quando não funciona.

Critérios de qualidade

  • O DescricaoIaService gera descrição curta e completa válidas, com parser que rejeita JSON inválido.
  • A geração em lote roda na fila, sem travar o painel, respeitando o rate limit da API.
  • O ChatIaService responde com contexto do catálogo e devolve mensagem em formato estruturado.
  • Toda a conversa é persistida em Chat e ChatMensagem, com tokens registrados por mensagem.
  • O custo é controlado por limite de histórico, cache de prompt e escolha de modelo adequada à tarefa.
  • Falhas de IA são capturadas e logadas sem quebrar o checkout, o painel ou o chat do cliente.

Checklist

  • Criar DescricaoIaService que gera descrição curta e completa de produto
  • Processar a geração de descrições em lote via fila
  • Criar ChatIaService com system prompt e histórico limitado
  • Persistir a conversa nos models Chat e ChatMensagem
  • Controlar custo com limite de tokens, cache e rate limit
  • Tratar falhas de IA sem quebrar o fluxo do usuário

Entregável

Serviços de IA em produção: geração de descrições de produto em lote e chat de atendimento com histórico persistido.

Aplicar o SEO técnico da loja

8h Intermediário

Implementar dados estruturados, sitemap, redirects 301 e Core Web Vitals na loja.

Por que isto importa

Em e-commerce, o tráfego orgânico paga as contas. Uma loja proprietária em Laravel só compete com os marketplaces se o Google entender cada produto, indexar o catálogo inteiro e confiar na velocidade das páginas. SEO técnico não é otimizar texto, é dar ao buscador um terreno limpo: dados estruturados corretos, um mapa do site sempre atualizado, redirects que preservam a autoridade quando uma URL muda e páginas que carregam rápido no celular. Esta atividade entrega essa fundação. O SEO local, com landing pages por cidade como vestidos-festa-maringa, fica em um playbook próprio, porque envolve estratégia de conteúdo e não apenas marcação. Aqui o foco é a engenharia que todo produto e toda categoria precisam ter por padrão.

Dados estruturados Product, Offer e AggregateRating

A página de produto deve emitir JSON-LD do tipo Product. O Google usa essa marcação para mostrar preço, disponibilidade e estrelas direto na busca, o que aumenta a taxa de cliques. Monte o bloco em uma partial Blade alimentada pelo model, nunca à mão:

<script type="application/ld+json">
{!! json_encode([
    '@context' => 'https://schema.org',
    '@type' => 'Product',
    'name' => $produto->nome,
    'image' => $produto->imagens->pluck('url'),
    'sku' => $variacao->sku,
    'offers' => [
        '@type' => 'Offer',
        'price' => number_format($variacao->preco, 2, '.', ''),
        'priceCurrency' => 'BRL',
        'availability' => $variacao->emEstoque()
            ? 'https://schema.org/InStock'
            : 'https://schema.org/OutOfStock',
    ],
    'aggregateRating' => $produto->avaliacoes_count > 0 ? [
        '@type' => 'AggregateRating',
        'ratingValue' => $produto->nota_media,
        'reviewCount' => $produto->avaliacoes_count,
    ] : null,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>

A regra de ouro é fidelidade: o preço e a disponibilidade do JSON-LD precisam bater com o que o usuário vê na tela. Marcação que mente vira penalidade. Só inclua AggregateRating quando existirem avaliações reais; nota inventada é violação de diretriz.

Sitemap dinâmico e robots

O sitemap não pode ser estático em uma loja viva, onde produtos nascem e saem de linha toda semana. Gere um sitemap.xml dinâmico, seja por rota que monta o XML em tempo real, seja por um command agendado que regrava o arquivo, padrão usado em produção com GerarSitemapEstatico. Inclua apenas URLs canônicas e indexáveis: produtos ativos, categorias e páginas institucionais. Não liste carrinho, checkout, busca interna nem filtros com query string, que poluem o índice.

O robots.txt aponta para o sitemap e bloqueia o que não deve ser rastreado:

User-agent: *
Disallow: /carrinho
Disallow: /checkout
Disallow: /*?ordenar=
Sitemap: https://www.loja.com.br/sitemap.xml

Redirects 301 e o model Redirect

Toda vez que uma URL muda, slug de produto editado, categoria renomeada, migração de plataforma, a URL antiga precisa de um 301 para a nova. Sem isso, você perde ranking e joga visitante em 404. O padrão da stack é o model Redirect, com colunas from, to, status_code (default 301), ativo e hits, gerenciado por um RedirectResource no Filament para que o time edite sem deploy.

Um middleware consulta um mapa em cache, evitando query a cada requisição:

$mapa = Redirect::mapaCacheado();
if (isset($mapa[$request->path()])) {
    $destino = $mapa[$request->path()];
    return redirect($destino['to'], $destino['status_code']);
}

O cache é invalidado no saved e no deleted do model, então o redirect entra no ar assim que salvo. O contador de hits mostra quais redirects ainda recebem tráfego e quais já podem ser aposentados.

URLs canônicas e Core Web Vitals

Filtros, ordenação e paginação criam várias URLs para o mesmo conteúdo. A tag canônica resolve isso apontando sempre para a versão preferida:

<link rel="canonical" href="{{ $produto->urlCanonica() }}">

Core Web Vitals são fator de ranqueamento e de conversão. Meça com o PageSpeedInsightsService, que extrai LCP, CLS e INP da API do PageSpeed e guarda o histórico. As vitórias mais comuns em loja: servir imagens em WebP com width e height definidos para zerar layout shift, usar loading="lazy" abaixo da dobra, e manter o LCP no banner principal sem JavaScript bloqueante. Rode a medição em mobile, que é onde a maioria compra e onde o Google julga.

Critérios de qualidade

  • A página de produto passa no Rich Results Test sem erro, com Product, Offer e AggregateRating válidos.
  • O preço e a disponibilidade do JSON-LD são idênticos aos exibidos na página.
  • O sitemap.xml é gerado dinamicamente, lista só URLs canônicas e exclui carrinho, checkout e filtros.
  • Toda troca de slug ou categoria gera um redirect 301 registrado no model Redirect.
  • Cada página tem exatamente uma tag canônica apontando para a versão preferida.
  • LCP, CLS e INP estão medidos em mobile via PageSpeedInsightsService e dentro das metas do Google.

Checklist

  • Emitir JSON-LD Product com Offer e AggregateRating
  • Gerar sitemap.xml dinâmico e robots.txt
  • Cadastrar redirects 301 com o model Redirect
  • Definir URLs canônicas em todas as páginas
  • Medir e corrigir Core Web Vitals

Entregável

Páginas de produto com JSON-LD válido, sitemap.xml dinâmico, redirects 301 e métricas de Core Web Vitals monitoradas.

Etapa 8 — Qualidade, deploy e pós-lançamento

Testes, publicação e operação contínua.

Cobrir o e-commerce com testes

10h Avançado

Garantir checkout, pedido, pagamento, estoque e cupom com testes automatizados em PHPUnit.

Por que isto importa

No e-commerce, um bug não é cosmético: vende sem estoque, cobra valor errado, perde pedido pago ou dá desconto que não existe. Cada um desses defeitos custa dinheiro real e confiança do cliente, e os piores aparecem justamente sob carga, na sexta-feira de uma promoção, quando ninguém quer estar depurando produção. Por isso o fluxo de compra é a parte do sistema que mais merece testes automatizados, porque é nele que dinheiro troca de mãos. Esta atividade entrega uma suíte que protege o checkout, a criação do pedido, a confirmação de pagamento, o controle de estoque e a aplicação de cupom. O projeto usa PHPUnit em estilo de classe, com classes que estendem TestCase e métodos test_*(), sem Pest. O banco de teste é isolado do de produção, e a prática é TDD: escrever o teste que descreve o comportamento esperado antes de implementar, deixá-lo vermelho e só então fazer passar. O retorno é direto: a suíte vira a rede de segurança que permite refatorar o checkout sem medo e fazer deploy de sexta com tranquilidade.

Banco isolado, factories e a base de tudo

Testes que tocam o banco de produção são um acidente esperando para acontecer. Configure um banco de teste dedicado e force o phpunit.xml a apontar para ele, nunca confiando no .env padrão. Use RefreshDatabase para um estado limpo a cada teste.

As factories são o que torna o teste legível, porque montam o cenário em uma linha:

$produto = Produto::factory()
    ->has(ProdutoVariacao::factory()->state(['estoque' => 5]), 'variacoes')
    ->create();

Sem factory, cada teste vira dez linhas de setup que ninguém entende. Crie factories para Produto, ProdutoVariacao, Cliente, Cupom e Pedido, com estados nomeados para os casos comuns, como variação sem estoque ou cupom expirado.

Checkout fim a fim e pedido em transação

O teste mais valioso percorre o fluxo como o cliente faria: monta o carrinho, aplica o frete, chama o checkout e verifica o pedido criado.

public function test_checkout_cria_pedido_e_baixa_estoque(): void
{
    $variacao = ProdutoVariacao::factory()->create(['estoque' => 3]);
    $cliente = Cliente::factory()->create();

    $pedido = app(CheckoutService::class)->finalizar($cliente, [
        ['variacao_id' => $variacao->id, 'quantidade' => 2],
    ]);

    $this->assertSame(PedidoStatus::Aguardando, $pedido->status);
    $this->assertSame(1, $variacao->fresh()->estoque);
}

A criação do pedido roda dentro de uma transação: validar estoque, aplicar cupom, calcular frete e gravar pedido com itens precisam ser tudo ou nada. Escreva um teste que força falha no meio, como estoque que acaba durante o checkout concorrente, e prove que o pedido não fica gravado pela metade. É esse teste que valida o DB::transaction do CheckoutService.

Webhook de pagamento com payload fake

Você não chama o gateway de verdade no teste. Use Http::fake() para simular a resposta do MercadoPago e dispare no endpoint de webhook o payload que ele enviaria quando um pagamento é aprovado:

public function test_webhook_aprovado_marca_pedido_como_pago(): void
{
    Http::fake();
    $pedido = Pedido::factory()->create(['status' => PedidoStatus::Aguardando]);

    $this->postJson('/webhook/mercadopago', [
        'type' => 'payment',
        'data' => ['id' => $pedido->pagamento->gateway_id, 'status' => 'approved'],
    ])->assertOk();

    $this->assertSame(PedidoStatus::Pago, $pedido->fresh()->status);
}

Teste também o caminho infeliz: pagamento recusado mantém o pedido aguardando e devolve o estoque reservado. E teste idempotência, porque gateways reenviam o mesmo webhook, e o segundo disparo não pode dar baixa de estoque duas vezes.

Estoque, cupom e e-mail

A reserva e a baixa de estoque merecem testes próprios, separados do checkout. Prove que dois checkouts simultâneos da última peça não vendem a mesma unidade duas vezes, que cancelar pedido devolve o estoque e que estoque zero bloqueia a compra com mensagem clara.

O cupom tem muitos casos de borda que só um teste cobre com segurança: percentual versus valor fixo, valor mínimo de pedido, data de validade, limite de usos e cupom já consumido pelo cliente. Cada regra é um método test_*().

Por fim, o e-mail de confirmação não pode disparar de verdade no teste. Use Mail::fake() e afirme o envio:

Mail::fake();
// ... finaliza o pedido pago
Mail::assertSent(ConfirmacaoPedido::class, fn ($m) => $m->hasTo($cliente->email));

Critérios de qualidade

  • A suíte roda em banco isolado e não toca o banco de produção em hipótese alguma.
  • Existe um teste de checkout fim a fim que cria pedido e baixa estoque.
  • A criação do pedido é provada atômica: falha no meio não deixa pedido pela metade.
  • O webhook de pagamento é testado com Http::fake, cobrindo aprovado, recusado e reenvio idempotente.
  • Estoque concorrente, cupom com casos de borda e e-mail com Mail::fake têm testes dedicados.
  • Os testes seguem PHPUnit em estilo de classe, com métodos test_*(), e foram escritos no fluxo TDD.

Checklist

  • Configurar banco de teste isolado e factories
  • Testar o fluxo de checkout fim a fim
  • Testar criação de pedido em transação
  • Testar webhook de pagamento com payload fake
  • Testar reserva e baixa de estoque
  • Testar cupom e e-mail com Mail::fake

Ferramentas

Entregável

Suíte de testes em PHPUnit cobrindo checkout, criação de pedido, webhook de pagamento, estoque e cupom, rodando em banco isolado.

Publicar a loja em produção (HestiaCP)

8h Avançado

Colocar o e-commerce no ar em servidor HestiaCP com filas, backup, health e go-live seguro.

Por que isto importa

Uma loja só gera receita quando está no ar, estável e segura. O deploy é onde decisões de arquitetura encontram o servidor real, e onde erros pequenos viram queda do site, webhook perdido ou banco sem backup. Esta atividade entrega o e-commerce publicado em um servidor HestiaCP com PHP-FPM, com as permissões certas, as filas processando, o agendador rodando, o backup diário ativo e o monitoramento de saúde de pé. O ambiente de referência segue o padrão /home/ivan/web/<dominio>/, com dono ivan:www-data, e o objetivo é um go-live previsível: nada de mexer no servidor às cegas no dia do lançamento, mas seguir um roteiro testado.

Document root e symlink de storage

No HestiaCP, o domínio mora em /home/ivan/web/<dominio>/, mas o navegador só pode enxergar public_html, jamais o código. O document root aponta para public_html, e o index.php do Laravel vive ali. Os arquivos públicos do storage chegam ao mundo por um symlink:

ln -sf ../storage/app/public /home/ivan/web/<dominio>/public_html/storage

Esse symlink é o que faz as imagens de produto enviadas pelo painel aparecerem na vitrine. Confirme também que APP_URL no .env está correto, senão os links de asset e os e-mails saem com domínio errado.

Permissões e ownership

A causa número um de erro 500 em produção é permissão. O dono dos arquivos é o usuário ivan e o grupo www-data, que é quem o PHP-FPM usa. As duas pastas que o Laravel precisa escrever, storage e bootstrap/cache, recebem 775 para que o grupo escreva:

chown -R ivan:www-data /home/ivan/web/<dominio>
chmod -R 775 /home/ivan/web/<dominio>/storage
chmod -R 775 /home/ivan/web/<dominio>/bootstrap/cache

Se aparecer 500 sem mensagem clara, o primeiro lugar a olhar é storage/logs/laravel.log e, em seguida, o ownership dos arquivos. Cache de view antigo ou arquivo com dono root derruba tudo.

.env de produção e build de assets

O .env de produção nunca é commitado e nunca é o de desenvolvimento. Os pontos críticos: APP_ENV=production, APP_DEBUG=false (debug ligado em produção vaza dados e credenciais), credenciais reais de banco, do gateway de pagamento e do serviço de frete, e QUEUE_CONNECTION=database para que webhook não se perca sob pico.

Os assets são compilados no deploy, não servidos do Vite em modo dev. Um detalhe que já quebrou site: um arquivo hot órfão em public_html faz o @vite apontar para um servidor de desenvolvimento morto. O build limpo resolve:

npm ci && npm run build
rm -f public_html/hot

Filas, agendador, backup e health

Webhook de pagamento, e-mail e conversão de imagem rodam em fila, então é preciso um worker sempre de pé. O ideal é o supervisor reiniciando o worker se ele cair:

[program:loja-worker]
command=php /home/ivan/web/<dominio>/artisan queue:work --tries=3 --timeout=90
user=ivan
autostart=true
autorestart=true

O agendador do Laravel precisa de uma única linha no cron, que dispara todas as tarefas programadas:

* * * * * cd /home/ivan/web/<dominio> && php artisan schedule:run >> /dev/null 2>&1

Backup e saúde fecham a operação. Use spatie/laravel-backup com backup diário do banco e dos arquivos enviados para um destino externo, e spatie/laravel-health para checar banco, fila, espaço em disco e o backup mais recente. Loja sem backup é questão de tempo até a perda irreversível.

Checklist de go-live

O dia do lançamento segue um roteiro, executado nesta ordem para não derrubar o site no ar:

php artisan down --render="errors::503"
git pull && composer install --no-dev --optimize-autoloader
php artisan migrate --force
npm ci && npm run build
php artisan optimize:clear && php artisan optimize
php artisan up

O --force na migração é obrigatório em produção, que não aceita confirmação interativa. Depois de subir, valide na ponta: faça uma compra real de teste, confirme que o webhook chegou e marcou o pedido como pago, que o e-mail saiu e que o estoque baixou. Cheque o HTTPS, o sitemap e o laravel.log limpo de erros antes de declarar o go-live concluído.

Critérios de qualidade

  • O document root aponta para public_html e o symlink de storage serve as imagens corretamente.
  • storage e bootstrap/cache têm 775 e tudo é de ivan:www-data, sem arquivos com dono root.
  • O .env de produção tem APP_DEBUG=false e credenciais reais, e não está versionado.
  • O worker de fila roda pelo supervisor com autorestart e o agendador está no cron.
  • Backup diário externo e health checks estão ativos e foram testados ao menos uma vez.
  • O checklist de go-live foi executado e validado com uma compra real de teste de ponta a ponta.

Checklist

  • Configurar document root e symlink de storage
  • Ajustar permissões e ownership ivan:www-data
  • Preparar o .env de produção
  • Subir filas via supervisor e o agendador via cron
  • Instalar backup diário e health checks
  • Rodar o checklist de go-live

Entregável

Loja publicada em produção no HestiaCP, com filas, backup diário, health checks e checklist de go-live cumprido.

Entrega final e encerramento

4h Básico

Hand-off completo, transferência de conhecimento e fechamento.

Para que serve

Encerrar formalmente o projeto, entregar tudo que foi prometido e garantir que o cliente sabe operar sem você.

Estrutura

  1. Conferência do escopo: tudo que estava no briefing foi entregue?
  2. Documentação de tudo que foi entregue.
  3. Treinamento ou repasse para o time do cliente.
  4. Pesquisa de satisfação.
  5. Próximos passos (manutenção, evoluções).

Critérios de qualidade

  • Cliente assinou aceite formal.
  • Documentação está completa e acessível.
  • NPS coletado.

Checklist

  • Conferir escopo entregue
  • Consolidar documentação
  • Treinar/repassar para o cliente
  • Coletar NPS
  • Obter aceite formal

Entregável

Aceite formal + documentação consolidada + NPS.

Termos do assunto

Ver o assunto completo →

Na plataforma

Este playbook vira um projeto executável.

Kanban com as etapas na ordem, checklists por passo e assistente de IA acompanhando a execução — o texto é aberto; a execução organizada é o produto.

Ivan
Jupiter Apple
Mensagens desta conversa ficam com a equipe.