' +
diff --git a/backend/public/js/render/modal.js b/backend/public/js/render/modal.js
index d2fecfd..0396ef9 100644
--- a/backend/public/js/render/modal.js
+++ b/backend/public/js/render/modal.js
@@ -1,19 +1,16 @@
// ===== COMÉRCIO BES — RENDER: MODAL =====
-// Perfil da loja, catálogo, avaliação e carrinho modal.
+// Perfil da loja, catalogo informativo e avaliacao.
-import { escapeHTML, gerarStars, formatCurrency } from '../modules/utils.js';
+import { escapeHTML, gerarStars, formatCurrency, safeImageUrl } from '../modules/utils.js';
import { registrarEstatistica, enviarAvaliacaoApi } from '../modules/api.js';
import { Auth } from '../modules/auth.js';
-import { Cart } from '../modules/cart.js';
import { Favorites } from '../modules/favorites.js';
import { mostrarToast } from '../modules/ui.js';
-import { abrirCheckout } from '../modules/checkout.js';
import { state } from '../modules/state.js';
import { GOOGLE_MAPS_KEY } from '../config.js';
// ===== STATE LOCAL =====
let avaliacao = 0;
-let carrinhoModal = {};
let lightboxFotos = [];
let lightboxIdx = 0;
@@ -24,14 +21,13 @@ export function abrirModal(id) {
if (!c) return;
state.comercioAtual = c;
avaliacao = 0;
- carrinhoModal = {};
resetStars();
registrarEstatistica(c.id, 'visita');
// Hero foto ou emoji no cabeçalho
const modalImgEl = document.getElementById('modal-img');
- const heroFoto = (c.fotos || []).find(f => f.startsWith('http') || f.startsWith('/'));
+ const heroFoto = (c.fotos || []).map(safeImageUrl).find(Boolean);
if (heroFoto) {
modalImgEl.style.backgroundImage = 'url(' + heroFoto + ')';
modalImgEl.classList.add('has-photo');
@@ -55,7 +51,7 @@ export function abrirModal(id) {
document.getElementById('modal-info').innerHTML =
'
📍 ' + escapeHTML(c.endereco) + '
' +
'
🕐 ' + escapeHTML(c.horario) + '
' +
- '
📞 ' + escapeHTML(c.tel.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3')) + '
' +
+ (c.tel ? '
📞 ' + escapeHTML(String(c.tel).replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3')) + '
' : '') +
'
' + (c.aberto ? '✅' : '❌') + ' ' + (c.aberto ? 'Aberto agora' : 'Fechado no momento') + '
' +
(c.promo ? '
🔥 Promoção: ' + escapeHTML(c.promo.desc) + ' — ' + escapeHTML(c.promo.preco) + '
' : '');
@@ -65,16 +61,20 @@ export function abrirModal(id) {
'
(' + parseInt(c.visitas) + ' avaliações)';
// Fotos — detecta URL real vs emoji placeholder
- lightboxFotos = (c.fotos || []).filter(f => f.startsWith('http') || f.startsWith('/'));
- document.getElementById('modal-fotos').innerHTML = (c.fotos || []).map((f, idx) => {
- const isUrl = f.startsWith('http') || f.startsWith('/');
- return '
' +
- (isUrl
- ? '
 + ')
'
+ lightboxFotos = (c.fotos || []).map(safeImageUrl).filter(Boolean);
+ const fotosEl = document.getElementById('modal-fotos');
+ fotosEl.innerHTML = (c.fotos || []).map((f, idx) => {
+ const safeUrl = safeImageUrl(f);
+ return '
';
+ '';
}).join('');
+ fotosEl.querySelectorAll('[data-lightbox-index]').forEach(button => {
+ button.addEventListener('click', () => abrirLightbox(Number(button.dataset.lightboxIndex)));
+ });
// Mapa embed — Google Maps (se tiver key) ou OpenStreetMap (gratuito)
const mapaEl = document.getElementById('modal-mapa');
@@ -95,30 +95,19 @@ export function abrirModal(id) {
catalogoContainer.style.display = 'block';
catalogoContainer.innerHTML =
'
' +
- '
Cardápio / Produtos
' +
+ '
Produtos em destaque
' +
'
' +
- c.catalogo.map((prod, idx) =>
+ c.catalogo.map(prod =>
'
' +
'
' +
'
' + escapeHTML(prod.nome_produto) + '
' +
'
' + escapeHTML(prod.descricao) + '
' +
'
R$ ' + formatCurrency(prod.preco) + '
' +
'
' +
- '
' +
- '' +
- '0' +
- '' +
- '
' +
'
'
).join('') +
'
' +
- '
' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '
';
+ '
Consulte disponibilidade e condicoes diretamente com o comercio.
';
} else {
catalogoContainer.style.display = 'none';
catalogoContainer.innerHTML = '';
@@ -139,98 +128,6 @@ export function fecharModal(e) {
document.body.style.overflow = '';
}
-// ===== CARRINHO MODAL =====
-
-export function alterarQtdModal(idx, delta) {
- const atual = carrinhoModal[idx] || 0;
- const novo = Math.max(0, atual + delta);
- carrinhoModal[idx] = novo;
- const el = document.getElementById('qtd-' + idx);
- if (el) el.textContent = novo;
- atualizarResumoModal();
-}
-
-function atualizarResumoModal() {
- if (!state.comercioAtual || !state.comercioAtual.catalogo) return;
- const itens = Object.entries(carrinhoModal)
- .filter(([, qtd]) => qtd > 0)
- .map(([idx, qtd]) => ({ produto: state.comercioAtual.catalogo[parseInt(idx)], qtd }));
-
- const resumoEl = document.getElementById('carrinho-resumo');
- const totalEl = document.getElementById('carrinho-total');
- if (itens.length === 0) { resumoEl.style.display = 'none'; return; }
-
- const total = itens.reduce((sum, i) => sum + (i.produto.preco * i.qtd), 0);
- resumoEl.style.display = 'block';
- totalEl.innerHTML = '
' + itens.length + ' ' + (itens.length === 1 ? 'item' : 'itens') + ' · Total:
R$ ' + formatCurrency(total) + '';
-}
-
-function mostrarAuthGateModal() {
- const container = document.getElementById('modal-catalogo');
- if (!container) return;
- const existing = document.getElementById('auth-gate-modal');
- if (existing) { existing.classList.add('visible'); existing.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); return; }
-
- const gate = document.createElement('div');
- gate.id = 'auth-gate-modal';
- gate.className = 'auth-gate-modal';
- gate.innerHTML =
- '
Faça login para fazer pedidos
' +
- '
Crie sua conta gratuita em menos de 1 minuto
' +
- '
Criar conta gratuita' +
- '
Já tenho conta · Entrar';
- container.appendChild(gate);
- requestAnimationFrame(() => gate.classList.add('visible'));
- gate.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
-}
-
-export function adicionarAoCarrinho() {
- if (!state.comercioAtual || !state.comercioAtual.catalogo) return;
- if (!Auth.isLoggedIn()) { mostrarAuthGateModal(); return; }
-
- const itens = Object.entries(carrinhoModal)
- .filter(([, qtd]) => qtd > 0)
- .map(([idx, qtd]) => ({ produto: state.comercioAtual.catalogo[parseInt(idx)], qtd }));
-
- if (itens.length === 0) { mostrarToast('🛒 Selecione pelo menos um item!'); return; }
-
- itens.forEach(i => {
- Cart.add(state.comercioAtual.id, state.comercioAtual.nome, state.comercioAtual.whatsapp, i.produto, i.qtd);
- });
-
- mostrarToast('✅ ' + itens.length + ' item(ns) adicionado(s) ao carrinho!');
- carrinhoModal = {};
- state.comercioAtual.catalogo.forEach((_, idx) => {
- const el = document.getElementById('qtd-' + idx);
- if (el) el.textContent = '0';
- });
- atualizarResumoModal();
-}
-
-export function enviarPedidoWhatsApp() {
- if (!state.comercioAtual || !state.comercioAtual.catalogo) return;
- if (!Auth.isLoggedIn()) { mostrarAuthGateModal(); return; }
-
- const produtos = Object.entries(carrinhoModal)
- .filter(([, qtd]) => qtd > 0)
- .map(([idx, qtd]) => ({ produto: state.comercioAtual.catalogo[parseInt(idx)], qtd }));
-
- if (produtos.length === 0) { mostrarToast('🛒 Selecione pelo menos um item!'); return; }
-
- produtos.forEach(i => {
- Cart.add(state.comercioAtual.id, state.comercioAtual.nome, state.comercioAtual.whatsapp, i.produto, i.qtd);
- });
- carrinhoModal = {};
- state.comercioAtual.catalogo.forEach((_, idx) => {
- const el = document.getElementById('qtd-' + idx);
- if (el) el.textContent = '0';
- });
- atualizarResumoModal();
- fecharModal();
- abrirCheckout();
- mostrarToast('📋 Preencha seus dados e clique em "Enviar via WhatsApp".');
-}
-
// ===== LIGHTBOX =====
export function abrirLightbox(idx) {
@@ -277,9 +174,7 @@ export async function enviarAvaliacao() {
if (state.apiDisponivel && state.comercioAtual.slug) {
try {
- const data = await enviarAvaliacaoApi(
- state.comercioAtual.slug, avaliacao, comentario, Auth.getToken()
- );
+ const data = await enviarAvaliacaoApi(state.comercioAtual.slug, avaliacao, comentario);
state.comercioAtual.rating = data.mediaAtual;
state.comercioAtual.totalAvaliacoes = data.totalAvaliacoes;
document.getElementById('modal-stars-big').innerHTML =
diff --git a/backend/public/js/render/orders-ui.js b/backend/public/js/render/orders-ui.js
deleted file mode 100644
index 62da574..0000000
--- a/backend/public/js/render/orders-ui.js
+++ /dev/null
@@ -1,45 +0,0 @@
-// ===== COMÉRCIO BES — RENDER: ORDERS UI =====
-
-import { Auth } from '../modules/auth.js';
-import { Orders } from '../modules/orders.js';
-import { escapeHTML, formatCurrency } from '../modules/utils.js';
-import { fecharContaMenu, abrirAuth } from '../modules/auth-ui.js';
-
-export function abrirPedidos() {
- fecharContaMenu();
- if (!Auth.isLoggedIn()) { abrirAuth(); return; }
-
- const orders = Orders.get();
- const container = document.getElementById('orders-list');
-
- if (orders.length === 0) {
- container.innerHTML = '
';
- } else {
- container.innerHTML = orders.map(o => {
- const data = new Date(o.criadoEm);
- const dataStr = data.toLocaleDateString('pt-BR') + ' ' + data.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
- const statusCor = o.status === 'pendente' ? '#EA580C' : o.status === 'confirmado' ? '#10B981' : '#aaa';
- return '
' +
- '' +
- '
' + escapeHTML(dataStr) + '
' +
- '
' +
- o.items.map(i => '
' + parseInt(i.qtd) + 'x ' + escapeHTML(i.produto.nome_produto) + ' (' + escapeHTML(i.lojaNome) + ')
').join('') +
- '
' +
- '
Total: R$ ' + formatCurrency(o.total) + '
' +
- '
Pagamento: ' + escapeHTML(o.pagamento) + '
' +
- '
';
- }).join('');
- }
-
- document.getElementById('orders-overlay').classList.add('open');
- document.body.style.overflow = 'hidden';
-}
-
-export function fecharPedidos(e) {
- if (e && e.target !== document.getElementById('orders-overlay')) return;
- document.getElementById('orders-overlay').classList.remove('open');
- document.body.style.overflow = '';
-}
diff --git a/backend/public/sw.js b/backend/public/sw.js
index 8e5cf80..0103d50 100644
--- a/backend/public/sw.js
+++ b/backend/public/sw.js
@@ -1,5 +1,5 @@
// ===== SERVICE WORKER — Comércio BES =====
-const CACHE_NAME = 'comercio-bes-v8';
+const CACHE_NAME = 'comercio-bes-v9';
const OFFLINE_ASSETS = [
'./',
'./index.html',
@@ -10,21 +10,17 @@ const OFFLINE_ASSETS = [
'./js/modules/utils.js',
'./js/modules/api.js',
'./js/modules/auth.js',
- './js/modules/cart.js',
'./js/modules/favorites.js',
- './js/modules/orders.js',
'./js/modules/merchants.js',
'./js/modules/theme.js',
'./js/modules/ui.js',
'./js/modules/map.js',
'./js/modules/search.js',
'./js/modules/auth-ui.js',
- './js/modules/checkout.js',
'./js/modules/merchant-ui.js',
'./js/render/promotions.js',
'./js/render/cards.js',
'./js/render/favorites.js',
- './js/render/orders-ui.js',
'./js/render/modal.js',
'./data/data.json',
'./manifest.json',
@@ -49,12 +45,21 @@ self.addEventListener('activate', event => {
self.clients.claim();
});
-// Fetch — network-first for data, cache-first for assets
+// Fetch — somente assets publicos entram no Cache Storage.
+// APIs e areas autenticadas devem sempre ir para a rede para evitar vazamento
+// de dados entre sessoes no mesmo navegador.
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
- // Skip non-GET and cross-origin (except fonts/tiles)
+ // Skip non-GET and qualquer endpoint privado/dinamico.
if (event.request.method !== 'GET') return;
+ if (url.origin === self.location.origin && (
+ url.pathname.startsWith('/api/') ||
+ url.pathname === '/api' ||
+ url.pathname.startsWith('/minha-conta') ||
+ url.pathname.startsWith('/html/login') ||
+ url.pathname.startsWith('/html/cadastro')
+ )) return;
// Network-first for data.json (always try fresh data)
if (url.pathname.endsWith('/data.json')) {
@@ -70,13 +75,14 @@ self.addEventListener('fetch', event => {
return;
}
- // Cache-first for everything else
+ // Cache-first apenas para assets declarados no shell ou recursos estaticos.
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
- // Only cache same-origin and fonts
- if (response.ok && (url.origin === self.location.origin || url.hostname.includes('googleapis') || url.hostname.includes('gstatic'))) {
+ const isStaticAsset = url.origin === self.location.origin && /\.(?:css|js|svg|png|jpe?g|webp|avif|woff2?)$/i.test(url.pathname);
+ const isApprovedFont = url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com';
+ if (response.ok && (isStaticAsset || isApprovedFont)) {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
}
diff --git a/backend/src/__tests__/auth.test.js b/backend/src/__tests__/auth.test.js
index 9bf166c..68bd0bd 100644
--- a/backend/src/__tests__/auth.test.js
+++ b/backend/src/__tests__/auth.test.js
@@ -27,7 +27,8 @@ describe('POST /api/auth/registro', () => {
expect(res.status).toBe(201);
expect(res.body.user.email).toBe('maria@example.com');
expect(res.body.user.tipo).toBe('cliente');
- expect(res.body.token).toBeDefined();
+ expect(res.body.token).toBeUndefined();
+ expect(res.headers['set-cookie'].join(';')).toMatch(/access_token=.*HttpOnly/i);
expect(res.body.user.senha).toBeUndefined(); // senha nunca retorna
});
@@ -137,7 +138,8 @@ describe('POST /api/auth/login', () => {
.send({ email: 'usuario@example.com', senha: 'correta123' });
expect(res.status).toBe(200);
- expect(res.body.token).toBeDefined();
+ expect(res.body.token).toBeUndefined();
+ expect(res.headers['set-cookie'].join(';')).toMatch(/access_token=.*HttpOnly/i);
expect(res.body.user.email).toBe('usuario@example.com');
});
@@ -171,19 +173,17 @@ describe('POST /api/auth/login', () => {
// GET /api/auth/perfil (rota protegida)
// ============================================================
describe('GET /api/auth/perfil', () => {
- let token;
+ let agent;
beforeEach(async () => {
- const res = await request(app)
+ agent = request.agent(app);
+ await agent
.post('/api/auth/registro')
.send({ nome: 'Perfil Teste', email: 'perfil@example.com', senha: 'senha123' });
- token = res.body.token;
});
- it('retorna perfil com token valido', async () => {
- const res = await request(app)
- .get('/api/auth/perfil')
- .set('Authorization', `Bearer ${token}`);
+ it('retorna perfil com cookie httpOnly valido', async () => {
+ const res = await agent.get('/api/auth/perfil');
expect(res.status).toBe(200);
expect(res.body.email).toBe('perfil@example.com');
@@ -202,3 +202,29 @@ describe('GET /api/auth/perfil', () => {
expect(res.status).toBe(401);
});
});
+
+// ============================================================
+// POST /api/auth/logout
+// ============================================================
+describe('POST /api/auth/logout', () => {
+ it('exige double-submit CSRF e encerra a sessao', async () => {
+ const agent = request.agent(app);
+ await agent
+ .post('/api/auth/registro')
+ .send({ nome: 'Logout Teste', email: 'logout@example.com', senha: 'senha123' });
+
+ const semCsrf = await agent.post('/api/auth/logout');
+ expect(semCsrf.status).toBe(403);
+
+ const csrf = await agent.get('/api/auth/csrf');
+ const logout = await agent
+ .post('/api/auth/logout')
+ .set('X-CSRF-Token', csrf.body.csrfToken);
+
+ expect(logout.status).toBe(200);
+ expect(logout.headers['set-cookie'].join(';')).toMatch(/access_token=;/i);
+
+ const perfil = await agent.get('/api/auth/perfil');
+ expect(perfil.status).toBe(401);
+ });
+});
diff --git a/backend/src/__tests__/helpers.js b/backend/src/__tests__/helpers.js
index cd8fb08..74fb892 100644
--- a/backend/src/__tests__/helpers.js
+++ b/backend/src/__tests__/helpers.js
@@ -27,7 +27,17 @@ async function criarCategoriaTeste(dados = {}) {
});
}
-// Cria um usuario de teste e retorna { user, token }
+function gerarTokenTeste(user) {
+ const jwt = require('jsonwebtoken');
+ return jwt.sign(
+ { id: user.id, email: user.email, tipo: user.tipo },
+ process.env.JWT_SECRET,
+ { expiresIn: '1h', algorithm: 'HS256' }
+ );
+}
+
+// Cria um usuario de teste e retorna { user, token }.
+// A API publica nao expoe mais o JWT; o helper o emite somente no processo de teste.
async function criarUsuarioTeste(app, dados = {}) {
const supertest = require('supertest');
const payload = {
@@ -46,7 +56,11 @@ async function criarUsuarioTeste(app, dados = {}) {
.post('/api/auth/registro')
.send(payload);
- return { user: res.body.user, token: res.body.token };
+ if (res.status !== 201) {
+ throw new Error(`Falha ao criar usuario de teste: ${res.status} ${JSON.stringify(res.body)}`);
+ }
+
+ return { user: res.body.user, token: gerarTokenTeste(res.body.user) };
}
// Cria um admin diretamente no banco (registro via API nao permite tipo admin)
diff --git a/backend/src/__tests__/productScope.test.js b/backend/src/__tests__/productScope.test.js
new file mode 100644
index 0000000..5063aca
--- /dev/null
+++ b/backend/src/__tests__/productScope.test.js
@@ -0,0 +1,68 @@
+const fs = require('fs');
+const path = require('path');
+const request = require('supertest');
+const app = require('../server');
+
+describe('Escopo publico da vitrine', () => {
+ it('serve a pagina inicial mesmo dentro de um worktree oculto', async () => {
+ const page = await request(app).get('/');
+
+ expect(page.status).toBe(200);
+ expect(page.headers['content-type']).toMatch(/text\/html/);
+ expect(page.text).toContain('Comércio BES');
+ });
+
+ it('nao anuncia nem monta endpoints transacionais', async () => {
+ const api = await request(app).get('/api');
+ expect(api.status).toBe(200);
+ expect(api.body.marketplace).toBe('fora deste servico');
+ expect(api.body.endpoints.pedidos).toBeUndefined();
+ expect(api.body.endpoints.pagamentos).toBeUndefined();
+
+ expect((await request(app).get('/api/pedidos')).status).toBe(404);
+ expect((await request(app).post('/api/pagamentos')).status).toBe(404);
+ expect((await request(app).get('/api/auth/enderecos')).status).toBe(404);
+ });
+
+ it('nao distribui modulos ou secoes do marketplace legado', () => {
+ const projectRoot = path.join(__dirname, '..', '..', '..');
+ const sections = fs.readFileSync(
+ path.join(projectRoot, 'backend', 'minha-conta', 'js', 'sections.js'),
+ 'utf8'
+ );
+ const config = fs.readFileSync(path.join(projectRoot, 'js', 'config.js'), 'utf8');
+
+ expect(sections).not.toMatch(/pedidos-loja|historico|pagamentos|enderecos|estoque/);
+ expect(config).not.toMatch(/\b(?:CART|ORDERS|API_TOKEN)\b/);
+ expect(fs.existsSync(path.join(projectRoot, 'js', 'modules', 'checkout.js'))).toBe(false);
+ expect(fs.existsSync(path.join(projectRoot, 'backend', 'src', 'routes', 'pedidos.js'))).toBe(false);
+ expect(fs.existsSync(path.join(projectRoot, 'backend', 'src', 'routes', 'pagamentos.js'))).toBe(false);
+ });
+
+ it('mantem APIs e areas autenticadas fora do Cache Storage', () => {
+ const serviceWorker = fs.readFileSync(
+ path.join(__dirname, '..', '..', '..', 'sw.js'),
+ 'utf8'
+ );
+
+ expect(serviceWorker).toContain("url.pathname.startsWith('/api/')");
+ expect(serviceWorker).toContain("url.pathname.startsWith('/minha-conta')");
+ expect(serviceWorker).not.toMatch(/cache\.put\([^\n]*(?:\/api|request\.url)/i);
+ });
+
+ it('aplica allowlist antes de renderizar imagens de comercios', () => {
+ const utils = fs.readFileSync(
+ path.join(__dirname, '..', '..', '..', 'js', 'modules', 'utils.js'),
+ 'utf8'
+ );
+ const cards = fs.readFileSync(
+ path.join(__dirname, '..', '..', '..', 'js', 'render', 'cards.js'),
+ 'utf8'
+ );
+
+ expect(utils).toContain("url.hostname === 'res.cloudinary.com'");
+ expect(utils).toContain("url.pathname.startsWith('/uploads/')");
+ expect(cards).toContain('.map(safeImageUrl).find(Boolean)');
+ expect(cards).not.toContain("f.startsWith('http')");
+ });
+});
diff --git a/backend/src/__tests__/rbac.test.js b/backend/src/__tests__/rbac.test.js
index e7b4587..bdd2a23 100644
--- a/backend/src/__tests__/rbac.test.js
+++ b/backend/src/__tests__/rbac.test.js
@@ -17,6 +17,12 @@ describe('RBAC Capabilities', () => {
expect(ROLE_CAPABILITIES.cliente).toBeDefined();
expect(ROLE_CAPABILITIES.comerciante).toBeDefined();
});
+
+ test('nao expoe capabilities transacionais no nucleo da vitrine', () => {
+ Object.values(ROLE_CAPABILITIES).flat().forEach((cap) => {
+ expect(cap).not.toMatch(/^(orders|payments|addresses)\./);
+ });
+ });
});
describe('Middleware requireCapability', () => {
diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js
index 3a7a4fa..040c46f 100644
--- a/backend/src/controllers/authController.js
+++ b/backend/src/controllers/authController.js
@@ -125,21 +125,12 @@ async function registro(req, res, next) {
data.telefoneComercial = telefoneComercial ? sanitize(telefoneComercial) : null;
}
- if (tipoPermitido === 'cliente') {
- const { cpf } = req.body;
- if (cpf) {
- const cpfLimpo = limparDocumento(cpf);
- if (!CPF_REGEX.test(cpfLimpo)) return res.status(400).json({ error: 'CPF invalido (11 digitos)' });
- data.cpf = cpfLimpo;
- }
- }
-
const user = await prisma.user.create({
data,
select: { id: true, nome: true, email: true, tipo: true, telefone: true, nomeFantasia: true, createdAt: true },
});
- // Emite token legado (Bearer) para compat + cookies novos
+ // A sessao e entregue exclusivamente por cookies httpOnly.
const accessToken = gerarAccessToken(user);
const refreshToken = generateRefreshToken();
const csrfToken = generateCsrfToken();
@@ -151,7 +142,7 @@ async function registro(req, res, next) {
setAuthCookies(res, accessToken, refreshToken, csrfToken);
- res.status(201).json({ message: 'Conta criada com sucesso', user, token: accessToken });
+ res.status(201).json({ message: 'Conta criada com sucesso', user });
} catch (err) {
next(err);
}
@@ -167,11 +158,10 @@ async function login(req, res, next) {
const fallbackUser = findFallbackUserByCredentials(email, senha);
if (fallbackUser) {
- const accessToken = issueFallbackSession(res, fallbackUser);
+ issueFallbackSession(res, fallbackUser);
return res.json({
message: 'Login demo realizado com sucesso',
user: publicUser(fallbackUser),
- token: accessToken,
fallbackAuth: true
});
}
@@ -199,7 +189,7 @@ async function login(req, res, next) {
};
if (user.tipo === 'comerciante') userData.nomeFantasia = user.nomeFantasia;
- res.json({ message: 'Login realizado com sucesso', user: userData, token: accessToken });
+ res.json({ message: 'Login realizado com sucesso', user: userData });
} catch (err) {
next(err);
}
@@ -392,8 +382,6 @@ async function perfil(req, res, next) {
return res.json({
...publicUser(fallbackUser),
comercios: fallbackUser.stores || [],
- enderecos: [],
- pedidos: [],
});
}
@@ -401,17 +389,8 @@ async function perfil(req, res, next) {
where: { id: req.userId },
select: {
id: true, nome: true, email: true, tipo: true, telefone: true, avatar: true, createdAt: true,
- nomeFantasia: true, cpfCnpj: true, enderecoComercial: true, telefoneComercial: true, cpf: true,
+ nomeFantasia: true, cpfCnpj: true, enderecoComercial: true, telefoneComercial: true,
comercios: { select: { id: true, nome: true, slug: true, emoji: true, aberto: true } },
- enderecos: {
- select: { id: true, apelido: true, rua: true, numero: true, complemento: true, bairro: true, cidade: true, estado: true, cep: true, principal: true },
- orderBy: { principal: 'desc' },
- },
- pedidos: {
- select: { id: true, codigo: true, status: true, total: true, tipoEntrega: true, createdAt: true, comercio: { select: { nome: true, slug: true, emoji: true } } },
- orderBy: { createdAt: 'desc' },
- take: 10,
- },
},
});
@@ -458,15 +437,6 @@ async function atualizarPerfil(req, res, next) {
if (telefoneComercial !== undefined) data.telefoneComercial = sanitize(telefoneComercial);
}
- if (currentUser.tipo === 'cliente') {
- const { cpf } = req.body;
- if (cpf) {
- const cpfLimpo = limparDocumento(cpf);
- if (!CPF_REGEX.test(cpfLimpo)) return res.status(400).json({ error: 'CPF invalido' });
- data.cpf = cpfLimpo;
- }
- }
-
if (novaSenha) {
if (!senhaAtual) return res.status(400).json({ error: 'Informe a senha atual' });
const senhaValida = await bcrypt.compare(senhaAtual, currentUser.senha);
@@ -478,7 +448,7 @@ async function atualizarPerfil(req, res, next) {
const user = await prisma.user.update({
where: { id: req.userId },
data,
- select: { id: true, nome: true, email: true, tipo: true, telefone: true, avatar: true, nomeFantasia: true, cpfCnpj: true, enderecoComercial: true, telefoneComercial: true, cpf: true },
+ select: { id: true, nome: true, email: true, tipo: true, telefone: true, avatar: true, nomeFantasia: true, cpfCnpj: true, enderecoComercial: true, telefoneComercial: true },
});
res.json({ message: 'Perfil atualizado', user });
@@ -487,73 +457,7 @@ async function atualizarPerfil(req, res, next) {
}
}
-// ---------------------------------------------------------------------------
-// Endereços (mantidos para compat)
-// ---------------------------------------------------------------------------
-async function listarEnderecos(req, res, next) {
- try {
- const enderecos = await prisma.endereco.findMany({ where: { userId: req.userId }, orderBy: { principal: 'desc' } });
- res.json(enderecos);
- } catch (err) { next(err); }
-}
-
-async function criarEndereco(req, res, next) {
- try {
- const { apelido, rua, numero, complemento, bairro, cidade, estado, cep, principal } = req.body;
- if (!rua || !numero || !bairro || !cep) return res.status(400).json({ error: 'Rua, numero, bairro e CEP sao obrigatorios' });
- if (principal) await prisma.endereco.updateMany({ where: { userId: req.userId }, data: { principal: false } });
- const endereco = await prisma.endereco.create({
- data: {
- userId: req.userId, apelido: sanitize(apelido) || 'Casa',
- rua: sanitize(rua), numero: sanitize(numero),
- complemento: complemento ? sanitize(complemento) : null,
- bairro: sanitize(bairro), cidade: cidade ? sanitize(cidade) : 'Boa Esperança do Sul',
- estado: estado ? sanitize(estado) : 'SP', cep: sanitize(cep).replace(/\D/g, ''),
- principal: principal || false,
- }
- });
- res.status(201).json(endereco);
- } catch (err) { next(err); }
-}
-
-async function atualizarEndereco(req, res, next) {
- try {
- const id = parseInt(req.params.id, 10);
- if (isNaN(id)) return res.status(400).json({ error: 'ID invalido' });
- const endereco = await prisma.endereco.findFirst({ where: { id, userId: req.userId } });
- if (!endereco) return res.status(404).json({ error: 'Endereco nao encontrado' });
- const { apelido, rua, numero, complemento, bairro, cidade, estado, cep, principal } = req.body;
- const data = {};
- if (apelido) data.apelido = sanitize(apelido);
- if (rua) data.rua = sanitize(rua);
- if (numero) data.numero = sanitize(numero);
- if (complemento !== undefined) data.complemento = complemento ? sanitize(complemento) : null;
- if (bairro) data.bairro = sanitize(bairro);
- if (cidade) data.cidade = sanitize(cidade);
- if (estado) data.estado = sanitize(estado);
- if (cep) data.cep = sanitize(cep).replace(/\D/g, '');
- if (principal) {
- await prisma.endereco.updateMany({ where: { userId: req.userId }, data: { principal: false } });
- data.principal = true;
- }
- const updated = await prisma.endereco.update({ where: { id }, data });
- res.json(updated);
- } catch (err) { next(err); }
-}
-
-async function excluirEndereco(req, res, next) {
- try {
- const id = parseInt(req.params.id, 10);
- if (isNaN(id)) return res.status(400).json({ error: 'ID invalido' });
- const endereco = await prisma.endereco.findFirst({ where: { id, userId: req.userId } });
- if (!endereco) return res.status(404).json({ error: 'Endereco nao encontrado' });
- await prisma.endereco.delete({ where: { id } });
- res.json({ message: 'Endereco excluido' });
- } catch (err) { next(err); }
-}
-
module.exports = {
registro, login, perfil, atualizarPerfil,
me, refresh, logout, csrf, activeStore,
- listarEnderecos, criarEndereco, atualizarEndereco, excluirEndereco,
};
diff --git a/backend/src/controllers/pagamentosController.js b/backend/src/controllers/pagamentosController.js
deleted file mode 100644
index 167c788..0000000
--- a/backend/src/controllers/pagamentosController.js
+++ /dev/null
@@ -1,322 +0,0 @@
-// ===========================================
-// Controller - Pagamentos (Mercado Pago)
-// ===========================================
-const prisma = require('../lib/prisma');
-
-// Mercado Pago SDK v2
-let mpClient = null;
-let mpPreference = null;
-let mpPayment = null;
-
-// Inicializar Mercado Pago (lazy load)
-function initMP() {
- if (mpClient) return true;
-
- const accessToken = process.env.MERCADO_PAGO_ACCESS_TOKEN;
- if (!accessToken || accessToken.includes('seu-access-token')) {
- console.warn('[PAGAMENTO] Mercado Pago nao configurado. Configure MERCADO_PAGO_ACCESS_TOKEN no .env');
- return false;
- }
-
- try {
- const { MercadoPagoConfig, Preference, Payment } = require('mercadopago');
- mpClient = new MercadoPagoConfig({ accessToken });
- mpPreference = new Preference(mpClient);
- mpPayment = new Payment(mpClient);
- console.log('[PAGAMENTO] Mercado Pago inicializado');
- return true;
- } catch (err) {
- console.error('[PAGAMENTO] Erro ao inicializar Mercado Pago:', err.message);
- return false;
- }
-}
-
-// POST /api/pagamentos/criar
-// Cria uma preferencia de pagamento no Mercado Pago
-async function criarPagamento(req, res, next) {
- try {
- const { pedidoCodigo, metodo } = req.body;
-
- if (!pedidoCodigo) {
- return res.status(400).json({ error: 'pedidoCodigo e obrigatorio' });
- }
-
- // Buscar pedido
- const pedido = await prisma.pedido.findUnique({
- where: { codigo: pedidoCodigo },
- include: {
- itens: true,
- comercio: { select: { nome: true, slug: true } },
- cliente: { select: { id: true, nome: true, email: true } },
- pagamento: true
- }
- });
-
- if (!pedido) return res.status(404).json({ error: 'Pedido nao encontrado' });
-
- // Verificar se e o dono do pedido
- if (pedido.clienteId !== req.userId) {
- return res.status(403).json({ error: 'Sem permissao' });
- }
-
- // Verificar se ja tem pagamento aprovado
- if (pedido.pagamento && pedido.pagamento.status === 'aprovado') {
- return res.status(400).json({ error: 'Este pedido ja foi pago' });
- }
-
- // Se metodo "na_entrega", criar pagamento local sem MP
- if (metodo === 'na_entrega') {
- const pagamento = await prisma.pagamento.upsert({
- where: { pedidoId: pedido.id },
- create: {
- pedidoId: pedido.id,
- metodo: 'na_entrega',
- status: 'pendente',
- valor: pedido.total
- },
- update: {
- metodo: 'na_entrega',
- status: 'pendente',
- valor: pedido.total
- }
- });
-
- // Confirmar pedido automaticamente
- await prisma.pedido.update({
- where: { id: pedido.id },
- data: { status: 'confirmado' }
- });
-
- return res.json({
- message: 'Pedido confirmado - pagamento na entrega',
- pagamento,
- tipo: 'na_entrega'
- });
- }
-
- // Integração com Mercado Pago
- if (!initMP()) {
- return res.status(503).json({
- error: 'Sistema de pagamento nao configurado. Use pagamento na entrega.',
- fallback: 'na_entrega'
- });
- }
-
- const webhookUrl = process.env.WEBHOOK_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
-
- // Criar preferencia de pagamento
- const preferenceData = {
- body: {
- items: pedido.itens.map(item => ({
- id: String(item.id),
- title: `${item.nome} (${pedido.comercio.nome})`,
- quantity: item.quantidade,
- unit_price: item.preco,
- currency_id: 'BRL'
- })),
- // Adicionar taxa de entrega como item se > 0
- ...(pedido.taxaEntrega > 0 ? {
- shipments: {
- cost: pedido.taxaEntrega,
- mode: 'not_specified'
- }
- } : {}),
- payer: {
- name: pedido.cliente.nome,
- email: pedido.cliente.email
- },
- back_urls: {
- success: `${process.env.FRONTEND_URL || 'http://localhost:5500'}?pedido=${pedido.codigo}&status=sucesso`,
- failure: `${process.env.FRONTEND_URL || 'http://localhost:5500'}?pedido=${pedido.codigo}&status=falha`,
- pending: `${process.env.FRONTEND_URL || 'http://localhost:5500'}?pedido=${pedido.codigo}&status=pendente`
- },
- auto_return: 'approved',
- notification_url: `${webhookUrl}/api/pagamentos/webhook`,
- external_reference: pedido.codigo,
- statement_descriptor: 'COMERCIO BES',
- // Excluir metodos se especificado
- ...(metodo === 'pix' ? {
- payment_methods: {
- excluded_payment_types: [
- { id: 'credit_card' },
- { id: 'debit_card' },
- { id: 'ticket' }
- ]
- }
- } : {})
- }
- };
-
- const preference = await mpPreference.create(preferenceData);
-
- // Salvar/atualizar pagamento no banco
- const pagamento = await prisma.pagamento.upsert({
- where: { pedidoId: pedido.id },
- create: {
- pedidoId: pedido.id,
- preferenceId: preference.id,
- metodo: metodo || null,
- status: 'pendente',
- valor: pedido.total
- },
- update: {
- preferenceId: preference.id,
- metodo: metodo || null,
- status: 'pendente',
- valor: pedido.total
- }
- });
-
- res.json({
- message: 'Preferencia de pagamento criada',
- pagamento,
- tipo: 'mercado_pago',
- checkout_url: preference.init_point,
- sandbox_url: preference.sandbox_init_point,
- preference_id: preference.id,
- public_key: process.env.MERCADO_PAGO_PUBLIC_KEY
- });
- } catch (err) {
- next(err);
- }
-}
-
-// POST /api/pagamentos/webhook
-// Webhook do Mercado Pago (recebe notificacoes de pagamento)
-async function webhook(req, res, next) {
- try {
- const { type, data } = req.body;
-
- // Mercado Pago envia "payment" para eventos de pagamento
- if (type === 'payment' && data && data.id) {
- if (!initMP()) {
- console.error('[WEBHOOK] Mercado Pago nao configurado');
- return res.sendStatus(200); // Responder 200 para MP nao reenviar
- }
-
- // Buscar detalhes do pagamento no MP
- const mpPaymentData = await mpPayment.get({ id: data.id });
-
- if (!mpPaymentData) {
- console.error('[WEBHOOK] Pagamento nao encontrado no MP:', data.id);
- return res.sendStatus(200);
- }
-
- const externalRef = mpPaymentData.external_reference; // = codigo do pedido
- const mpStatus = mpPaymentData.status;
-
- // Mapear status do MP para nosso status
- const statusMap = {
- 'approved': 'aprovado',
- 'pending': 'pendente',
- 'in_process': 'em_analise',
- 'rejected': 'recusado',
- 'refunded': 'devolvido',
- 'cancelled': 'recusado',
- 'charged_back': 'devolvido'
- };
-
- const nossoStatus = statusMap[mpStatus] || 'pendente';
-
- // Atualizar pagamento no banco
- const pedido = await prisma.pedido.findUnique({
- where: { codigo: externalRef },
- include: { pagamento: true }
- });
-
- if (pedido && pedido.pagamento) {
- await prisma.pagamento.update({
- where: { id: pedido.pagamento.id },
- data: {
- mercadoPagoId: String(data.id),
- status: nossoStatus,
- metodo: mpPaymentData.payment_type_id || null,
- mercadoPagoStatus: mpStatus,
- detalhes: JSON.stringify({
- payment_method_id: mpPaymentData.payment_method_id,
- status_detail: mpPaymentData.status_detail,
- transaction_amount: mpPaymentData.transaction_amount
- }),
- ...(mpStatus === 'approved' ? { paidAt: new Date() } : {})
- }
- });
-
- // Se aprovado, confirmar pedido automaticamente
- if (mpStatus === 'approved' && pedido.status === 'pendente') {
- await prisma.pedido.update({
- where: { id: pedido.id },
- data: { status: 'confirmado' }
- });
- }
-
- console.log(`[WEBHOOK] Pedido ${externalRef}: ${mpStatus} -> ${nossoStatus}`);
- }
- }
-
- // Sempre responder 200 para o MP
- res.sendStatus(200);
- } catch (err) {
- console.error('[WEBHOOK] Erro:', err.message);
- res.sendStatus(200); // Responder 200 mesmo com erro para MP nao reenviar
- }
-}
-
-// GET /api/pagamentos/:pedidoCodigo
-// Consultar status do pagamento
-async function consultarPagamento(req, res, next) {
- try {
- const { pedidoCodigo } = req.params;
-
- const pedido = await prisma.pedido.findUnique({
- where: { codigo: pedidoCodigo },
- include: {
- pagamento: true,
- comercio: { select: { nome: true } }
- }
- });
-
- if (!pedido) return res.status(404).json({ error: 'Pedido nao encontrado' });
-
- // Verificar acesso
- const isCliente = pedido.clienteId === req.userId;
- const isComercio = await prisma.comercio.findFirst({
- where: { id: pedido.comercioId, ownerId: req.userId }
- });
- const isAdmin = req.userTipo === 'admin';
-
- if (!isCliente && !isComercio && !isAdmin) {
- return res.status(403).json({ error: 'Sem permissao' });
- }
-
- if (!pedido.pagamento) {
- return res.json({ status: 'sem_pagamento', pedido: pedido.codigo });
- }
-
- // Se tem mercadoPagoId e o MP esta configurado, consultar status atualizado
- if (pedido.pagamento.mercadoPagoId && initMP()) {
- try {
- const mpData = await mpPayment.get({ id: parseInt(pedido.pagamento.mercadoPagoId, 10) });
- if (mpData) {
- res.json({
- pagamento: pedido.pagamento,
- mercadoPago: {
- status: mpData.status,
- status_detail: mpData.status_detail,
- payment_method_id: mpData.payment_method_id,
- transaction_amount: mpData.transaction_amount
- }
- });
- return;
- }
- } catch (e) {
- // Se nao conseguir consultar MP, retornar dados locais
- }
- }
-
- res.json({ pagamento: pedido.pagamento });
- } catch (err) {
- next(err);
- }
-}
-
-module.exports = { criarPagamento, webhook, consultarPagamento };
diff --git a/backend/src/controllers/pedidosController.js b/backend/src/controllers/pedidosController.js
deleted file mode 100644
index 9b3f1e6..0000000
--- a/backend/src/controllers/pedidosController.js
+++ /dev/null
@@ -1,364 +0,0 @@
-// ===========================================
-// Controller - Pedidos (Orders)
-// ===========================================
-const prisma = require('../lib/prisma');
-const sanitize = require('../lib/sanitize');
-
-// Gerar codigo do pedido: BES-00001
-async function gerarCodigo() {
- const ultimo = await prisma.pedido.findFirst({
- orderBy: { id: 'desc' },
- select: { id: true }
- });
- const num = (ultimo ? ultimo.id : 0) + 1;
- return `BES-${String(num).padStart(5, '0')}`;
-}
-
-// POST /api/pedidos
-// Cria um pedido a partir do carrinho do cliente
-async function criar(req, res, next) {
- try {
- const { comercioId, enderecoId, tipoEntrega, itens, observacao } = req.body;
-
- // Validacoes
- if (!comercioId || !itens || !Array.isArray(itens) || itens.length === 0) {
- return res.status(400).json({
- error: 'comercioId e itens (array) sao obrigatorios'
- });
- }
-
- const tipo = tipoEntrega || 'entrega';
- if (!['entrega', 'retirada'].includes(tipo)) {
- return res.status(400).json({ error: 'tipoEntrega deve ser "entrega" ou "retirada"' });
- }
-
- // Se entrega, precisa de endereco
- if (tipo === 'entrega' && !enderecoId) {
- return res.status(400).json({
- error: 'enderecoId e obrigatorio para entregas'
- });
- }
-
- // Verificar comercio existe
- const comercio = await prisma.comercio.findUnique({
- where: { id: parseInt(comercioId, 10) },
- select: { id: true, nome: true, aberto: true }
- });
- if (!comercio) return res.status(404).json({ error: 'Comercio nao encontrado' });
- if (!comercio.aberto) return res.status(400).json({ error: 'Este comercio esta fechado no momento' });
-
- // Verificar endereco pertence ao usuario
- if (enderecoId) {
- const endereco = await prisma.endereco.findFirst({
- where: { id: parseInt(enderecoId, 10), userId: req.userId }
- });
- if (!endereco) return res.status(404).json({ error: 'Endereco nao encontrado' });
- }
-
- // Validar itens e calcular subtotal
- const produtoIds = itens.map(i => parseInt(i.produtoId, 10)).filter(id => !isNaN(id));
- const produtos = await prisma.produto.findMany({
- where: {
- id: { in: produtoIds },
- comercioId: parseInt(comercioId, 10),
- disponivel: true
- }
- });
-
- const produtosMap = new Map(produtos.map(p => [p.id, p]));
-
- const itensValidados = [];
- let subtotal = 0;
-
- for (const item of itens) {
- const prodId = parseInt(item.produtoId, 10);
- const produto = produtosMap.get(prodId);
- if (!produto) {
- return res.status(400).json({
- error: `Produto ID ${prodId} nao encontrado ou indisponivel neste comercio`
- });
- }
-
- const qtd = Math.max(1, Math.min(parseInt(item.quantidade, 10) || 1, 99));
- const precoItem = produto.preco * qtd;
- subtotal += precoItem;
-
- itensValidados.push({
- produtoId: produto.id,
- nome: produto.nome,
- preco: produto.preco,
- quantidade: qtd
- });
- }
-
- // Limitar valor maximo por pedido (seguranca)
- if (subtotal > 10000) {
- return res.status(400).json({ error: 'Valor maximo por pedido: R$ 10.000,00' });
- }
-
- const taxaEntrega = tipo === 'entrega' ? 5.0 : 0; // Taxa fixa por enquanto
- const total = subtotal + taxaEntrega;
- const codigo = await gerarCodigo();
-
- const pedido = await prisma.pedido.create({
- data: {
- codigo,
- clienteId: req.userId,
- comercioId: parseInt(comercioId, 10),
- enderecoId: enderecoId ? parseInt(enderecoId, 10) : null,
- status: 'pendente',
- tipoEntrega: tipo,
- subtotal,
- taxaEntrega,
- desconto: 0,
- total,
- observacao: observacao ? sanitize(observacao) : null,
- itens: {
- create: itensValidados
- }
- },
- include: {
- itens: true,
- comercio: { select: { nome: true, slug: true, emoji: true, whatsapp: true } },
- endereco: true
- }
- });
-
- res.status(201).json({
- message: 'Pedido criado com sucesso',
- pedido
- });
- } catch (err) {
- next(err);
- }
-}
-
-// GET /api/pedidos
-// Lista pedidos do usuario (cliente) ou pedidos recebidos (comerciante)
-async function listar(req, res, next) {
- try {
- const page = Math.max(1, parseInt(req.query.page, 10) || 1);
- const limit = Math.min(50, Math.max(1, parseInt(req.query.limit, 10) || 10));
- const status = req.query.status;
-
- const where = {};
-
- // Cliente ve seus pedidos, comerciante ve pedidos das suas lojas
- if (req.userTipo === 'comerciante') {
- // Buscar IDs dos comercios do usuario
- const comercios = await prisma.comercio.findMany({
- where: { ownerId: req.userId },
- select: { id: true }
- });
- const idsDoUsuario = comercios.map(c => c.id);
-
- // Se passou ?comercioId, filtrar apenas por essa loja (validando pertencimento)
- const comercioIdParam = parseInt(req.query.comercioId, 10);
- if (!isNaN(comercioIdParam) && idsDoUsuario.includes(comercioIdParam)) {
- where.comercioId = comercioIdParam;
- } else {
- where.comercioId = { in: idsDoUsuario };
- }
- } else if (req.userTipo === 'admin') {
- // Admin ve tudo (mas pode filtrar por ?comercioId se quiser)
- const comercioIdParam = parseInt(req.query.comercioId, 10);
- if (!isNaN(comercioIdParam)) where.comercioId = comercioIdParam;
- } else {
- // Cliente ve apenas seus pedidos
- where.clienteId = req.userId;
- }
-
- if (status) where.status = status;
-
- const [pedidos, total] = await Promise.all([
- prisma.pedido.findMany({
- where,
- skip: (page - 1) * limit,
- take: limit,
- orderBy: { createdAt: 'desc' },
- include: {
- itens: true,
- comercio: { select: { nome: true, slug: true, emoji: true } },
- cliente: { select: { nome: true, email: true, telefone: true } },
- endereco: true,
- pagamento: { select: { status: true, metodo: true, mercadoPagoId: true } }
- }
- }),
- prisma.pedido.count({ where })
- ]);
-
- res.json({
- pedidos,
- paginacao: {
- total,
- pagina: page,
- porPagina: limit,
- totalPaginas: Math.ceil(total / limit)
- }
- });
- } catch (err) {
- next(err);
- }
-}
-
-// GET /api/pedidos/:codigo
-// Detalhes de um pedido
-async function buscarPorCodigo(req, res, next) {
- try {
- const { codigo } = req.params;
-
- const pedido = await prisma.pedido.findUnique({
- where: { codigo },
- include: {
- itens: { include: { produto: { select: { imagem: true, disponivel: true } } } },
- comercio: { select: { nome: true, slug: true, emoji: true, whatsapp: true, endereco: true, horario: true } },
- cliente: { select: { nome: true, email: true, telefone: true } },
- endereco: true,
- pagamento: true
- }
- });
-
- if (!pedido) return res.status(404).json({ error: 'Pedido nao encontrado' });
-
- // Verificar acesso: dono do pedido, dono do comercio, ou admin
- const isCliente = pedido.clienteId === req.userId;
- const isComercio = await prisma.comercio.findFirst({
- where: { id: pedido.comercioId, ownerId: req.userId }
- });
- const isAdmin = req.userTipo === 'admin';
-
- if (!isCliente && !isComercio && !isAdmin) {
- return res.status(403).json({ error: 'Sem permissao para ver este pedido' });
- }
-
- res.json(pedido);
- } catch (err) {
- next(err);
- }
-}
-
-// PUT /api/pedidos/:codigo/status
-// Atualizar status do pedido (comerciante/admin)
-async function atualizarStatus(req, res, next) {
- try {
- const { codigo } = req.params;
- const { status, motivoCancelamento } = req.body;
-
- const statusValidos = ['pendente', 'confirmado', 'preparando', 'saiu_entrega', 'entregue', 'cancelado'];
- if (!status || !statusValidos.includes(status)) {
- return res.status(400).json({
- error: `Status invalido. Valores aceitos: ${statusValidos.join(', ')}`
- });
- }
-
- const pedido = await prisma.pedido.findUnique({
- where: { codigo },
- include: { comercio: { select: { ownerId: true } } }
- });
-
- if (!pedido) return res.status(404).json({ error: 'Pedido nao encontrado' });
-
- // Verificar permissao
- const isComercio = pedido.comercio.ownerId === req.userId;
- const isCliente = pedido.clienteId === req.userId;
- const isAdmin = req.userTipo === 'admin';
-
- // Cliente so pode cancelar
- if (isCliente && status !== 'cancelado') {
- return res.status(403).json({ error: 'Cliente so pode cancelar pedidos' });
- }
-
- // Apenas comerciante/admin podem mudar status (exceto cancelar)
- if (!isComercio && !isAdmin && !isCliente) {
- return res.status(403).json({ error: 'Sem permissao' });
- }
-
- // Nao pode alterar pedido ja entregue ou cancelado
- if (['entregue', 'cancelado'].includes(pedido.status)) {
- return res.status(400).json({ error: `Pedido ja esta ${pedido.status}` });
- }
-
- // Se cancelando, exigir motivo
- const data = { status };
- if (status === 'cancelado') {
- if (!motivoCancelamento) {
- return res.status(400).json({ error: 'Motivo do cancelamento e obrigatorio' });
- }
- data.motivoCancelamento = sanitize(motivoCancelamento);
- }
-
- const updated = await prisma.pedido.update({
- where: { codigo },
- data,
- include: {
- itens: true,
- comercio: { select: { nome: true, slug: true } },
- cliente: { select: { nome: true, email: true } },
- pagamento: { select: { status: true, metodo: true } }
- }
- });
-
- res.json({ message: `Status atualizado para ${status}`, pedido: updated });
- } catch (err) {
- next(err);
- }
-}
-
-// GET /api/pedidos/resumo
-// Resumo de pedidos para o comerciante (dashboard)
-async function resumo(req, res, next) {
- try {
- let comercioIds;
-
- if (req.userTipo === 'admin') {
- const comercios = await prisma.comercio.findMany({ select: { id: true } });
- comercioIds = comercios.map(c => c.id);
- } else {
- const comercios = await prisma.comercio.findMany({
- where: { ownerId: req.userId },
- select: { id: true }
- });
- comercioIds = comercios.map(c => c.id);
- }
-
- if (comercioIds.length === 0) {
- return res.json({
- total: 0, pendentes: 0, confirmados: 0,
- preparando: 0, emEntrega: 0, entregues: 0,
- cancelados: 0, faturamento: 0
- });
- }
-
- const [total, pendentes, confirmados, preparando, emEntrega, entregues, cancelados] =
- await Promise.all([
- prisma.pedido.count({ where: { comercioId: { in: comercioIds } } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'pendente' } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'confirmado' } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'preparando' } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'saiu_entrega' } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'entregue' } }),
- prisma.pedido.count({ where: { comercioId: { in: comercioIds }, status: 'cancelado' } })
- ]);
-
- // Faturamento (pedidos entregues)
- const entreguesData = await prisma.pedido.aggregate({
- where: { comercioId: { in: comercioIds }, status: 'entregue' },
- _sum: { total: true }
- });
-
- res.json({
- total,
- pendentes,
- confirmados,
- preparando,
- emEntrega,
- entregues,
- cancelados,
- faturamento: entreguesData._sum.total || 0
- });
- } catch (err) {
- next(err);
- }
-}
-
-module.exports = { criar, listar, buscarPorCodigo, atualizarStatus, resumo };
diff --git a/backend/src/rbac/capabilities.js b/backend/src/rbac/capabilities.js
index af81498..8fda1fd 100644
--- a/backend/src/rbac/capabilities.js
+++ b/backend/src/rbac/capabilities.js
@@ -9,20 +9,15 @@ const ROLE_CAPABILITIES = {
'account.view',
'users.manage',
'stores.moderate',
- 'orders.view.global',
],
comerciante: [
'account.view',
'account.view.store',
'stores.manage.own',
'products.manage.own',
- 'orders.view.ownStore',
],
cliente: [
'account.view',
- 'orders.view.own',
- 'payments.manage.own',
- 'addresses.manage.own',
],
};
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index 9e37fd7..1c4dbb1 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -28,7 +28,7 @@ router.get('/me', auth, ctrl.me);
router.post('/refresh', ctrl.refresh);
// --- Logout ---
-router.post('/logout', ctrl.logout);
+router.post('/logout', auth, csrfGuard, ctrl.logout);
// --- CSRF ---
router.get('/csrf', ctrl.csrf);
@@ -40,10 +40,4 @@ router.patch('/active-store', auth, csrfGuard, ctrl.activeStore);
router.get('/perfil', auth, ctrl.perfil);
router.put('/perfil', auth, csrfGuard, ctrl.atualizarPerfil);
-// --- Endereços de entrega ---
-router.get('/enderecos', auth, ctrl.listarEnderecos);
-router.post('/enderecos', auth, csrfGuard, ctrl.criarEndereco);
-router.put('/enderecos/:id', auth, csrfGuard, ctrl.atualizarEndereco);
-router.delete('/enderecos/:id', auth, csrfGuard, ctrl.excluirEndereco);
-
module.exports = router;
diff --git a/backend/src/routes/pagamentos.js b/backend/src/routes/pagamentos.js
deleted file mode 100644
index 7b1f514..0000000
--- a/backend/src/routes/pagamentos.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// ===========================================
-// Rotas - Pagamentos (Mercado Pago)
-// ===========================================
-const express = require('express');
-const router = express.Router();
-const { auth, csrfGuard } = require('../middleware/auth');
-const ctrl = require('../controllers/pagamentosController');
-
-// Criar pagamento (cliente autenticado)
-router.post('/criar', auth, csrfGuard, ctrl.criarPagamento);
-
-// Webhook do Mercado Pago (publico - chamado pelo MP)
-router.post('/webhook', ctrl.webhook);
-
-// Consultar pagamento (autenticado)
-router.get('/:pedidoCodigo', auth, ctrl.consultarPagamento);
-
-module.exports = router;
diff --git a/backend/src/routes/pedidos.js b/backend/src/routes/pedidos.js
deleted file mode 100644
index f89ea9e..0000000
--- a/backend/src/routes/pedidos.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// ===========================================
-// Rotas - Pedidos
-// ===========================================
-const express = require('express');
-const router = express.Router();
-const { auth, requireTipo, csrfGuard } = require('../middleware/auth');
-const ctrl = require('../controllers/pedidosController');
-
-// Resumo (comerciante/admin) - ANTES de /:codigo para nao conflitar
-router.get('/resumo', auth, requireTipo('comerciante', 'admin'), ctrl.resumo);
-
-// Criar pedido (cliente autenticado)
-router.post('/', auth, csrfGuard, ctrl.criar);
-
-// Listar pedidos (todos os tipos autenticados)
-router.get('/', auth, ctrl.listar);
-
-// Detalhes do pedido
-router.get('/:codigo', auth, ctrl.buscarPorCodigo);
-
-// Atualizar status (comerciante/admin/cliente para cancelar)
-router.put('/:codigo/status', auth, csrfGuard, ctrl.atualizarStatus);
-
-module.exports = router;
diff --git a/backend/src/server.js b/backend/src/server.js
index baba7e9..7c7bb0c 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -19,9 +19,6 @@ const categoriasRoutes = require('./routes/categorias');
const avaliacoesRoutes = require('./routes/avaliacoes');
const uploadRoutes = require('./routes/upload');
const estatisticasRoutes = require('./routes/estatisticas');
-const pedidosRoutes = require('./routes/pedidos');
-const pagamentosRoutes = require('./routes/pagamentos');
-
const app = express();
app.set('trust proxy', 1);
@@ -37,8 +34,6 @@ const SITE_PASSWORD = process.env.SITE_PASSWORD || '';
function privateSiteGate(req, res, next) {
if (!SITE_PASSWORD || process.env.NODE_ENV !== 'production') return next();
- if (req.path === '/api/pagamentos/webhook') return next();
-
const header = req.headers.authorization || '';
const [scheme, encoded] = header.split(' ');
@@ -65,12 +60,12 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://sdk.mercadopago.com", "https://http2.mlstatic.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://unpkg.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https://*.tile.openstreetmap.org", "https://res.cloudinary.com", "https://http2.mlstatic.com", "blob:"],
- connectSrc: ["'self'", "https://api.comerciobes.com.br", "https://unpkg.com", "https://api.mercadopago.com", "https://api.mercadolibre.com"],
- frameSrc: ["https://sdk.mercadopago.com", "https://*.mercadopago.com"],
+ connectSrc: ["'self'", "https://api.comerciobes.com.br", "https://unpkg.com"],
+ frameSrc: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
@@ -195,14 +190,14 @@ app.use(
express.static(path.join(BACKEND_ROOT, 'minha-conta'))
);
app.get(/^\/minha-conta\/.*$/, (req, res) => {
- res.sendFile(path.join(BACKEND_ROOT, 'minha-conta', 'index.html'));
+ res.sendFile('index.html', { root: path.join(BACKEND_ROOT, 'minha-conta') });
});
// --- Frontend Principal (Se acessado pela porta 3000) ---
-app.get('/', (req, res) => res.sendFile(path.join(FRONTEND_ROOT, 'index.html')));
-app.get('/index.html', (req, res) => res.sendFile(path.join(FRONTEND_ROOT, 'index.html')));
-app.get('/manifest.json', (req, res) => res.sendFile(path.join(FRONTEND_ROOT, 'manifest.json')));
-app.get('/sw.js', (req, res) => res.sendFile(path.join(FRONTEND_ROOT, 'sw.js')));
+app.get('/', (req, res) => res.sendFile('index.html', { root: FRONTEND_ROOT }));
+app.get('/index.html', (req, res) => res.sendFile('index.html', { root: FRONTEND_ROOT }));
+app.get('/manifest.json', (req, res) => res.sendFile('manifest.json', { root: FRONTEND_ROOT }));
+app.get('/sw.js', (req, res) => res.sendFile('sw.js', { root: FRONTEND_ROOT }));
app.get('/login', (req, res) => res.redirect(301, '/html/login.html'));
app.get('/cadastro', (req, res) => res.redirect(301, '/html/cadastro.html'));
@@ -235,9 +230,6 @@ app.use('/api/categorias', categoriasRoutes);
app.use('/api/avaliacoes', avaliacoesRoutes);
app.use('/api/upload', uploadRoutes);
app.use('/api/estatisticas', estatisticasRoutes);
-app.use('/api/pedidos', pedidosRoutes);
-app.use('/api/pagamentos', pagamentosRoutes);
-
// Rota raiz da API
app.get('/api', (req, res) => {
res.json({
@@ -249,13 +241,12 @@ app.get('/api', (req, res) => {
comercios: '/api/comercios',
categorias: '/api/categorias',
avaliacoes: '/api/avaliacoes',
- pedidos: '/api/pedidos',
- pagamentos: '/api/pagamentos',
upload: '/api/upload',
estatisticas: '/api/estatisticas',
health: '/api/health',
painel: '/minha-conta'
- }
+ },
+ marketplace: 'fora deste servico'
});
});
diff --git a/docker-compose.yml b/docker-compose.yml
index bd48ce1..6a75bdd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,7 +28,6 @@ services:
PORT: 3000
DATABASE_URL: postgresql://${POSTGRES_USER:-comerciobes}:${POSTGRES_PASSWORD:-troque-esta-senha}@postgres:5432/${POSTGRES_DB:-comerciobes}?schema=public
FRONTEND_URL: ${FRONTEND_URL:-https://comerciobes.com.br}
- WEBHOOK_BASE_URL: https://${API_DOMAIN:-api.comerciobes.com.br}
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.comerciobes.com.br}
PRISMA_CLIENT_ENGINE_TYPE: binary
volumes:
diff --git a/docs/OPERACAO.md b/docs/OPERACAO.md
index a90c8ce..a3ad580 100644
--- a/docs/OPERACAO.md
+++ b/docs/OPERACAO.md
@@ -66,7 +66,7 @@ O lojista seed fica vinculado a uma loja demo para o dashboard abrir com context
## Rodar local sem Docker instalado
-Este modo requer Node.js 24+ e serve apenas para validar vitrine, login demo e navegacao do painel. Ele nao substitui PostgreSQL: cadastro real, pedidos, edicao de produtos e persistencia dependem do banco.
+Este modo requer Node.js 24+ e serve apenas para validar vitrine, login demo e navegacao do painel. Ele nao substitui PostgreSQL: cadastro real, edicao de produtos e persistencia dependem do banco.
```powershell
cd backend
@@ -114,7 +114,7 @@ Antes de migrar para VPS, faca um backup manual e teste restore em uma maquina s
6. Troque o DNS de `api.comerciobes.com.br` para o IP da VPS.
7. Confira `/api/health`, login e painel.
-Escolha inicial recomendada: KVM 2. Suba para KVM 4 quando houver loja usando diariamente, pagamento real, instabilidade ou consumo alto.
+Escolha inicial recomendada: KVM 2. Suba para KVM 4 quando houver uso diario relevante, instabilidade ou consumo alto.
## Checklist antes de expor
@@ -124,7 +124,6 @@ Escolha inicial recomendada: KVM 2. Suba para KVM 4 quando houver loja usando di
- Banco sem porta publica.
- `COOKIE_DOMAIN=.comerciobes.com.br`.
- `FRONTEND_URL=https://comerciobes.com.br`.
-- `WEBHOOK_BASE_URL=https://api.comerciobes.com.br`.
- `AUTH_FALLBACK_ENABLED=false`.
- `/api/health` com banco reachable.
- Backup gerado e restore testado.
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 2c0a0c7..a4ab5f1 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -1,96 +1,74 @@
-# Roadmap - Rebase do Marketplace
+# Roadmap - Comercio BES como vitrine local
-Este roadmap substitui a ideia de "continuar adicionando features" por um fechamento pragmático do marketplace real.
+Este roadmap e o espelho versionado do planejamento no GitHub. O objetivo e entregar uma vitrine local pequena, confiavel e integravel ao Braga Commerce, sem reconstruir e-commerce dentro deste repositorio.
## Regra de escopo
-Nao entram cupons, notificacoes push, automacoes comerciais ou novas telas antes de:
+Entram no nucleo:
-- checkout criar pedido real no backend;
-- cliente e lojista enxergarem o mesmo pedido;
-- CI rodar testes com PostgreSQL real;
-- pagamento Mercado Pago ter testes e webhook seguro;
-- vitrine React/Next cobrir o fluxo essencial antes de substituir a vanilla.
+- descoberta por busca, categoria e mapa;
+- perfil publico do comercio, fotos, horario e contatos;
+- cadastro, moderacao e administracao da informacao local;
+- metricas de visualizacao sem dados transacionais;
+- integracao read-only com o Braga Commerce.
-## Milestones para GitHub
+Ficam fora do nucleo:
-### M0 - Fundacao e Checks
+- carrinho, checkout, pedido, pagamento e cupom;
+- estoque, entrega, devolucao e pos-venda;
+- duplicacao do catalogo transacional do Braga Commerce.
-Objetivo: deixar a base verificavel antes de qualquer feature nova.
+## Milestones do GitHub
-Issues sugeridas:
+### v0.1 - Scope Reset
-1. `CI: rodar testes backend com PostgreSQL service`
- - Check: workflow sobe Postgres 16 e executa `npm test`.
- - Check: PR falha se testes de integracao falharem.
+Resultado: produto e codigo deixam explicito que o Comercio BES e uma vitrine.
-2. `Seguranca: aplicar CSRF nas rotas mutantes autenticadas`
- - Check: mutacoes via cookie exigem `X-CSRF-Token`.
- - Check: testes com Bearer continuam funcionando.
+- Epic #12: redefinir o produto como vitrine local.
+- #19: ADR e ownership do Braga Commerce.
+- #20: desativar checkout, pedidos e pagamentos do nucleo.
+- #36: documentacao e ADRs.
-3. `Testes: cobrir criacao e listagem de pedidos`
- - Check: cliente cria pedido com produto valido.
- - Check: lojista ve apenas pedidos das proprias lojas.
- - Check: outro lojista nao acessa pedido alheio.
+### v0.2 - Production Baseline
-4. `Schema: trocar valores monetarios para Decimal`
- - Check: `Produto.preco`, `Pedido.total`, `Pagamento.valor` e snapshots nao usam `Float`.
- - Check: seed e controllers continuam convertendo valores corretamente.
+Resultado: baseline de seguranca, dados, testes e CI capaz de sustentar deploys.
-### M1 - Checkout Real
+- Epic #13: seguranca e privacidade (#22 a #25 e #27).
+- Epic #14: arquitetura e modelo de dados (#21, #26 e #28).
+- Epic #15: qualidade e CI (#29 e #30).
-Objetivo: acabar com a duplicidade entre pedido local e pedido backend.
+### v0.3 - Directory MVP
-Issues sugeridas:
+Resultado: fluxo principal busca -> perfil -> contato rapido, acessivel e performatico.
-1. `Vitrine: confirmar pedido usando /api/pedidos`
- - Check: botao principal cria pedido real.
- - Check: carrinho com lojas diferentes cria pedidos separados.
- - Check: WhatsApp fica marcado como fluxo externo, sem historico falso.
+- Epic #16: Directory MVP.
+- #31: imagens e payload inicial.
+- #32: acessibilidade, teclado, foco e mobile.
+- #33: home centrada em descoberta e contato.
-2. `Cliente: historico mostra pedidos criados pela vitrine`
- - Check: pedido criado aparece em `/minha-conta/historico`.
- - Check: cliente consegue cancelar pedido pendente/confirmado.
+### v0.4 - Braga Commerce Adapter
-3. `Lojista: painel opera pedidos reais`
- - Check: pedido aparece em `/minha-conta/pedidos-loja`.
- - Check: lojista altera status seguindo o fluxo permitido.
+Resultado: lojas elegiveis podem apontar para uma experiencia transacional externa sem compartilhar credenciais ou dados indevidos.
-### M2 - Pagamento Mercado Pago
+- Epic #17: adapter Braga Commerce.
+- #34: contrato, ownership matrix e seguranca.
+- #35: adapter read-only observavel.
-Objetivo: deixar pagamento real seguro antes de expor publicamente.
+### v1.0 - Public Launch
-Issues sugeridas:
+Resultado: lancamento com staging, observabilidade, backup, rollback e runbooks validados.
-1. `Mercado Pago: validar webhook com assinatura`
- - Check: eventos sem assinatura valida sao recusados ou ignorados.
- - Check: evento duplicado nao duplica efeito.
+- Epic #18: lancamento e operacao.
+- #37: staging, observabilidade, backup e rollback.
-2. `Pagamento: testes de criacao e consulta`
- - Check: pagamento na entrega confirma pedido sem Mercado Pago.
- - Check: PIX retorna preferencia quando credenciais estao configuradas.
+## Ordem de entrega
-3. `UI: estado de pagamento no historico do cliente`
- - Check: pendente/aprovado/recusado aparecem no painel.
- - Check: erro de pagamento tem mensagem clara.
-
-### M3 - Frontend Next
-
-Objetivo: migrar a vitrine sem apagar o que ainda funciona.
-
-Issues sugeridas:
-
-1. `Next: criar app frontend-next consumindo API atual`
- - Check: rotas de home, busca e loja usam `/api/comercios`.
- - Check: `.env.example` documenta `NEXT_PUBLIC_API_BASE`.
-
-2. `Next: carrinho e checkout real`
- - Check: carrinho usa estado local apenas como rascunho.
- - Check: confirmacao chama `/api/pedidos`.
-
-3. `Next: paridade antes de substituir vanilla`
- - Check: home, busca, loja, carrinho, login e checkout passam em teste manual.
- - Check: vitrine antiga continua disponivel ate paridade minima.
+1. Fechar P0 de escopo, cache, autenticacao e XSS.
+2. Congelar um contrato publico pequeno para a vitrine.
+3. Criar migracoes versionadas, testes e gates de CI.
+4. Simplificar a experiencia publica e validar acessibilidade/performance.
+5. Homologar o contrato do Braga Commerce antes de implementar o adapter.
+6. Lancar apenas depois de exercitar backup, restore e rollback em staging.
## Checks locais antes de PR
diff --git a/docs/adr/0001-product-scope-and-braga-commerce.md b/docs/adr/0001-product-scope-and-braga-commerce.md
new file mode 100644
index 0000000..6b664cb
--- /dev/null
+++ b/docs/adr/0001-product-scope-and-braga-commerce.md
@@ -0,0 +1,98 @@
+# ADR 0001 - Escopo do produto e fronteira com o Braga Commerce
+
+- Status: aceito
+- Data: 2026-07-16
+- Decisores: mantenedores do Comercio BES
+- Issues relacionadas: #12, #19, #20, #34 e #35
+
+## Contexto
+
+O repositorio evoluiu de guia comercial para marketplace, acumulando carrinho, pedido e pagamento. Essa direcao aumenta custo operacional, superficie de seguranca e duplicacao de dados. Ao mesmo tempo, o Braga Commerce ja e o sistema natural para capacidades transacionais.
+
+O objetivo do Comercio BES e permitir que um morador encontre um estabelecimento de Boa Esperanca do Sul e obtenha informacoes confiaveis para visita-lo ou contata-lo.
+
+## Decisao
+
+O Comercio BES sera uma vitrine digital local.
+
+### Comercio BES e dono de
+
+- identidade publica e status de moderacao do estabelecimento;
+- categoria, descricao, endereco, coordenadas, horario e contatos;
+- fotos institucionais, destaques e indicadores de descoberta;
+- busca, mapa, favoritos e avaliacoes, quando habilitadas;
+- vinculo tecnico com um comercio no Braga Commerce.
+
+### Braga Commerce e dono de
+
+- catalogo transacional e precos de venda;
+- estoque e disponibilidade para compra;
+- carrinho, checkout, pedido e pagamento;
+- entrega, cancelamento, devolucao e pos-venda;
+- dados fiscais e demais dados sensiveis da transacao.
+
+### Integracao
+
+1. O primeiro contrato sera read-only do ponto de vista do Comercio BES.
+2. O vinculo usara um identificador externo estavel, nunca email ou nome como chave.
+3. A vitrine podera exibir disponibilidade resumida e um link para a loja externa, desde que esses campos tenham origem e horario de sincronizacao visiveis.
+4. Credenciais de integracao permanecem apenas no backend e devem ter menor privilegio possivel.
+5. Falhas de sincronizacao nao removem o perfil local; dados externos ficam marcados como indisponiveis ou desatualizados.
+6. Nenhum pedido ou pagamento sera criado pelo Comercio BES.
+7. Contrato, autenticacao, rate limit, retry, idempotencia, observabilidade e LGPD devem ser homologados antes do adapter de producao.
+
+## Modelo inicial do vinculo
+
+O desenho alvo inclui, no minimo:
+
+- `provider`: identificador do Braga Commerce;
+- `externalStoreId`: chave opaca da loja externa;
+- `storefrontUrl`: destino aprovado para a experiencia de compra;
+- `syncStatus`: estado da ultima sincronizacao;
+- `lastSyncedAt`: horario da ultima resposta valida;
+- `syncErrorCode`: codigo operacional sem segredos ou dados pessoais.
+
+O schema definitivo sera criado por migracao versionada depois que o contrato do Braga Commerce for aprovado.
+
+## Consequencias
+
+### Positivas
+
+- escopo menor e lancamento mais seguro;
+- uma unica fonte de verdade para transacoes;
+- menor tratamento de dados pessoais e financeiros;
+- evolucao independente da vitrine e do e-commerce.
+
+### Custos
+
+- remocao gradual do marketplace legado;
+- necessidade de contrato formal entre os sistemas;
+- experiencia de compra ocorre fora da vitrine;
+- sincronizacao exige monitoramento e estrategia de degradacao.
+
+## Plano de transicao
+
+1. Remover pontos de entrada de carrinho e checkout do frontend publico.
+2. Desabilitar rotas transacionais por padrao, preservando dados ate existir plano de retencao/exportacao.
+3. Corrigir riscos P0 de cache, autenticacao e conteudo nao confiavel.
+4. Reduzir o schema em migracoes seguras, sem apagar dados em producao sem backup e aprovacao.
+5. Homologar o contrato Braga Commerce e implementar o adapter atras de feature flag.
+6. Excluir o codigo legado somente depois da verificacao de dados e rollback.
+
+## Retencao do marketplace legado
+
+As tabelas `Endereco`, `Pedido`, `ItemPedido` e `Pagamento` nao serao apagadas neste milestone. A politica operacional e:
+
+1. manter removidas as interfaces, rotas e dependencias transacionais, impedindo novos dados no produto publico;
+2. antes de alterar o schema, gerar backup restauravel e inventario por tabela com contagem, menor e maior data;
+3. definir com os responsaveis pelo produto a necessidade e o prazo de retencao, considerando privacidade e obrigacoes aplicaveis;
+4. exportar somente o necessario, com acesso restrito, e validar a restauracao em staging;
+5. executar remocao por migracao versionada em issue separada, com aprovacao explicita e plano de rollback.
+
+Codigo de interface sem dados autoritativos — carrinho e historico em `localStorage` — e APIs transacionais foram removidos imediatamente. Apenas os modelos persistentes permanecem temporariamente para inventario/exportacao controlada, sem fazer parte do core da vitrine.
+
+## Alternativas rejeitadas
+
+- Manter dois marketplaces completos: cria fontes de verdade concorrentes e duplica operacao.
+- Reescrever todo o produto antes de estabilizar: posterga valor e nao resolve a fronteira de dominio.
+- Integrar diretamente no navegador: exporia credenciais e reduziria controle de seguranca e observabilidade.
diff --git a/index.html b/index.html
index 667c1ad..a98261f 100644
--- a/index.html
+++ b/index.html
@@ -197,27 +197,6 @@
background: rgba(255, 68, 68, 0.15);
}
- /* Cart badge counter */
- .nav-badge {
- position: absolute;
- top: -6px;
- right: -6px;
- background: var(--verde);
- color: #fff;
- font-size: 11px;
- font-weight: 700;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- border: 2px solid var(--creme);
- }
-
- body.dark-mode .nav-badge {
- border-color: #0f0f0f;
- }
@@ -376,11 +355,6 @@
-