saveinmed/frontend/src/services/mercadoPagoService.ts

396 lines
No EOL
10 KiB
TypeScript

/**
* Serviço para integração com Mercado Pago via BFF
* Baseado na API: https://bff-dev.saveinmed.com.br/mercadopago/preference
*/
const BFF_BASE_URL = process.env.NEXT_PUBLIC_BFF_API_URL!;
const WEBHOOK_BASE = process.env.NEXT_PUBLIC_BFF_API_URL_MP!;
const FRONT_URL = process.env.NEXT_PUBLIC_FRONTEND_URL;
/**
* Interface para item do Mercado Pago
*/
export interface MercadoPagoItem {
currency_id: string;
id: string;
quantity: number;
title: string;
unit_price: number;
}
/**
* Interface para dados do pagador
*/
export interface MercadoPagoPayer {
email: string;
name: string;
surname: string;
}
/**
* Interface para URLs de retorno
*/
export interface MercadoPagoBackUrls {
failure: string;
success: string;
pending?: string;
}
/**
* Interface para configurações de pagamento
*/
export interface MercadoPagoPaymentMethods {
excluded_payment_methods?: Array<{ id: string }>;
excluded_payment_types?: Array<{ id: string }>;
installments?: number;
default_installments?: number;
}
/**
* Interface para dados da preferência do Mercado Pago
*/
export interface MercadoPagoPreferenceData {
back_urls: MercadoPagoBackUrls;
external_reference: string;
items: MercadoPagoItem[];
notification_url: string;
payer: MercadoPagoPayer;
payment_methods?: MercadoPagoPaymentMethods;
}
/**
* Interface para resposta da API
*/
export interface MercadoPagoResponse {
success: boolean;
data?: any;
error?: string;
message?: string;
preference_id?: string;
init_point?: string;
sandbox_init_point?: string;
}
/**
* Serviço do Mercado Pago
*/
export const mercadoPagoService = {
/**
* Obtém o token de autenticação do localStorage
*/
getAuthToken: (): string | null => {
return localStorage.getItem('access_token');
},
/**
* Obtém dados do usuário atual
*/
getUserData: (): any => {
try {
const userStr = localStorage.getItem('user');
if (!userStr) return null;
return JSON.parse(userStr);
} catch (error) {
console.error('Erro ao obter dados do usuário:', error);
return null;
}
},
/**
* Cria uma preferência de pagamento no Mercado Pago
*/
criarPreferencia: async (
pedidoId: string,
itens: Array<{
produto: {
id: string;
nome?: string;
descricao?: string;
preco_venda?: number;
preco_base?: number;
preco_final?: number;
};
quantidade: number;
}>,
dadosComprador?: {
nome?: string;
email?: string;
}
): Promise<MercadoPagoResponse> => {
try {
const token = mercadoPagoService.getAuthToken();
const userData = mercadoPagoService.getUserData();
// Mapear itens para formato do Mercado Pago
const mercadoPagoItems: MercadoPagoItem[] = itens.map((item, index) => {
const preco = (item.produto as any).preco_final || 0;
const nome = item.produto.nome || item.produto.descricao || `Produto ${index + 1}`;
return {
currency_id: "BRL",
id: item.produto.id || `item-${index}`,
quantity: item.quantidade,
title: nome.substring(0, 255), // Mercado Pago tem limite de caracteres
unit_price: preco
};
});
// Dados do comprador
const nomeCompleto = dadosComprador?.nome || userData?.nome || userData?.name || 'Cliente';
const email = dadosComprador?.email || userData?.email || 'cliente@example.com';
// Separar nome e sobrenome
const partesNome = nomeCompleto.split(' ');
const nome = partesNome[0] || 'Cliente';
const sobrenome = partesNome.slice(1).join(' ') || 'Teste';
// Montar dados da preferência
const preferenceData: MercadoPagoPreferenceData = {
back_urls: {
failure: `${FRONT_URL}/pagamento/erro`,
success: `${FRONT_URL}/pagamento/sucesso`,
pending: `${FRONT_URL}/pagamento/mercadopago/pendente?ref=${pedidoId}`
},
external_reference: pedidoId,
items: mercadoPagoItems,
notification_url: `${WEBHOOK_BASE}/mercadopago/webhook`,
payer: {
email: email,
name: nome,
surname: sobrenome
},
payment_methods: {
installments: 1, // Máximo de 1 parcela (à vista)
default_installments: 1, // Padrão 1 parcela (à vista)
excluded_payment_types: [ // Excluir boletos
{ id: "ticket" } // Remove boletos bancários
]
}
};
// Headers para a requisição
const headers: HeadersInit = {
'accept': 'application/json',
'Content-Type': 'application/json',
};
// Adicionar token se disponível
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL_MP}/mercadopago/preference`, {
method: 'POST',
headers,
body: JSON.stringify(preferenceData),
});
const data = await response.json();
if (response.ok) {
return {
success: true,
data,
preference_id: data.id,
init_point: data.init_point,
sandbox_init_point: data.sandbox_init_point
};
} else {
console.error('❌ Erro ao criar preferência Mercado Pago:', data);
return {
success: false,
error: data.message || data.error || 'Erro ao criar preferência de pagamento'
};
}
} catch (error) {
console.error('💥 Erro na criação da preferência Mercado Pago:', error);
return {
success: false,
error: 'Erro de conexão ao criar preferência de pagamento'
};
}
},
/**
* Verifica status de um pagamento
*/
verificarStatusPagamento: async (paymentId: string): Promise<MercadoPagoResponse> => {
try {
const response = await fetch(`/api/mercadopago/verificar-pagamento?id=${paymentId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.success) {
return { success: true, data: data.data };
} else {
console.error('❌ Erro ao verificar status:', data);
return {
success: false,
error: data.error || 'Erro ao verificar status do pagamento'
};
}
} catch (error) {
console.error('💥 Erro ao verificar status:', error);
return {
success: false,
error: 'Erro de conexão ao verificar status'
};
}
},
/**
* Verifica status de um pagamento por external_reference
*/
verificarStatusPorReferencia: async (externalReference: string): Promise<MercadoPagoResponse> => {
try {
const response = await fetch(`/api/mercadopago/verificar-pagamento?external_reference=${externalReference}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.success) {
return { success: true, data: data.data };
} else {
console.error('❌ Erro ao verificar status por referência:', data);
return {
success: false,
error: data.error || 'Erro ao verificar status do pagamento'
};
}
} catch (error) {
console.error('💥 Erro ao verificar status por referência:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erro de conexão ao verificar status'
};
}
},
/**
* Processa notificação de webhook do Mercado Pago
*/
processarNotificacao: async (notificationData: any): Promise<MercadoPagoResponse> => {
try {
const token = mercadoPagoService.getAuthToken();
const headers: HeadersInit = {
'accept': 'application/json',
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${BFF_BASE_URL}/mercadopago/webhook`, {
method: 'POST',
headers,
body: JSON.stringify(notificationData),
});
const data = await response.json();
if (response.ok) {
return { success: true, data };
} else {
console.error('❌ Erro ao processar notificação:', data);
return {
success: false,
error: data.message || 'Erro ao processar notificação'
};
}
} catch (error) {
console.error('💥 Erro ao processar notificação:', error);
return {
success: false,
error: 'Erro de conexão ao processar notificação'
};
}
},
/**
* Calcula valor total dos itens
*/
calcularValorTotal: (itens: Array<{
produto: {
preco_venda?: number;
preco_base?: number;
preco_final?: number;
};
quantidade: number;
}>): number => {
return itens.reduce((total, item) => {
const preco = (item.produto as any).preco_final || 0;
return total + (preco * item.quantidade);
}, 0);
},
/**
* Formata preço para exibição
*/
formatarPreco: (valor: number): string => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
},
/**
* Valida dados de preferência antes de enviar
*/
validarDadosPreferencia: (
pedidoId: string,
itens: Array<any>
): { valido: boolean; erros: string[] } => {
const erros: string[] = [];
// Validar pedido ID
if (!pedidoId || pedidoId.trim() === '') {
erros.push('ID do pedido é obrigatório');
}
// Validar itens
if (!itens || itens.length === 0) {
erros.push('Pelo menos um item é obrigatório');
} else {
itens.forEach((item, index) => {
if (!item.produto) {
erros.push(`Item ${index + 1}: dados do produto são obrigatórios`);
} else {
if (!item.produto.id) {
erros.push(`Item ${index + 1}: ID do produto é obrigatório`);
}
const preco = (item.produto as any).preco_final;
if (!preco || preco <= 0) {
erros.push(`Item ${index + 1}: preço final deve ser maior que zero`);
}
if (!item.quantidade || item.quantidade <= 0) {
erros.push(`Item ${index + 1}: quantidade deve ser maior que zero`);
}
}
});
}
return {
valido: erros.length === 0,
erros
};
}
};