import { z } from "zod"; import validator from "validator"; import { isValidDocument } from "./validators/document-validator"; import sanitizeHtml from "sanitize-html"; /** * Schemas de validação usando Zod * Garante type-safety e validação robusta de dados de entrada */ // Schema para email - validação RFC 5322 export const emailSchema = z .string() .min(1, "Email é obrigatório") .email("Formato de email inválido") .refine((email) => validator.isEmail(email), { message: "Email inválido", }) .transform((email) => email.trim().toLowerCase()); // Schema para telefone brasileiro export const phoneSchema = z .string() .min(1, "Telefone é obrigatório") .refine((phone) => { // Remove todos os caracteres não numéricos const cleaned = phone.replace(/[^\d]/g, ""); // Deve ter 10 ou 11 dígitos (telefone fixo ou celular com DDD) return cleaned.length >= 10 && cleaned.length <= 11; }, "Telefone deve ter 10 ou 11 dígitos (incluindo DDD)") .transform((phone) => phone.replace(/[^\d]/g, "")); // Schema para CPF/CNPJ export const documentSchema = z .string() .min(1, "CPF/CNPJ é obrigatório") .refine((doc) => isValidDocument(doc), { message: "CPF ou CNPJ inválido", }) .transform((doc) => doc.replace(/[^\d]/g, "")); // Normaliza removendo formatação // Schema para nome (alfabético, sem números) export const nameSchema = z .string() .min(3, "Nome deve ter no mínimo 3 caracteres") .max(100, "Nome deve ter no máximo 100 caracteres") .refine( (name) => /^[A-Za-zÀ-ÿ\s'-]+$/.test(name.trim()), "Nome deve conter apenas letras, espaços e acentos" ) .transform((name) => name.trim()); // Schema para mensagem (sanitiza HTML e previne XSS) export const messageSchema = z .string() .max(280, "Mensagem deve ter no máximo 280 caracteres") .transform((message) => { // Sanitiza HTML para prevenir XSS const sanitized = sanitizeHtml(message, { allowedTags: [], // Remove todas as tags HTML allowedAttributes: {}, }); return sanitized.trim(); }) .optional(); // Schema para tier de doação export const donationTierSchema = z.enum( ["coffee", "snack", "lunch", "custom"], { message: "Tier de doação inválido", } ); // Schema para valor customizado export const customAmountSchema = z .number() .min(5, "Valor mínimo é R$ 5,00") .max(100000, "Valor máximo é R$ 100.000,00"); // Schema completo para criação de doação export const createDonationSchema = z .discriminatedUnion("anonymous", [ // Doação anônima z.object({ tier: donationTierSchema, anonymous: z.literal(true), customAmount: z.number().optional(), }), // Doação identificada z.object({ tier: donationTierSchema, anonymous: z.literal(false), donorName: nameSchema, donorEmail: emailSchema, donorPhone: phoneSchema, donorTaxId: documentSchema, donorMessage: messageSchema, customAmount: z.number().optional(), }), ]) .refine( (data) => { // Se tier for custom, customAmount é obrigatório if (data.tier === "custom") { return data.customAmount !== undefined; } return true; }, { message: "customAmount é obrigatório quando tier é 'custom'", path: ["customAmount"], } ) .refine( (data) => { // Se tier for custom, customAmount deve estar no range válido if (data.tier === "custom" && data.customAmount) { return data.customAmount >= 5 && data.customAmount <= 100000; } return true; }, { message: "Valor deve estar entre R$ 5,00 e R$ 100.000,00", path: ["customAmount"], } ); // Schema para verificação de status export const verifyStatusSchema = z.object({ pixId: z.string().min(1, "pixId é obrigatório"), }); // Schema para verificação de billing export const verifyBillingSchema = z.object({ billingId: z.string().min(1, "ID da cobrança é obrigatório"), }); // Type exports para uso no código export type CreateDonationInput = z.infer; export type VerifyStatusInput = z.infer; export type VerifyBillingInput = z.infer;