chore: sincroniza projeto para gitea

This commit is contained in:
Tiago
2025-11-29 21:31:52 -03:00
parent 33d8645eb4
commit 7e7a0f8867
129 changed files with 24999 additions and 6757 deletions

View File

@@ -0,0 +1,116 @@
-- Adaptar a estrutura da tabela configuracoes existente
-- 1. Ver estrutura atual
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
ORDER BY ordinal_position;
-- 2. Se a coluna é 'chave' e não 'tipo', vamos renomear
DO $$
BEGIN
-- Verificar se existe a coluna 'chave'
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name = 'chave'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name = 'tipo'
) THEN
-- Renomear 'chave' para 'tipo'
ALTER TABLE public.configuracoes RENAME COLUMN chave TO tipo;
RAISE NOTICE 'Coluna renomeada de chave para tipo';
END IF;
-- Garantir que a coluna tipo existe
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name = 'tipo'
) THEN
ALTER TABLE public.configuracoes ADD COLUMN tipo character varying NOT NULL DEFAULT '';
RAISE NOTICE 'Coluna tipo adicionada';
END IF;
-- Garantir que a coluna valor existe e é jsonb
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name = 'valor'
) THEN
ALTER TABLE public.configuracoes ADD COLUMN valor jsonb;
RAISE NOTICE 'Coluna valor adicionada';
END IF;
-- Se existe coluna 'valor_str' ou 'valor_string', migrar para 'valor' jsonb
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name IN ('valor_str', 'valor_string')
) THEN
-- Tentar converter os dados
UPDATE public.configuracoes
SET valor = COALESCE(valor_str::jsonb, valor_string::jsonb)
WHERE valor IS NULL;
RAISE NOTICE 'Dados migrados para coluna valor';
END IF;
END $$;
-- 3. Remover constraint NOT NULL da coluna chave se ainda existir
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
AND column_name = 'chave'
AND is_nullable = 'NO'
) THEN
ALTER TABLE public.configuracoes ALTER COLUMN chave DROP NOT NULL;
RAISE NOTICE 'Constraint NOT NULL removida da coluna chave';
END IF;
END $$;
-- 4. Adicionar constraint UNIQUE na coluna tipo
DO $$
BEGIN
-- Primeiro remover duplicados
DELETE FROM public.configuracoes
WHERE id IN (
SELECT id
FROM (
SELECT id, tipo,
ROW_NUMBER() OVER (PARTITION BY tipo ORDER BY updated_at DESC NULLS LAST, created_at DESC) as rn
FROM public.configuracoes
) t
WHERE rn > 1
);
-- Adicionar constraint
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'configuracoes_tipo_key'
AND conrelid = 'public.configuracoes'::regclass
) THEN
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
RAISE NOTICE 'Constraint UNIQUE adicionada em tipo';
END IF;
END $$;
-- 5. Verificar resultado final
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
ORDER BY ordinal_position;
-- 6. Mostrar dados
SELECT * FROM public.configuracoes ORDER BY tipo;

View File

@@ -0,0 +1,123 @@
-- =====================================================
-- SCRIPT COMPLETO: SISTEMA DE PARCELAS INDIVIDUAIS
-- Execute este script no Supabase SQL Editor
-- =====================================================
-- Verificar se a tabela já existe
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'venda_parcelas') THEN
RAISE NOTICE '⚠️ Tabela venda_parcelas já existe. Pulando criação.';
ELSE
RAISE NOTICE '✅ Criando tabela venda_parcelas...';
END IF;
END $$;
-- Criar tabela de parcelas
CREATE TABLE IF NOT EXISTS venda_parcelas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
venda_id UUID NOT NULL REFERENCES vendas(id) ON DELETE CASCADE,
numero_parcela INTEGER NOT NULL,
valor DECIMAL(10,2) NOT NULL,
data_vencimento DATE NOT NULL,
status TEXT DEFAULT 'pendente' CHECK (status IN ('pendente', 'pago', 'vencida', 'cancelada')),
data_pagamento TIMESTAMP WITH TIME ZONE,
pix_payment_id TEXT,
pix_qr_code TEXT,
pix_qr_code_base64 TEXT,
observacoes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(venda_id, numero_parcela)
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_venda ON venda_parcelas(venda_id);
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_status ON venda_parcelas(status);
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_vencimento ON venda_parcelas(data_vencimento);
-- Trigger para atualização automática do campo updated_at
DROP TRIGGER IF EXISTS update_venda_parcelas_updated_at ON venda_parcelas;
CREATE TRIGGER update_venda_parcelas_updated_at
BEFORE UPDATE ON venda_parcelas
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Habilitar Row Level Security
ALTER TABLE venda_parcelas ENABLE ROW LEVEL SECURITY;
-- Remover política antiga se existir
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON venda_parcelas;
-- Criar política de acesso (permitir todas operações)
CREATE POLICY "Enable all operations for authenticated users"
ON venda_parcelas
FOR ALL
USING (true);
-- Comentários para documentação
COMMENT ON TABLE venda_parcelas IS 'Armazena as parcelas individuais de cada venda parcelada com controle de status e PIX';
COMMENT ON COLUMN venda_parcelas.numero_parcela IS 'Número sequencial da parcela (1, 2, 3, etc)';
COMMENT ON COLUMN venda_parcelas.valor IS 'Valor da parcela individual';
COMMENT ON COLUMN venda_parcelas.data_vencimento IS 'Data de vencimento da parcela';
COMMENT ON COLUMN venda_parcelas.status IS 'Status da parcela: pendente, pago, vencida, cancelada';
COMMENT ON COLUMN venda_parcelas.data_pagamento IS 'Data e hora em que a parcela foi paga';
COMMENT ON COLUMN venda_parcelas.pix_payment_id IS 'ID do pagamento PIX no MercadoPago';
COMMENT ON COLUMN venda_parcelas.pix_qr_code IS 'Código PIX para cópia e cola';
COMMENT ON COLUMN venda_parcelas.pix_qr_code_base64 IS 'QR Code em base64 para exibição';
-- =====================================================
-- VERIFICAÇÃO E DIAGNÓSTICO
-- =====================================================
-- Verificar se a tabela foi criada
DO $$
DECLARE
table_count INTEGER;
index_count INTEGER;
policy_count INTEGER;
BEGIN
-- Contar tabela
SELECT COUNT(*) INTO table_count
FROM information_schema.tables
WHERE table_name = 'venda_parcelas';
-- Contar índices
SELECT COUNT(*) INTO index_count
FROM pg_indexes
WHERE tablename = 'venda_parcelas';
-- Contar políticas
SELECT COUNT(*) INTO policy_count
FROM pg_policies
WHERE tablename = 'venda_parcelas';
-- Exibir resultados
RAISE NOTICE '========================================';
RAISE NOTICE '✅ VERIFICAÇÃO DO SISTEMA DE PARCELAS';
RAISE NOTICE '========================================';
RAISE NOTICE 'Tabela venda_parcelas: % %',
CASE WHEN table_count > 0 THEN '✅ CRIADA' ELSE '❌ NÃO ENCONTRADA' END,
CASE WHEN table_count > 0 THEN '' ELSE ' - Execute o script novamente!' END;
RAISE NOTICE 'Índices criados: % índice(s)', index_count;
RAISE NOTICE 'Políticas RLS: % política(s)', policy_count;
RAISE NOTICE '========================================';
IF table_count > 0 THEN
RAISE NOTICE '🎉 Sistema de parcelas instalado com sucesso!';
RAISE NOTICE '📝 Próximo passo: Reinicie o servidor Node.js';
RAISE NOTICE '🚀 Depois: Teste criando uma venda parcelada';
ELSE
RAISE NOTICE '⚠️ Erro na instalação. Verifique as mensagens acima.';
END IF;
RAISE NOTICE '========================================';
END $$;
-- Verificar estrutura da tabela
SELECT
column_name as "Coluna",
data_type as "Tipo",
is_nullable as "Nullable"
FROM information_schema.columns
WHERE table_name = 'venda_parcelas'
ORDER BY ordinal_position;

View File

@@ -0,0 +1,61 @@
-- Configurar permissões para as tabelas configuracoes e mensagens_whatsapp
-- ============================================
-- TABELA: configuracoes
-- ============================================
-- Desabilitar RLS temporariamente para testes
ALTER TABLE public.configuracoes DISABLE ROW LEVEL SECURITY;
-- Ou criar uma política que permite todas as operações
-- (use esta opção se precisar manter o RLS ativado)
/*
ALTER TABLE public.configuracoes ENABLE ROW LEVEL SECURITY;
-- Remover políticas existentes
DROP POLICY IF EXISTS "Permitir tudo em configuracoes" ON public.configuracoes;
-- Criar política permissiva
CREATE POLICY "Permitir tudo em configuracoes"
ON public.configuracoes
FOR ALL
TO public
USING (true)
WITH CHECK (true);
*/
-- ============================================
-- TABELA: mensagens_whatsapp
-- ============================================
-- Desabilitar RLS temporariamente para testes
ALTER TABLE public.mensagens_whatsapp DISABLE ROW LEVEL SECURITY;
-- Ou criar uma política que permite todas as operações
/*
ALTER TABLE public.mensagens_whatsapp ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Permitir tudo em mensagens_whatsapp" ON public.mensagens_whatsapp;
CREATE POLICY "Permitir tudo em mensagens_whatsapp"
ON public.mensagens_whatsapp
FOR ALL
TO public
USING (true)
WITH CHECK (true);
*/
-- ============================================
-- VERIFICAÇÃO
-- ============================================
-- Verificar se as tabelas existem e estão acessíveis
SELECT
'configuracoes' as tabela,
COUNT(*) as total_registros
FROM public.configuracoes
UNION ALL
SELECT
'mensagens_whatsapp' as tabela,
COUNT(*) as total_registros
FROM public.mensagens_whatsapp;

View File

@@ -0,0 +1,64 @@
-- Corrigir constraints da tabela configuracoes
-- 1. Remover a constraint incorreta (configuracoes_tipo_key)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'configuracoes_tipo_key'
AND conrelid = 'public.configuracoes'::regclass
) THEN
ALTER TABLE public.configuracoes DROP CONSTRAINT configuracoes_tipo_key;
RAISE NOTICE 'Constraint configuracoes_tipo_key removida';
END IF;
END $$;
-- 2. Garantir que existe constraint UNIQUE na coluna 'chave'
DO $$
BEGIN
-- Primeiro remover duplicados na coluna 'chave' (mantendo o mais recente)
DELETE FROM public.configuracoes
WHERE id IN (
SELECT id
FROM (
SELECT id, chave,
ROW_NUMBER() OVER (PARTITION BY chave ORDER BY updated_at DESC NULLS LAST, created_at DESC) as rn
FROM public.configuracoes
WHERE chave IS NOT NULL
) t
WHERE rn > 1
);
RAISE NOTICE 'Duplicados removidos da coluna chave';
-- Adicionar constraint UNIQUE em 'chave' se não existir
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'configuracoes_chave_key'
AND conrelid = 'public.configuracoes'::regclass
) THEN
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_chave_key UNIQUE (chave);
RAISE NOTICE 'Constraint UNIQUE adicionada na coluna chave';
END IF;
END $$;
-- 3. Verificar constraints existentes
SELECT
conname as constraint_name,
contype as constraint_type,
a.attname as column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.conrelid = 'public.configuracoes'::regclass
ORDER BY conname;
-- 4. Verificar estrutura final
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'configuracoes'
ORDER BY ordinal_position;
-- 5. Mostrar dados
SELECT id, chave, tipo, descricao, created_at, updated_at
FROM public.configuracoes
ORDER BY chave;

View File

@@ -0,0 +1,65 @@
-- =====================================================
-- SCRIPT CORRIGIDO: CRIAR TABELA DE PARCELAS
-- Execute este script no Supabase SQL Editor
-- =====================================================
-- Criar tabela de parcelas (se não existir)
CREATE TABLE IF NOT EXISTS venda_parcelas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
venda_id UUID NOT NULL REFERENCES vendas(id) ON DELETE CASCADE,
numero_parcela INTEGER NOT NULL,
valor DECIMAL(10,2) NOT NULL,
data_vencimento DATE NOT NULL,
status TEXT DEFAULT 'pendente' CHECK (status IN ('pendente', 'pago', 'vencida', 'cancelada')),
data_pagamento TIMESTAMP WITH TIME ZONE,
pix_payment_id TEXT,
pix_qr_code TEXT,
pix_qr_code_base64 TEXT,
observacoes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(venda_id, numero_parcela)
);
-- Criar índices (se não existirem)
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_venda ON venda_parcelas(venda_id);
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_status ON venda_parcelas(status);
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_vencimento ON venda_parcelas(data_vencimento);
-- Remover trigger antiga se existir e criar nova
DROP TRIGGER IF EXISTS update_venda_parcelas_updated_at ON venda_parcelas;
CREATE TRIGGER update_venda_parcelas_updated_at
BEFORE UPDATE ON venda_parcelas
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Habilitar RLS
ALTER TABLE venda_parcelas ENABLE ROW LEVEL SECURITY;
-- Remover política antiga se existir
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON venda_parcelas;
-- Criar política de acesso
CREATE POLICY "Enable all operations for authenticated users"
ON venda_parcelas
FOR ALL
USING (true);
-- Verificar se foi criado com sucesso
DO $$
DECLARE
table_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'venda_parcelas'
) INTO table_exists;
IF table_exists THEN
RAISE NOTICE '✅ Tabela venda_parcelas criada/verificada com sucesso!';
RAISE NOTICE '📝 Próximo passo: Reinicie o servidor Node.js';
RAISE NOTICE '🚀 Depois: Crie uma nova venda parcelada para testar';
ELSE
RAISE NOTICE '❌ Erro: Tabela não foi criada';
END IF;
END $$;

157
scripts/deploy-servidor.sh Executable file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# 🚀 Script de Deploy Automático - Liberi Kids
# Deploy no servidor local com PM2
set -e # Parar em caso de erro
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}🚀 Deploy Automático - Liberi Kids${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Função para verificar se comando existe
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# 1. Verificar Node.js
echo -e "${YELLOW}📦 Verificando Node.js...${NC}"
if ! command_exists node; then
echo -e "${RED}❌ Node.js não encontrado!${NC}"
echo -e "Instale com: ${BLUE}curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs${NC}"
exit 1
fi
echo -e "${GREEN}✅ Node.js $(node --version) encontrado${NC}"
echo ""
# 2. Verificar PM2
echo -e "${YELLOW}📦 Verificando PM2...${NC}"
if ! command_exists pm2; then
echo -e "${YELLOW}⚠️ PM2 não encontrado. Instalando...${NC}"
sudo npm install -g pm2
echo -e "${GREEN}✅ PM2 instalado com sucesso${NC}"
else
echo -e "${GREEN}✅ PM2 $(pm2 --version) encontrado${NC}"
fi
echo ""
# 3. Verificar arquivo .env
echo -e "${YELLOW}🔧 Verificando configurações...${NC}"
if [ ! -f ".env" ]; then
echo -e "${RED}❌ Arquivo .env não encontrado!${NC}"
if [ -f ".env.example" ]; then
echo -e "${YELLOW}Criando .env a partir do .env.example...${NC}"
cp .env.example .env
echo -e "${YELLOW}⚠️ ATENÇÃO: Configure o arquivo .env com suas credenciais!${NC}"
echo -e "Execute: ${BLUE}nano .env${NC}"
exit 1
else
echo -e "${RED}Nem .env nem .env.example encontrados!${NC}"
exit 1
fi
else
echo -e "${GREEN}✅ Arquivo .env encontrado${NC}"
fi
echo ""
# 4. Instalar dependências do backend
echo -e "${YELLOW}📦 Instalando dependências do backend...${NC}"
npm install
echo -e "${GREEN}✅ Dependências do backend instaladas${NC}"
echo ""
# 5. Instalar dependências do frontend
echo -e "${YELLOW}📦 Instalando dependências do frontend...${NC}"
cd client
npm install
echo -e "${GREEN}✅ Dependências do frontend instaladas${NC}"
echo ""
# 6. Build do frontend
echo -e "${YELLOW}🔨 Fazendo build do frontend...${NC}"
npm run build
cd ..
echo -e "${GREEN}✅ Build do frontend concluído${NC}"
echo ""
# 7. Criar diretório de logs
echo -e "${YELLOW}📁 Criando diretórios necessários...${NC}"
mkdir -p logs
mkdir -p uploads
echo -e "${GREEN}✅ Diretórios criados${NC}"
echo ""
# 8. Parar processo anterior (se existir)
echo -e "${YELLOW}🛑 Parando processos anteriores...${NC}"
if pm2 list | grep -q "liberi-kids-estoque"; then
pm2 stop liberi-kids-estoque
pm2 delete liberi-kids-estoque
echo -e "${GREEN}✅ Processo anterior removido${NC}"
else
echo -e "${BLUE} Nenhum processo anterior encontrado${NC}"
fi
echo ""
# 9. Iniciar com PM2
echo -e "${YELLOW}🚀 Iniciando aplicação com PM2...${NC}"
pm2 start ecosystem.config.js
echo -e "${GREEN}✅ Aplicação iniciada${NC}"
echo ""
# 10. Salvar configuração PM2
echo -e "${YELLOW}💾 Salvando configuração do PM2...${NC}"
pm2 save
echo -e "${GREEN}✅ Configuração salva${NC}"
echo ""
# 11. Verificar status
echo -e "${YELLOW}📊 Verificando status...${NC}"
pm2 status
echo ""
# 12. Configurar auto-start (opcional)
echo -e "${YELLOW}🔄 Deseja configurar inicialização automática no boot? (s/n)${NC}"
read -r resposta
if [ "$resposta" = "s" ] || [ "$resposta" = "S" ]; then
echo -e "${YELLOW}Configurando auto-start...${NC}"
pm2 startup
echo ""
echo -e "${YELLOW}⚠️ IMPORTANTE: Execute o comando mostrado acima com sudo!${NC}"
echo -e "Depois execute: ${BLUE}pm2 save${NC}"
fi
echo ""
# 13. Verificar porta
PORT=$(grep "^PORT=" .env | cut -d '=' -f2 || echo "5000")
PORT=${PORT:-5000}
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}✅ Deploy concluído com sucesso!${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${GREEN}📍 Aplicação rodando em:${NC}"
echo -e " ${BLUE}http://localhost:${PORT}${NC}"
echo -e " ${BLUE}http://$(hostname -I | awk '{print $1}'):${PORT}${NC}"
echo ""
echo -e "${GREEN}📦 Catálogo público em:${NC}"
echo -e " ${BLUE}http://localhost:${PORT}/catalogo${NC}"
echo ""
echo -e "${YELLOW}📊 Comandos úteis:${NC}"
echo -e " ${BLUE}pm2 status${NC} - Ver status"
echo -e " ${BLUE}pm2 logs${NC} - Ver logs"
echo -e " ${BLUE}pm2 monit${NC} - Monitorar"
echo -e " ${BLUE}pm2 restart all${NC} - Reiniciar"
echo -e " ${BLUE}pm2 stop all${NC} - Parar"
echo ""
echo -e "${GREEN}📝 Ver logs agora:${NC}"
echo -e " ${BLUE}pm2 logs liberi-kids-estoque${NC}"
echo ""
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"

View File

@@ -0,0 +1,260 @@
#!/usr/bin/env node
/**
* Script para Enviar Alertas Atrasados
*
* Este script envia manualmente alertas que não foram enviados
* (para quando o cron não estava configurado)
*/
const { createClient } = require('@supabase/supabase-js');
const axios = require('axios');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(query) {
return new Promise(resolve => rl.question(query, resolve));
}
// Configurações
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://ydhzylfnpqlxnzfcclla.supabase.co';
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY;
if (!SUPABASE_KEY) {
console.error('❌ SUPABASE_KEY não configurado!');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
function formatarDataBR(data) {
const d = new Date(data);
return d.toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' });
}
function formatarMoeda(valor) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
}
async function enviarWhatsApp(telefone, mensagem, evolutionConfig) {
try {
const url = `${evolutionConfig.apiUrl}/message/sendText/${evolutionConfig.instanceName}`;
const response = await axios.post(url, {
number: telefone,
textMessage: {
text: mensagem
}
}, {
headers: {
'apikey': evolutionConfig.apiKey,
'Content-Type': 'application/json'
}
});
return { success: true, data: response.data };
} catch (error) {
return { success: false, error: error.message };
}
}
async function gerarPix(parcela, mercadoPagoToken) {
try {
const response = await axios.post('https://api.mercadopago.com/v1/payments', {
transaction_amount: parseFloat(parcela.valor),
description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`,
payment_method_id: 'pix',
payer: {
email: 'cliente@liberi.com.br'
}
}, {
headers: {
'Authorization': `Bearer ${mercadoPagoToken}`,
'Content-Type': 'application/json'
}
});
return {
success: true,
qrCode: response.data.point_of_interaction?.transaction_data?.qr_code,
qrCodeBase64: response.data.point_of_interaction?.transaction_data?.qr_code_base64,
paymentId: response.data.id
};
} catch (error) {
return { success: false, error: error.message };
}
}
async function main() {
console.log('\n🔔 ENVIO MANUAL DE ALERTAS ATRASADOS\n');
console.log('Este script enviará alertas que não foram enviados automaticamente.\n');
try {
// Buscar configurações
const { data: configData } = await supabase
.from('configuracoes')
.select('chave, valor')
.in('chave', [
'evolution_api_url',
'evolution_instance_name',
'evolution_api_key',
'mercadopago_access_token'
]);
const config = {};
configData?.forEach(item => {
config[item.chave] = item.valor;
});
const evolutionConfig = {
apiUrl: config.evolution_api_url,
instanceName: config.evolution_instance_name,
apiKey: config.evolution_api_key
};
const mercadoPagoToken = config.mercadopago_access_token;
// Buscar parcelas pendentes com vencimento hoje ou passado
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const { data: parcelas, error } = await supabase
.from('venda_parcelas')
.select(`
*,
vendas (
id_venda,
cliente_id,
clientes (
nome_completo,
whatsapp
)
)
`)
.eq('status', 'pendente')
.lte('data_vencimento', hoje.toISOString())
.order('data_vencimento', { ascending: true });
if (error) throw error;
if (!parcelas || parcelas.length === 0) {
console.log('✅ Não há parcelas vencidas ou vencendo hoje.\n');
rl.close();
return;
}
console.log(`📦 Encontradas ${parcelas.length} parcela(s) vencida(s) ou vencendo hoje:\n`);
parcelas.forEach((parcela, index) => {
const cliente = parcela.vendas?.clientes;
console.log(`${index + 1}. ${cliente?.nome_completo || 'N/A'} - Parcela ${parcela.numero_parcela}`);
console.log(` Valor: ${formatarMoeda(parcela.valor)}`);
console.log(` Vencimento: ${formatarDataBR(parcela.data_vencimento)}`);
console.log(` WhatsApp: ${cliente?.whatsapp || 'N/A'}`);
console.log('');
});
const resposta = await question('Deseja enviar alertas + PIX para essas parcelas? (s/N): ');
if (!resposta.match(/^[Ss]$/)) {
console.log('\n❌ Operação cancelada.\n');
rl.close();
return;
}
console.log('\n🚀 Iniciando envio...\n');
let enviados = 0;
let erros = 0;
for (const parcela of parcelas) {
const cliente = parcela.vendas?.clientes;
if (!cliente || !cliente.whatsapp) {
console.log(`⚠️ Parcela ${parcela.numero_parcela}: Cliente sem WhatsApp`);
erros++;
continue;
}
const nomeCliente = cliente.nome_completo.split(' ')[0];
const valorFormatado = formatarMoeda(parcela.valor);
// Gerar PIX
console.log(`📱 Gerando PIX para ${cliente.nome_completo}...`);
const pixResult = await gerarPix(parcela, mercadoPagoToken);
let mensagem = `Olá ${nomeCliente}! 👋\n\n`;
mensagem += `Você tem uma parcela no valor de ${valorFormatado} `;
mensagem += `com vencimento em ${formatarDataBR(parcela.data_vencimento)}.\n\n`;
if (pixResult.success && pixResult.qrCode) {
mensagem += `📱 *PIX Copia e Cola:*\n\`\`\`${pixResult.qrCode}\`\`\`\n\n`;
// Atualizar parcela com PIX
await supabase
.from('venda_parcelas')
.update({
pix_payment_id: pixResult.paymentId,
pix_qr_code: pixResult.qrCode,
pix_qr_code_base64: pixResult.qrCodeBase64
})
.eq('id', parcela.id);
console.log(` ✅ PIX gerado`);
} else {
mensagem += `Entre em contato para obter forma de pagamento.\n\n`;
console.log(` ⚠️ Não foi possível gerar PIX`);
}
mensagem += `Agradecemos! 😊\n*Liberi Kids - Moda Infantil* 👗👕`;
// Enviar mensagem
console.log(`📤 Enviando para ${cliente.whatsapp}...`);
const resultado = await enviarWhatsApp(cliente.whatsapp, mensagem, evolutionConfig);
if (resultado.success) {
enviados++;
console.log(` ✅ Enviado com sucesso!\n`);
// Registrar histórico
await supabase
.from('mensagens_whatsapp')
.insert({
telefone_cliente: cliente.whatsapp,
cliente_nome: cliente.nome_completo,
mensagem: mensagem,
tipo: 'enviada',
status: 'enviada'
});
} else {
erros++;
console.log(` ❌ Erro: ${resultado.error}\n`);
}
// Delay entre mensagens
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log('\n' + '='.repeat(50));
console.log('📊 RESUMO');
console.log('='.repeat(50));
console.log(`✅ Alertas enviados: ${enviados}`);
console.log(`❌ Erros: ${erros}`);
console.log(`📦 Total processado: ${parcelas.length}`);
console.log('='.repeat(50) + '\n');
} catch (error) {
console.error('\n💥 Erro:', error);
} finally {
rl.close();
}
}
main();

View File

@@ -0,0 +1,326 @@
#!/usr/bin/env node
/**
* Script de Envio Automático de Alertas de Vencimento
*
* Este script deve ser executado diariamente às 09:00 (horário de Brasília)
* via cron job para enviar alertas de parcelas vencendo.
*
* Cron: 0 9 * * * /usr/bin/node /caminho/para/enviar-alertas-parcelas.js
*/
const { createClient } = require('@supabase/supabase-js');
const axios = require('axios');
// Configurações do Supabase
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://ydhzylfnpqlxnzfcclla.supabase.co';
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY;
if (!SUPABASE_KEY) {
console.error('❌ SUPABASE_KEY não configurado!');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Função para formatar data no padrão brasileiro
function formatarDataBR(data) {
const d = new Date(data);
return d.toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' });
}
// Função para formatar moeda
function formatarMoeda(valor) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
}
// Função para calcular dias entre datas
function calcularDiasEntre(data1, data2) {
const umDia = 24 * 60 * 60 * 1000;
const d1 = new Date(data1);
const d2 = new Date(data2);
return Math.round((d2 - d1) / umDia);
}
// Função para enviar mensagem via Evolution API
async function enviarWhatsApp(telefone, mensagem, evolutionConfig) {
try {
const url = `${evolutionConfig.apiUrl}/message/sendText/${evolutionConfig.instanceName}`;
const response = await axios.post(url, {
number: telefone,
textMessage: {
text: mensagem
}
}, {
headers: {
'apikey': evolutionConfig.apiKey,
'Content-Type': 'application/json'
}
});
return { success: true, data: response.data };
} catch (error) {
console.error(`❌ Erro ao enviar WhatsApp para ${telefone}:`, error.message);
return { success: false, error: error.message };
}
}
// Função para gerar PIX via Mercado Pago
async function gerarPix(parcela, mercadoPagoToken) {
try {
const response = await axios.post('https://api.mercadopago.com/v1/payments', {
transaction_amount: parseFloat(parcela.valor),
description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`,
payment_method_id: 'pix',
payer: {
email: 'cliente@liberi.com.br',
identification: {
type: 'CPF',
number: '00000000000'
}
}
}, {
headers: {
'Authorization': `Bearer ${mercadoPagoToken}`,
'Content-Type': 'application/json'
}
});
return {
success: true,
qrCode: response.data.point_of_interaction?.transaction_data?.qr_code,
qrCodeBase64: response.data.point_of_interaction?.transaction_data?.qr_code_base64,
paymentId: response.data.id
};
} catch (error) {
console.error(`❌ Erro ao gerar PIX para parcela ${parcela.id}:`, error.message);
return { success: false, error: error.message };
}
}
// Função principal
async function enviarAlertasVencimento() {
console.log('\n🕐 Iniciando envio de alertas de vencimento...');
console.log(`⏰ Horário: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}\n`);
try {
// 1. Buscar configurações WhatsApp
const { data: configData } = await supabase
.from('configuracoes')
.select('chave, valor')
.in('chave', [
'whatsapp_primeiro_alerta_dias',
'whatsapp_segundo_alerta_dias',
'whatsapp_alerta_apos_vencimento_dias',
'whatsapp_primeiro_alerta_ativo',
'whatsapp_segundo_alerta_ativo',
'whatsapp_alerta_apos_vencimento_ativo',
'whatsapp_primeiro_alerta_mensagem',
'whatsapp_segundo_alerta_mensagem',
'whatsapp_alerta_apos_vencimento_mensagem',
'evolution_api_url',
'evolution_instance_name',
'evolution_api_key',
'mercadopago_access_token'
]);
const config = {};
configData?.forEach(item => {
config[item.chave] = item.valor;
});
// Configurações padrão
const primeiroAlertaDias = parseInt(config.whatsapp_primeiro_alerta_dias) || 3;
const segundoAlertaDias = parseInt(config.whatsapp_segundo_alerta_dias) || 0;
const alertaAposVencimentoDias = parseInt(config.whatsapp_alerta_apos_vencimento_dias) || 3;
const primeiroAlertas = config.whatsapp_primeiro_alerta_ativo === 'true';
const segundoAlertas = config.whatsapp_segundo_alerta_ativo === 'true';
const alertaAposVencimento = config.whatsapp_alerta_apos_vencimento_ativo === 'true';
const evolutionConfig = {
apiUrl: config.evolution_api_url,
instanceName: config.evolution_instance_name,
apiKey: config.evolution_api_key
};
const mercadoPagoToken = config.mercadopago_access_token;
console.log('📋 Configurações carregadas:');
console.log(` - Primeiro alerta: ${primeiroAlertaDias} dias antes (${primeiroAlertas ? 'ATIVO' : 'INATIVO'})`);
console.log(` - Segundo alerta: ${segundoAlertaDias} dias antes (${segundoAlertas ? 'ATIVO' : 'INATIVO'})`);
console.log(` - Alerta pós-vencimento: ${alertaAposVencimentoDias} dias após (${alertaAposVencimento ? 'ATIVO' : 'INATIVO'})\n`);
// 2. Buscar parcelas pendentes
const { data: parcelas, error } = await supabase
.from('venda_parcelas')
.select(`
*,
vendas (
id_venda,
cliente_id,
clientes (
nome_completo,
whatsapp
)
)
`)
.eq('status', 'pendente')
.order('data_vencimento', { ascending: true });
if (error) {
throw error;
}
if (!parcelas || parcelas.length === 0) {
console.log(' Nenhuma parcela pendente encontrada.\n');
return;
}
console.log(`📦 ${parcelas.length} parcela(s) pendente(s) encontrada(s)\n`);
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
let alertasEnviados = 0;
let erros = 0;
// 3. Processar cada parcela
for (const parcela of parcelas) {
const dataVencimento = new Date(parcela.data_vencimento);
dataVencimento.setHours(0, 0, 0, 0);
const diasParaVencimento = calcularDiasEntre(hoje, dataVencimento);
const cliente = parcela.vendas?.clientes;
if (!cliente || !cliente.whatsapp) {
console.log(`⚠️ Parcela ${parcela.numero_parcela} (Venda #${parcela.vendas?.id_venda}): Cliente sem WhatsApp`);
continue;
}
let deveEnviar = false;
let tipoAlerta = '';
let mensagemTemplate = '';
// Verificar qual tipo de alerta enviar
if (diasParaVencimento === primeiroAlertaDias && primeiroAlertas) {
deveEnviar = true;
tipoAlerta = 'primeiro_alerta';
mensagemTemplate = config.whatsapp_primeiro_alerta_mensagem ||
'Olá {cliente}! 👋\n\nLembramos que você tem uma parcela no valor de {valor} com vencimento {quando}.\n\nAgradecemos a atenção!';
} else if (diasParaVencimento === segundoAlertaDias && segundoAlertas) {
deveEnviar = true;
tipoAlerta = 'segundo_alerta';
mensagemTemplate = config.whatsapp_segundo_alerta_mensagem ||
'Olá {cliente}! 👋\n\nSua parcela de {valor} vence {quando}.\n\n📱 Gerando PIX para facilitar o pagamento...';
} else if (diasParaVencimento < 0 && Math.abs(diasParaVencimento) === alertaAposVencimentoDias && alertaAposVencimento) {
deveEnviar = true;
tipoAlerta = 'alerta_apos_vencimento';
mensagemTemplate = config.whatsapp_alerta_apos_vencimento_mensagem ||
'Olá {cliente}! 👋\n\nIdentificamos que a parcela {parcela} no valor de {valor} venceu {quando}.\n\nPor favor, regularize o pagamento.';
}
if (!deveEnviar) {
continue;
}
// Preparar dados da mensagem
const nomeCliente = cliente.nome_completo.split(' ')[0];
const valorFormatado = formatarMoeda(parcela.valor);
const dataVencFormatada = formatarDataBR(parcela.data_vencimento);
let quando = '';
if (diasParaVencimento > 0) {
quando = diasParaVencimento === 1 ? 'amanhã' : `em ${diasParaVencimento} dias (${dataVencFormatada})`;
} else if (diasParaVencimento === 0) {
quando = 'hoje';
} else {
quando = `${Math.abs(diasParaVencimento)} dias (${dataVencFormatada})`;
}
// Substituir variáveis na mensagem
let mensagem = mensagemTemplate
.replace(/{cliente}/g, nomeCliente)
.replace(/{valor}/g, valorFormatado)
.replace(/{quando}/g, quando)
.replace(/{parcela}/g, `${parcela.numero_parcela}/${parcela.vendas?.parcelas || '?'}`);
// Se for no dia do vencimento, gerar PIX
if (tipoAlerta === 'segundo_alerta' && mercadoPagoToken) {
console.log(`🔄 Gerando PIX para parcela ${parcela.numero_parcela}...`);
const pixResult = await gerarPix(parcela, mercadoPagoToken);
if (pixResult.success && pixResult.qrCode) {
mensagem += `\n\n📱 *PIX Copia e Cola:*\n\`\`\`${pixResult.qrCode}\`\`\``;
// Atualizar parcela com dados do PIX
await supabase
.from('venda_parcelas')
.update({
pix_payment_id: pixResult.paymentId,
pix_qr_code: pixResult.qrCode,
pix_qr_code_base64: pixResult.qrCodeBase64
})
.eq('id', parcela.id);
}
}
// Enviar mensagem
console.log(`📤 Enviando ${tipoAlerta} para ${cliente.nome_completo} (${cliente.whatsapp})...`);
const resultado = await enviarWhatsApp(cliente.whatsapp, mensagem, evolutionConfig);
if (resultado.success) {
alertasEnviados++;
console.log(` ✅ Enviado com sucesso!\n`);
// Registrar no histórico
await supabase
.from('mensagens_whatsapp')
.insert({
telefone_cliente: cliente.whatsapp,
cliente_nome: cliente.nome_completo,
mensagem: mensagem,
tipo: 'enviada',
status: 'enviada'
});
} else {
erros++;
console.log(` ❌ Falha no envio: ${resultado.error}\n`);
}
// Pequeno delay entre mensagens
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Resumo
console.log('\n' + '='.repeat(50));
console.log('📊 RESUMO DO ENVIO');
console.log('='.repeat(50));
console.log(`✅ Alertas enviados: ${alertasEnviados}`);
console.log(`❌ Erros: ${erros}`);
console.log(`📦 Total de parcelas verificadas: ${parcelas.length}`);
console.log('='.repeat(50) + '\n');
} catch (error) {
console.error('\n💥 Erro fatal:', error);
process.exit(1);
}
}
// Executar
enviarAlertasVencimento()
.then(() => {
console.log('✅ Script finalizado com sucesso!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Erro fatal:', error);
process.exit(1);
});

View File

@@ -1,58 +0,0 @@
#!/bin/bash
echo "🔧 Script de Correção Google Drive - Liberi Kids"
echo "=============================================="
echo ""
echo "📋 Checklist de Configuração:"
echo ""
echo "1. ✅ Verificar se Google Drive API está ativa"
echo " - Acesse: https://console.cloud.google.com/apis/library/drive.googleapis.com"
echo " - Clique em 'Ativar' se não estiver ativo"
echo ""
echo "2. ✅ Configurar Tela de Consentimento OAuth"
echo " - Acesse: https://console.cloud.google.com/apis/credentials/consent"
echo " - Tipo: Externo"
echo " - Nome do app: Liberi Kids - Sistema de Estoque"
echo " - Adicionar escopos: drive.file e drive.readonly"
echo ""
echo "3. 🎯 CRÍTICO: Adicionar Usuários de Teste"
echo " - Na tela de consentimento, seção 'Usuários de teste'"
echo " - Adicionar SEU EMAIL (o mesmo que vai usar para autorizar)"
echo " - Sem isso, você receberá erro de autorização"
echo ""
echo "4. ✅ Verificar Credenciais OAuth"
echo " - Acesse: https://console.cloud.google.com/apis/credentials"
echo " - URIs de redirecionamento: http://localhost:5000/auth/google-drive/callback"
echo ""
echo "5. 🔄 Testar Configuração"
echo " - Recarregar página de Configurações"
echo " - Tentar autorizar novamente"
echo " - Usar o email que foi adicionado como usuário de teste"
echo ""
echo "📞 URLs Importantes:"
echo " - Google Cloud Console: https://console.cloud.google.com/"
echo " - APIs & Services: https://console.cloud.google.com/apis/"
echo " - OAuth Consent: https://console.cloud.google.com/apis/credentials/consent"
echo " - Credentials: https://console.cloud.google.com/apis/credentials"
echo ""
echo "🚨 Lembre-se: Use o MESMO EMAIL em 'Usuários de teste' e na autorização!"
echo ""
# Verificar se o servidor está rodando
if curl -s http://localhost:5000/api/google-drive/status > /dev/null; then
echo "✅ Servidor está rodando"
echo "🔗 Acesse: http://localhost:3000/configuracoes"
else
echo "❌ Servidor não está rodando"
echo "Execute: npm start"
fi
echo ""
echo "Após configurar no Google Cloud Console, recarregue a página e tente novamente!"

View File

@@ -0,0 +1,33 @@
-- Garante que a tabela configuracoes existe e tem a estrutura correta
-- Se a tabela já existe, vamos garantir que ela tenha a estrutura correta
DO $$
BEGIN
-- Criar tabela se não existir
IF NOT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'configuracoes') THEN
CREATE TABLE public.configuracoes (
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
tipo character varying NOT NULL,
valor jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
END IF;
-- Garantir que a constraint UNIQUE existe
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'configuracoes_tipo_key'
AND conrelid = 'public.configuracoes'::regclass
) THEN
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
END IF;
END $$;
-- Adiciona índice no campo tipo para melhor performance (se não existir)
CREATE INDEX IF NOT EXISTS idx_configuracoes_tipo ON public.configuracoes(tipo);
-- Adiciona comentários
COMMENT ON TABLE public.configuracoes IS 'Armazena todas as configurações do sistema';
COMMENT ON COLUMN public.configuracoes.tipo IS 'Tipo da configuração (chave única)';
COMMENT ON COLUMN public.configuracoes.valor IS 'Valor da configuração em formato JSON';

View File

@@ -0,0 +1,44 @@
-- Garante que a tabela mensagens_whatsapp exista e tenha as colunas corretas.
CREATE TABLE IF NOT EXISTS public.mensagens_whatsapp (
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
created_at timestamp with time zone DEFAULT now() NOT NULL,
telefone_cliente character varying,
cliente_nome character varying,
mensagem text,
tipo character varying DEFAULT 'enviada'::character varying,
status character varying DEFAULT 'enviada'::character varying,
evolution_message_id character varying,
venda_id uuid,
cobranca_id uuid
);
-- Adiciona colunas que podem estar faltando, sem gerar erro se já existirem.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'venda_id') THEN
ALTER TABLE public.mensagens_whatsapp ADD COLUMN venda_id uuid;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'cobranca_id') THEN
ALTER TABLE public.mensagens_whatsapp ADD COLUMN cobranca_id uuid;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'cliente_nome') THEN
ALTER TABLE public.mensagens_whatsapp ADD COLUMN cliente_nome character varying;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'evolution_message_id') THEN
ALTER TABLE public.mensagens_whatsapp ADD COLUMN evolution_message_id character varying;
END IF;
END;
$$;
-- Adiciona relacionamento com a tabela de vendas, se não existir.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mensagens_whatsapp_venda_id_fkey') THEN
ALTER TABLE public.mensagens_whatsapp ADD CONSTRAINT mensagens_whatsapp_venda_id_fkey FOREIGN KEY (venda_id) REFERENCES public.vendas(id) ON DELETE SET NULL;
END IF;
END;
$$;
COMMENT ON TABLE public.mensagens_whatsapp IS 'Armazena o histórico de mensagens enviadas e recebidas pelo WhatsApp.';

View File

@@ -0,0 +1,147 @@
#!/bin/bash
##############################################
# Instalador de Cron Job para Alertas de Vencimento
#
# Este script configura o cron para executar
# alertas automáticos às 09:00 (horário de Brasília)
##############################################
set -e
echo "=========================================="
echo " INSTALADOR CRON - ALERTAS PARCELAS"
echo "=========================================="
echo ""
# Cores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Detectar diretório do projeto
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
NODE_SCRIPT="$PROJECT_DIR/scripts/enviar-alertas-parcelas.js"
echo "📁 Diretório do projeto: $PROJECT_DIR"
echo "📜 Script de alertas: $NODE_SCRIPT"
echo ""
# Verificar se o script existe
if [ ! -f "$NODE_SCRIPT" ]; then
echo -e "${RED}❌ Script de alertas não encontrado!${NC}"
echo " Caminho esperado: $NODE_SCRIPT"
exit 1
fi
# Tornar o script executável
chmod +x "$NODE_SCRIPT"
echo -e "${GREEN}✅ Permissões de execução configuradas${NC}"
# Detectar path do Node.js
NODE_PATH=$(which node)
if [ -z "$NODE_PATH" ]; then
echo -e "${RED}❌ Node.js não encontrado!${NC}"
echo " Instale o Node.js primeiro: https://nodejs.org"
exit 1
fi
echo -e "${GREEN}✅ Node.js encontrado: $NODE_PATH${NC}"
# Criar linha do cron
# Executa todos os dias às 09:00 (horário de Brasília - UTC-3)
# Nota: Cron usa UTC, então 09:00 BRT = 12:00 UTC
CRON_LINE="0 12 * * * TZ='America/Sao_Paulo' $NODE_PATH $NODE_SCRIPT >> $PROJECT_DIR/logs/alertas-cron.log 2>&1"
echo ""
echo "🕐 Configuração do cron:"
echo " - Horário: 09:00 (Brasília)"
echo " - Frequência: Diariamente"
echo " - Timezone: America/Sao_Paulo"
echo ""
# Criar diretório de logs
mkdir -p "$PROJECT_DIR/logs"
echo -e "${GREEN}✅ Diretório de logs criado${NC}"
# Verificar se já existe o cron
EXISTING_CRON=$(crontab -l 2>/dev/null | grep -F "$NODE_SCRIPT" || true)
if [ -n "$EXISTING_CRON" ]; then
echo -e "${YELLOW}⚠️ Já existe um cron job configurado para este script${NC}"
echo ""
echo "Cron atual:"
echo " $EXISTING_CRON"
echo ""
read -p "Deseja substituir? (s/N): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Ss]$ ]]; then
echo "❌ Instalação cancelada pelo usuário"
exit 0
fi
# Remover linha antiga
(crontab -l 2>/dev/null | grep -v -F "$NODE_SCRIPT") | crontab -
echo -e "${GREEN}✅ Cron antigo removido${NC}"
fi
# Adicionar novo cron
(crontab -l 2>/dev/null; echo "$CRON_LINE") | crontab -
echo ""
echo -e "${GREEN}✅ Cron job instalado com sucesso!${NC}"
echo ""
# Verificar instalação
echo "📋 Cron jobs ativos:"
echo "────────────────────────────────────────"
crontab -l | grep -F "$NODE_SCRIPT" || echo "Nenhum cron encontrado"
echo "────────────────────────────────────────"
echo ""
# Teste manual
echo "🧪 Deseja executar um teste agora? (s/N): "
read -p "" -n 1 -r
echo ""
if [[ $REPLY =~ ^[Ss]$ ]]; then
echo ""
echo "🚀 Executando teste..."
echo "────────────────────────────────────────"
TZ='America/Sao_Paulo' $NODE_PATH $NODE_SCRIPT
echo "────────────────────────────────────────"
echo ""
fi
echo ""
echo "=========================================="
echo " INSTALAÇÃO CONCLUÍDA! ✅"
echo "=========================================="
echo ""
echo "📝 Próximos passos:"
echo ""
echo "1. Configure as variáveis de ambiente em .env:"
echo " - SUPABASE_URL"
echo " - SUPABASE_SERVICE_KEY (ou SUPABASE_ANON_KEY)"
echo ""
echo "2. Configure no painel admin:"
echo " - Evolution API (URL, Instance, API Key)"
echo " - Mercado Pago (Access Token)"
echo " - Mensagens de alerta personalizadas"
echo " - Dias de antecedência para alertas"
echo ""
echo "3. Monitore os logs:"
echo " tail -f $PROJECT_DIR/logs/alertas-cron.log"
echo ""
echo "4. Para testar manualmente:"
echo " cd $PROJECT_DIR"
echo " node scripts/enviar-alertas-parcelas.js"
echo ""
echo "5. Para desinstalar o cron:"
echo " crontab -e (e remova a linha do script)"
echo ""
echo "=========================================="
echo ""

82
scripts/install-systemd.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
# 🔧 Script de Instalação do Serviço Systemd - Liberi Kids
set -e
# Cores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}🔧 Instalação do Serviço Systemd${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Verificar se está rodando como root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}❌ Este script precisa ser executado como root${NC}"
echo -e "Execute: ${BLUE}sudo ./scripts/install-systemd.sh${NC}"
exit 1
fi
# 1. Criar diretórios necessários
echo -e "${YELLOW}📁 Criando diretórios...${NC}"
mkdir -p logs
chown -R tiago:tiago logs
echo -e "${GREEN}✅ Diretórios criados${NC}"
echo ""
# 2. Copiar arquivo de serviço
echo -e "${YELLOW}📋 Instalando serviço systemd...${NC}"
cp liberi-kids.service /etc/systemd/system/
echo -e "${GREEN}✅ Arquivo copiado para /etc/systemd/system/${NC}"
echo ""
# 3. Recarregar systemd
echo -e "${YELLOW}🔄 Recarregando systemd...${NC}"
systemctl daemon-reload
echo -e "${GREEN}✅ Systemd recarregado${NC}"
echo ""
# 4. Habilitar serviço
echo -e "${YELLOW}✅ Habilitando inicialização automática...${NC}"
systemctl enable liberi-kids
echo -e "${GREEN}✅ Serviço habilitado para iniciar no boot${NC}"
echo ""
# 5. Iniciar serviço
echo -e "${YELLOW}🚀 Iniciando serviço...${NC}"
systemctl start liberi-kids
sleep 2
echo -e "${GREEN}✅ Serviço iniciado${NC}"
echo ""
# 6. Verificar status
echo -e "${YELLOW}📊 Status do serviço:${NC}"
systemctl status liberi-kids --no-pager
echo ""
# 7. Informações finais
PORT=$(grep "^PORT=" .env | cut -d '=' -f2 2>/dev/null || echo "5000")
PORT=${PORT:-5000}
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}✅ Instalação concluída!${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${GREEN}📍 Aplicação rodando em:${NC}"
echo -e " ${BLUE}http://localhost:${PORT}${NC}"
echo ""
echo -e "${YELLOW}📊 Comandos úteis:${NC}"
echo -e " ${BLUE}sudo systemctl status liberi-kids${NC} - Ver status"
echo -e " ${BLUE}sudo systemctl restart liberi-kids${NC} - Reiniciar"
echo -e " ${BLUE}sudo systemctl stop liberi-kids${NC} - Parar"
echo -e " ${BLUE}sudo systemctl start liberi-kids${NC} - Iniciar"
echo -e " ${BLUE}sudo journalctl -u liberi-kids -f${NC} - Ver logs"
echo ""
echo -e "${GREEN}🔄 O serviço irá iniciar automaticamente no boot!${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"

View File

@@ -0,0 +1,43 @@
-- Limpar registros duplicados na tabela configuracoes antes de adicionar constraint UNIQUE
-- 1. Ver quais registros estão duplicados
SELECT tipo, COUNT(*) as total
FROM public.configuracoes
GROUP BY tipo
HAVING COUNT(*) > 1;
-- 2. Remover duplicados, mantendo apenas o mais recente de cada tipo
DELETE FROM public.configuracoes
WHERE id IN (
SELECT id
FROM (
SELECT id, tipo,
ROW_NUMBER() OVER (PARTITION BY tipo ORDER BY updated_at DESC, created_at DESC) as rn
FROM public.configuracoes
) t
WHERE rn > 1
);
-- 3. Verificar se ainda existem duplicados (deve retornar vazio)
SELECT tipo, COUNT(*) as total
FROM public.configuracoes
GROUP BY tipo
HAVING COUNT(*) > 1;
-- 4. Agora adicionar a constraint UNIQUE
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'configuracoes_tipo_key'
AND conrelid = 'public.configuracoes'::regclass
) THEN
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
RAISE NOTICE 'Constraint UNIQUE adicionada com sucesso!';
ELSE
RAISE NOTICE 'Constraint UNIQUE já existe.';
END IF;
END $$;
-- 5. Verificar o resultado final
SELECT * FROM public.configuracoes ORDER BY tipo;