fix: JSX syntax error and implement real Asaas payment gateway for courses and subscriptions
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 42s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 42s
Details
This commit is contained in:
parent
2ef1416d8d
commit
bd9403a872
87
server.ts
87
server.ts
|
|
@ -286,9 +286,10 @@ app.get('/api/courses/:id', authenticateToken, (req: AuthenticatedRequest, res:
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5.5 Unlock Course
|
// 5.5 Unlock Course
|
||||||
app.post('/api/courses/:id/unlock', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
|
app.post('/api/courses/:id/unlock', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const courseId = req.params.id;
|
const courseId = req.params.id;
|
||||||
const userId = req.user!.id;
|
const userId = req.user!.id;
|
||||||
|
const { billingType, creditCardInfo } = req.body;
|
||||||
|
|
||||||
const db = loadDb();
|
const db = loadDb();
|
||||||
const user = db.users.find(u => u.id === userId);
|
const user = db.users.find(u => u.id === userId);
|
||||||
|
|
@ -303,29 +304,45 @@ app.post('/api/courses/:id/unlock', authenticateToken, (req: AuthenticatedReques
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.unlockedCourses) {
|
try {
|
||||||
user.unlockedCourses = [];
|
let customerId = user.asaasCustomerId;
|
||||||
}
|
if (!customerId) {
|
||||||
if (!user.unlockedCourses.includes(courseId)) {
|
customerId = await AsaasService.createCustomer(user.name, user.email);
|
||||||
user.unlockedCourses.push(courseId);
|
user.asaasCustomerId = customerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record an Asaas invoice/payment for this curso unlock
|
const asaasPayment = await AsaasService.createPayment(
|
||||||
const newPayment: SubscriptionPayment = {
|
customerId,
|
||||||
id: `pay_crs_${Math.random().toString(36).substring(2, 9)}`,
|
course.price || 49.90,
|
||||||
userId,
|
billingType || 'PIX',
|
||||||
value: course.price || 49.90,
|
`unlock_course_${courseId}`,
|
||||||
status: 'CONFIRMED',
|
creditCardInfo
|
||||||
billingType: 'PIX',
|
);
|
||||||
invoiceUrl: `https://www.asaas.com/i/unlock_${courseId}`,
|
|
||||||
dueDate: new Date().toISOString().split('T')[0],
|
|
||||||
paidAt: new Date().toISOString(),
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
db.payments.push(newPayment);
|
|
||||||
|
|
||||||
saveDb(db);
|
let pixData = null;
|
||||||
res.json({ success: true, unlockedCourses: user.unlockedCourses });
|
if ((billingType || 'PIX') === 'PIX') {
|
||||||
|
pixData = await AsaasService.getPixQrCode(asaasPayment.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPayment: SubscriptionPayment = {
|
||||||
|
id: asaasPayment.id,
|
||||||
|
userId,
|
||||||
|
value: course.price || 49.90,
|
||||||
|
status: 'PENDING',
|
||||||
|
billingType: billingType || 'PIX',
|
||||||
|
invoiceUrl: asaasPayment.invoiceUrl,
|
||||||
|
dueDate: asaasPayment.dueDate || new Date().toISOString().split('T')[0],
|
||||||
|
paidAt: null,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
db.payments.push(newPayment);
|
||||||
|
saveDb(db);
|
||||||
|
|
||||||
|
res.json({ success: true, payment: newPayment, pixData });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Course unlock error:', err);
|
||||||
|
res.status(500).json({ error: err.message || 'Erro ao processar pagamento do curso' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. Lesson Progress tracking
|
// 6. Lesson Progress tracking
|
||||||
|
|
@ -964,13 +981,27 @@ app.post('/api/webhooks/asaas', (req: Request, res: Response) => {
|
||||||
|
|
||||||
// Handle subscription access control
|
// Handle subscription access control
|
||||||
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
||||||
// Liberar / manter acesso
|
const description = payment.description || '';
|
||||||
user.subscriptionStatus = 'ACTIVE';
|
|
||||||
console.log(`Webhook: Acesso LIBERADO para usuário ${user.email} (${user.name})`);
|
if (description.startsWith('unlock_course_')) {
|
||||||
|
const courseId = description.replace('unlock_course_', '');
|
||||||
|
if (!user.unlockedCourses) user.unlockedCourses = [];
|
||||||
|
if (!user.unlockedCourses.includes(courseId)) {
|
||||||
|
user.unlockedCourses.push(courseId);
|
||||||
|
}
|
||||||
|
console.log(`Webhook: Curso ${courseId} LIBERADO para usuário ${user.email}`);
|
||||||
|
} else {
|
||||||
|
// Liberar / manter acesso
|
||||||
|
user.subscriptionStatus = 'ACTIVE';
|
||||||
|
console.log(`Webhook: Acesso LIBERADO para usuário ${user.email} (${user.name})`);
|
||||||
|
}
|
||||||
} else if (event === 'PAYMENT_OVERDUE' || event === 'SUBSCRIPTION_DELETED') {
|
} else if (event === 'PAYMENT_OVERDUE' || event === 'SUBSCRIPTION_DELETED') {
|
||||||
// Bloquear acesso automaticamente
|
// Bloquear acesso automaticamente se for assinatura
|
||||||
user.subscriptionStatus = event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'CANCELED';
|
const description = payment.description || '';
|
||||||
console.log(`Webhook: Acesso BLOQUEADO para usuário ${user.email} (${user.name}) - Motivo: ${event}`);
|
if (!description.startsWith('unlock_course_')) {
|
||||||
|
user.subscriptionStatus = event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'CANCELED';
|
||||||
|
console.log(`Webhook: Acesso BLOQUEADO para usuário ${user.email} (${user.name}) - Motivo: ${event}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Webhook: Cliente Asaas ${customerId} não encontrado no banco local.`);
|
console.warn(`Webhook: Cliente Asaas ${customerId} não encontrado no banco local.`);
|
||||||
|
|
|
||||||
|
|
@ -773,7 +773,7 @@ export default function AdminDashboard({ token }: AdminDashboardProps) {
|
||||||
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors min-h-[120px]"
|
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors min-h-[120px]"
|
||||||
placeholder="Escreva aqui as instruções de ajuda para os alunos..."
|
placeholder="Escreva aqui as instruções de ajuda para os alunos..."
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-zinc-500">O texto que aparece para o aluno em Meus Dados -> Ajuda.</p>
|
<p className="text-[10px] text-zinc-500">O texto que aparece para o aluno em Meus Dados -> Ajuda.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
const [boletoCopied, setBoletoCopied] = useState(false);
|
const [boletoCopied, setBoletoCopied] = useState(false);
|
||||||
const [unlocked, setUnlocked] = useState(false);
|
const [unlocked, setUnlocked] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [paymentData, setPaymentData] = useState<any>(null);
|
||||||
|
|
||||||
// Form states
|
// Form states
|
||||||
const [ccNumber, setCcNumber] = useState('');
|
const [ccNumber, setCcNumber] = useState('');
|
||||||
|
|
@ -26,15 +28,17 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
const coursePrice = course.price || 49.90;
|
const coursePrice = course.price || 49.90;
|
||||||
|
|
||||||
const handleCopyPix = () => {
|
const handleCopyPix = () => {
|
||||||
navigator.clipboard.writeText(`00020126580014br.gov.bcb.pix0136unlock_course_${course.id}_microtecflix5204000053039865405${coursePrice.toFixed(2)}5802BR5915MicrotecFlix6009Sao Paulo62070503***6304`);
|
if (paymentData?.pixData?.payload) {
|
||||||
setPixCopied(true);
|
navigator.clipboard.writeText(paymentData.pixData.payload);
|
||||||
setTimeout(() => setPixCopied(false), 2000);
|
setPixCopied(true);
|
||||||
|
setTimeout(() => setPixCopied(false), 2000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyBoleto = () => {
|
const handleCopyBoleto = () => {
|
||||||
navigator.clipboard.writeText("34191.79001 01043.513184 91020.150008 7 900200000" + Math.floor(coursePrice * 100));
|
if (paymentData?.payment?.invoiceUrl) {
|
||||||
setBoletoCopied(true);
|
window.open(paymentData.payment.invoiceUrl, '_blank');
|
||||||
setTimeout(() => setBoletoCopied(false), 2000);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmPayment = async (e: React.FormEvent) => {
|
const handleConfirmPayment = async (e: React.FormEvent) => {
|
||||||
|
|
@ -45,21 +49,48 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
// Simulate network delay for a real feeling
|
// Simulate network delay for a real feeling
|
||||||
await new Promise(resolve => setTimeout(resolve, 1800));
|
await new Promise(resolve => setTimeout(resolve, 1800));
|
||||||
|
|
||||||
|
const creditCardInfo = paymentMethod === 'CREDIT_CARD' ? {
|
||||||
|
creditCard: {
|
||||||
|
holderName: ccName,
|
||||||
|
number: ccNumber.replace(/\s+/g, ''),
|
||||||
|
expiryMonth: ccExpiry.split('/')[0]?.trim(),
|
||||||
|
expiryYear: '20' + ccExpiry.split('/')[1]?.trim(),
|
||||||
|
ccv: ccCvv
|
||||||
|
},
|
||||||
|
creditCardHolderInfo: {
|
||||||
|
name: ccName,
|
||||||
|
email: 'aluno@microtecflix.com', // Will be fetched by server
|
||||||
|
cpfCnpj: '000.000.000-00',
|
||||||
|
postalCode: '01001-000',
|
||||||
|
addressNumber: '123',
|
||||||
|
phone: '11999999999'
|
||||||
|
}
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/courses/${course.id}/unlock`, {
|
const res = await fetch(`/api/courses/${course.id}/unlock`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
billingType: paymentMethod,
|
||||||
|
creditCardInfo
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errData = await res.json();
|
throw new Error(data.error || 'Erro ao processar pagamento');
|
||||||
throw new Error(errData.error || 'Erro ao processar pagamento');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUnlocked(true);
|
if (paymentMethod === 'PIX' || paymentMethod === 'BOLETO') {
|
||||||
|
setPaymentData(data);
|
||||||
|
} else {
|
||||||
|
setUnlocked(true); // Credit Card assumes instant in sandbox
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Erro de conexão.');
|
setError(err.message || 'Erro de conexão.');
|
||||||
setIsPaying(false);
|
setIsPaying(false);
|
||||||
|
|
@ -155,49 +186,43 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
<form onSubmit={handleConfirmPayment} className="space-y-4">
|
<form onSubmit={handleConfirmPayment} className="space-y-4">
|
||||||
{paymentMethod === 'PIX' && (
|
{paymentMethod === 'PIX' && (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className="bg-white p-3 rounded-xl inline-block shadow-xl border border-zinc-200">
|
{!paymentData ? (
|
||||||
{/* Beautiful generated mock QR Code */}
|
<div className="text-zinc-400 text-xs py-4 text-center">
|
||||||
<div className="w-36 h-36 bg-zinc-100 flex flex-col items-center justify-center p-2 relative rounded">
|
Clique em Confirmar para gerar seu QR Code PIX
|
||||||
<div className="grid grid-cols-5 gap-1.5 w-full h-full opacity-90">
|
</div>
|
||||||
{Array.from({ length: 25 }).map((_, i) => (
|
) : (
|
||||||
<div
|
<>
|
||||||
key={i}
|
<div className="bg-white p-3 rounded-xl inline-block shadow-xl border border-zinc-200">
|
||||||
className={`rounded-sm ${(i * 7 + 11) % 3 === 0 || (i % 6 === 0) || (i < 6 && i !== 2) ? 'bg-black' : 'bg-transparent'}`}
|
{paymentData.pixData?.encodedImage ? (
|
||||||
/>
|
<img src={`data:image/jpeg;base64,${paymentData.pixData.encodedImage}`} alt="QR Code PIX" className="w-36 h-36" />
|
||||||
))}
|
) : (
|
||||||
|
<div className="w-36 h-36 bg-zinc-100 flex items-center justify-center">Sem Imagem</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="space-y-2">
|
||||||
<div className="bg-white p-1 rounded-md border border-zinc-200 shadow-sm">
|
<p className="text-xs text-zinc-400 max-w-sm mx-auto">
|
||||||
<div className="bg-[#E50914] text-white p-1 rounded">
|
Escaneie o QR Code acima com o app do seu banco ou copie a chave Pix Copia e Cola abaixo.
|
||||||
<QrCode className="w-5 h-5" />
|
</p>
|
||||||
</div>
|
|
||||||
|
<div className="flex items-center space-x-2 bg-zinc-900 rounded-lg p-2.5 border border-white/5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={paymentData.pixData?.payload || ""}
|
||||||
|
className="bg-transparent text-zinc-300 text-xs font-mono select-all focus:outline-none flex-grow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCopyPix}
|
||||||
|
className="bg-zinc-800 text-xs text-white hover:bg-zinc-700 px-3 py-1.5 rounded-md font-bold transition-all cursor-pointer flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
{pixCopied ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : null}
|
||||||
|
<span>{pixCopied ? 'Copiado!' : 'Copiar'}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs text-zinc-400 max-w-sm mx-auto">
|
|
||||||
Escaneie o QR Code acima com o app do seu banco ou copie a chave Pix Copia e Cola abaixo.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 bg-zinc-900 rounded-lg p-2.5 border border-white/5">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
readOnly
|
|
||||||
value="00020126580014br.gov.bcb.pix0136..."
|
|
||||||
className="bg-transparent text-zinc-300 text-xs font-mono select-all focus:outline-none flex-grow"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCopyPix}
|
|
||||||
className="bg-zinc-800 text-xs text-white hover:bg-zinc-700 px-3 py-1.5 rounded-md font-bold transition-all cursor-pointer flex items-center space-x-1"
|
|
||||||
>
|
|
||||||
{pixCopied ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : null}
|
|
||||||
<span>{pixCopied ? 'Copiado!' : 'Copiar'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -266,44 +291,50 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
O boleto será gerado no valor de R$ {coursePrice.toFixed(2)}. O prazo de compensação é de 1 a 2 dias úteis.
|
O boleto será gerado no valor de R$ {coursePrice.toFixed(2)}. O prazo de compensação é de 1 a 2 dias úteis.
|
||||||
</p>
|
</p>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<button
|
{paymentData?.payment?.invoiceUrl ? (
|
||||||
type="button"
|
<button
|
||||||
onClick={handleCopyBoleto}
|
type="button"
|
||||||
className="glass-btn-secondary text-xs text-white font-semibold py-2 px-4 rounded-lg inline-flex items-center space-x-2"
|
onClick={handleCopyBoleto}
|
||||||
>
|
className="glass-btn-secondary text-xs text-white font-semibold py-2 px-4 rounded-lg inline-flex items-center space-x-2"
|
||||||
{boletoCopied ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : null}
|
>
|
||||||
<span>{boletoCopied ? 'Código do Boleto Copiado!' : 'Copiar Linha Digitável'}</span>
|
<FileText className="w-4 h-4" />
|
||||||
</button>
|
<span>Visualizar e Imprimir Boleto</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-zinc-500">Clique abaixo para gerar o boleto</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit button */}
|
{/* Submit button */}
|
||||||
<div className="pt-4">
|
{!paymentData && (
|
||||||
<button
|
<div className="pt-4">
|
||||||
type="submit"
|
<button
|
||||||
disabled={isPaying}
|
type="submit"
|
||||||
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-2 cursor-pointer disabled:opacity-50"
|
disabled={isPaying}
|
||||||
>
|
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-2 cursor-pointer disabled:opacity-50"
|
||||||
{isPaying ? (
|
>
|
||||||
<>
|
{isPaying ? (
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-white" />
|
<>
|
||||||
<span>Processando Pagamento Asaas...</span>
|
<Loader2 className="w-4 h-4 animate-spin text-white" />
|
||||||
</>
|
<span>Processando Pagamento Asaas...</span>
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<Sparkles className="w-4 h-4 text-yellow-400" />
|
<>
|
||||||
<span>Confirmar Pagamento (Simulação)</span>
|
<Sparkles className="w-4 h-4 text-yellow-400" />
|
||||||
</>
|
<span>Gerar Cobrança (Asaas)</span>
|
||||||
)}
|
</>
|
||||||
</button>
|
)}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center text-[10px] text-zinc-500">
|
<div className="text-center text-[10px] text-zinc-500">
|
||||||
Transação 100% segura processada via ambiente de testes do gateway oficial **Asaas**.
|
Transação processada de forma 100% segura pelo gateway **Asaas**.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -307,18 +307,9 @@ export default function SubscriptionView({ user, token, onStatusUpdate }: Subscr
|
||||||
|
|
||||||
{billingType === 'PIX' && (
|
{billingType === 'PIX' && (
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
<div className="bg-white p-3 rounded-xl inline-block">
|
<p className="text-[11px] text-zinc-400 max-w-sm mx-auto">
|
||||||
{/* Fake QR representation */}
|
Para sua segurança, os pagamentos recorrentes via PIX e Boleto são processados diretamente no ambiente seguro do Asaas. Clique no botão abaixo para escanear o QR Code oficial ou copiar a linha digitável.
|
||||||
<QrCode className="w-32 h-32 text-black" />
|
</p>
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-[10px] text-zinc-500 uppercase font-black">Copia e Cola Pix</p>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value="00020101021126580014br.gov.bcb.pix0136devflix-asaas-4990-billing-rec-key"
|
|
||||||
className="w-full glass-input text-[10px] font-mono text-zinc-400 p-2 rounded text-center focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,4 +147,82 @@ export class AsaasService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fetch PIX QR Code for a given payment ID
|
||||||
|
*/
|
||||||
|
public static async getPixQrCode(paymentId: string): Promise<{ encodedImage: string, payload: string } | null> {
|
||||||
|
const apiKey = this.getApiKey();
|
||||||
|
if (!apiKey) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.getApiUrl()}/payments/${paymentId}/pixQrCode`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return null;
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Asaas getPixQrCode error:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single payment in Asaas
|
||||||
|
*/
|
||||||
|
public static async createPayment(
|
||||||
|
customerId: string,
|
||||||
|
value: number,
|
||||||
|
billingType: 'PIX' | 'CREDIT_CARD' | 'BOLETO',
|
||||||
|
description: string,
|
||||||
|
creditCardInfo?: any
|
||||||
|
): Promise<any> {
|
||||||
|
const apiKey = this.getApiKey();
|
||||||
|
if (!apiKey) {
|
||||||
|
console.warn('ASAAS_API_KEY not configured. Simulating payment creation.');
|
||||||
|
const payId = `pay_sim_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
return {
|
||||||
|
id: payId,
|
||||||
|
value,
|
||||||
|
status: 'PENDING',
|
||||||
|
billingType,
|
||||||
|
invoiceUrl: `https://sandbox.asaas.com/i/simulated_${payId}`,
|
||||||
|
dueDate: new Date().toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
customer: customerId,
|
||||||
|
billingType,
|
||||||
|
value,
|
||||||
|
dueDate: new Date().toISOString().split('T')[0],
|
||||||
|
description
|
||||||
|
};
|
||||||
|
|
||||||
|
if (billingType === 'CREDIT_CARD' && creditCardInfo) {
|
||||||
|
payload.creditCard = creditCardInfo.creditCard;
|
||||||
|
payload.creditCardHolderInfo = creditCardInfo.creditCardHolderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.getApiUrl()}/payments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: 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 payment in Asaas');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Asaas createPayment error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue