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) => {
|
app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, 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, installmentCount } = req.body;
|
const { billingType, creditCardInfo, installmentCount, cpf } = req.body;
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
||||||
|
|
@ -489,18 +489,35 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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;
|
let customerId = user.asaasCustomerId;
|
||||||
const apiKey = await AsaasService.getApiKey();
|
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 || ''))) {
|
if (apiKey && (customerId?.startsWith('cus_sim_') || customerId?.startsWith('cus_fallback_') || ['cus_test', 'cus_00001', 'cus_00002', 'cus_00003', 'cus_00004'].includes(customerId || ''))) {
|
||||||
customerId = null;
|
customerId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customerId) {
|
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({
|
await prisma.user.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { asaasCustomerId: customerId }
|
data: { asaasCustomerId: customerId }
|
||||||
});
|
});
|
||||||
user.asaasCustomerId = customerId;
|
user.asaasCustomerId = customerId;
|
||||||
|
} else {
|
||||||
|
await AsaasService.updateCustomer(customerId, { cpfCnpj: activeCpf });
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedCardInfo = creditCardInfo;
|
let processedCardInfo = creditCardInfo;
|
||||||
|
|
@ -672,7 +689,7 @@ app.get('/api/subscription/status', authenticateToken, async (req: Authenticated
|
||||||
|
|
||||||
// 9. Subscribe to plan
|
// 9. Subscribe to plan
|
||||||
app.post('/api/subscription/subscribe', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
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;
|
const userId = req.user!.id;
|
||||||
|
|
||||||
if (!billingType) {
|
if (!billingType) {
|
||||||
|
|
@ -687,18 +704,35 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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;
|
let customerId = user.asaasCustomerId;
|
||||||
const apiKey = await AsaasService.getApiKey();
|
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 || ''))) {
|
if (apiKey && (customerId?.startsWith('cus_sim_') || customerId?.startsWith('cus_fallback_') || ['cus_test', 'cus_00001', 'cus_00002', 'cus_00003', 'cus_00004'].includes(customerId || ''))) {
|
||||||
customerId = null;
|
customerId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customerId) {
|
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({
|
await prisma.user.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { asaasCustomerId: customerId }
|
data: { asaasCustomerId: customerId }
|
||||||
});
|
});
|
||||||
user.asaasCustomerId = customerId;
|
user.asaasCustomerId = customerId;
|
||||||
|
} else {
|
||||||
|
await AsaasService.updateCustomer(customerId, { cpfCnpj: activeCpf });
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = planId ? await prisma.plan.findUnique({ where: { id: planId } }) : null;
|
let plan = planId ? await prisma.plan.findUnique({ where: { id: planId } }) : null;
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,7 @@ export default function App() {
|
||||||
{unlockingCourse && (
|
{unlockingCourse && (
|
||||||
<CourseUnlockModal
|
<CourseUnlockModal
|
||||||
course={unlockingCourse}
|
course={unlockingCourse}
|
||||||
|
user={user}
|
||||||
onClose={() => setUnlockingCourse(null)}
|
onClose={() => setUnlockingCourse(null)}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setUnlockingCourse(null);
|
setUnlockingCourse(null);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import { Course, Plan } from '../types';
|
||||||
|
|
||||||
interface CourseUnlockModalProps {
|
interface CourseUnlockModalProps {
|
||||||
course: Course & { price?: number };
|
course: Course & { price?: number };
|
||||||
|
user: any;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
token: string;
|
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 [paymentMethod, setPaymentMethod] = useState<'PIX' | 'CREDIT_CARD' | 'BOLETO'>('PIX');
|
||||||
const [isPaying, setIsPaying] = useState(false);
|
const [isPaying, setIsPaying] = useState(false);
|
||||||
const [pixCopied, setPixCopied] = useState(false);
|
const [pixCopied, setPixCopied] = useState(false);
|
||||||
|
|
@ -24,6 +25,7 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
const [ccExpiry, setCcExpiry] = useState('');
|
const [ccExpiry, setCcExpiry] = useState('');
|
||||||
const [ccCvv, setCcCvv] = useState('');
|
const [ccCvv, setCcCvv] = useState('');
|
||||||
const [installments, setInstallments] = useState(1);
|
const [installments, setInstallments] = useState(1);
|
||||||
|
const [checkoutCpf, setCheckoutCpf] = useState(user?.cpf && user?.cpf !== '000.000.000-00' ? user.cpf : '');
|
||||||
|
|
||||||
// Pricing & Plans Selection
|
// Pricing & Plans Selection
|
||||||
const [availablePlans, setAvailablePlans] = useState<Plan[]>([]);
|
const [availablePlans, setAvailablePlans] = useState<Plan[]>([]);
|
||||||
|
|
@ -100,11 +102,18 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
}
|
}
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
|
if (!checkoutCpf || checkoutCpf.replace(/\D/g, '').length < 11) {
|
||||||
|
setError('Por favor, informe um CPF ou CNPJ válido.');
|
||||||
|
setIsPaying(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let endpoint = '';
|
let endpoint = '';
|
||||||
let bodyData: any = {
|
let bodyData: any = {
|
||||||
billingType: paymentMethod,
|
billingType: paymentMethod,
|
||||||
creditCardInfo
|
creditCardInfo,
|
||||||
|
cpf: checkoutCpf
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedPlanId) {
|
if (selectedPlanId) {
|
||||||
|
|
@ -441,6 +450,22 @@ export default function CourseUnlockModal({ course, onClose, onSuccess, token }:
|
||||||
</div>
|
</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 */}
|
{/* Submit button */}
|
||||||
{!paymentData && (
|
{!paymentData && (
|
||||||
<div className="pt-4 mt-auto">
|
<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
|
* Create a recurring subscription in Asaas
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue