fix: global document and phone handling — remove Brazil-specific formatting
Frontend (jobs/new): - Replace isValidCNPJ (checksum algorithm) with isValidDocument: accepts any tax document with 5–30 alphanumeric chars (CNPJ, EIN, VAT, etc.) - Add cleanPhone(): strips formatting chars (dashes, spaces, parens) and keeps only digits + optional leading '+'; replaces cleanDigits+prepend - Phone sent as '+5511999998888' if user typed '+55...', or '11999998888' if no country code was provided — no '+' blindly prepended anymore - Company document sent stripped of all non-alphanumeric before API call - Update label placeholder from '00.000.000/0000-00' to 'CNPJ, EIN, VAT...' - Rename error key invalidCnpj → invalidDocument in all 3 locales (pt, en, es) Backend (create_company use case): - Add SanitizePhone() to utils/sanitizer.go: strips all non-digit chars except a leading '+'; '(11) 99999-8888' → '11999998888' - Apply SanitizePhone to input.Phone before persisting to DB Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd085ec193
commit
3a26af3df5
3 changed files with 54 additions and 33 deletions
|
|
@ -71,7 +71,10 @@ func (uc *CreateCompanyUseCase) Execute(ctx context.Context, input dto.CreateCom
|
|||
|
||||
// Map optional fields
|
||||
if input.Phone != "" {
|
||||
company.Phone = &input.Phone
|
||||
clean := utils.SanitizePhone(input.Phone)
|
||||
if clean != "" {
|
||||
company.Phone = &clean
|
||||
}
|
||||
}
|
||||
if input.Website != nil {
|
||||
company.Website = input.Website
|
||||
|
|
|
|||
|
|
@ -87,3 +87,25 @@ func StripHTML(input string) string {
|
|||
reg := regexp.MustCompile(`<[^>]*>`)
|
||||
return reg.ReplaceAllString(input, "")
|
||||
}
|
||||
|
||||
// SanitizePhone cleans a phone number, keeping only digits and an optional
|
||||
// leading '+' (international dialing prefix). Removes all formatting characters
|
||||
// such as spaces, dashes, parentheses, and dots.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "+55 (11) 99999-8888" → "+5511999998888"
|
||||
// "(11) 99999-8888" → "11999998888"
|
||||
// "+1-800-555-0100" → "+18005550100"
|
||||
func SanitizePhone(phone string) string {
|
||||
phone = strings.TrimSpace(phone)
|
||||
if phone == "" {
|
||||
return ""
|
||||
}
|
||||
hasPlus := strings.HasPrefix(phone, "+")
|
||||
digitsOnly := regexp.MustCompile(`\D`).ReplaceAllString(phone, "")
|
||||
if hasPlus {
|
||||
return "+" + digitsOnly
|
||||
}
|
||||
return digitsOnly
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,25 +39,21 @@ type ApiCountry = {
|
|||
|
||||
const cleanDigits = (value: string) => value.replace(/\D/g, "");
|
||||
|
||||
const isValidCNPJ = (value: string) => {
|
||||
const cnpj = cleanDigits(value);
|
||||
if (!cnpj) return true;
|
||||
if (cnpj.length !== 14) return false;
|
||||
if (/^(\d)\1+$/.test(cnpj)) return false;
|
||||
// Keeps only digits and a leading '+'. Strips all other formatting.
|
||||
// "+55 (11) 99999-8888" → "+5511999998888"
|
||||
// "(11) 99999-8888" → "11999998888"
|
||||
const cleanPhone = (value: string): string => {
|
||||
const v = value.trim();
|
||||
if (!v) return "";
|
||||
const hasPlus = v.startsWith("+");
|
||||
const digits = v.replace(/\D/g, "");
|
||||
return hasPlus ? `+${digits}` : digits;
|
||||
};
|
||||
|
||||
const calcCheckDigit = (base: string, weights: number[]) => {
|
||||
const sum = base
|
||||
.split("")
|
||||
.reduce((acc, current, index) => acc + Number(current) * weights[index], 0);
|
||||
const mod = sum % 11;
|
||||
return mod < 2 ? 0 : 11 - mod;
|
||||
};
|
||||
|
||||
const base12 = cnpj.slice(0, 12);
|
||||
const digit1 = calcCheckDigit(base12, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
||||
const digit2 = calcCheckDigit(`${base12}${digit1}`, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
||||
|
||||
return cnpj === `${base12}${digit1}${digit2}`;
|
||||
// Accepts any tax document with 5–30 alphanumeric characters (global).
|
||||
const isValidDocument = (value: string): boolean => {
|
||||
const clean = value.replace(/[^a-zA-Z0-9]/g, "");
|
||||
return clean.length >= 5 && clean.length <= 30;
|
||||
};
|
||||
|
||||
const isHttpsUrl = (value: string) => /^https:\/\/.+/i.test(value);
|
||||
|
|
@ -125,8 +121,8 @@ const contentByLocale = {
|
|||
employeeCount: "Número de empregados",
|
||||
foundedYear: "Ano de fundação",
|
||||
foundedYearPlaceholder: "Ex: 2012",
|
||||
cnpj: "CNPJ",
|
||||
cnpjPlaceholder: "00.000.000/0000-00",
|
||||
cnpj: "Documento fiscal",
|
||||
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||
companyDescription: "Descrição da empresa",
|
||||
companyDescPlaceholder: "Convide os candidatos a conhecer a organização...",
|
||||
applicationChannel: "Como receber candidaturas",
|
||||
|
|
@ -232,7 +228,7 @@ const contentByLocale = {
|
|||
invalidPhone: "Informe um telefone com DDI válido. Exemplo: +55 11999998888",
|
||||
salaryFixedRequired: "Informe o salário fixo.",
|
||||
salaryRangeRequired: "Informe a faixa salarial (mínimo e máximo).",
|
||||
invalidCnpj: "CNPJ da empresa inválido.",
|
||||
invalidDocument: "Documento da empresa inválido (mínimo 5 caracteres).",
|
||||
billingRequired: "Preencha documento, país, endereço e e-mail de faturamento.",
|
||||
billingEmailInvalid: "Informe um e-mail de faturamento válido.",
|
||||
termsRequired: "Você precisa aceitar as condições legais para continuar.",
|
||||
|
|
@ -289,8 +285,8 @@ const contentByLocale = {
|
|||
employeeCount: "Number of employees",
|
||||
foundedYear: "Founded year",
|
||||
foundedYearPlaceholder: "e.g. 2012",
|
||||
cnpj: "Tax ID",
|
||||
cnpjPlaceholder: "00.000.000/0000-00",
|
||||
cnpj: "Tax document",
|
||||
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||
companyDescription: "Company description",
|
||||
companyDescPlaceholder: "Invite candidates to learn about the organization...",
|
||||
applicationChannel: "How to receive applications",
|
||||
|
|
@ -396,7 +392,7 @@ const contentByLocale = {
|
|||
invalidPhone: "Please enter a valid phone with country code. Example: +1 5551234567",
|
||||
salaryFixedRequired: "Please enter the fixed salary.",
|
||||
salaryRangeRequired: "Please enter the salary range (min and max).",
|
||||
invalidCnpj: "Invalid company tax ID.",
|
||||
invalidDocument: "Invalid company tax document (minimum 5 characters).",
|
||||
billingRequired: "Please fill in document, country, address and billing email.",
|
||||
billingEmailInvalid: "Please enter a valid billing email.",
|
||||
termsRequired: "You must accept the legal terms to continue.",
|
||||
|
|
@ -453,8 +449,8 @@ const contentByLocale = {
|
|||
employeeCount: "Número de empleados",
|
||||
foundedYear: "Año de fundación",
|
||||
foundedYearPlaceholder: "Ej: 2012",
|
||||
cnpj: "ID Fiscal",
|
||||
cnpjPlaceholder: "00.000.000/0000-00",
|
||||
cnpj: "Documento fiscal",
|
||||
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||
companyDescription: "Descripción de la empresa",
|
||||
companyDescPlaceholder: "Invite a los candidatos a conocer la organización...",
|
||||
applicationChannel: "Cómo recibir candidaturas",
|
||||
|
|
@ -560,7 +556,7 @@ const contentByLocale = {
|
|||
invalidPhone: "Ingrese un teléfono con código de país válido. Ejemplo: +34 612345678",
|
||||
salaryFixedRequired: "Ingrese el salario fijo.",
|
||||
salaryRangeRequired: "Ingrese el rango salarial (mínimo y máximo).",
|
||||
invalidCnpj: "ID fiscal de la empresa inválido.",
|
||||
invalidDocument: "Documento fiscal de la empresa inválido (mínimo 5 caracteres).",
|
||||
billingRequired: "Complete documento, país, dirección y email de facturación.",
|
||||
billingEmailInvalid: "Ingrese un email de facturación válido.",
|
||||
termsRequired: "Debe aceptar las condiciones legales para continuar.",
|
||||
|
|
@ -739,8 +735,8 @@ export default function PostJobPage() {
|
|||
toast.error(c.errors.salaryRangeRequired);
|
||||
return false;
|
||||
}
|
||||
if (company.document && !isValidCNPJ(company.document)) {
|
||||
toast.error(c.errors.invalidCnpj);
|
||||
if (company.document && !isValidDocument(company.document)) {
|
||||
toast.error(c.errors.invalidDocument);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -796,7 +792,7 @@ export default function PostJobPage() {
|
|||
setLoading(true);
|
||||
try {
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
const billingPhone = cleanDigits(billing.contactMobile || billing.contactPhone || job.applicationPhone);
|
||||
const billingPhone = cleanPhone(billing.contactMobile || billing.contactPhone || job.applicationPhone || "");
|
||||
const contactFullName = [billing.contactName, billing.contactLastName].filter(Boolean).join(" ");
|
||||
|
||||
const registerRes = await fetch(`${apiBase}/api/v1/auth/register/company`, {
|
||||
|
|
@ -806,9 +802,9 @@ export default function PostJobPage() {
|
|||
companyName: company.name,
|
||||
email: billing.contactEmail,
|
||||
contact: contactFullName || null,
|
||||
document: cleanDigits(company.document) || null,
|
||||
document: company.document.replace(/[^a-zA-Z0-9]/g, "") || null,
|
||||
password: billing.password,
|
||||
phone: billingPhone ? `+${billingPhone}` : null,
|
||||
phone: billingPhone || null,
|
||||
website: company.website || null,
|
||||
employeeCount: company.employeeCount || null,
|
||||
foundedYear: company.foundedYear ? Number(company.foundedYear) : null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue