diff --git a/server.ts b/server.ts index 4e824da..2da5935 100644 --- a/server.ts +++ b/server.ts @@ -474,7 +474,7 @@ const checkoutRateLimiter = (req: Request, res: Response, next: Function) => { app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, async (req: AuthenticatedRequest, res: Response) => { const courseId = req.params.id; const userId = req.user!.id; - const { billingType, creditCardInfo, installmentCount } = req.body; + const { billingType, creditCardInfo, installmentCount, cpf } = req.body; const user = await prisma.user.findUnique({ where: { id: userId } }); const course = await prisma.course.findUnique({ where: { id: courseId } }); @@ -489,18 +489,35 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn } try { + const activeCpf = cpf || user.cpf; + if (!activeCpf || activeCpf === '000.000.000-00') { + res.status(400).json({ error: 'Para realizar o pagamento é obrigatório fornecer um CPF ou CNPJ válido.' }); + return; + } + + if (activeCpf && activeCpf !== user.cpf) { + await prisma.user.update({ + where: { id: userId }, + data: { cpf: activeCpf } + }); + user.cpf = activeCpf; + } + let customerId = user.asaasCustomerId; const apiKey = await AsaasService.getApiKey(); if (apiKey && (customerId?.startsWith('cus_sim_') || customerId?.startsWith('cus_fallback_') || ['cus_test', 'cus_00001', 'cus_00002', 'cus_00003', 'cus_00004'].includes(customerId || ''))) { customerId = null; } + if (!customerId) { - customerId = await AsaasService.createCustomer(user.name, user.email, user.cpf); + customerId = await AsaasService.createCustomer(user.name, user.email, activeCpf); await prisma.user.update({ where: { id: userId }, data: { asaasCustomerId: customerId } }); user.asaasCustomerId = customerId; + } else { + await AsaasService.updateCustomer(customerId, { cpfCnpj: activeCpf }); } let processedCardInfo = creditCardInfo; @@ -672,7 +689,7 @@ app.get('/api/subscription/status', authenticateToken, async (req: Authenticated // 9. Subscribe to plan app.post('/api/subscription/subscribe', authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const { billingType, creditCardInfo, planId } = req.body; + const { billingType, creditCardInfo, planId, cpf } = req.body; const userId = req.user!.id; if (!billingType) { @@ -687,18 +704,35 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic } try { + const activeCpf = cpf || user.cpf; + if (!activeCpf || activeCpf === '000.000.000-00') { + res.status(400).json({ error: 'Para realizar a assinatura é obrigatório fornecer um CPF ou CNPJ válido.' }); + return; + } + + if (activeCpf && activeCpf !== user.cpf) { + await prisma.user.update({ + where: { id: userId }, + data: { cpf: activeCpf } + }); + user.cpf = activeCpf; + } + let customerId = user.asaasCustomerId; const apiKey = await AsaasService.getApiKey(); if (apiKey && (customerId?.startsWith('cus_sim_') || customerId?.startsWith('cus_fallback_') || ['cus_test', 'cus_00001', 'cus_00002', 'cus_00003', 'cus_00004'].includes(customerId || ''))) { customerId = null; } + if (!customerId) { - customerId = await AsaasService.createCustomer(user.name, user.email, user.cpf); + customerId = await AsaasService.createCustomer(user.name, user.email, activeCpf); await prisma.user.update({ where: { id: userId }, data: { asaasCustomerId: customerId } }); user.asaasCustomerId = customerId; + } else { + await AsaasService.updateCustomer(customerId, { cpfCnpj: activeCpf }); } let plan = planId ? await prisma.plan.findUnique({ where: { id: planId } }) : null; diff --git a/src/App.tsx b/src/App.tsx index e4d7ca3..9269f46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -303,6 +303,7 @@ export default function App() { {unlockingCourse && ( setUnlockingCourse(null)} onSuccess={() => { setUnlockingCourse(null); diff --git a/src/components/CourseUnlockModal.tsx b/src/components/CourseUnlockModal.tsx index 95b2b5f..ab6e10c 100644 --- a/src/components/CourseUnlockModal.tsx +++ b/src/components/CourseUnlockModal.tsx @@ -4,12 +4,13 @@ import { Course, Plan } from '../types'; interface CourseUnlockModalProps { course: Course & { price?: number }; + user: any; onClose: () => void; onSuccess: () => void; token: string; } -export default function CourseUnlockModal({ course, onClose, onSuccess, token }: CourseUnlockModalProps) { +export default function CourseUnlockModal({ course, user, onClose, onSuccess, token }: CourseUnlockModalProps) { const [paymentMethod, setPaymentMethod] = useState<'PIX' | 'CREDIT_CARD' | 'BOLETO'>('PIX'); const [isPaying, setIsPaying] = useState(false); const [pixCopied, setPixCopied] = useState(false); @@ -24,6 +25,7 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }: const [ccExpiry, setCcExpiry] = useState(''); const [ccCvv, setCcCvv] = useState(''); const [installments, setInstallments] = useState(1); + const [checkoutCpf, setCheckoutCpf] = useState(user?.cpf && user?.cpf !== '000.000.000-00' ? user.cpf : ''); // Pricing & Plans Selection const [availablePlans, setAvailablePlans] = useState([]); @@ -100,11 +102,18 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }: } } : undefined; + if (!checkoutCpf || checkoutCpf.replace(/\D/g, '').length < 11) { + setError('Por favor, informe um CPF ou CNPJ válido.'); + setIsPaying(false); + return; + } + try { let endpoint = ''; let bodyData: any = { billingType: paymentMethod, - creditCardInfo + creditCardInfo, + cpf: checkoutCpf }; if (selectedPlanId) { @@ -441,6 +450,22 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }: )} + {/* CPF/CNPJ Input (Required for all Asaas payments) */} + {!paymentData && ( +
+ + setCheckoutCpf(e.target.value)} + className="w-full bg-zinc-900 border border-white/10 rounded-xl py-3 px-4 text-xs text-white focus:border-[#E50914] focus:outline-none transition-colors" + required + /> +

Necessário para emissão do Pix, boleto ou cobrança de cartão via Asaas.

+
+ )} + {/* Submit button */} {!paymentData && (
diff --git a/src/services/asaas.ts b/src/services/asaas.ts index 329031b..2ec7039 100644 --- a/src/services/asaas.ts +++ b/src/services/asaas.ts @@ -112,6 +112,39 @@ export class AsaasService { } } + /** + * Update an existing customer in Asaas + */ + public static async updateCustomer(customerId: string, data: { name?: string, email?: string, cpfCnpj?: string }): Promise { + 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 */