396 lines
No EOL
10 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}; |