- Add rate limiting to prevent abuse on /api/donations/create endpoint - Replace manual input validation with Zod schema for better error handling and type safety - Switch from console.log to secureLogger to comply with LGPD and avoid exposing sensitive data - Remove sensitive request body logging in AbacatePay client for improved security
196 lines
5.1 KiB
TypeScript
196 lines
5.1 KiB
TypeScript
interface AbacatePayConfig {
|
|
apiKey: string;
|
|
baseUrl?: string;
|
|
}
|
|
|
|
interface CreateBillingParams {
|
|
amount: number;
|
|
metadata?: {
|
|
donorName?: string;
|
|
donorEmail?: string;
|
|
message?: string;
|
|
};
|
|
returnUrl?: string;
|
|
completionUrl?: string;
|
|
}
|
|
|
|
interface BillingResponse {
|
|
id: string;
|
|
amount: number;
|
|
status: "PENDING" | "PAID" | "EXPIRED" | "CANCELLED";
|
|
brCode?: string; // Código PIX copia-e-cola
|
|
brCodeBase64?: string; // QR Code em Base64
|
|
expiresAt?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface CreatePixParams {
|
|
amount: number; // em centavos
|
|
description: string;
|
|
expiresIn?: number; // em segundos, padrão 600 (10 min)
|
|
customer?: {
|
|
name: string;
|
|
email?: string;
|
|
cellphone?: string;
|
|
taxId?: string;
|
|
};
|
|
}
|
|
|
|
class AbacatePayClient {
|
|
private config: AbacatePayConfig;
|
|
|
|
constructor(config: AbacatePayConfig) {
|
|
this.config = {
|
|
...config,
|
|
baseUrl: config.baseUrl || "https://api.abacatepay.com/v1",
|
|
};
|
|
}
|
|
|
|
async createPixQrCode(params: CreatePixParams): Promise<BillingResponse> {
|
|
const requestBody: any = {
|
|
amount: params.amount,
|
|
expiresIn: params.expiresIn || 600, // 10 minutos por padrão
|
|
description: params.description,
|
|
};
|
|
|
|
if (params.customer) {
|
|
requestBody.customer = params.customer;
|
|
}
|
|
|
|
// Request body logging removido para segurança LGPD
|
|
// Dados sensíveis não devem aparecer em logs
|
|
|
|
const response = await fetch(`${this.config.baseUrl}/pixQrCode/create`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
console.error("PIX Error:", errorData);
|
|
throw new Error(`Failed to create PIX: ${JSON.stringify(errorData)}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
async createBilling(params: CreateBillingParams): Promise<BillingResponse> {
|
|
const response = await fetch(`${this.config.baseUrl}/billing`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
amount: params.amount * 100, // centavos
|
|
metadata: params.metadata,
|
|
return_url: params.returnUrl,
|
|
completion_url: params.completionUrl,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
`Failed to create billing: ${errorData.message || response.statusText}`
|
|
);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async getBillingStatus(billingId: string): Promise<BillingResponse> {
|
|
const response = await fetch(
|
|
`${this.config.baseUrl}/billing/${billingId}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
`Failed to get billing status: ${
|
|
errorData.message || response.statusText
|
|
}`
|
|
);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// AbacatePay retorna { error: null, data: { ... } }
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
// Retornar apenas o data
|
|
return result.data;
|
|
}
|
|
|
|
async getPixStatus(pixId: string): Promise<BillingResponse> {
|
|
// AbacatePay usa o endpoint /billing/{id} para todos os tipos de billing
|
|
const response = await fetch(`${this.config.baseUrl}/billing/${pixId}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
`Failed to get PIX status: ${errorData.message || response.statusText}`
|
|
);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
verifyWebhookSignature(payload: string, signature: string): boolean {
|
|
// Método mantido para compatibilidade, mas validação agora é feita via query param
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Instância do cliente (será configurada com variáveis de ambiente)
|
|
const isProduction = process.env.NODE_ENV === "production";
|
|
const apiKey = isProduction
|
|
? process.env.ABACATE_APIKEY || "demo_key"
|
|
: process.env.ABACATE_TEST_APIKEY || "demo_key";
|
|
|
|
// Log das variáveis de ambiente (sem expor valores sensíveis)
|
|
console.log("\n🔧 Configuração do AbacatePay:");
|
|
console.log(`📊 Environment: ${isProduction ? "PRODUCTION" : "TEST"}`);
|
|
console.log(`📊 API Key: ${apiKey ? "✅ Configurada" : "❌ NÃO CONFIGURADA"}`);
|
|
console.log(
|
|
`📊 Webhook Secret: ${
|
|
process.env.ABACATEPAY_WEBHOOK_SECRET
|
|
? "✅ Configurada"
|
|
: "❌ NÃO CONFIGURADA"
|
|
}`
|
|
);
|
|
console.log("🔧 ==========================================\n");
|
|
|
|
export const abacatePay = new AbacatePayClient({
|
|
apiKey,
|
|
});
|
|
|
|
export type { CreateBillingParams, BillingResponse };
|