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.
143 lines
4.2 KiB
TypeScript
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>;
|