Files
pagodera/lib/validation-schemas.ts
T
Evandro Fonseca Junior c7d4dadbbb refactor(validation): refactor createDonationSchema to use discriminatedUnion
Refactored the createDonationSchema from a combination of .object().and(z.union()) to a discriminatedUnion based on the "anonymous" field. This improves type safety and validation logic by explicitly handling anonymous and identified donations, ensuring customAmount is properly validated in both cases. The change maintains the same validation rules while simplifying the schema structure.
2025-10-26 23:11:21 -03:00

143 lines
4.2 KiB
TypeScript

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(500, "Valor máximo é R$ 500,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 <= 500;
}
return true;
},
{
message: "Valor deve estar entre R$ 5,00 e R$ 500,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<typeof createDonationSchema>;
export type VerifyStatusInput = z.infer<typeof verifyStatusSchema>;
export type VerifyBillingInput = z.infer<typeof verifyBillingSchema>;