Primeiro commit
This commit is contained in:
13
config/google-credentials.example.json
Normal file
13
config/google-credentials.example.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"web": {
|
||||
"client_id": "SEU_CLIENT_ID_AQUI.apps.googleusercontent.com",
|
||||
"project_id": "seu-projeto-id",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_secret": "SUA_CLIENT_SECRET_AQUI",
|
||||
"redirect_uris": [
|
||||
"http://localhost:5000/auth/google/callback"
|
||||
]
|
||||
}
|
||||
}
|
||||
295
config/google-drive.js
Normal file
295
config/google-drive.js
Normal file
@@ -0,0 +1,295 @@
|
||||
const { google } = require('googleapis');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class GoogleDriveService {
|
||||
constructor() {
|
||||
this.auth = null;
|
||||
this.drive = null;
|
||||
this.credentialsPath = path.join(__dirname, 'google-credentials.json');
|
||||
this.tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa a autenticação com credenciais salvas no Supabase ou arquivo
|
||||
*/
|
||||
async initializeAuth(credentials = null) {
|
||||
try {
|
||||
let creds = credentials;
|
||||
|
||||
// Se não foram passadas credenciais, tenta carregar do arquivo
|
||||
if (!creds && fs.existsSync(this.credentialsPath)) {
|
||||
creds = JSON.parse(fs.readFileSync(this.credentialsPath, 'utf8'));
|
||||
}
|
||||
|
||||
if (!creds) {
|
||||
throw new Error('Credenciais do Google não encontradas');
|
||||
}
|
||||
|
||||
// Configurar OAuth2 com scopes
|
||||
this.auth = new google.auth.OAuth2(
|
||||
creds.client_id,
|
||||
creds.client_secret,
|
||||
creds.redirect_uris[0]
|
||||
);
|
||||
|
||||
// Definir scopes para Google Drive
|
||||
this.scopes = [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive.readonly'
|
||||
];
|
||||
|
||||
// Carregar tokens salvos se existirem
|
||||
if (fs.existsSync(this.tokensPath)) {
|
||||
const tokens = JSON.parse(fs.readFileSync(this.tokensPath, 'utf8'));
|
||||
this.auth.setCredentials(tokens);
|
||||
}
|
||||
|
||||
// Inicializar Google Drive API
|
||||
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao inicializar Google Drive:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera URL de autenticação para o usuário
|
||||
*/
|
||||
getAuthUrl() {
|
||||
if (!this.auth) {
|
||||
throw new Error('Autenticação não inicializada');
|
||||
}
|
||||
|
||||
return this.auth.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: this.scopes,
|
||||
prompt: 'consent'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processa o código de autorização retornado pelo Google
|
||||
*/
|
||||
async handleAuthCallback(code) {
|
||||
try {
|
||||
const { tokens } = await this.auth.getToken(code);
|
||||
this.auth.setCredentials(tokens);
|
||||
|
||||
// Salvar tokens para uso futuro
|
||||
fs.writeFileSync(this.tokensPath, JSON.stringify(tokens, null, 2));
|
||||
|
||||
console.log('✅ Autenticação Google Drive realizada com sucesso');
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
console.error('❌ Erro na autenticação Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o usuário está autenticado
|
||||
*/
|
||||
isAuthenticated() {
|
||||
return this.auth && this.auth.credentials && this.auth.credentials.access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria uma pasta no Google Drive (se não existir)
|
||||
*/
|
||||
async createFolder(folderName, parentFolderId = null) {
|
||||
try {
|
||||
// Verificar se a pasta já existe
|
||||
const query = `name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
||||
const existingFolders = await this.drive.files.list({
|
||||
q: parentFolderId ? `${query} and '${parentFolderId}' in parents` : query,
|
||||
fields: 'files(id, name)'
|
||||
});
|
||||
|
||||
if (existingFolders.data.files.length > 0) {
|
||||
return existingFolders.data.files[0].id;
|
||||
}
|
||||
|
||||
// Criar nova pasta
|
||||
const folderMetadata = {
|
||||
name: folderName,
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
parents: parentFolderId ? [parentFolderId] : undefined
|
||||
};
|
||||
|
||||
const folder = await this.drive.files.create({
|
||||
resource: folderMetadata,
|
||||
fields: 'id'
|
||||
});
|
||||
|
||||
console.log(`📁 Pasta '${folderName}' criada no Google Drive: ${folder.data.id}`);
|
||||
return folder.data.id;
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar pasta no Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Faz upload de um arquivo para o Google Drive
|
||||
*/
|
||||
async uploadFile(filePath, fileName, folderId = null, mimeType = 'image/jpeg') {
|
||||
try {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Usuário não autenticado no Google Drive');
|
||||
}
|
||||
|
||||
const fileMetadata = {
|
||||
name: fileName,
|
||||
parents: folderId ? [folderId] : undefined
|
||||
};
|
||||
|
||||
const media = {
|
||||
mimeType: mimeType,
|
||||
body: fs.createReadStream(filePath)
|
||||
};
|
||||
|
||||
const file = await this.drive.files.create({
|
||||
resource: fileMetadata,
|
||||
media: media,
|
||||
fields: 'id, name, webViewLink, webContentLink'
|
||||
});
|
||||
|
||||
// Tornar o arquivo público para visualização
|
||||
await this.drive.permissions.create({
|
||||
fileId: file.data.id,
|
||||
resource: {
|
||||
role: 'reader',
|
||||
type: 'anyone'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📤 Arquivo '${fileName}' enviado para Google Drive: ${file.data.id}`);
|
||||
|
||||
return {
|
||||
id: file.data.id,
|
||||
name: file.data.name,
|
||||
webViewLink: file.data.webViewLink,
|
||||
webContentLink: file.data.webContentLink,
|
||||
publicUrl: `https://drive.google.com/uc?id=${file.data.id}`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload para Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Faz upload de múltiplos arquivos
|
||||
*/
|
||||
async uploadMultipleFiles(files, folderId = null) {
|
||||
const uploadPromises = files.map(file =>
|
||||
this.uploadFile(file.path, file.name, folderId, file.mimeType)
|
||||
);
|
||||
|
||||
try {
|
||||
const results = await Promise.all(uploadPromises);
|
||||
console.log(`📤 ${results.length} arquivos enviados para Google Drive`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload múltiplo:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleta um arquivo do Google Drive
|
||||
*/
|
||||
async deleteFile(fileId) {
|
||||
try {
|
||||
await this.drive.files.delete({
|
||||
fileId: fileId
|
||||
});
|
||||
console.log(`🗑️ Arquivo deletado do Google Drive: ${fileId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar arquivo do Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista arquivos de uma pasta
|
||||
*/
|
||||
async listFiles(folderId = null, pageSize = 10) {
|
||||
try {
|
||||
const query = folderId ? `'${folderId}' in parents and trashed=false` : 'trashed=false';
|
||||
|
||||
const response = await this.drive.files.list({
|
||||
q: query,
|
||||
pageSize: pageSize,
|
||||
fields: 'files(id, name, mimeType, createdTime, size, webViewLink)'
|
||||
});
|
||||
|
||||
return response.data.files;
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar arquivos do Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o token está próximo do vencimento
|
||||
*/
|
||||
isTokenExpiringSoon() {
|
||||
if (!this.auth.credentials || !this.auth.credentials.expiry_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiryTime = new Date(this.auth.credentials.expiry_date);
|
||||
const now = new Date();
|
||||
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
||||
|
||||
return expiryTime <= fiveMinutesFromNow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renova o token se necessário
|
||||
*/
|
||||
async refreshTokenIfNeeded() {
|
||||
if (this.isTokenExpiringSoon()) {
|
||||
try {
|
||||
const { credentials } = await this.auth.refreshAccessToken();
|
||||
this.auth.setCredentials(credentials);
|
||||
|
||||
// Salvar tokens atualizados
|
||||
fs.writeFileSync(this.tokensPath, JSON.stringify(credentials, null, 2));
|
||||
console.log('🔄 Token Google Drive renovado automaticamente');
|
||||
} catch (error) {
|
||||
console.error('Erro ao renovar token Google Drive:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações sobre o espaço de armazenamento
|
||||
*/
|
||||
async getStorageInfo() {
|
||||
try {
|
||||
const response = await this.drive.about.get({
|
||||
fields: 'storageQuota'
|
||||
});
|
||||
|
||||
const quota = response.data.storageQuota;
|
||||
return {
|
||||
limit: parseInt(quota.limit),
|
||||
usage: parseInt(quota.usage),
|
||||
usageInDrive: parseInt(quota.usageInDrive),
|
||||
usageInDriveTrash: parseInt(quota.usageInDriveTrash)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter informações de armazenamento:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleDriveService;
|
||||
384
config/google-sheets.js
Normal file
384
config/google-sheets.js
Normal file
@@ -0,0 +1,384 @@
|
||||
const { google } = require('googleapis');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class GoogleSheetsService {
|
||||
constructor() {
|
||||
this.auth = null;
|
||||
this.sheets = null;
|
||||
this.drive = null;
|
||||
}
|
||||
|
||||
// Inicializar autenticação OAuth 2.0
|
||||
async initializeAuth(credentialsData = null) {
|
||||
try {
|
||||
let credentials;
|
||||
|
||||
if (credentialsData) {
|
||||
// Usar credenciais fornecidas via parâmetro
|
||||
credentials = credentialsData;
|
||||
} else {
|
||||
// Tentar carregar do arquivo (fallback)
|
||||
const credentialsPath = path.join(__dirname, 'google-credentials.json');
|
||||
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
throw new Error('Credenciais do Google não configuradas. Configure na página de Configurações.');
|
||||
}
|
||||
|
||||
credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
|
||||
}
|
||||
|
||||
const { client_id, client_secret, redirect_uris } = credentials.web || credentials;
|
||||
|
||||
this.auth = new google.auth.OAuth2(
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uris[0]
|
||||
);
|
||||
|
||||
this.sheets = google.sheets({ version: 'v4', auth: this.auth });
|
||||
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao inicializar autenticação Google:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Gerar URL de autorização
|
||||
getAuthUrl() {
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/spreadsheets',
|
||||
'https://www.googleapis.com/auth/drive.file'
|
||||
];
|
||||
|
||||
return this.auth.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: scopes,
|
||||
prompt: 'consent'
|
||||
});
|
||||
}
|
||||
|
||||
// Processar código de autorização
|
||||
async handleAuthCallback(code) {
|
||||
try {
|
||||
const { tokens } = await this.auth.getToken(code);
|
||||
this.auth.setCredentials(tokens);
|
||||
|
||||
// Salvar tokens para uso futuro
|
||||
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||
fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar callback de autorização:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar tokens salvos
|
||||
async loadSavedTokens() {
|
||||
try {
|
||||
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||
|
||||
if (fs.existsSync(tokensPath)) {
|
||||
const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
|
||||
this.auth.setCredentials(tokens);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar tokens salvos:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Criar ou atualizar planilha persistente
|
||||
async createOrUpdatePersistentSpreadsheet(spreadsheetId = null, title = 'Liberi Kids - Sistema de Estoque') {
|
||||
try {
|
||||
if (spreadsheetId) {
|
||||
// Tentar verificar se planilha existente ainda existe
|
||||
try {
|
||||
const existingSheet = await this.sheets.spreadsheets.get({
|
||||
spreadsheetId: spreadsheetId
|
||||
});
|
||||
|
||||
console.log('Planilha existente encontrada, usando a mesma...');
|
||||
return {
|
||||
spreadsheetId,
|
||||
url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`,
|
||||
exists: true
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Planilha não encontrada ou inacessível, criando nova...');
|
||||
}
|
||||
}
|
||||
|
||||
// Criar nova planilha
|
||||
const spreadsheet = await this.sheets.spreadsheets.create({
|
||||
resource: {
|
||||
properties: {
|
||||
title: title
|
||||
},
|
||||
sheets: [
|
||||
{
|
||||
properties: {
|
||||
sheetId: 0,
|
||||
title: 'Produtos',
|
||||
gridProperties: {
|
||||
rowCount: 1000,
|
||||
columnCount: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
sheetId: 1,
|
||||
title: 'Vendas',
|
||||
gridProperties: {
|
||||
rowCount: 1000,
|
||||
columnCount: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const newSpreadsheetId = spreadsheet.data.spreadsheetId;
|
||||
|
||||
// Aguardar um pouco para garantir que a planilha foi criada
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Configurar cabeçalhos
|
||||
await this.setupHeaders(newSpreadsheetId);
|
||||
|
||||
return {
|
||||
spreadsheetId: newSpreadsheetId,
|
||||
url: `https://docs.google.com/spreadsheets/d/${newSpreadsheetId}/edit`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar planilha:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Configurar cabeçalhos das abas
|
||||
async setupHeaders(spreadsheetId) {
|
||||
try {
|
||||
const produtosHeaders = [
|
||||
'ID da Roupa',
|
||||
'Nome do Produto',
|
||||
'Fornecedor',
|
||||
'Tamanho',
|
||||
'Estação',
|
||||
'Gênero',
|
||||
'Valor da Compra',
|
||||
'Valor da Venda',
|
||||
'Data da Compra',
|
||||
'Data da Venda',
|
||||
'Estoque Atual',
|
||||
'Marca'
|
||||
];
|
||||
|
||||
const vendasHeaders = [
|
||||
'ID da Venda',
|
||||
'Cliente',
|
||||
'Data da Venda',
|
||||
'Tipo de Pagamento',
|
||||
'Valor Total',
|
||||
'Desconto',
|
||||
'Valor Final',
|
||||
'Status',
|
||||
'Observações',
|
||||
'Produtos Vendidos'
|
||||
];
|
||||
|
||||
// Configurar cabeçalhos da aba Produtos
|
||||
await this.sheets.spreadsheets.values.update({
|
||||
spreadsheetId,
|
||||
range: 'Produtos!A1:L1',
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
resource: {
|
||||
values: [produtosHeaders]
|
||||
}
|
||||
});
|
||||
|
||||
// Configurar cabeçalhos da aba Vendas
|
||||
await this.sheets.spreadsheets.values.update({
|
||||
spreadsheetId,
|
||||
range: 'Vendas!A1:J1',
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
resource: {
|
||||
values: [vendasHeaders]
|
||||
}
|
||||
});
|
||||
|
||||
// Formatar cabeçalhos (opcional, pode falhar sem quebrar)
|
||||
try {
|
||||
const requests = [
|
||||
{
|
||||
repeatCell: {
|
||||
range: {
|
||||
sheetId: 0,
|
||||
startRowIndex: 0,
|
||||
endRowIndex: 1,
|
||||
startColumnIndex: 0,
|
||||
endColumnIndex: produtosHeaders.length
|
||||
},
|
||||
cell: {
|
||||
userEnteredFormat: {
|
||||
backgroundColor: { red: 0.2, green: 0.6, blue: 1.0 },
|
||||
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
||||
}
|
||||
},
|
||||
fields: 'userEnteredFormat'
|
||||
}
|
||||
},
|
||||
{
|
||||
repeatCell: {
|
||||
range: {
|
||||
sheetId: 1,
|
||||
startRowIndex: 0,
|
||||
endRowIndex: 1,
|
||||
startColumnIndex: 0,
|
||||
endColumnIndex: vendasHeaders.length
|
||||
},
|
||||
cell: {
|
||||
userEnteredFormat: {
|
||||
backgroundColor: { red: 0.2, green: 0.8, blue: 0.2 },
|
||||
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
||||
}
|
||||
},
|
||||
fields: 'userEnteredFormat'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.sheets.spreadsheets.batchUpdate({
|
||||
spreadsheetId,
|
||||
resource: { requests }
|
||||
});
|
||||
} catch (formatError) {
|
||||
console.log('Aviso: Não foi possível formatar cabeçalhos, mas planilha foi criada:', formatError.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao configurar cabeçalhos:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar dados de produtos
|
||||
async exportProdutos(spreadsheetId, produtos) {
|
||||
try {
|
||||
const values = produtos.map(produto => [
|
||||
produto.id || '',
|
||||
produto.nome || '',
|
||||
produto.fornecedor_nome || '',
|
||||
produto.tamanho || '',
|
||||
produto.estacao || '',
|
||||
produto.genero || '',
|
||||
produto.valor_compra || 0,
|
||||
produto.valor_revenda || 0,
|
||||
produto.created_at ? new Date(produto.created_at).toLocaleDateString('pt-BR') : '',
|
||||
'', // Data da venda será preenchida quando houver venda
|
||||
produto.quantidade_total || 0,
|
||||
produto.marca || ''
|
||||
]);
|
||||
|
||||
await this.sheets.spreadsheets.values.update({
|
||||
spreadsheetId,
|
||||
range: 'Produtos!A2:L' + (values.length + 1),
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
resource: { values }
|
||||
});
|
||||
|
||||
return { success: true, rowsUpdated: values.length };
|
||||
} catch (error) {
|
||||
console.error('Erro ao exportar produtos:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar dados de vendas
|
||||
async exportVendas(spreadsheetId, vendas) {
|
||||
try {
|
||||
const values = vendas.map(venda => [
|
||||
venda.id || '',
|
||||
venda.cliente_nome || 'Cliente não informado',
|
||||
venda.data_venda ? new Date(venda.data_venda).toLocaleDateString('pt-BR') : '',
|
||||
venda.tipo_pagamento || '',
|
||||
venda.valor_total || 0,
|
||||
venda.desconto || 0,
|
||||
venda.valor_final || 0,
|
||||
venda.status || 'Concluída',
|
||||
venda.observacoes || '',
|
||||
venda.produtos ? venda.produtos.map(p => `${p.nome} (${p.quantidade}x)`).join(', ') : ''
|
||||
]);
|
||||
|
||||
await this.sheets.spreadsheets.values.update({
|
||||
spreadsheetId,
|
||||
range: 'Vendas!A2:J' + (values.length + 1),
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
resource: { values }
|
||||
});
|
||||
|
||||
return { success: true, rowsUpdated: values.length };
|
||||
} catch (error) {
|
||||
console.error('Erro ao exportar vendas:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se está autenticado
|
||||
isAuthenticated() {
|
||||
return this.auth && this.auth.credentials && this.auth.credentials.access_token;
|
||||
}
|
||||
|
||||
// Verificar se o token está próximo do vencimento
|
||||
isTokenExpiringSoon() {
|
||||
if (!this.auth || !this.auth.credentials || !this.auth.credentials.expiry_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
const expiry = this.auth.credentials.expiry_date;
|
||||
const timeUntilExpiry = expiry - now;
|
||||
|
||||
// Considera que está expirando se faltam menos de 5 minutos
|
||||
return timeUntilExpiry < 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
// Renovar token automaticamente se necessário
|
||||
async refreshTokenIfNeeded() {
|
||||
if (this.isTokenExpiringSoon()) {
|
||||
try {
|
||||
const { credentials } = await this.auth.refreshAccessToken();
|
||||
this.auth.setCredentials(credentials);
|
||||
|
||||
// Salvar tokens atualizados
|
||||
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||
fs.writeFileSync(tokensPath, JSON.stringify(credentials, null, 2));
|
||||
|
||||
console.log('Token Google renovado automaticamente');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao renovar token Google:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resetar o serviço (limpar autenticação)
|
||||
reset() {
|
||||
this.auth = null;
|
||||
this.sheets = null;
|
||||
this.drive = null;
|
||||
console.log('Serviço Google Sheets resetado');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new GoogleSheetsService();
|
||||
7
config/google-tokens.json
Normal file
7
config/google-tokens.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"access_token": "ya29.a0AQQ_BDTf90ixT9y_x6M_zK1qmBhM88FscZ9xmiUDv0acKlSDkge5lxiUEYIi3yxFFiwxv3Az9P-PnV1wWnfEscTcht6z5uvp3LT0uzS3uFWndQ3HWN3X-YVxnvpyBD7P3gCc6YGHz0zHeN1QtyXTAIM9iW0hwLf8R5d88b_EI2DftLLZ7F68jo_MOQwN9pwUkmb6DWlaaCgYKAVoSARcSFQHGX2Milg4J_31KZx9930lNX7B8Hg0207",
|
||||
"scope": "https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/spreadsheets",
|
||||
"token_type": "Bearer",
|
||||
"expiry_date": 1759955985106,
|
||||
"refresh_token": "1//0hZ-EhpsmoFVGCgYIARAAGBESNwF-L9IrIlcr7zua7Q-wDP3Eg6-hje3BoKCDYdaMVWJMqa3VGDx57VgI0O40iu5hYtsbIsg_bTc"
|
||||
}
|
||||
82
config/mercadopago-demo.js
Normal file
82
config/mercadopago-demo.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// Versão DEMO do Mercado Pago para desenvolvimento
|
||||
// Simula a geração de PIX sem credenciais reais
|
||||
|
||||
class MercadoPagoServiceDemo {
|
||||
constructor() {
|
||||
console.log('🎭 Modo DEMO: Simulando Mercado Pago para desenvolvimento');
|
||||
}
|
||||
|
||||
async gerarPix(dados) {
|
||||
try {
|
||||
console.log('🏦 [DEMO] Gerando PIX com dados:', dados);
|
||||
|
||||
// Simular delay da API
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Gerar dados fictícios mas realistas
|
||||
const payment_id = `demo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const qr_code = this.gerarQRCodeDemo();
|
||||
const qr_code_base64 = this.gerarQRCodeBase64Demo();
|
||||
|
||||
console.log('✅ [DEMO] PIX gerado com sucesso!');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
payment_id: payment_id,
|
||||
qr_code: qr_code,
|
||||
qr_code_base64: qr_code_base64,
|
||||
pix_copy_paste: qr_code,
|
||||
expiration_date: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
||||
transaction_amount: parseFloat(dados.valor),
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [DEMO] Erro ao gerar PIX:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async consultarPagamento(payment_id) {
|
||||
try {
|
||||
console.log('🔍 [DEMO] Consultando pagamento:', payment_id);
|
||||
|
||||
// Simular delay da API
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Para demo, sempre retorna pendente
|
||||
return {
|
||||
success: true,
|
||||
status: 'pending',
|
||||
status_detail: 'pending_waiting_payment',
|
||||
transaction_amount: 10.00,
|
||||
date_approved: null,
|
||||
external_reference: payment_id.replace('demo_', '')
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ [DEMO] Erro ao consultar pagamento:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
gerarQRCodeDemo() {
|
||||
// QR Code PIX fictício mas com formato real
|
||||
const timestamp = Date.now().toString();
|
||||
const random = Math.random().toString(36).substr(2, 10);
|
||||
|
||||
return `00020126580014br.gov.bcb.pix0136${random}5204000053039865802BR5925LIBERI KIDS DEMO STORE6009SAO PAULO62070503***6304${timestamp.substr(-4)}`;
|
||||
}
|
||||
|
||||
gerarQRCodeBase64Demo() {
|
||||
// QR Code base64 fictício (imagem 1x1 pixel transparente)
|
||||
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MercadoPagoServiceDemo();
|
||||
81
config/mercadopago.js
Normal file
81
config/mercadopago.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { MercadoPagoConfig, Payment } = require('mercadopago');
|
||||
|
||||
class MercadoPagoService {
|
||||
constructor() {
|
||||
if (process.env.MERCADOPAGO_ACCESS_TOKEN) {
|
||||
this.client = new MercadoPagoConfig({
|
||||
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN,
|
||||
options: { timeout: 5000 }
|
||||
});
|
||||
this.payment = new Payment(this.client);
|
||||
}
|
||||
}
|
||||
|
||||
async gerarPix(dados) {
|
||||
try {
|
||||
console.log('🏦 Gerando PIX com dados:', dados);
|
||||
console.log('🔑 Access Token:', process.env.MERCADOPAGO_ACCESS_TOKEN ? 'Configurado' : 'NÃO CONFIGURADO');
|
||||
|
||||
const payment_data = {
|
||||
transaction_amount: parseFloat(dados.valor),
|
||||
description: dados.descricao || `Venda #${dados.venda_id} - Liberi Kids`,
|
||||
payment_method_id: 'pix',
|
||||
payer: {
|
||||
email: dados.cliente_email || 'cliente@liberikids.com',
|
||||
first_name: dados.cliente_nome || 'Cliente',
|
||||
identification: {
|
||||
type: 'CPF',
|
||||
number: dados.cliente_cpf || '00000000000'
|
||||
}
|
||||
},
|
||||
external_reference: dados.venda_id.toString(),
|
||||
// notification_url removida para desenvolvimento local
|
||||
date_of_expiration: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 minutos
|
||||
};
|
||||
|
||||
console.log('📤 Enviando dados para Mercado Pago:', JSON.stringify(payment_data, null, 2));
|
||||
const payment = await this.payment.create({ body: payment_data });
|
||||
console.log('✅ Resposta do Mercado Pago:', payment);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
payment_id: payment.id,
|
||||
qr_code: payment.point_of_interaction.transaction_data.qr_code,
|
||||
qr_code_base64: payment.point_of_interaction.transaction_data.qr_code_base64,
|
||||
pix_copy_paste: payment.point_of_interaction.transaction_data.qr_code,
|
||||
expiration_date: payment.date_of_expiration,
|
||||
transaction_amount: payment.transaction_amount,
|
||||
status: payment.status
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PIX:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async consultarPagamento(payment_id) {
|
||||
try {
|
||||
const payment = await this.payment.get({ id: payment_id });
|
||||
return {
|
||||
success: true,
|
||||
status: payment.status,
|
||||
status_detail: payment.status_detail,
|
||||
transaction_amount: payment.transaction_amount,
|
||||
date_approved: payment.date_approved,
|
||||
external_reference: payment.external_reference
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao consultar pagamento:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MercadoPagoService();
|
||||
8
config/supabase.js
Normal file
8
config/supabase.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
|
||||
const supabaseUrl = 'https://xyqmlesqdqybiyjofysb.supabase.co';
|
||||
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5cW1sZXNxZHF5Yml5am9meXNiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk2NjEzMzcsImV4cCI6MjA3NTIzNzMzN30.uXPONkstd_xXbzX1ZwlB9gK05zjwQL0Ymj94_3NnOGE';
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
module.exports = supabase;
|
||||
Reference in New Issue
Block a user