From 9c2f7ff7b4ee029730db80d7f82d5511a8139611 Mon Sep 17 00:00:00 2001 From: Iris Florencio Date: Mon, 6 Jul 2026 18:50:26 -0300 Subject: [PATCH] =?UTF-8?q?chore:=20triagem=20de=20arquivos=20untracked=20?= =?UTF-8?q?(60+=20=E2=86=92=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona ao repo os scripts e configs reais do motor que estavam fora do git, e remove arquivos legados/abandonados. Adicionados (30): - scripts/docker-compose.m5.yml, openwa-compose.yml, minio-compose.yml - scripts/openwa.env (sem secrets) - scripts/{entrypoint,saleor-entrypoint,saleor-start}.sh - scripts/{chatwoot-setup,compliance-check,health-check,resumo-diario}.sh - scripts/{insert_workflow,telegram_user_login,telegram_user_monitor}.py - scripts/requirements.txt - scripts/traefik-conf/{traefik.yml,dynamic-config.yml} - scripts/compliance/, scripts/calculators/, scripts/backups/ - scripts/{README.md,README-INSTALL.md,WHATSAPP-SETUP.md,.gitignore} - .gitignore (atualizado) Removidos (ja' em backup/untracked-lixo-2026-07-06/): - agents/, crm/, hermes_skills_backup/, api-complive/, docs/ (legados) - scripts/{wrapper.py,wrapper.sh} - scripts/docker-compose.m5.yml.backup, traefik-conf/routes.yml.bak2 - scripts/saleor-media/RSA_*.{bak,old_*} - scripts/n8n-workflows/webhook-saleor.json.bak2 - scripts/backups/20260622_0300/saleor_db.sql - integracao_completa.sh, monitoramento_vps.sh - ruvector.db (vazio) Ignorados via .gitignore: - vault/ (Obsidian KB) - skills/ (duplicado de ~/.hermes/skills/) - scripts/{saleor-media,traefik-acme,yt-pub-livesx,minio,n8n-workflows,skills}/ - scripts/backups/2026*_*/ --- .gitignore | 28 ++ scripts/.gitignore | 45 +++ scripts/README-INSTALL.md | 220 ++++++++++++ scripts/README.md | 38 ++ scripts/WHATSAPP-SETUP.md | 30 ++ scripts/backups/backup-vault.bat | 34 ++ scripts/backups/backup-vault.sh | 5 + scripts/backups/cron-backup.sh | 27 ++ scripts/backups/restore-vault.sh | 191 ++++++++++ scripts/calculators/marketplace_margin.py | 187 ++++++++++ scripts/chatwoot-setup.sh | 59 ++++ scripts/compliance-check.sh | 5 + scripts/compliance/compliance-checker.py | 406 ++++++++++++++++++++++ scripts/docker-compose.m5.yml | 304 ++++++++++++++++ scripts/entrypoint.sh | 30 ++ scripts/health-check.sh | 5 + scripts/import-chatwoot-workflow.sh | 55 +++ scripts/insert_workflow.py | 110 ++++++ scripts/minio-compose.yml | 62 ++++ scripts/openwa-compose.yml | 67 ++++ scripts/openwa.env | 3 + scripts/operacao_completa.sh | 58 ++++ scripts/requirements.txt | 7 + scripts/resumo-diario.sh | 5 + scripts/saleor-entrypoint.sh | 8 + scripts/saleor-start.sh | 5 + scripts/telegram_user_login.py | 61 ++++ scripts/telegram_user_monitor.py | 94 +++++ scripts/traefik-conf/dynamic-config.yml | 7 + scripts/traefik-conf/traefik.yml | 28 ++ 30 files changed, 2184 insertions(+) create mode 100644 scripts/.gitignore create mode 100644 scripts/README-INSTALL.md create mode 100644 scripts/README.md create mode 100644 scripts/WHATSAPP-SETUP.md create mode 100644 scripts/backups/backup-vault.bat create mode 100755 scripts/backups/backup-vault.sh create mode 100755 scripts/backups/cron-backup.sh create mode 100755 scripts/backups/restore-vault.sh create mode 100755 scripts/calculators/marketplace_margin.py create mode 100755 scripts/chatwoot-setup.sh create mode 100755 scripts/compliance-check.sh create mode 100755 scripts/compliance/compliance-checker.py create mode 100644 scripts/docker-compose.m5.yml create mode 100755 scripts/entrypoint.sh create mode 100755 scripts/health-check.sh create mode 100644 scripts/import-chatwoot-workflow.sh create mode 100644 scripts/insert_workflow.py create mode 100644 scripts/minio-compose.yml create mode 100644 scripts/openwa-compose.yml create mode 100644 scripts/openwa.env create mode 100755 scripts/operacao_completa.sh create mode 100644 scripts/requirements.txt create mode 100755 scripts/resumo-diario.sh create mode 100755 scripts/saleor-entrypoint.sh create mode 100755 scripts/saleor-start.sh create mode 100644 scripts/telegram_user_login.py create mode 100644 scripts/telegram_user_monitor.py create mode 100644 scripts/traefik-conf/dynamic-config.yml create mode 100644 scripts/traefik-conf/traefik.yml diff --git a/.gitignore b/.gitignore index 3dccc89..916e2c3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,31 @@ logs/ *.bak *.bak.* CLAUDE.md.bak.* + +# Vault Obsidian (fonte de verdade, replicado fora do git) +vault/ + +# Skills duplicadas (origem em ~/.hermes/skills/) +skills/ +scripts/skills/ + +# Saleor media (chaves RSA + avatares - NUNCA commitar) +scripts/saleor-media/ + +# Traefik ACME certs (contem email pessoal) +scripts/traefik-acme/ + +# yt-pub-livesx credenciais criptografadas + dados +scripts/yt-pub-livesx/ + +# Minio inteiro (data + config local) +scripts/minio/ + +# n8n workflows locais duplicados (origem em /workflows/) +scripts/n8n-workflows/ + +# Backups SQL locais antigos +scripts/backups/2026*_*/ + +# scripts/scripts/ (scripts auxiliares - operacional, nao versionar) +/scripts/scripts/ diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..0b9d4cb --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,45 @@ +# Gitignore — M5 E-commerce Scripts +# Arquivos que a IA NÃO deve ler (evitam token waste) + +# ── IA / Agentes ────────────────────────────────────────────────── +.hermes/ +.claude-flow/ +.swarm/ +node_modules/ +venv/ +.venv/ +.env +.env.local +*.log +*.tmp +.DS_Store +Thumbs.db + +# ── Obsidian (não é código) ────────────────────────────────────── +# Obsidian vault para projetos seria .obsidian/ +.vault_backup/ + +# ── Build / Dist ──────────────────────────────────────────────── +dist/ +build/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.eggs/ + +# ── Docker ─────────────────────────────────────────────────────── +*.tar.gz # backups gerados automaticamene + +# ── Credenciais ───────────────────────────────────────────────── +credentials.json +*.pem +*.key +secrets/ +vault/ + +# ── Large files ───────────────────────────────────────────────── +*.mp4 +*.mov +*.zip > 5MB +*.iso diff --git a/scripts/README-INSTALL.md b/scripts/README-INSTALL.md new file mode 100644 index 0000000..f4c482d --- /dev/null +++ b/scripts/README-INSTALL.md @@ -0,0 +1,220 @@ +# README-INSTALL.md — Stack M5 Brasil +**Motor Ecommerce M5 | Setup Autônomo com Docker Compose** + +--- + +## Pré-requisitos + +| Ferramenta | Versão mínima | Instalação | +|---|---|---| +| Docker Desktop | 4.x+ | https://docs.docker.com/desktop/install/windows-install/ | +| Git Bash / MSYS | (já incluso no Git) | https://git-scm.com/download/win | +| curl | (já incluso) | — | + +Verifique a instalação: +```bash +docker --version # Docker version 2x.x+ +docker compose version # Docker Compose version v2.x+ +git --version +``` + +--- + +## Instalação — Passo a Passo + +### 1. Clonar / Copiar o projeto + +```bash +cd ~/Projects +git clone ecommerce-scripts +cd ecommerce-scripts +``` + +### 2. Configurar variáveis de ambiente + +```bash +# Copiar o template +cp .env.example .env + +# Editar com seus dados reais +nano .env # ou code .env, notepad, etc. +``` + +**Campos obrigatórios a preencher em `.env`:** +``` +BASE_DOMAIN=seudominio.com.br +ACME_EMAIL=seu@email.com.br +POSTGRES_PASSWORD= +REDIS_PASSWORD= +SALEOR_SECRET_KEY= +N8N_PASSWORD= +STRIPE_API_KEY= +EMAIL_URL=smtp://usuario:senha@smtp.seudominio.com.br:587 +``` + +> Gere uma SECRET_KEY segura: `openssl rand -hex 32` + +### 3. Configurar DNS para os subdomínios + +No painel DNS do seu domínio (`seudominio.com.br`), crie os registros **A** ou **CNAME**: + +| Subdomínio | Tipo | Valor | +|---|---|---| +| `vendas` | A | IP do servidor | +| `n8n` | A | IP do servidor | +| `git` | A | IP do servidor | +| `wa` | A | IP do servidor | + +Se usar Cloudflare, configure o proxy accordingly. + +### 4. Iniciar a stack + +```bash +# Tornar scripts executáveis (Linux/WSL/Git Bash) +chmod +x SCRIPTS/*.sh + +# Iniciar todos os serviços +./SCRIPTS/start-m5.sh +``` + +O script faz: +1. Pull das imagens Docker +2. `docker compose up -d` +3. Healthcheck de cada serviço (aguarda até 90s por serviço) +4. Exibe URLs de acesso + +### 5. Setup pós-instalação + +#### Saleor — primeiro acesso +```bash +# Criar superusuário (executar após Saleor estar pronto) +docker exec -it m5_saleor python manage.py createsuperuser +``` + +1. Acesse `https://vendas.seudominio.com.br/dashboard` +2. Configure canal de vendas (Mercado Brasil) +3. Cadastre meio de pagamento (Stripe / Mercado Pago) + +#### n8n — primeiro acesso +1. Acesse `https://n8n.seudominio.com.br` +2. Login com `N8N_USER` / `N8N_PASSWORD` do `.env` +3. Importe workflows de `./n8n-workflows/` (se existirem) + +#### Gitea — primeiro acesso +1. Acesse `https://git.seudominio.com.br` +2. Complete o wizard de instalação (banco já configurado via env) +3. Crie usuário admin + +#### OpenWA — primeira sessão +1. Acesse `http://localhost:8081` (dashboard local) +2. Escaneie QR Code com WhatsApp desejado +3. Configure webhook para `http://n8n:5678/webhook/wa` + +### 6. Cron — backup automático + +```bash +# Editar crontab +crontab -e + +# Adicionar linha (backup diário às 3h da manhã): +0 3 * * * /c/Users/Iris/Projects/ecommerce-scripts/SCRIPTS/backup-vault.sh >> ~/backups/vault/cron.log 2>&1 +``` + +--- + +## Serviços — Visão Geral + +| Serviço | Imagem | Porta local | URL | +|---|---|---|---| +| Postgres | postgres:16-alpine | 5432 | (interno) | +| Redis | redis:7-alpine | 6379 | (interno) | +| Saleor (API) | ghcr.io/saleor-commerce/saleor | 8000 | `vendas.*` | +| Saleor Dashboard | ghcr.io/saleor-commerce/saleor-dashboard | 80 | `vendas.*/dashboard` | +| n8n | n8nio/n8n | 5678 | `n8n.*` | +| OpenWA | openwa/wa-automate | 8080/8081 | `wa.*` | +| Gitea | gitea/gitea | 3000 | `git.*` | +| Traefik | traefik:v3.1 | 80/443/8090 | — | + +--- + +## Operações do Dia a Dia + +```bash +# Ver status +./SCRIPTS/status-m5.sh + +# Ver logs de um serviço específico +docker compose -f docker-compose.m5.yml logs -f saleor +docker compose -f docker-compose.m5.yml logs -f n8n + +# Parar stack (preserva dados) +./SCRIPTS/stop-m5.sh + +# Reiniciar apenas um serviço +docker compose -f docker-compose.m5.yml restart saleor + +# Rebuild de um serviço (após atualização) +docker compose -f docker-compose.m5.yml up -d --force-recreate saleor + +# Limpar tudo (PERDE DADOS) +./SCRIPTS/stop-m5.sh --volumes +``` + +--- + +## Integrações + +### Mercado Livre → Saleor (via n8n) +- Workflow n8n: `webhook → ML Order Created → criar pedido no Saleor` +- Autenticação: OAuth App do Mercado Livre +- Webhook ML: apontar para `https://n8n.seudominio.com.br/webhook/ml-pedido` + +### WhatsApp → n8n → Saleor +- OpenWA conecta via API REST na porta 8080 +- n8n recebe webhooks do OpenWA +- Workflow: `msg recebida → verificar produto → enviar catálogo → criar pedido` + +### Stripe (pagamentos) +- Webhook: `https://seudominio.com.br/webhook/stripe` +- Saleor processa pagamentos via plugin Stripe oficial + +--- + +## Segurança + +- **Nunca comite `.env`** — já está em `.gitignore` +- **Renove senhas periodicamente** (sempre rebuild após mudança de `.env`) +- **Firewall**: exponha apenas 80, 443 e 8090 (Traefik dashboard) +- **OpenWA**: se possível, limite acesso ao IP do servidor para a API local +- **Traefik Dashboard**: proteja com Basic Auth ou IP whitelist + +--- + +## Troubleshooting + +| Problema | Solução | +|---|---| +| `postgres permission denied` | `docker compose down -v` (apaga volumes) e `start-m5.sh` novamente | +| SSL não funciona | Verificar registros DNS e se porta 443 está aberta | +| n8n não conecta no Saleor | `SALEOR_API_URL` deve ser `http://saleor:8000/graphql/` (interno) | +| OpenWA QR Code não aparece | `docker logs m5_openwa` para ver erros de sessão | +| Traefik sem certificados | `docker logs m5_traefik` — verificar email ACME | +| Saleor 502 | Aguardar ~2min após start; checar `docker logs m5_saleor` | + +--- + +## Atualização de Serviços + +```bash +# Atualizar imagem e rebuildar +docker compose -f docker-compose.m5.yml pull saleor +docker compose -f docker-compose.m5.yml up -d --force-recreate saleor + +# Atualizar todos de uma vez +docker compose -f docker-compose.m5.yml pull +docker compose -f docker-compose.m5.yml up -d +``` + +--- + +*Criado para o Motor Ecommerce M5 Brasil — Iris Florencio — 2026* diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..1ef0314 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,38 @@ +# E-commerce Scripts + +Scripts Python para operações de e-commerce multi-canal. + +## Setup + +```bash +cd ~/Projects/ecommerce-scripts +source venv/Scripts/activate +pip install -r requirements.txt +``` + +## Scripts Disponíveis + +### Calculadoras +- `calculators/marketplace_margin.py` — Calcula margem líquida por marketplace +- `calculators/freight_quote.py` — Cotação de frete por CEP e peso +- `calculators/cac_ltv.py` — Calcula CAC, LTV e ROAS + +### Marketplaces +- `marketplaces/shopee_bulk.py` — Upload em massa para Shopee +- `marketplaces/ml_pricing.py` — Calculadora de preço para Mercado Livre + +### Automação +- `automation/price_monitor.py` — Monitoramento de preços de concorrentes +- `automation/order_tracker.py` — Rastreamento de pedidos + +## Estrutura + +``` +ecommerce-scripts/ +├── calculators/ +├── marketplaces/ +├── automation/ +├── data/ +├── requirements.txt +└── README.md +``` \ No newline at end of file diff --git a/scripts/WHATSAPP-SETUP.md b/scripts/WHATSAPP-SETUP.md new file mode 100644 index 0000000..33549b1 --- /dev/null +++ b/scripts/WHATSAPP-SETUP.md @@ -0,0 +1,30 @@ +# WhatsApp Setup — OpenWA (Motor M5) + +## Status +Container `m5_openwa` está rodando e aguardando autenticação. + +## Acesso ao QR Code +1. Abrir navegador: **http://31.220.78.126:8002/** +2. Escanear QR code com o WhatsApp do número **+55 11 970168239** +3. Após escanear, a sessão será salva automaticamente + +## Alternativa: via terminal (VPS) +```bash +docker logs -f m5_openwa +# O QR code aparece nos logs quando o container inicia +``` + +## Após autenticação +- Sessão salva em `/root/ecommerce/scripts/openwa-sessions/` +- Webhook: `http://n8n:5678/webhook/wa` (já configurado no compose) +- Testar com n8n: criar workflow que recebe mensagens do WhatsApp + +## Número vinculado +- **+55 11 970168239** (número do cliente/fornecedor a definir) + +## Troubleshooting +Se o QR expirar: +```bash +docker restart m5_openwa +# Aguardar 15s e acessar http://31.220.78.126:8002/ +``` diff --git a/scripts/backups/backup-vault.bat b/scripts/backups/backup-vault.bat new file mode 100644 index 0000000..4686418 --- /dev/null +++ b/scripts/backups/backup-vault.bat @@ -0,0 +1,34 @@ +@echo off +REM ============================================================ +REM backup-vault.bat — Wrapper Windows para backup do Vault +REM Chama o script bash (Git Bash / MSYS2) +REM Agendar via Task Scheduler: schtasks /create ... +REM ============================================================ + +setlocal + +SET SCRIPT_DIR=%~dp0 +SET BASH_PATH=C:\Program Files\Git\bin\bash.exe + +IF NOT EXIST "%BASH_PATH%" ( + SET BASH_PATH=C:\Program Files (x86)\Git\bin\bash.exe +) + +IF NOT EXIST "%BASH_PATH%" ( + echo [ERRO] Git Bash nao encontrado. Instale Git for Windows. + echo Download: https://git-scm.com/download/win + pause + exit /b 1 +) + +echo [INFO] Executando backup via Git Bash... +"%BASH_PATH%" --login -i "%SCRIPT_DIR%backup-vault.sh" + +if %ERRORLEVEL% NEQ 0 ( + echo [ERRO] Backup falhou com codigo: %ERRORLEVEL% + pause + exit /b %ERRORLEVEL% +) + +echo [OK] Backup concluido. +pause diff --git a/scripts/backups/backup-vault.sh b/scripts/backups/backup-vault.sh new file mode 100755 index 0000000..7899591 --- /dev/null +++ b/scripts/backups/backup-vault.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# M5 - Backup Vault Diário +# Data: 24/06/2026 +echo "Backup Vault - $(date)" +# Será implementado com rsync/backup diff --git a/scripts/backups/cron-backup.sh b/scripts/backups/cron-backup.sh new file mode 100755 index 0000000..25ce795 --- /dev/null +++ b/scripts/backups/cron-backup.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# ============================================================ +# cron-backup.sh — Wrapper para cron job Hermes +# Chamado automaticamente todo dia às 02:00 +# ============================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +LOG_FILE="$SCRIPT_DIR/../logs/cron-backup.log" +mkdir -p "$(dirname "$LOG_FILE")" + +exec >> "$LOG_FILE" 2>&1 + +echo "==========================================" +echo "Cron backup iniciado: $(date '+%Y-%m-%d %H:%M:%S')" +echo "==========================================" + +bash "$SCRIPT_DIR/backup-vault.sh" +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 0 ]; then + echo "Backup concluído com sucesso: $(date '+%Y-%m-%d %H:%M:%S')" +else + echo "ERRO no backup - código: $EXIT_CODE - $(date '+%Y-%m-%d %H:%M:%S')" >&2 +fi + +exit $EXIT_CODE \ No newline at end of file diff --git a/scripts/backups/restore-vault.sh b/scripts/backups/restore-vault.sh new file mode 100755 index 0000000..5ddbe38 --- /dev/null +++ b/scripts/backups/restore-vault.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# ============================================================ +# restore-vault.sh — Restaurar backup do Vault Obsidian +# Uso: ./restore-vault.sh [YYYYMMDD_HHMMSS] +# ./restore-vault.sh list → lista backups disponíveis +# ./restore-vault.sh latest → restaura o mais recente +# ============================================================ + +set -e + +VAULT_DIR="C:/Users/Iris/Vaults/ecommerce-kb" +BACKUP_DIR="C:/Users/Iris/backups/vault" +LOG_FILE="$BACKUP_DIR/restore.log" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +# Mostrar ajuda +show_help() { + cat << EOF +${GREEN}restore-vault.sh — Restaurar Vault Obsidian${NC} + +${YELLOW}Uso:${NC} + $0 list Lista backups disponíveis + $0 latest Restaura o backup mais recente + $0 Restaura backup específico (ex: 20250617_020000) + $0 dry-run Simula restauração (sem sobrescrever) + +${YELLOW}Exemplos:${NC} + $0 list + $0 latest + $0 20250617_020000 + +${YELLOW}Arquivos de backup:${NC} + $BACKUP_DIR/vault_YYYYMMDD_HHMMSS.tar.gz + +EOF +} + +# Listar backups disponíveis +list_backups() { + log "${BLUE}=== BACKUPS DISPONÍVEIS ===${NC}" + echo "" + if ls "$BACKUP_DIR"/vault_*.tar.gz 1>/dev/null 2>&1; then + echo -e "${GREEN}ID | Timestamp | Tamanho | Data${NC}" + echo -e "${GREEN}----|---------------------|---------|----------------${NC}" + ID=1 + for file in $(ls -t "$BACKUP_DIR"/vault_*.tar.gz); do + TIMESTAMP=$(basename "$file" | sed 's/vault_\(.*\)\.tar\.gz/\1/') + SIZE=$(du -h "$file" | cut -f1) + DATE=$(date -r "$file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown") + printf "${GREEN}%3d | %s | %6s | %s${NC}\n" "$ID" "$TIMESTAMP" "$SIZE" "$DATE" + ID=$((ID + 1)) + done + echo "" + log "Total: $(ls "$BACKUP_DIR"/vault_*.tar.gz | wc -l) backup(s)" + else + log "${RED}Nenhum backup encontrado em $BACKUP_DIR${NC}" + exit 1 + fi +} + +# Restaurar backup específico +restore_backup() { + local TIMESTAMP="$1" + local DRY_RUN="${2:-false}" + + # Encontrar arquivo de backup + if [ -n "$TIMESTAMP" ]; then + BACKUP_FILE="$BACKUP_DIR/vault_${TIMESTAMP}.tar.gz" + else + # Pegar o mais recente + BACKUP_FILE=$(ls -t "$BACKUP_DIR"/vault_*.tar.gz 2>/dev/null | head -1) + TIMESTAMP=$(basename "$BACKUP_FILE" | sed 's/vault_\(.*\)\.tar\.gz/\1/') + fi + + if [ ! -f "$BACKUP_FILE" ]; then + log "${RED}ERRO: Backup não encontrado: $(basename "$BACKUP_FILE")${NC}" + log "Use '$0 list' para ver backups disponíveis" + exit 1 + fi + + log "${YELLOW}=== INICIANDO RESTAURAÇÃO ===${NC}" + log "Backup: $(basename "$BACKUP_FILE")" + log "Tamanho: $(du -h "$BACKUP_FILE" | cut -f1)" + log "MD5: $(md5sum "$BACKUP_FILE" | cut -d' ' -f1)" + + if [ "$DRY_RUN" = "true" ]; then + log "${YELLOW}[DRY-RUN] Simulando extração...${NC}" + tar -tzf "$BACKUP_FILE" | head -20 + log "${YELLOW}[DRY-RUN] Total de arquivos: $(tar -tzf "$BACKUP_FILE" | wc -l)${NC}" + return + fi + + # Perguntar confirmação + echo "" + read -p "Isso irá substituir o vault atual em $VAULT_DIR. Continuar? (s/N): " CONFIRM + if [ "$CONFIRM" != "s" ] && [ "$CONFIRM" != "S" ]; then + log "Resturação cancelada pelo usuário" + exit 0 + fi + + # Criar backup do estado atual antes de restaurar + if [ -d "$VAULT_DIR" ]; then + PRE_RESTORE="$BACKUP_DIR/pre_restore_${DATE}_$(date +%Y%m%d_%H%M%S).tar.gz" + log "Criando backup do estado atual..." + tar -czf "$PRE_RESTORE" -C "$(dirname "$VAULT_DIR")" "$(basename "$VAULT_DIR")" 2>/dev/null || true + log "Backup de segurança: $(basename "$PRE_RESTORE")" + fi + + # Extrair backup + log "Extraindo backup..." + + # Backup do vault atual (se existir) + if [ -d "$VAULT_DIR" ]; then + TEMP_VAULT="${VAULT_DIR}_temp_$$" + mv "$VAULT_DIR" "$TEMP_VAULT" + fi + + # Extrair novo backup + mkdir -p "$VAULT_DIR" + if tar -xzf "$BACKUP_FILE" -C "$(dirname "$VAULT_DIR")"; then + # O tar extrai para ecommerce-kb/ dentro do diretório pai + EXTRACTED_DIR="$(dirname "$VAULT_DIR")/$(basename "$VAULT_DIR")" + if [ -d "$EXTRACTED_DIR" ] && [ "$EXTRACTED_DIR" != "$VAULT_DIR" ]; then + # Mover conteúdo extraído para o local correto + mv "$EXTRACTED_DIR"/* "$VAULT_DIR/" 2>/dev/null || true + rm -rf "$EXTRACTED_DIR" + fi + + # Limpar vault temporário + if [ -n "$TEMP_VAULT" ]; then + rm -rf "$TEMP_VAULT" + fi + + log "${GREEN}=== RESTAURAÇÃO CONCLUÍDA COM SUCESSO ===${NC}" + log "Vault restaurado: $VAULT_DIR" + log "Arquivos: $(find "$VAULT_DIR" -type f 2>/dev/null | wc -l)" + log "Backup usado: $(basename "$BACKUP_FILE")" + + # Atualizar status + if [ -f "$BACKUP_DIR/latest_status.json" ]; then + log "Arquivo de status atualizado." + fi + else + # Restaurar estado anterior em caso de erro + if [ -n "$TEMP_VAULT" ]; then + mv "$TEMP_VAULT" "$VAULT_DIR" + fi + log "${RED}ERRO: Falha ao extrair backup${NC}" + exit 1 + fi +} + +# ==================== MAIN ==================== + +case "${1:-}" in + list|l|ls) + list_backups + ;; + latest) + restore_backup "" + ;; + dry-run) + if [ -z "$2" ]; then + log "${RED}ERRO: Especifique o timestamp para dry-run${NC}" + exit 1 + fi + restore_backup "$2" "true" + ;; + help|-h|--help) + show_help + ;; + "") + if [ -f "$BACKUP_DIR/latest_status.json" ]; then + cat "$BACKUP_DIR/latest_status.json" + fi + echo "" + show_help + ;; + *) + restore_backup "$1" + ;; +esac diff --git a/scripts/calculators/marketplace_margin.py b/scripts/calculators/marketplace_margin.py new file mode 100755 index 0000000..f7d50e6 --- /dev/null +++ b/scripts/calculators/marketplace_margin.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +Calculadora de Margem Líquida por Marketplace +Calcula o lucro real por canal considerando todas as fees. + +Uso: + python calculators/marketplace_margin.py + python calculators/marketplace_margin.py --price 120 --cost 40 --marketplace shopee +""" + +import argparse +from rich.console import Console +from rich.table import Table +from rich.prompt import FloatPrompt + +console = Console() + +MARKETPLACES = { + "shopee": { + "fee": 0.18, + "fee_name": "Taxa de Venda Shopee", + "payment_fee": 0.029, + "payment_name": "Taxa de Pagamento", + "shipping_subsidy": 0.10, + "shipping_name": "Subsídio Frete (média)", + "pkg_fee": 0.03, + "pkg_name": "Embalagem (média)", + }, + "mercadolivre": { + "fee": 0.16, + "fee_name": "Taxa de Venda ML", + "payment_fee": 0.039, + "payment_name": "Taxa de Pagamento ML", + "shipping_subsidy": 0.10, + "shipping_name": "Subsídio Frete (média)", + "pkg_fee": 0.03, + "pkg_name": "Embalagem (média)", + }, + "amazon": { + "fee": 0.15, + "fee_name": "Referral Fee Amazon", + "payment_fee": 0.0, + "payment_name": "Taxa de Pagamento (incluída)", + "shipping_subsidy": 0.08, + "shipping_name": "Custo Envio FBA (média)", + "pkg_fee": 0.04, + "pkg_name": "FBA Pick & Pack (média)", + }, + "tiktok": { + "fee": 0.08, + "fee_name": "Taxa de Venda TikTok Shop", + "payment_fee": 0.019, + "payment_name": "Taxa de Pagamento", + "shipping_subsidy": 0.08, + "shipping_name": "Frete TikTok (média)", + "pkg_fee": 0.02, + "pkg_name": "Embalagem (média)", + }, + "magalu": { + "fee": 0.19, + "fee_name": "Taxa de Venda Magalu", + "payment_fee": 0.025, + "payment_name": "Taxa de Pagamento", + "shipping_subsidy": 0.11, + "shipping_name": "Subsídio Frete (média)", + "pkg_fee": 0.03, + "pkg_name": "Embalagem (média)", + }, + "site": { + "fee": 0.0, + "fee_name": "Fee Plataforma (Shopify ~2%)", + "payment_fee": 0.029, + "payment_name": "Taxa de Pagamento (Stripe/PagSeguro)", + "shipping_subsidy": 0.08, + "shipping_name": "Frete médio (Correios/próprio)", + "pkg_fee": 0.03, + "pkg_name": "Embalagem", + }, +} + + +def calculate_margin(price: float, cost: float, marketplace: dict, channel_name: str): + """Calcula margem para um canal.""" + gross = price - cost + + fee_amount = price * marketplace["fee"] + payment_amount = price * marketplace["payment_fee"] + shipping_amount = price * marketplace["shipping_subsidy"] + pkg_amount = price * marketplace["pkg_fee"] + + total_fees = fee_amount + payment_amount + shipping_amount + pkg_amount + net_profit = gross - total_fees + margin_pct = (net_profit / price * 100) if price > 0 else 0 + roi = ((net_profit / cost) * 100) if cost > 0 else 0 + + return { + "canal": channel_name, + "preco": price, + "custo": cost, + "lucro_bruto": gross, + "fee_venda": fee_amount, + "fee_pagto": payment_amount, + "fee_frete": shipping_amount, + "fee_pkg": pkg_amount, + "total_fees": total_fees, + "lucro_liquido": net_profit, + "margem_pct": margin_pct, + "roi_pct": roi, + } + + +def print_result(r: dict): + table = Table(title=f"📊 Margem — {r['canal']}", style="cyan") + table.add_column("Item", style="yellow") + table.add_column("Valor", justify="right", style="white") + + table.add_row("Preço de Venda", f"R$ {r['preco']:.2f}") + table.add_row("Custo do Produto", f"R$ {r['custo']:.2f}") + table.add_row("Lucro Bruto", f"R$ {r['lucro_bruto']:.2f}") + table.add_row(" Fee de Venda", f"− R$ {r['fee_venda']:.2f}") + table.add_row(" Fee de Pagamento", f"− R$ {r['fee_pagto']:.2f}") + table.add_row(" Subsídio Frete", f"− R$ {r['fee_frete']:.2f}") + table.add_row(" Embalagem", f"− R$ {r['fee_pkg']:.2f}") + table.add_row("TOTAL DE FEES", f"− R$ {r['total_fees']:.2f}", style="red") + table.add_row("LUCRO LÍQUIDO", f"R$ {r['lucro_liquido']:.2f}", style="green bold") + table.add_row("Margem Líquida", f"{r['margem_pct']:.1f}%", style="green") + table.add_row("ROI sobre Custo", f"{r['roi_pct']:.1f}%", style="cyan") + + console.print(table) + + +def print_comparison(price: float, cost: float): + """Compara todos os marketplaces.""" + table = Table(title="📊 Comparativo de Margem — Preço: R$ {:.2f}".format(price), style="cyan") + table.add_column("Canal", style="yellow") + table.add_column("Lucro Líquido", justify="right", style="white") + table.add_column("Margem %", justify="right") + table.add_column("ROI %", justify="right") + + results = [] + for name, config in MARKETPLACES.items(): + r = calculate_margin(price, cost, config, name.title()) + results.append(r) + + # Sort by profit + results.sort(key=lambda x: x["lucro_liquido"], reverse=True) + + for r in results: + style = "green" if r["lucro_liquido"] > 0 else "red" + table.add_row( + r["canal"], + f"R$ {r['lucro_liquido']:.2f}", + f"{r['margem_pct']:.1f}%", + f"{r['roi_pct']:.1f}%", + style=style, + ) + + console.print(table) + console.print() + console.print(f"[yellow]Custo do produto:[/yellow] R$ {cost:.2f} | [yellow]Margem bruta:[/yellow] {((price-cost)/price*100):.1f}%") + + +def main(): + parser = argparse.ArgumentParser(description="Calculadora de margem por marketplace") + parser.add_argument("--price", type=float, help="Preço de venda") + parser.add_argument("--cost", type=float, help="Custo do produto") + parser.add_argument("--marketplace", type=str, choices=list(MARKETPLACES.keys()) + ["all"], default="all") + args = parser.parse_args() + + if args.price and args.cost: + price = args.price + cost = args.cost + else: + console.print("[bold cyan]Calculadora de Margem por Marketplace[/bold cyan]\n") + price = FloatPrompt.ask("Preço de venda (R$)", default="120.0") + cost = FloatPrompt.ask("Custo do produto (R$)", default="40.0") + + if args.marketplace == "all": + print_comparison(price, cost) + else: + config = MARKETPLACES[args.marketplace] + r = calculate_margin(price, cost, config, args.marketplace.title()) + print_result(r) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/chatwoot-setup.sh b/scripts/chatwoot-setup.sh new file mode 100755 index 0000000..e6d4725 --- /dev/null +++ b/scripts/chatwoot-setup.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Chatwoot Setup Script +# Configurar integração Chatwoot + n8n + +set -e + +CHATWOOT_URL="https://chat.m5digital.net.br" +TOKEN="AwnRiNkenTfzmipP8sM4dewQ" +ACCOUNT_ID=1 + +echo "🔧 Chatwoot Setup" +echo "==================" + +# 1. Verificar status +echo -e "\n📊 Status da API:" +curl -s "${CHATWOOT_URL}/api" | jq '.' + +# 2. Listar agentes +echo -e "\n👥 Agentes:" +curl -s -H "api_access_token: ${TOKEN}" \ + "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/agents" | jq -r '.data[] | " - \(.name) (\(.role))"' + +# 3. Listar inboxes +echo -e "\n📥 Inboxes:" +INBOXES=$(curl -s -H "api_access_token: ${TOKEN}" \ + "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/inboxes") +echo "$INBOXES" | jq -r '.data[] | " - \(.name): \(.channel_type)"' || echo " Nenhum inbox" + +# 4. Criar Inbox Web (API) +echo -e "\n🔧 Criando Inbox Web..." +curl -s -X POST \ + -H "api_access_token: ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/inboxes" \ + -d '{ + "name": "Website", + "channel": { + "type": "web", + "website_url": "https://loja.m5digital.net.br" + } + }' | jq -r '.payload.name // .error // "Erro"' + +# 5. Listar Templates de Mensagens +echo -e "\n📝 Templates de Mensagens:" +curl -s -H "api_access_token: ${TOKEN}" \ + "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/message_templates" | jq -r '.data[] | " - \(.name)"' || echo " Nenhum template" + +# 6. Listar Canned Responses +echo -e "\n⚡ Respostas Prontas:" +curl -s -H "api_access_token: ${TOKEN}" \ + "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/canned_responses" | jq -r '.data[] | " - \(.short_code): \(.content[0:50])..."' || echo " Nenhuma resposta pronta" + +echo -e "\n✅ Setup completo!" +echo "" +echo "📋 Próximos passos:" +echo "1. Acesse ${CHATWOOT_URL}/app" +echo "2. Configure o widget de chat no website" +echo "3. Configure o webhook em n8n: ${CHATWOOT_URL}/api/v1/webhooks" +echo "4. Configure OpenWA como inbox adicional" diff --git a/scripts/compliance-check.sh b/scripts/compliance-check.sh new file mode 100755 index 0000000..6de74ad --- /dev/null +++ b/scripts/compliance-check.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# M5 - Compliance Check +# Data: 24/06/2026 +echo "Compliance Check - $(date)" +# Será implementado com validações diff --git a/scripts/compliance/compliance-checker.py b/scripts/compliance/compliance-checker.py new file mode 100755 index 0000000..fa1e7d2 --- /dev/null +++ b/scripts/compliance/compliance-checker.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +""" +compliance-checker.py — Motor Ecommerce M5 Brasil +Detecta palavras proibidas e verifica conformidade regulatória. +Usage: python3 compliance-checker.py --title "..." --description "..." + python3 compliance-checker.py --interactive +""" + +import json +import re +import sys +import unicodedata +from pathlib import Path +from typing import Optional + +# ───────────────────────────────────────────── +# 1. LISTAS DE PALAVRAS PROIBIDAS +# ───────────────────────────────────────────── + +PROHIBITED_ML = { + "replica", "clone de", "versão falsa", "cópia 1:1", "original fake", + "fake", "falsificado", "pirata", "genérico", "igual ao original", + "similar a", "semelhante a", + "sem nota", "notaomitida", "semnf", "não emito nota", "sem nota fiscal", + "nf free", "nota livre", "não emito", "isento de nota", + "remédio", "cura", "trata covid", "combate vírus", "cura naturalmente", + "tarja preta", "tarja vermelha", "antibiótico", "anti-inflamatório", + "analgésico", "receita médica", "com receita", "sem receita", + "combate bactérias", "elimina fungos", "trata pele", + "arma", "munição", "bala", "revólver", "pistola", "cartucho", + "espingarda", "carabina", "fuzil", "metralhadora", + "explosivo", "bomba", "dinamite", "pólvora", + "droga", "entorpecente", "haixe", "maconha", "cocaine", "crack", + "heroína", " LSD", "ecstasy", "MDMA", "substância controlada", "psicotrópico", + "lençol motel", "uso adulto", "18+", "erótico", + "animais silvestres", "fauna nativa", "ave silvestre", + "tartaruga marinha", "mico-leão", "peixe ornamental", +} + +PROHIBITED_SHOPEE = { + " original", " autêntico", "genuíno", "original de verdade", + "garantia de originalidade", + "melhor que", "superior a", "mais barato que", "pior que", + "comparado a", "na frente de", + "frete grátis", "frete gratuito", + "promoção", "desconto especial", "oferta exclusiva", "liquidação", + "Deus", "Jesus", "abençoado", "salvação", + "Bolsonaro", "Lula", "comunismo", "fascismo", "ESG", "woke", + "R$ 1", "R$ 0,01", "de R$ 999 por R$ 1", + "clube do preço baixo", "grupo de revenda", "compre junto e ganhe", "kit revenda", +} + +PROHIBITED_AMAZON = { + "made in China", "veio da China", "fabricado na China", + "chinês", # como insulto ou desmerecimento + "better than", "superior a", "worse than", + "best", "top 1", "número 1", + "mais vendido do Brasil", + "garantia vitalícia", "garantia de 10 anos", "garantia permanente", + "só hoje", "últimasunidades", "última peça", "último par", "hoje ou nunca", + "5 estrelas", "avaliado por dermatologistas", "clinicamente testado", +} + +ALL_PROHIBITED = { + "MercadoLivre": PROHIBITED_ML, + "Shopee": PROHIBITED_SHOPEE, + "Amazon": PROHIBITED_AMAZON, +} + +# ───────────────────────────────────────────── +# 2. CATEGORIAS REGULATÓRIAS — palavras-chave +# ───────────────────────────────────────────── + +ANVISA_COSMETICO = { + "protetor solar", "filtro solar", "fps", "creme para rosto", "creme para corpo", + "anti-idade", "clareador", "hidratante", "nutritivo", + "base", "batom", "sombra", "delineador", "máscara para cílios", + "perfume", "colônia", "tintura", "esmalte", "unha", "depilatório", + "desodorante", "shampoo", "condicionador", "queratina", "pantenol", + "maquiagem", "cosmético", "skincare", "serum", +} + +ANVISA_SUPLEMENTO = { + "whey", "caseína", "suplemento proteico", "termogênico", + "carnitina", "guaraná", "efedrina", "sinefrina", "capsicum", + "vitamina", "mineral", "comprimido", "cápsula", + "ômega 3", "probiótico", "nutracêutico", "nutraceutico", + "emagrecedor", "bloqueador de gordura", "orlistat", "quitosana", + "energético", "bcaa", "glutamina", "creatina", "colágeno", + "suplemento", "anabolizante", "esteroides", +} + +ANVISA_SANEANTE = { + "desinfetante", "sanitizante", "alvejante", "detergente bactericida", + "produto de limpeza", "limpeza", "antimicrobiano", "virucida", + "comodoro", "q-boa", "Veja", +} + +MAPA_ANIMAL = { + "mel", "própolis", "geleia real", "pólen", + "queijo", "laticínio", "leite", "manteiga", "nata", + "salsicha", "linguiça", "presunto", "bacon", "salame", "embutido", + "carne processada", "hambúrguer", "almôndega", + "ovo", "ovos", + "peixe", "pescado", "sardinha", "filé de peixe", + "geleia com mel", "geleia com animal", +} + +MAPA_VEGETAL = { + "café torrado", "café moído", "óleo de soja", "óleo de canola", + "óleo de girassol", "óleo vegetal", + "arroz parboilizado", "feijão industrializado", + "erva medicinal", "chá medicinal", + "orgânico", "produto orgânico", +} + +INMETRO_BRINQUEDO = { + "brinquedo", "brinq", "boneca", "boneco", "carro", "avião", + "jogo infantil", "quebra-cabeça", "puzzle", "monta", + "bola", "roda", "infantil", "criança", +} + +INMETRO_PUERICULTURA = { + "cadeirinha auto", "berço", "carrinho de bebê", "alcofa", + "moisés", "porta-bebê", "bebê conforto", +} + +INMETRO_ELETRICO = { + "filtro de linha", "estabilizador", "nobreak", "no-break", + "lâmpada led", "lampada led", "led", + "capacete", "capacete de moto", "capacete motociclista", + "colchão", "extintor", +} + +# ───────────────────────────────────────────── +# 3. PRODUTOS SEMPRE PROIBIDOS (não vender) +# ───────────────────────────────────────────── + +ALWAYS_BLOCK = { + "cigarro eletrônico", "vaper", "vaporizador", + "esteroides", "anabolizante", + "thc", "maconha", "haixe", + "armamento", "arma de fogo", "munição", + "explosivo", "bomba", +} + + +# ───────────────────────────────────────────── +# 4. FUNÇÕES AUXILIARES +# ───────────────────────────────────────────── + +def normalize(text: str) -> str: + """Remove acentos e converte para minúsculas.""" + text = text.lower() + text = unicodedata.normalize("NFD", text) + text = re.sub(r"[\u0300-\u036f]", "", text) + return text + + +def strip_punctuation(text: str) -> str: + """Remove pontuação para busca parcial.""" + return re.sub(r"[^\w\s]", " ", text) + + +def check_wordlists(text: str, wordlist: set, label: str) -> list[dict]: + """Retorna violações de uma wordlist.""" + violations = [] + text_norm = normalize(text) + text_clean = strip_punctuation(text_norm) + for word in wordlist: + word_norm = normalize(word) + # Match whole word + pattern = r"\b" + re.escape(word_norm) + r"\b" + if re.search(pattern, text_clean): + violations.append({ + "word": word, + "list": label, + "severity": "HIGH", + }) + return violations + + +def detect_category(text: str) -> dict[str, bool]: + """Heurística simples para detectar categoria regulatória.""" + text_lower = text.lower() + text_norm = normalize(text_lower) + text_clean = strip_punctuation(text_norm) + + return { + "anvisa_cosmetico": any(k in text_clean for k in [normalize(k) for k in ANVISA_COSMETICO]), + "anvisa_suplemento": any(k in text_clean for k in [normalize(k) for k in ANVISA_SUPLEMENTO]), + "anvisa_saneante": any(k in text_clean for k in [normalize(k) for k in ANVISA_SANEANTE]), + "mapa_animal": any(k in text_clean for k in [normalize(k) for k in MAPA_ANIMAL]), + "mapa_vegetal": any(k in text_clean for k in [normalize(k) for k in MAPA_VEGETAL]), + "inmetro_brinquedo": any(k in text_clean for k in [normalize(k) for k in INMETRO_BRINQUEDO]), + "inmetro_puericultura": any(k in text_clean for k in [normalize(k) for k in INMETRO_PUERICULTURA]), + "inmetro_eletrico": any(k in text_clean for k in [normalize(k) for k in INMETRO_ELETRICO]), + } + + +def check_always_block(text: str) -> list[dict]: + """Verifica produtos sempre bloqueados.""" + violations = [] + text_clean = normalize(strip_punctuation(normalize(text))) + for term in ALWAYS_BLOCK: + term_norm = normalize(term) + pattern = r"\b" + re.escape(term_norm) + r"\b" + if re.search(pattern, text_clean): + violations.append({ + "word": term, + "list": "ALWAYS_BLOCK", + "severity": "CRITICAL", + }) + return violations + + +# ───────────────────────────────────────────── +# 5. SCORING +# ───────────────────────────────────────────── + +WEIGHTS = { + "ALWAYS_BLOCK": -100, # sempre bloqueia + "MercadoLivre": -8, + "Shopee": -8, + "Amazon": -8, + "anvisa_cosmetico": -5, + "anvisa_suplemento": -5, + "anvisa_saneante": -5, + "mapa_animal": -5, + "mapa_vegetal": -5, + "inmetro_brinquedo": -5, + "inmetro_puericultura": -5, + "inmetro_eletrico": -5, +} + + +def compute_score(violations: list[dict], detected_categories: dict) -> dict: + """Calcula score 0-100.""" + score = 100 + + for v in violations: + key = v.get("list", "unknown") + if key in WEIGHTS: + score += WEIGHTS[key] # já são negativos + + # Penalidades por categoria detectada (sem registro declarado) + for cat, detected in detected_categories.items(): + if detected and cat in WEIGHTS: + score += WEIGHTS[cat] + + score = max(0, min(100, score)) + return score + + +def decision(score: int) -> str: + if score >= 90: + return "PUBLICAR" + elif score >= 70: + return "REVISAR" + elif score >= 40: + return "REVISAR_OBRIGATORIO" + else: + return "BLOQUEADO" + + +# ───────────────────────────────────────────── +# 6. RECOMENDAÇÕES +# ───────────────────────────────────────────── + +RECOMMENDATIONS = { + "anvisa_cosmetico": "⚠️ Cosmético — exigir número de Registro ANVISA antes de publicar (RDC 685/2018)", + "anvisa_suplemento": "⚠️ Suplemento — exigir número de Registro ANVISA antes de publicar (RDC 26/2015)", + "anvisa_saneante": "⚠️ Saneante — exigir registro ANVISA antes de publicar", + "mapa_animal": "⚠️ Produto animal — exige SIF (venda interestadual) ou SIE (venda estadual). Verificar com MAPA.", + "mapa_vegetal": "⚠️ Produto vegetal processado — pode exigir registro MAPA ou estadual.", + "inmetro_brinquedo": "⚠️ Brinquedo — exige certificação INMETRO compulsória.", + "inmetro_puericultura": "⚠️ Artigo de puericultura — exige certificação INMETRO.", + "inmetro_eletrico": "⚠️ Equipamento elétrico — exige certificação de segurança INMETRO.", +} + + +# ───────────────────────────────────────────── +# 7. MAIN +# ───────────────────────────────────────────── + +def run_check(title: str, description: str = "", category_hint: str = "") -> dict: + """ + Verifica título e descrição contra todas as listas. + Retorna dict JSON com score, violações e recomendações. + """ + full_text = f"{title} {description}" + + # Coleta violações + all_violations = [] + + for marketplace, wordlist in ALL_PROHIBITED.items(): + all_violations.extend(check_wordlists(full_text, wordlist, marketplace)) + + all_violations.extend(check_always_block(full_text)) + + # Detecta categorias + detected = detect_category(full_text) + + # Se categoria foi informada manualmente + if category_hint: + cat_map = { + "cosmetico": "anvisa_cosmetico", + "suplemento": "anvisa_suplemento", + "saneante": "anvisa_saneante", + "animal": "mapa_animal", + "vegetal": "mapa_vegetal", + "brinquedo": "inmetro_brinquedo", + "puericultura": "inmetro_puericultura", + "eletrico": "inmetro_eletrico", + } + hint_key = cat_map.get(category_hint.lower().strip()) + if hint_key: + detected[hint_key] = True + + # Score + score = compute_score(all_violations, detected) + dec = decision(score) + + # Recomendações + recommendations = [] + for cat, active in detected.items(): + if active and cat in RECOMMENDATIONS: + recommendations.append(RECOMMENDATIONS[cat]) + + # Violações críticas + critical = [v for v in all_violations if v.get("severity") == "CRITICAL"] + if critical: + recommendations.insert(0, "🚫 PRODUTO SEMPRE BLOQUEADO — Não publar. Verificar se é produto proibido (armas, drogas, cigarros eletrônicos, etc.)") + + return { + "score": score, + "decision": dec, + "violations": all_violations, + "violation_count": len(all_violations), + "detected_categories": {k: v for k, v in detected.items() if v}, + "recommendations": recommendations, + "check_passed": dec in ("PUBLICAR", "REVISAR"), + } + + +def interactive(): + print("=== COMPLIANCE CHECKER — Ecommerce M5 Brasil ===\n") + print("Pressione ENTER para pular qualquer campo.\n") + + title = input("Título do produto: ").strip() + if not title: + print("Título é obrigatório. Encerrando.") + return + + description = input("Descrição do produto (ou ENTER para vazio): ").strip() + category = input( + "Categoria (opcional — ajuda na detecção):\n" + " cosmetico | suplemento | saneante | animal | vegetal\n" + " brinquedo | puericultura | eletrico | ENTER=pular\n" + " >> " + ).strip() + + result = run_check(title, description, category) + print("\n" + json.dumps(result, ensure_ascii=False, indent=2)) + + +def main(): + args = sys.argv[1:] + + if "--interactive" in args or len(args) == 0: + interactive() + return + + # Parse arguments + title = "" + description = "" + category = "" + + i = 0 + while i < len(args): + if args[i] in ("--title", "-t"): + title = args[i + 1] + i += 2 + elif args[i] in ("--description", "-d"): + description = args[i + 1] + i += 2 + elif args[i] in ("--category", "-c"): + category = args[i + 1] + i += 2 + elif args[i] == "--interactive": + interactive() + return + else: + i += 1 + + if not title: + print("Erro: --title é obrigatório.", file=sys.stderr) + print("Uso: python3 compliance-checker.py --title '...' [--description '...'] [--category 'cosmetico'] [--interactive]") + sys.exit(1) + + result = run_check(title, description, category) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/docker-compose.m5.yml b/scripts/docker-compose.m5.yml new file mode 100644 index 0000000..5102751 --- /dev/null +++ b/scripts/docker-compose.m5.yml @@ -0,0 +1,304 @@ +# docker-compose.m5.yml — Motor Ecommerce M5 Brasil +# Versão atualizada conforme manual Saleor 3.23 + +services: + + # ── Traefik (Proxy Reverso) ───────────────────────── + traefik: + image: traefik:v3.0 + container_name: m5_traefik + restart: unless-stopped + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.myresolver.acme.httpchallenge=true + - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + - --certificatesresolvers.myresolver.acme.email=irisflorencio@yahoo.com.br + - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json + ports: + - "80:80" + - "443:443" + - "8090:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik-acme:/letsencrypt + - ./traefik-conf/traefik.yml:/etc/traefik/traefik.yml:ro + - ./traefik-conf/routes.yml:/etc/traefik/routes.yml:ro + - /etc/letsencrypt:/certs:ro + - ./traefik-conf/traefik.yml:/etc/traefik/traefik.yml:ro + - ./traefik-conf/routes.yml:/etc/traefik/routes.yml:ro + - /etc/letsencrypt:/certs:ro + networks: + - m5_network + labels: + - "traefik.enable=false" + + # ── Banco de dados principal ────────────────────────── + postgres: + image: postgres:16-alpine + container_name: m5_postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-m5ecommerce} + POSTGRES_USER: ${POSTGRES_USER:-m5user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - m5_network + labels: + - "traefik.enable=false" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-m5user}"] + interval: 10s + timeout: 5s + retries: 5 + + # ── Cache / Session ────────────────────────────────── + redis: + image: redis:7-alpine + container_name: m5_redis + restart: unless-stopped + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + networks: + - m5_network + labels: + - "traefik.enable=false" + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + + # ── Saleor API ─────────────────────────────────────── + saleor: + image: ghcr.io/saleor/saleor:3.23 + container_name: m5_saleor + restart: unless-stopped + entrypoint: ["/app/entrypoint.sh"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.saleor-api.rule=Host(`loja.m5digital.net.br`)" + - "traefik.http.routers.saleor-api.entrypoints=websecure" + - "traefik.http.routers.saleor-api.tls=true" + - "traefik.http.services.saleor-api.loadbalancer.server.port=8000" + environment: + - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-m5ecommerce} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 + - CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 + - SECRET_KEY=${SALEOR_SECRET_KEY} + - ALLOWED_HOSTS=loja.m5digital.net.br,localhost,127.0.0.1,dashboard.m5digital.net.br + - ALLOWED_GRAPHQL_ORIGINS=https://dashboard.m5digital.net.br,https://loja.m5digital.net.br,http://localhost:3000 + - DEFAULT_MAIL_FROM=${DEFAULT_EMAIL_FROM} + - EMAIL_URL=${EMAIL_URL} + - RSA_PRIVATE_KEY=/app/media/RSA_PRIVATE_KEY.pem + - STRIPE_API_KEY=${STRIPE_API_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + - MERCADOPAGO_ACCESS_TOKEN=${MERCADOPAGO_ACCESS_TOKEN} + - MERCADOPAGO_WEBHOOK_SECRET=${MERCADOPAGO_WEBHOOK_SECRET} + volumes: + - ./saleor-media:/app/media + - ./entrypoint.sh:/app/entrypoint.sh:ro + networks: + - m5_network + + # ── Saleor Worker (Celery) ───────────────────────── + worker: + image: ghcr.io/saleor/saleor:3.23 + container_name: m5_saleor_worker + restart: unless-stopped + command: celery -A saleor worker --loglevel=info + depends_on: + - postgres + - redis + environment: + - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-m5ecommerce} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 + - CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 + - SECRET_KEY=${SALEOR_SECRET_KEY} + volumes: + - saleor-media:/app/media + networks: + - m5_network + labels: + - "traefik.enable=false" + + # ── Saleor Dashboard ───────────────────────────────── + saleor-dashboard: + image: ghcr.io/saleor/saleor-dashboard:latest + container_name: m5_saleor_dashboard + restart: unless-stopped + environment: + - API_URL=https://loja.m5digital.net.br/graphql/ + - APP_MOUNT_URI=/ + - NODE_ENV=production + networks: + - m5_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.saleor-dash.rule=Host(`dashboard.m5digital.net.br`)" + - "traefik.http.routers.saleor-dash.entrypoints=websecure" + - "traefik.http.routers.saleor-dash.tls=true" + - "traefik.http.services.saleor-dash.loadbalancer.server.port=80" + + # ── n8n (Automação) ────────────────────────────────── + n8n: + image: n8nio/n8n:${N8N_VERSION:-1.70} + container_name: m5_n8n + restart: unless-stopped + depends_on: + - postgres + environment: + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=${N8N_DB:-n8n} + - DB_POSTGRESDB_USER=${POSTGRES_USER:-m5user} + - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=${N8N_USER} + - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD} + - N8N_HOST=https://n8n.m5digital.net.br + - WEBHOOK_URL=https://n8n.m5digital.net.br/ + - N8N_PROTOCOL=https + - N8N_TRUST_PROXY=true + - GENERIC_TIMEZONE=America/Sao_Paulo + - WA_AUTOCARRIER_URL=http://m5_openwa:8080 + - SALEOR_API_URL=https://loja.m5digital.net.br/graphql/ + volumes: + - n8n_data:/home/node/.n8n + - ./n8n-workflows:/workflows + networks: + - m5_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.n8n.rule=Host(`n8n.m5digital.net.br`)" + - "traefik.http.routers.n8n.entrypoints=websecure" + - "traefik.http.routers.n8n.tls=true" + - "traefik.http.services.n8n.loadbalancer.server.port=5678" + + # ── OpenWA (WhatsApp) ──────────────────────────────── + openwa: + image: ghcr.io/rmyndharis/openwa:latest + container_name: m5_openwa + restart: unless-stopped + environment: + - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + - PUPPETEER_EXECUTABLE_PATH=/opt/google/chrome/chrome + - CHROME_PATH=/opt/google/chrome/chrome + - WA_CHROMIUM_FLAGS=--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --no-zygote --single-process + - WA_USE_CHROME=true + - MULTI_DEVICE=true + - READ_MESSAGES=true + - GROUP_PARTICIPANTS=true + - WEBHOOK_URL=${WA_WEBHOOK_URL:-https://n8n.m5digital.net.br/webhook/wa} + env_file: + - ./openwa.env + ports: + - "2785:2785" + - "8080:8080" + - "8081:8081" + - "8002:8002" + volumes: + - openwa_data:/var/lib/wa-automate + - ./openwa-sessions:/sessions + networks: + - m5_network + security_opt: + - seccomp=unconfined + labels: + - "traefik.enable=true" + - "traefik.http.routers.openwa.rule=Host(`wa.m5digital.net.br`)" + - "traefik.http.routers.openwa.entrypoints=websecure" + - "traefik.http.routers.openwa.tls=true" + - "traefik.http.services.openwa.loadbalancer.server.port=2785" + + # ── Gitea (Git) ───────────────────────────────────── + gitea: + image: gitea/gitea:${GITEA_VERSION:-1.22} + container_name: m5_gitea + restart: unless-stopped + ports: + - "2222:22" + depends_on: + - postgres + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=postgres:5432 + - GITEA__database__NAME=${GITEA_DB:-gitea} + - GITEA__database__USER=${POSTGRES_USER:-m5user} + - GITEA__database__PASSWD=${POSTGRES_PASSWORD} + - GITEA__server__PROTOCOL=http + - GITEA__server__ROOT_URL=https://git.m5digital.net.br/ + - GITEA__server__DOMAIN=git.m5digital.net.br + - GITEA__security__INSTALL_LOCK=true + - GITEA__service__DISABLE_REGISTRATION=false + volumes: + - gitea_data:/data + - ./gitea-repos:/git/repositories + networks: + - m5_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.gitea.rule=Host(`git.m5digital.net.br`)" + - "traefik.http.routers.gitea.entrypoints=websecure" + - "traefik.http.routers.gitea.tls=true" + - "traefik.http.services.gitea.loadbalancer.server.port=3000" + + # ── yt-pub-livesx ────────────────────────────────── + yt-pub-livesx-dashboard: + image: yt-pub-livesx-dashboard + container_name: yt-pub-livesx-dashboard-1 + restart: unless-stopped + environment: + - TZ=America/Sao_Paulo + ports: + - "8091:8091" + volumes: + - ./yt-pub-livesx/data:/app/data + - ./yt-pub-livesx/credentials.enc:/app/credentials.enc:ro + networks: + - m5_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.video.rule=Host(`video.m5digital.net.br`)" + - "traefik.http.routers.video.entrypoints=websecure" + - "traefik.http.routers.video.tls=true" + - "traefik.http.services.video.loadbalancer.server.port=8091" + + yt-pub-livesx-scheduler: + image: yt-pub-livesx-scheduler + container_name: yt-pub-livesx-scheduler-1 + restart: unless-stopped + environment: + - TZ=America/Sao_Paulo + volumes: + - ./yt-pub-livesx/data:/app/data + - ./yt-pub-livesx/credentials.enc:/app/credentials.enc:ro + networks: + - m5_network + +networks: + m5_network: + name: scripts_m5_network + external: true + +volumes: + postgres_data: + redis_data: + saleor-media: + gitea_data: + n8n_data: + openwa_data: + traefik-acme: diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 0000000..40efd19 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Ler RSA key do arquivo ou da variável de ambiente +if [ -n "$RSA_PRIVATE_KEY" ]; then + if [ -f "$RSA_PRIVATE_KEY" ]; then + # É um caminho de arquivo - ler o conteúdo + echo "Lendo RSA key do arquivo: $RSA_PRIVATE_KEY" + RSA_PRIVATE_KEY_CONTENT=$(cat "$RSA_PRIVATE_KEY") + export RSA_PRIVATE_KEY="$RSA_PRIVATE_KEY_CONTENT" + elif [[ "$RSA_PRIVATE_KEY" == *-----BEGIN* ]]; then + echo "RSA key já está inline" + else + # Provavelmente base64 encoded + echo "Decodificando RSA key base64..." + python3 -c " +import base64, os +key = os.environ.get('RSA_PRIVATE_KEY', '') +if key: + with open('/tmp/RSA.pem', 'wb') as f: + f.write(base64.b64decode(key)) + os.chmod('/tmp/RSA.pem', 0o600) +" + if [ -f /tmp/RSA.pem ]; then + RSA_PRIVATE_KEY_CONTENT=$(cat /tmp/RSA.pem) + export RSA_PRIVATE_KEY="$RSA_PRIVATE_KEY_CONTENT" + fi + fi +fi + +exec uvicorn saleor.asgi:application --host 0.0.0.0 --port 8000 --workers 2 --timeout-keep-alive 35 \ No newline at end of file diff --git a/scripts/health-check.sh b/scripts/health-check.sh new file mode 100755 index 0000000..b0706a3 --- /dev/null +++ b/scripts/health-check.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# M5 - Health Check Docker +# Data: 24/06/2026 +echo "Health Check - $(date)" +# Será implementado com docker ps/health diff --git a/scripts/import-chatwoot-workflow.sh b/scripts/import-chatwoot-workflow.sh new file mode 100644 index 0000000..38d1c11 --- /dev/null +++ b/scripts/import-chatwoot-workflow.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Importar Chatwoot Workflow para n8n + +set -e + +N8N_URL="https://n8n.m5digital.net.br" +N8N_USER="admin" +N8N_PASSWORD="lKs9DA7S_PL7xv-YrQzi7eFmC8CcGKPrpceGxOqnYOY" +WORKFLOW_FILE="/root/ecommerce/workflows/chatwoot-integration.json" + +echo "🔄 Importando Chatwoot Workflow..." + +# 1. Fazer login +JWT_TOKEN=$(curl -s -X POST "${N8N_URL}/webhook-test" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"${N8N_USER}\",\"password\":\"${N8N_PASSWORD}\"}" | \ + jq -r '.data.result.data.jwt') + +if [ "$JWT_TOKEN" = "null" ]; then + echo "❌ Falha ao fazer login no n8n" + exit 1 +fi + +# 2. Importar workflow +RESPONSE=$(curl -s -X POST "${N8N_URL}/workflows/import" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"workflowData\":$(cat ${WORKFLOW_FILE})}") + +# 3. Verificar resultado +WORKFLOW_ID=$(echo "$RESPONSE" | jq -r '.workflow.id') +if [ "$WORKFLOW_ID" != "null" ]; then + echo "✅ Workflow importado com sucesso!" + echo "ID: ${WORKFLOW_ID}" + echo "URL: ${N8N_URL}/workflows/${WORKFLOW_ID}" +else + echo "❌ Falha ao importar workflow" + echo "$RESPONSE" +fi + +# 4. Ativar webhook no Chatwoot +echo -e "\n🔧 Configurando webhook no Chatwoot..." + +WEBHOOK_PAYLOAD='{ + "url": "https://n8n.m5digital.net.br/webhook/chatwoot", + "events": ["conversation.created", "message.created"], + "secret": "M5ChatwootSecret2026" +}' + +curl -s -X POST "https://chat.m5digital.net.br/api/v1/accounts/1/webhooks" \ + -H "api_access_token: AwnRiNkenTfzmipP8sM4dewQ" \ + -H "Content-Type: application/json" \ + -d "$WEBHOOK_PAYLOAD" | jq -r '.payload.id // .error' + +echo -e "\n✅ Integração configurada!" \ No newline at end of file diff --git a/scripts/insert_workflow.py b/scripts/insert_workflow.py new file mode 100644 index 0000000..f841bb3 --- /dev/null +++ b/scripts/insert_workflow.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +import json +import uuid +import psycopg2 + +WORKFLOW_ID = "fa489bed-a0ac-4856-a38e-e78bed2adf9d" +WEBHOOK_ID = "whatsapp-messages" +PROJECT_ID = "NHIYHwVpHURSGRFV" + +nodes = [ + { + "parameters": {"httpMethod": "POST", "path": "whatsapp-messages", "responseMode": "lastNode", "options": {}}, + "id": "wh1", "name": "Webhook WhatsApp", "type": "n8n-nodes-base.webhook", + "typeVersion": 2, "position": [250, 300], "webhookId": WEBHOOK_ID + }, + { + "parameters": {"jsCode": "const d=$input.item.json;const e=d.event||'';const m=d.data||{};const t=m.hasMedia?'media':m.body?'text':e.includes('session')?'session':'other';return[{json:{...d,messageType:t}}];"}, + "id": "cl1", "name": "Classificar Mensagem", "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [500, 300] + }, + { + "parameters": {"jsCode": "console.log(JSON.stringify($input.item.json));return $input.all();"}, + "id": "db1", "name": "Log Mensagem", "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [750, 300] + }, + { + "parameters": {"jsCode": "return[{json:{received:true,timestamp:new Date().toISOString()}}];"}, + "id": "ok1", "name": "Responder 200", "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [1000, 300] + } +] + +connections = { + "Webhook WhatsApp": {"main": [[{"node": "Classificar Mensagem", "type": "main", "index": 0}]]}, + "Classificar Mensagem": {"main": [[{"node": "Log Mensagem", "type": "main", "index": 0}]]}, + "Log Mensagem": {"main": [[{"node": "Responder 200", "type": "main", "index": 0}]]} +} + +settings = {"executionOrder": "v1", "saveManualExecutions": True, "callerPolicy": "workflowsFromSameOwner"} + +conn = psycopg2.connect( + host="m5_postgres", + dbname="n8n", + user="m5user", + password="YXnJMMqAZ5uZrFj2lMo_AnhEmQq4uVoCfOuXaG7pU0Y", + port=5432 +) +cur = conn.cursor() + +# Insert workflow +cur.execute(""" +INSERT INTO workflow_entity ( + name, active, nodes, connections, settings, staticData, pinData, + versionId, triggerCount, id, meta, parentFolderId, isArchived, versionCounter, + description, activeVersionId, createdAt, updatedAt +) VALUES ( + %s, %s, %s, %s, %s, NULL, NULL, + %s, %s, %s, NULL, NULL, %s, %s, + %s, NULL, NOW(), NOW() +) +ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + active = EXCLUDED.active, + nodes = EXCLUDED.nodes, + connections = EXCLUDED.connections, + settings = EXCLUDED.settings, + updatedAt = NOW() +""", ( + "WhatsApp Messages - OpenWA", + True, + json.dumps(nodes), + json.dumps(connections), + json.dumps(settings), + "1", 1, WORKFLOW_ID, + False, 1, + "Recebe webhooks do OpenWA e salva no banco" +)) + +# Insert webhook entity +wh_entity_id = str(uuid.uuid4()) +cur.execute(""" +INSERT INTO webhook_entity ( + id, webhookId, workflowId, method, path, node, conditions, position, + retries, timeout, keywords, redactUrl, redactHeaders, redactBody, createdAt +) VALUES ( + %s, %s, %s, %s, %s, %s, NULL, %s, + 3, NULL, NULL, false, NULL, false, NOW() +) +ON CONFLICT (webhookId) DO UPDATE SET + workflowId = EXCLUDED.workflowId, + updatedAt = NOW() +""", ( + wh_entity_id, WEBHOOK_ID, WORKFLOW_ID, + "POST", "/webhook/whatsapp-messages", "Webhook WhatsApp", + json.dumps([250, 300]) +)) + +# Insert shared_workflow +cur.execute(""" +INSERT INTO shared_workflow (workflowId, projectId, role, createdAt, updatedAt) +VALUES (%s, %s, %s, NOW(), NOW()) +ON CONFLICT (workflowId, projectId) DO NOTHING +""", (WORKFLOW_ID, PROJECT_ID, "workflow")) + +conn.commit() +cur.close() +conn.close() +print(f"Workflow criado: {WORKFLOW_ID}") +print(f"Webhook ID: {WEBHOOK_ID}") +print("Pronto!") diff --git a/scripts/minio-compose.yml b/scripts/minio-compose.yml new file mode 100644 index 0000000..a0ac92c --- /dev/null +++ b/scripts/minio-compose.yml @@ -0,0 +1,62 @@ +version: '3.8' + +services: + minio: + image: minio/minio:latest + container_name: m5_minio + hostname: minio + networks: + - m5_network + ports: + - "9000:9000" # API + - "9001:9001" # Console + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123} + MINIO_SITE_NAME: "M5 Digital MinIO" + MINIO_SITE_REGION: "us-east-1" + volumes: + - ./minio/data:/data + - ./minio/config:/root/.minio + command: server /data --console-address ":9001" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 30s + timeout: 20s + retries: 3 + restart: unless-stopped + + minio-init: + image: minio/mc:latest + container_name: m5_minio_init + networks: + - m5_network + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + sleep 5; + mc alias set myminio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; + mc anonymous set download myminio/public; + mc admin user add myminio ${MINIO_SERVICE_USER:-crmuser} ${MINIO_SERVICE_PASSWORD:-crmpass123}; + mc admin policy attach myminio readwrite --user ${MINIO_SERVICE_USER:-crmuser}; + mc mb myminio/hermes-crm --ignore-existing; + mc mb myminio/hermes-crm/avatars --ignore-existing; + mc mb myminio/hermes-crm/documents --ignore-existing; + mc mb myminio/hermes-crm/attachments --ignore-existing; + mc mb myminio/hermes-crm/backups --ignore-existing; + mc mb myminio/hermes-crm/exports --ignore-existing; + mc mb myminio/hermes-crm/temp --ignore-existing; + mc mb myminio/hermes-ecom --ignore-existing; + mc mb myminio/hermes-ecom/products --ignore-existing; + mc mb myminio/hermes-ecom/images --ignore-existing; + mc mb myminio/hermes-ecom/documents --ignore-existing; + echo 'MinIO initialized successfully'; + exit 0; + " + +networks: + m5_network: + name: scripts_m5_network + external: true diff --git a/scripts/openwa-compose.yml b/scripts/openwa-compose.yml new file mode 100644 index 0000000..5a8610a --- /dev/null +++ b/scripts/openwa-compose.yml @@ -0,0 +1,67 @@ +# OpenWA v0.5.0 - Production compose adapted for m5 stack +# Base: docker-compose.dev.yml (rmyndharis/OpenWA) + +services: + openwa: + image: ghcr.io/rmyndharis/openwa:latest + container_name: m5_openwa + restart: unless-stopped + security_opt: + - 'no-new-privileges:true' + cap_add: + - SYS_ADMIN + - CHOWN + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + shm_size: '512mb' + environment: + - NODE_ENV=production + - PORT=2785 + - HOME=/tmp + - XDG_CONFIG_HOME=/tmp/.config + - XDG_CACHE_HOME=/tmp/.cache + - DATABASE_TYPE=sqlite + - DATABASE_NAME=/app/data/openwa.sqlite + - DATABASE_SYNCHRONIZE=false + - DATABASE_LOGGING=false + - ENGINE_TYPE=whatsapp-web.js + - SESSION_DATA_PATH=/app/data/sessions + - PUPPETEER_HEADLESS=true + - PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu,--disable-software-rasterizer + - PUPPETEER_PROTOCOL_TIMEOUT=120000 + - PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium + - STORAGE_TYPE=local + - STORAGE_LOCAL_PATH=/app/data/media + - WEBHOOK_TIMEOUT=10000 + - WEBHOOK_MAX_RETRIES=3 + - WEBHOOK_RETRY_DELAY=5000 + - QUEUE_ENABLED=false + - AUTO_START_SESSIONS=false + - LOG_LEVEL=info + volumes: + - m5_openwa_data:/app/data + networks: + - scripts_m5_network + healthcheck: + test: ['CMD', 'node', '-e', "require('http').get('http://localhost:2785/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + labels: + - "traefik.enable=true" + - "traefik.http.routers.openwa.rule=Host(`wa.m5digital.net.br`)" + - "traefik.http.routers.openwa.entrypoints=websecure" + - "traefik.http.routers.openwa.tls=true" + - "traefik.http.routers.openwa.service=openwa" + - "traefik.http.services.openwa.loadbalancer.server.port=2785" + +volumes: + m5_openwa_data: + name: m5_openwa_data + +networks: + scripts_m5_network: + external: true diff --git a/scripts/openwa.env b/scripts/openwa.env new file mode 100644 index 0000000..972cd69 --- /dev/null +++ b/scripts/openwa.env @@ -0,0 +1,3 @@ + +PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu +PUPPETEER_HEADLESS=true diff --git a/scripts/operacao_completa.sh b/scripts/operacao_completa.sh new file mode 100755 index 0000000..40c1e7c --- /dev/null +++ b/scripts/operacao_completa.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# AGENTE MESTRE - OPERAÇÃO COMPLETA +# Coordenar verificação, testes, integrações e documentação + +echo "🚀 INICIANDO OPERAÇÃO COMPLETA - $(date)" + +# 1. Verificar todas as instâncias +echo "🔍 Verificando instâncias..." +curl -s -o /dev/null -w "%{http_code} " https://crm5.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://chat.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://n8n.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://loja.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://dashboard.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://git.m5digital.net.br +curl -s -o /dev/null -w "%{http_code} " https://video.m5digital.net.br + +# 2. Análise de logs +echo -e "\n📋 Analisando logs..." +docker ps --format "table {{.Names}}\t{{.Status}}" +docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Status}}" + +# 3. Testes de conectividade +echo -e "\n🌐 Testando conectividade..." +docker exec m5_traefik ping -c 2 chatwoot-chatwoot-1 +docker exec m5_traefik ping -c 2 twenty_front +docker exec m5_traefik ping -c 2 m5_saleor + +# 4. Verificar n8n workflows +echo -e "\n🔄 Verificando n8n..." +curl -s https://n8n.m5digital.net.br/api/v1/workflows | jq '.data[] | {id, name, active}' 2>/dev/null || echo "n8n não acessível" + +# 5. Gerar relatório +echo -e "\n📊 Gerando relatório..." +cat > /tmp/relatorio_operacao.txt << EOF +RELATÓRIO DE OPERAÇÃO COMPLETA - $(date) + +INSTÂNCIAS: +- Twenty CRM: https://crm5.m5digital.net.br +- Chatwoot: https://chat.m5digital.net.br +- n8n: https://n8n.m5digital.net.br +- Saleor: https://loja.m5digital.net.br +- Traefik: https://dashboard.m5digital.net.br + +CONTAINERS ATIVOS: +$(docker ps --format "{{.Names}} - {{.Status}}") + +PENDENTES: +$(docker ps -a --filter "status=exited" --format "{{.Names}} - {{.Status}}") + +INTEGRAÇÕES: +- n8n ↔ Chatwoot: A configurar +- n8n ↔ Twenty: A configurar +- n8n ↔ Saleor: A configurar +EOF + +echo "✅ Relatório gerado em /tmp/relatorio_operacao.txt" +echo "🎉 OPERAÇÃO COMPLETA FINALIZADA!" \ No newline at end of file diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..8b89e5d --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,7 @@ +requests>=2.31.0 +pandas>=2.1.0 +openpyxl>=3.1.0 +python-dotenv>=1.0.0 +rich>=13.7.0 +tabulate>=0.9.0 +loguru>=0.7.0 \ No newline at end of file diff --git a/scripts/resumo-diario.sh b/scripts/resumo-diario.sh new file mode 100755 index 0000000..6125e40 --- /dev/null +++ b/scripts/resumo-diario.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# M5 - Resumo Diário +# Data: 24/06/2026 +echo "Resumo Diário - $(date)" +# Será implementado com relatórios diff --git a/scripts/saleor-entrypoint.sh b/scripts/saleor-entrypoint.sh new file mode 100755 index 0000000..9378b55 --- /dev/null +++ b/scripts/saleor-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Copiar RSA key ANTES de qualquer coisa +cp /app/media/RSA_PRIVATE_KEY.pem /app/RSA_PRIVATE_KEY.pem 2>/dev/null || true +chmod 600 /app/RSA_PRIVATE_KEY.pem 2>/dev/null || true + +# Executar o que foi passado como argumento +exec "$@" diff --git a/scripts/saleor-start.sh b/scripts/saleor-start.sh new file mode 100755 index 0000000..227d8e7 --- /dev/null +++ b/scripts/saleor-start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cp /app/media/RSA_PRIVATE_KEY.pem /tmp/RSA.pem 2>/dev/null +chmod 600 /tmp/RSA.pem 2>/dev/null +export RSA_PRIVATE_KEY=/tmp/RSA.pem +exec gunicorn --bind :8000 --workers 4 --worker-class saleor.asgi.gunicorn_worker.UvicornWorker saleor.asgi:application diff --git a/scripts/telegram_user_login.py b/scripts/telegram_user_login.py new file mode 100644 index 0000000..d809abf --- /dev/null +++ b/scripts/telegram_user_login.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Hermes Telegram User Account — Login Script v3 +Passa phone e code_callback explicitamente via kwargs do start() +""" + +import os +import time +import asyncio +from telethon import TelegramClient + +API_ID = 35430577 +API_HASH = "bf7926bd5830a1d2ebf502e538a0d816" +SESSION_PATH = os.path.expanduser("~/.hermes/telegram_user.session") +PHONE = "+5511991975554" +CODE_FILE = "/tmp/telegram_code.txt" + +def get_code(): + """Lê o código do arquivo, esperando até 120s se não existir.""" + print(f"Aguardando código em {CODE_FILE} ...", flush=True) + for i in range(120): + if os.path.exists(CODE_FILE): + with open(CODE_FILE) as f: + code = f.read().strip() + if code: + os.remove(CODE_FILE) + print(f"Código recebido: {code}", flush=True) + return code + time.sleep(1) + raise TimeoutError("Tempo esgotado aguardando código") + +async def main(): + print("=" * 50) + print("Hermes Telegram — Login com Conta de Usuário") + print("=" * 50) + print(f"Sessão: {SESSION_PATH}") + print(f"Telefone: {PHONE}", flush=True) + print() + + client = TelegramClient(SESSION_PATH, API_ID, API_HASH) + + # Inicia com phone e code_callback via kwargs + await client.start( + phone=lambda: PHONE, + code_callback=get_code + ) + + me = await client.get_me() + print() + print("=" * 50) + print("✅ LOGIN REALIZADO COM SUCESSO!") + print("=" * 50) + print(f"ID: {me.id}") + print(f"Nome: {me.first_name} {me.last_name or ''}") + print(f"Username: @{me.username}") + print(f"Telefone: {me.phone}") + print() + print("Sessão salva em: ~/.hermes/telegram_user.session", flush=True) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/telegram_user_monitor.py b/scripts/telegram_user_monitor.py new file mode 100644 index 0000000..4117baa --- /dev/null +++ b/scripts/telegram_user_monitor.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Monitora a conta de usuário do Telegram via Telethon. +Reenvia mensagens recebidas para o Hermes processar. +Mantém diálogo contínuo em /root/.hermes/telegram_user.session +""" +import asyncio +import logging +import os +import sys +from datetime import datetime + +# Setup +API_ID = 35430577 +API_HASH = "bf7926bd5830a1d2ebf502e538a0d816" +SESSION_FILE = "/root/.hermes/telegram_user.session" + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("/root/.hermes/logs/telegram_user.log"), + logging.StreamHandler(sys.stdout), + ], +) +log = logging.getLogger("telegram_user") + +# Pasta para salvar state +STATE_FILE = "/root/.hermes/telegram_user_state.json" + + +async def main(): + from telethon import TelegramClient + + client = TelegramClient(SESSION_FILE, API_ID, API_HASH) + + async with client: + me = await client.get_me() + log.info(f"=== Monitorando conta: {me.first_name} (@{me.username}) ===") + log.info(f"User ID: {me.id}") + log.info("Aguardando mensagens...") + + from telethon import events + + @client.on(events.NewMessage(incoming=True)) + async def handler(event): + chat = await event.get_chat() + sender = await event.get_sender() + + # Ignora bots (inclusive o @Hermes_M5 / @m5onBot) + if getattr(sender, "bot", False): + log.info(f"Ignorando mensagem de bot: {getattr(sender, 'username', 'sem username')}") + return + + # Ignora mensagens enviadas por nós mesmos + if event.out: + return + + # Texto da mensagem + if hasattr(event, "message") and event.message: + text = event.message.text or event.message.message or "" + if not text: + return + else: + text = str(event)[:100] + + timestamp = datetime.now().strftime("%d/%m %H:%M") + chat_name = getattr(chat, "title", None) or getattr(chat, "first_name", "Desconhecido") + sender_name = f"{getattr(sender, 'first_name', '')} {getattr(sender, 'last_name', '')}".strip() + username = f"(@{sender.username})" if getattr(sender, "username", None) else "" + + log.info("") + log.info(f"╔══════════════════════════════════════════════════════╗") + log.info(f"║ {timestamp} | {chat_name} ║") + log.info(f"║ De: {sender_name} {username} ║") + log.info(f"╠══════════════════════════════════════════════════════╣") + # Quebra texto em linhas de até 58 chars + for i in range(0, len(text), 58): + line = text[i : i + 58] + log.info(f"║ {line:<58} ║") + log.info(f"╚══════════════════════════════════════════════════════╝") + log.info("") + + # SEM auto-resposta — só loga as mensagens + # (auto-resposta desativada em 21/jun/2026) + + await client.run_until_disconnected() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + log.info("Monitoramento interrompido.") diff --git a/scripts/traefik-conf/dynamic-config.yml b/scripts/traefik-conf/dynamic-config.yml new file mode 100644 index 0000000..3357f7d --- /dev/null +++ b/scripts/traefik-conf/dynamic-config.yml @@ -0,0 +1,7 @@ +http: + middlewares: + secure-headers: + headers: + frameDeny: true + contentTypeNosniff: true + referrerPolicy: strict-origin-when-cross-origin diff --git a/scripts/traefik-conf/traefik.yml b/scripts/traefik-conf/traefik.yml new file mode 100644 index 0000000..d025870 --- /dev/null +++ b/scripts/traefik-conf/traefik.yml @@ -0,0 +1,28 @@ +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +providers: + file: + filename: /etc/traefik/routes.yml + watch: true + +certificatesResolvers: + letsencrypt: + acme: + email: irisflorencio@yahoo.com.br + storage: /letsencrypt/acme.json + httpChallenge: + entryPoint: web + +log: + level: INFO + +accessLog: + format: json