Fix checkout payment: add CPF validation, update Asaas customer and add CPF field in checkout modal
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 56s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 56s
Details
This commit is contained in:
parent
772a308667
commit
7dcd2cfa69
42
server.ts
42
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;
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ export default function App() {
|
|||
{unlockingCourse && (
|
||||
<CourseUnlockModal
|
||||
course={unlockingCourse}
|
||||
user={user}
|
||||
onClose={() => setUnlockingCourse(null)}
|
||||
onSuccess={() => {
|
||||
setUnlockingCourse(null);
|
||||
|
|
|
|||
|
|
@ -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<Plan[]>([]);
|
||||
|
|
@ -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 }:
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* CPF/CNPJ Input (Required for all Asaas payments) */}
|
||||
{!paymentData && (
|
||||
<div className="space-y-1 mt-4">
|
||||
<label className="text-zinc-400 text-xs font-bold block">CPF ou CNPJ do Titular</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="000.000.000-00"
|
||||
value={checkoutCpf}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<p className="text-[10px] text-zinc-500">Necessário para emissão do Pix, boleto ou cobrança de cartão via Asaas.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit button */}
|
||||
{!paymentData && (
|
||||
<div className="pt-4 mt-auto">
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue