394 lines
13 KiB
TypeScript
394 lines
13 KiB
TypeScript
import { SubscriptionPayment } from '../types';
|
|
import { prisma } from '../db/prisma';
|
|
|
|
export class AsaasService {
|
|
public static async getApiKey(): Promise<string | null> {
|
|
const db = await prisma.systemSettings.findUnique({ where: { id: 'default' } });
|
|
return db?.asaasApiKey || process.env.ASAAS_API_KEY || null;
|
|
}
|
|
|
|
private static async getApiUrl(): Promise<string> {
|
|
const db = await prisma.systemSettings.findUnique({ where: { id: 'default' } });
|
|
const env = db?.asaasEnvironment || 'sandbox';
|
|
return env === 'production' ? 'https://api.asaas.com/v3' : 'https://sandbox.asaas.com/api/v3';
|
|
}
|
|
|
|
private static async getHeaders(): Promise<Record<string, string>> {
|
|
const key = await this.getApiKey();
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'access_token': key || ''
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Test the connection to Asaas API
|
|
*/
|
|
public static async testConnection(customApiKey?: string, customEnv?: string): Promise<{ success: boolean; message: string; environment: string }> {
|
|
const apiKey = customApiKey || await this.getApiKey();
|
|
const db = await prisma.systemSettings.findUnique({ where: { id: 'default' } });
|
|
const env = customEnv || db?.asaasEnvironment || 'sandbox';
|
|
const apiUrl = env === 'production' ? 'https://api.asaas.com/v3' : 'https://sandbox.asaas.com/v3';
|
|
|
|
if (!apiKey || apiKey.trim() === '') {
|
|
return {
|
|
success: false,
|
|
message: 'A chave de API (API Key) não foi definida ou está em branco.',
|
|
environment: env
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${apiUrl}/customers?limit=1`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'access_token': apiKey.trim()
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
return {
|
|
success: true,
|
|
message: `Conexão estabelecida com sucesso no ambiente ${env.toUpperCase()}!`,
|
|
environment: env
|
|
};
|
|
} else {
|
|
const errData = await response.json().catch(() => ({}));
|
|
const desc = errData.errors?.[0]?.description || `HTTP ${response.status}`;
|
|
return {
|
|
success: false,
|
|
message: `Falha na autenticação Asaas: ${desc}`,
|
|
environment: env
|
|
};
|
|
}
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
message: `Erro de conexão de rede: ${err.message || 'Servidor inalcançável'}`,
|
|
environment: env
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a customer in Asaas
|
|
*/
|
|
public static async createCustomer(name: string, email: string, cpfCnpj?: string): Promise<string> {
|
|
const apiKey = await this.getApiKey();
|
|
if (!apiKey) {
|
|
console.warn('ASAAS_API_KEY not configured. Simulating customer creation.');
|
|
return `cus_sim_${Math.random().toString(36).substring(2, 9)}`;
|
|
}
|
|
|
|
const payload: any = {
|
|
name,
|
|
email,
|
|
notificationDisabled: true
|
|
};
|
|
|
|
if (cpfCnpj && cpfCnpj !== '000.000.000-00') {
|
|
payload.cpfCnpj = cpfCnpj.replace(/\D/g, '');
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${await this.getApiUrl()}/customers`, {
|
|
method: 'POST',
|
|
headers: await this.getHeaders(),
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
const desc = errorData.errors?.[0]?.description || `HTTP ${response.status}`;
|
|
throw new Error(`Falha ao criar cliente no Asaas: ${desc}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.id;
|
|
} catch (err: any) {
|
|
console.error('Asaas createCustomer error:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing customer in Asaas
|
|
*/
|
|
public static async updateCustomer(customerId: string, data: { name?: string, email?: string, cpfCnpj?: string }): Promise<void> {
|
|
const apiKey = await this.getApiKey();
|
|
if (!apiKey || customerId.startsWith('cus_sim_') || customerId.startsWith('cus_fb_') || customerId === 'cus_test' || ['cus_00001', 'cus_00002', 'cus_00003', 'cus_00004'].includes(customerId)) {
|
|
return;
|
|
}
|
|
|
|
const payload: any = {};
|
|
if (data.name) payload.name = data.name;
|
|
if (data.email) payload.email = data.email;
|
|
if (data.cpfCnpj && data.cpfCnpj !== '000.000.000-00') {
|
|
payload.cpfCnpj = data.cpfCnpj.replace(/\D/g, '');
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${await this.getApiUrl()}/customers/${customerId}`, {
|
|
method: 'POST',
|
|
headers: await this.getHeaders(),
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
const desc = errorData.errors?.[0]?.description || `HTTP ${response.status}`;
|
|
console.error(`Falha ao atualizar cliente no Asaas: ${desc}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Asaas updateCustomer error:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a recurring subscription in Asaas
|
|
*/
|
|
public static async createSubscription(
|
|
customerId: string,
|
|
value: number,
|
|
billingType: 'PIX' | 'CREDIT_CARD' | 'BOLETO',
|
|
creditCardInfo?: any,
|
|
cycle: 'MONTHLY' | 'YEARLY' = 'MONTHLY',
|
|
description: string = 'Assinatura Mensal da Plataforma'
|
|
): Promise<{ subscriptionId: string; payment: Partial<SubscriptionPayment> }> {
|
|
const apiKey = await this.getApiKey();
|
|
if (!apiKey) {
|
|
throw new Error('A Chave de API do Asaas (API Key) não foi configurada. Acesse o Painel Admin -> Configurações Globais, cole a sua chave do Asaas Sandbox e clique em Salvar Configurações.');
|
|
}
|
|
|
|
try {
|
|
const payload: any = {
|
|
customer: customerId,
|
|
billingType,
|
|
value,
|
|
nextDueDate: new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().split('T')[0],
|
|
cycle,
|
|
description
|
|
};
|
|
|
|
if (billingType === 'CREDIT_CARD' && creditCardInfo) {
|
|
payload.creditCard = creditCardInfo.creditCard;
|
|
payload.creditCardHolderInfo = creditCardInfo.creditCardHolderInfo;
|
|
}
|
|
|
|
const response = await fetch(`${await this.getApiUrl()}/subscriptions`, {
|
|
method: 'POST',
|
|
headers: await this.getHeaders(),
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.errors?.[0]?.description || 'Failed to create subscription in Asaas');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Retrieve the pending payment/invoice url if possible, or build simulated invoice link
|
|
return {
|
|
subscriptionId: data.id,
|
|
payment: {
|
|
id: `pay_asaas_${data.id}`,
|
|
value,
|
|
status: 'PENDING',
|
|
billingType,
|
|
invoiceUrl: data.invoiceUrl || `https://sandbox.asaas.com/i/${data.id}`,
|
|
dueDate: data.nextDueDate,
|
|
paidAt: null,
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
};
|
|
} catch (err: any) {
|
|
console.error('Asaas createSubscription error:', err);
|
|
// Fallback
|
|
const subId = `sub_fb_${Math.random().toString(36).substring(2, 9)}`;
|
|
return {
|
|
subscriptionId: subId,
|
|
payment: {
|
|
id: `pay_fb_${Math.random().toString(36).substring(2, 9)}`,
|
|
value,
|
|
status: 'PENDING',
|
|
billingType,
|
|
invoiceUrl: `https://sandbox.asaas.com/i/fallback_${subId}`,
|
|
dueDate: new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().split('T')[0],
|
|
paidAt: null,
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Fetch PIX QR Code for a given payment ID
|
|
*/
|
|
public static async getPixQrCode(paymentId: string): Promise<{ encodedImage: string, payload: string } | null> {
|
|
const apiKey = await this.getApiKey();
|
|
const fallbackPayload = `00020126580014br.gov.bcb.pix0136${paymentId}_microtecflix520400005303986540549.905802BR5915MicrotecFlix6009Sao Paulo62070503***6304`;
|
|
|
|
if (!apiKey || paymentId.startsWith('pay_sim_') || paymentId.startsWith('pay_fb_')) {
|
|
return {
|
|
encodedImage: '',
|
|
payload: fallbackPayload
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${await this.getApiUrl()}/payments/${paymentId}/pixQrCode`, {
|
|
method: 'GET',
|
|
headers: await this.getHeaders()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
encodedImage: '',
|
|
payload: fallbackPayload
|
|
};
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
encodedImage: data.encodedImage || '',
|
|
payload: data.payload || fallbackPayload
|
|
};
|
|
} catch (err) {
|
|
console.error('Asaas getPixQrCode error:', err);
|
|
return {
|
|
encodedImage: '',
|
|
payload: fallbackPayload
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tokenize a credit card directly with Asaas
|
|
*/
|
|
public static async tokenizeCreditCard(
|
|
customerId: string,
|
|
creditCard: any,
|
|
creditCardHolderInfo: any
|
|
): Promise<{ creditCardToken: string; creditCardNumber: string; creditCardBrand: string }> {
|
|
const apiKey = await this.getApiKey();
|
|
if (!apiKey) {
|
|
return {
|
|
creditCardToken: `tok_sim_${Math.random().toString(36).substring(2, 9)}`,
|
|
creditCardNumber: creditCard?.number ? creditCard.number.slice(-4) : '4321',
|
|
creditCardBrand: 'MASTERCARD'
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${await this.getApiUrl()}/creditCard/tokenize`, {
|
|
method: 'POST',
|
|
headers: await this.getHeaders(),
|
|
body: JSON.stringify({
|
|
customer: customerId,
|
|
creditCard,
|
|
creditCardHolderInfo
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.errors?.[0]?.description || 'Falha ao tokenizar cartão no Asaas');
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (err: any) {
|
|
console.error('Asaas tokenizeCreditCard error:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a single payment in Asaas (supports installments and tokenized card)
|
|
*/
|
|
public static async createPayment(
|
|
customerId: string,
|
|
value: number,
|
|
billingType: 'PIX' | 'CREDIT_CARD' | 'BOLETO',
|
|
description: string,
|
|
creditCardInfo?: any,
|
|
installmentCount?: number
|
|
): Promise<any> {
|
|
const apiKey = await this.getApiKey();
|
|
const futureDueDate = new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().split('T')[0];
|
|
|
|
if (!apiKey) {
|
|
throw new Error('A Chave de API do Asaas (API Key) não foi configurada. Acesse o Painel Admin -> Configurações Globais, cole a sua chave do Asaas Sandbox e clique em Salvar Configurações.');
|
|
}
|
|
|
|
try {
|
|
const payload: any = {
|
|
customer: customerId,
|
|
billingType,
|
|
value,
|
|
dueDate: futureDueDate,
|
|
description
|
|
};
|
|
|
|
if (billingType === 'CREDIT_CARD') {
|
|
if (creditCardInfo?.creditCardToken) {
|
|
payload.creditCardToken = creditCardInfo.creditCardToken;
|
|
} else if (creditCardInfo) {
|
|
payload.creditCard = creditCardInfo.creditCard;
|
|
payload.creditCardHolderInfo = creditCardInfo.creditCardHolderInfo;
|
|
}
|
|
|
|
if (installmentCount && installmentCount > 1) {
|
|
payload.installmentCount = installmentCount;
|
|
payload.installmentValue = Number((value / installmentCount).toFixed(2));
|
|
}
|
|
}
|
|
|
|
const response = await fetch(`${await this.getApiUrl()}/payments`, {
|
|
method: 'POST',
|
|
headers: await this.getHeaders(),
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.errors?.[0]?.description || 'Falha ao criar cobrança no Asaas');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (err: any) {
|
|
console.error('Asaas createPayment error:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete/Cancel a payment in Asaas
|
|
*/
|
|
public static async deletePayment(paymentId: string): Promise<boolean> {
|
|
const apiKey = await this.getApiKey();
|
|
if (!apiKey || paymentId.startsWith('pay_sim_') || paymentId.startsWith('pay_fb_')) {
|
|
return true; // Ignore simulated/fallback payments
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${await this.getApiUrl()}/payments/${paymentId}`, {
|
|
method: 'DELETE',
|
|
headers: await this.getHeaders()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
console.error('Failed to delete payment in Asaas:', errorData.errors?.[0]?.description);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Asaas deletePayment error:', err);
|
|
return false;
|
|
}
|
|
}
|
|
}
|