# Prompt mestre: site integrado a uma loja SoftPay

Você é um desenvolvedor front-end sênior. Construa o site de uma loja que vende pelo SoftPay. O visual é livre. Os dados e as regras de venda vêm da API da loja descrita abaixo. Siga este documento à risca: ele é o contrato entre o site e a loja.

## 1. O que eu quero

- Loja no SoftPay: `{{SLUG_DA_LOJA}}`
- Visual, páginas e público: {{DESCREVA AQUI: estilo, cores extras, páginas, tom de voz, referências de sites que você gosta}}
- Tecnologia: a que você preferir (HTML + JavaScript, React, Next.js, Vue…). Sem preferência, use React + Vite.

## 2. Regras que não mudam

1. Nada da loja fica fixo no código. Nome, logo, cores, produtos, preços, taxas, horários, formas de pagamento e contatos vêm da API. Se o lojista mudar algo no SoftPay, o site muda sozinho.
2. Quem decide o preço é o servidor. Mostre a estimativa no carrinho, mas o valor que o cliente paga é o `total` devolvido ao criar o pedido.
3. A API é pública: não tem chave secreta, token nem login. Não invente autenticação nem cadastro de cliente.
4. Ofereça só o que a loja aceita: as formas de `pagamento.formas`, entrega só se `entrega.fazEntrega`, retirada só se `entrega.fazRetirada`.
5. Todo pedido leva uma `chave` (UUID v4) criada uma única vez por tentativa de compra. Se a rede falhar, reenvie o MESMO pedido com a MESMA chave: a loja devolve o pedido já criado, sem duplicar.
6. Guarde `id` e `chave` do pedido (por exemplo, em sessionStorage). A chave autoriza gerar o Pix e consultar o pagamento. Nunca coloque a chave na URL.
7. Os erros da API já vêm com mensagem em português, pronta para o cliente. Mostre `erro.mensagem`.
8. Celular primeiro, acessível (contraste, foco visível, botões com texto claro) e rápido (imagens com `loading="lazy"`).

## 3. A API

- Base: `https://sdk.softpaybr.com/v1`
- JSON em UTF-8. Nos POST, envie `Content-Type: application/json`.
- Endereço desta loja: `https://sdk.softpaybr.com/v1/lojas/{{SLUG_DA_LOJA}}`

| Método e caminho | Corpo | Resposta |
|---|---|---|
| GET /lojas/{loja} | — | `InfoDaLoja` |
| GET /lojas/{loja}/filtros | — | `Filtros` |
| GET /lojas/{loja}/produtos?busca=&categoria=&secao=&tamanho=&cor=&sem_tamanho=true&limite=24&pagina=1 | — | `{ produtos: Produto[], pagina, limite }` |
| GET /lojas/{loja}/produtos/{id} | — | `Produto`, com a galeria completa em `fotos` |
| GET /lojas/{loja}/sabores | — | `{ sabores: Sabor[] }` |
| POST /lojas/{loja}/cupons/validar | `{ codigo, subtotal }` | `Cupom` |
| POST /lojas/{loja}/pedidos | `DadosDoPedido` | `Pedido` (201 quando é novo, 200 quando a chave já tinha virado pedido) |
| POST /lojas/{loja}/pedidos/{id}/pix | `{ chave }` | `CobrancaPix` |
| POST /lojas/{loja}/pedidos/{id}/pagamento | `{ chave }` | `{ status: "pendente" \| "pago" \| "cancelado" \| null }` |
| POST /lojas/{loja}/visitas | `{ evento, produtoId?, sessao? }` | 204, sem corpo |

Detalhes:

- `limite` vai de 1 a 48 (padrão 24) e `pagina` começa em 1. Quando vier menos itens que o limite, a lista acabou.
- `secao` aceita `promocao`, `destaque`, `novidades` ou `ofertas`. Use só as seções que vierem `true` em `filtros.secoes`.
- Produto que não existe responde 404.
- `sessao` (opcional, em pedidos e visitas): um UUID por navegador, guardado em localStorage. Ajuda a loja a contar visitas e a barrar pedidos repetidos.

## 4. Formatos (TypeScript)

```ts
type FormaDePagamento = 'pix' | 'dinheiro' | 'cartao' | 'outro'; // cartao = maquininha na entrega ou retirada

interface InfoDaLoja {
  slug: string;
  nome: string;
  titulo?: string;
  descricao?: string;
  logo?: string;
  banner?: string;
  bannerCelular?: string;
  cor: string;                       // cor da marca, '#RRGGBB'
  tema: 'claro' | 'escuro';
  pais?: string;
  contato: { whatsapp?: string; instagram?: string; telefone?: string; endereco?: string };
  canal: 'whatsapp' | 'painel';      // como o lojista recebe o pedido
  horarios?: unknown[];
  recusaPedidoFechada: boolean;      // a API decide na hora se a loja está aberta
  entrega: {
    fazEntrega: boolean;
    fazRetirada: boolean;
    taxa: number;                    // taxa única; com bairros, vale a do bairro
    bairros: { nome: string; taxa: number }[];
    pedidoMinimo: number;
    informacoes?: string;
  };
  pagamento: {
    formas: FormaDePagamento[];      // na ordem em que a loja mostra
    pixAutomatico: boolean;          // Pix com QR Code e confirmação automática
    provedor: 'mercadopago' | null;
    chavePix?: string;               // Pix manual, quando não há Pix automático
    pixCombinado: boolean;           // o lojista combina o Pix com o cliente
  };
  meioAMeio: 'media' | 'maior';      // como a loja cobra pizza de vários sabores
  exigeConta: boolean;               // true = a API recusa pedidos deste site
  mostraEstoque: boolean;
  promocao?: { titulo: string; subtitulo?: string; ativa: boolean; regras: { tamanho: string; categoria: string; preco: number }[] };
  rastreamento: { metaPixel?: string; googleAnalytics?: string; googleTagManager?: string; tiktokPixel?: string; utmify?: string };
}

interface Variacao { id: string; nome: string; tamanho?: string; sabor?: string; cor?: string; corHex?: string; preco?: number; estoque: number | null; medida?: number }
interface Adicional { id: string; nome: string; preco: number }

interface Produto {
  id: string;
  nome: string;
  descricao?: string;
  categoria?: string;
  imagem?: string;
  fotos: string[];
  preco: number;
  precoAntigo?: number;              // preço "de", riscado
  atacado?: { preco: number; aPartirDe: number };
  estoque: number | null;            // null = a loja não mostra estoque
  destaque: boolean;
  oferta: boolean;
  tipo: 'produto' | 'servico';
  unidade?: string;
  duracaoMinutos?: number;
  variacoes: Variacao[];             // sem preço próprio = herda o do produto
  adicionais: Adicional[];
  aceitaMeioAMeio: boolean;
  fatoresDeTamanho?: Record<string, number>;
  garantia?: { periodo: number; unidade: 'dias' | 'meses'; termos?: string };
  precoNoTamanho?: number;           // preço no tamanho filtrado em produtos?tamanho=
  criadoEm?: string;
}

interface Filtros {
  total: number;
  categorias: string[];
  tamanhos: { nome: string; quantidade: number; precoDe?: number; precoAte?: number; imagem?: string; maxSabores?: number }[];
  cores: { nome: string; hex?: string; quantidade: number }[];
  secoes: { promocao: boolean; destaque: boolean; novidades: boolean; ofertas: boolean };
  semTamanho: number;
}

interface Sabor { id: string; nome: string; categoria?: string; imagem?: string; preco: number; fatoresDeTamanho?: Record<string, number>; variacoes: Variacao[] }

interface Cupom { codigo: string; tipo: 'percentual' | 'fixo'; valor: number; descontoMaximo?: number; compraMinima: number; desconto: number }

interface DadosDoPedido {
  chave: string;                     // UUID v4, uma por tentativa de compra
  sessao?: string;
  cliente: { nome: string; telefone: string; cpf?: string };
  entrega: {
    tipo: 'entrega' | 'retirada';
    bairro?: string;                 // obrigatório quando a loja tem bairros
    endereco?: { rua?: string; numero?: string; bairro?: string; complemento?: string; referencia?: string };
  };
  pagamento: FormaDePagamento;
  trocoPara?: number;                // só em dinheiro
  observacao?: string;
  cupom?: string;
  itens: { produtoId: string; variacaoId?: string; quantidade?: number; observacao?: string; adicionais?: string[]; sabores?: string[] }[];
}

interface Pedido {
  id: string;
  chave: string;
  total: number;                     // calculado pelo servidor: é o que o cliente paga
  subtotal?: number;
  taxaDeEntrega?: number;
  desconto?: number;
  duplicado: boolean;
  itensPulados: { produtoId?: string; nome?: string; quantidade?: number; motivo?: string }[];
}

interface CobrancaPix { jaPago: boolean; copiaECola: string; expiraEm: string; segundosRestantes: number; valor: number }

interface ErroDaApi { erro: { codigo: string; mensagem: string } }
```

## 5. Fluxo de compra

1. **Abrir o site.** Chame `GET /lojas/{loja}`. Aplique `cor`, `tema`, `logo`, `banner` (e `bannerCelular` no celular), `nome`, `titulo` e `descricao`. Se `promocao.ativa`, mostre `promocao.titulo`.
2. **Catálogo.** Chame `GET /lojas/{loja}/filtros` para montar categorias, tamanhos, cores e seções. Liste com `GET /lojas/{loja}/produtos`, paginado. Mostre `precoAntigo` riscado quando existir e `estoque` só quando `mostraEstoque` for `true`.
3. **Produto.** Se houver `variacoes`, o cliente escolhe uma antes de adicionar ao carrinho (tamanho, cor ou sabor). Variação sem `preco` usa o `preco` do produto. `adicionais` são opcionais e somam no preço. Se `aceitaMeioAMeio`, ofereça sabores de `GET /lojas/{loja}/sabores` no mesmo tamanho, respeitando `maxSabores` do tamanho em `filtros.tamanhos` (total de sabores, contando o principal).
4. **Carrinho.** A estimativa de cada item é o preço da variação (ou do produto), ajustado pelo meio a meio, mais os adicionais, vezes a quantidade. No meio a meio, `meioAMeio = "media"` cobra a média dos sabores e `"maior"` cobra o sabor mais caro. Com a promoção ativa, uma regra `{ tamanho, categoria, preco }` vale para o item com aquele tamanho e categoria, e só quando deixa mais barato. Com o SDK da seção 7, `precoDoItem` já faz essa conta. Escreva "valor final confirmado no pedido" perto do total.
5. **Checkout.**
   - Nome (obrigatório) e WhatsApp com DDD (obrigatório, pelo menos 10 dígitos).
   - Entrega ou retirada, conforme a loja.
   - Na entrega: rua e número obrigatórios; complemento e referência opcionais. Se `entrega.bairros` tiver itens, o cliente escolhe um (obrigatório) e a taxa é a do bairro. Sem bairros, a taxa é `entrega.taxa`.
   - Forma de pagamento entre `pagamento.formas`. Em `dinheiro`, pergunte "Troco para quanto?" (opcional).
   - Cupom opcional: valide com `POST /lojas/{loja}/cupons/validar` e mostre o `desconto`.
   - CPF opcional, para a nota fiscal.
   - Mostre `entrega.pedidoMinimo` quando for maior que zero e `entrega.informacoes` quando existir.
   - Se `exigeConta` for `true`, não deixe finalizar: mostre "Esta loja só aceita pedidos de clientes com conta" e o WhatsApp da loja.
6. **Criar o pedido.** `POST /lojas/{loja}/pedidos` com `DadosDoPedido`, incluindo `chave` e `sessao`. Na resposta, mostre o `total` do servidor. Se `itensPulados` vier com itens, avise quais ficaram de fora e o motivo.
7. **Pagamento.**
   - `pix` com `pagamento.pixAutomatico = true`: chame `POST /lojas/{loja}/pedidos/{id}/pix` com `{ chave }`. Mostre o QR Code desenhado no próprio navegador a partir de `copiaECola` (nunca por um serviço de imagem externo), um botão "Copiar código Pix" e um contador a partir de `segundosRestantes`. Consulte `POST /lojas/{loja}/pedidos/{id}/pagamento` a cada 3 segundos até `pago` (mostre a confirmação) ou `cancelado` (ofereça gerar outro Pix). Pare de consultar ao sair da página ou depois de 30 minutos. Se o contador zerar, ofereça gerar outro Pix com a mesma chamada. Se `jaPago` vier `true`, vá direto para a confirmação.
   - `pix` sem Pix automático: mostre `pagamento.chavePix` com botão de copiar e peça o comprovante pelo WhatsApp da loja. Sem chave e com `pixCombinado`, diga que o Pix é combinado com a loja.
   - `dinheiro`, `cartao` ou `outro`: o pedido já está registrado.
   - Se `canal = "whatsapp"`, termine com o botão "Enviar pedido no WhatsApp": `https://wa.me/{número}?text={resumo do pedido}`. O número são só os dígitos de `contato.whatsapp`; se tiver até 11 dígitos e a loja for do Brasil (`pais` vazio ou `BR`), acrescente `55` na frente. Se `canal = "painel"`, mostre "Pedido recebido pela loja".
8. **Visitas** (opcional, recomendado). `POST /lojas/{loja}/visitas` com `evento = "visita"` ao abrir o site, `"produto"` ao abrir um produto (com `produtoId`), `"carrinho"` ao adicionar e `"checkout"` ao começar a finalizar. Ignore qualquer erro dessa chamada.

## 6. Erros

Todo erro vem assim: `{ "erro": { "codigo": "…", "mensagem": "…" } }`.

| HTTP | codigo | O que o site faz |
|---|---|---|
| 400 | corpo_invalido, chave_invalida, slug_invalido, evento_invalido | Erro de programação: corrija a chamada. |
| 403 | site_nao_autorizado | O endereço do site não está autorizado. O lojista autoriza em Catálogo → API. |
| 403 | api_indisponivel | A loja não está vendendo pelo site agora. Mostre a mensagem e o WhatsApp da loja. |
| 403 | exige_conta | A loja exige conta. Não finalize o pedido. |
| 404 | loja_nao_encontrada, pedido_nao_encontrado, rota_desconhecida | Endereço errado ou item que não existe mais. |
| 409 | sem_pix_automatico, pedido_nao_pendente, pedido_nao_e_pix, valor_invalido | Siga pelo Pix manual ou recomece o pagamento. |
| 422 | pagamento_invalido, forma_indisponivel, entrega_invalida, entrega_indisponivel, retirada_indisponivel, nome_obrigatorio, telefone_invalido, endereco_obrigatorio, bairro_obrigatorio, carrinho_vazio, item_invalido | Destaque o campo e mostre a mensagem. |
| 422 | pedido_recusado, cupom_invalido | Mostre a mensagem (bairro fora da área, pedido mínimo, produto indisponível, cupom…). |
| 429 | muitos_pedidos, muitos_pix, loja_ocupada_ou_fechada | Mostre a mensagem e peça para tentar de novo em instantes. |
| 500, 502, 503 | indisponivel, pix_indisponivel, api_em_manutencao | Tente de novo em alguns segundos. Num pedido, com a MESMA chave. |

## 7. Atalho opcional: SDK JavaScript

No navegador, dá para usar o SDK em vez de chamar a API na mão. Ele gera a chave, reenvia com segurança, desenha o QR Code e acompanha o Pix.

```html
<script src="https://sdk.softpaybr.com/v1.js"></script>
<script type="module">
  const loja = SoftPayLoja.conectar('{{SLUG_DA_LOJA}}');

  const info = await loja.info();
  const produtos = await loja.produtos({ categoria: 'Pizzas', limite: 24 });
  const preco = SoftPayLoja.precoDoItem(produto, { variacaoId, adicionais, sabores, quantidade }, info); // { unitario, total, emPromocao }

  const pedido = await loja.criarPedido(dados);   // devolve id, chave e o total do servidor
  const pix = await loja.gerarPix(pedido);
  await SoftPayLoja.desenharQrCode('#qr', pix.copiaECola);
  const parar = loja.acompanharPagamento(pedido, { aoPagar: () => mostrarConfirmacao() });

  const link = await loja.linkDoWhatsApp('Olá! Acabei de fazer um pedido.');
</script>
```

Também existem `loja.filtros()`, `loja.produto(id)`, `loja.sabores()`, `loja.validarCupom(codigo, subtotal)`, `loja.statusDoPagamento(pedido)` e `loja.registrarVisita(evento, produtoId)`. Os erros chegam como `SoftPayLoja.ErroDaLoja`, com `codigo`, `status` e `message`. Em React ou Next.js, carregue o script uma vez e use `window.SoftPayLoja`.

## 8. Pixels de anúncio (opcional)

`rastreamento` traz os ids públicos dos pixels do lojista: `metaPixel`, `googleAnalytics`, `googleTagManager`, `tiktokPixel` e `utmify`. Instale só os que vierem preenchidos e só depois do consentimento de cookies do visitante.

## 9. Testar e publicar

- No computador, rode um servidor local: `http://localhost:porta` ou `http://127.0.0.1:porta`. Abrir o arquivo direto do disco (file://) não funciona.
- Em produção, o lojista precisa autorizar o endereço exato do site em Catálogo → API, por exemplo `https://minhaloja.com.br`. Sem isso, a API responde `site_nao_autorizado`.
- Pedido de teste cai de verdade no painel da loja. Use o nome "TESTE" no cliente e avise o lojista.
- Guia completo: https://sdk.softpaybr.com

## 10. O que entregar

- O site funcionando com os dados reais da loja `{{SLUG_DA_LOJA}}`.
- Catálogo, produto, carrinho, checkout, Pix e confirmação seguindo a seção 5.
- Um README curto: como rodar no computador, onde trocar o endereço da loja e como publicar.
