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
|
// Map optional fields
|
||||||
if input.Phone != "" {
|
if input.Phone != "" {
|
||||||
company.Phone = &input.Phone
|
clean := utils.SanitizePhone(input.Phone)
|
||||||
|
if clean != "" {
|
||||||
|
company.Phone = &clean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if input.Website != nil {
|
if input.Website != nil {
|
||||||
company.Website = input.Website
|
company.Website = input.Website
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,25 @@ func StripHTML(input string) string {
|
||||||
reg := regexp.MustCompile(`<[^>]*>`)
|
reg := regexp.MustCompile(`<[^>]*>`)
|
||||||
return reg.ReplaceAllString(input, "")
|
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 cleanDigits = (value: string) => value.replace(/\D/g, "");
|
||||||
|
|
||||||
const isValidCNPJ = (value: string) => {
|
// Keeps only digits and a leading '+'. Strips all other formatting.
|
||||||
const cnpj = cleanDigits(value);
|
// "+55 (11) 99999-8888" → "+5511999998888"
|
||||||
if (!cnpj) return true;
|
// "(11) 99999-8888" → "11999998888"
|
||||||
if (cnpj.length !== 14) return false;
|
const cleanPhone = (value: string): string => {
|
||||||
if (/^(\d)\1+$/.test(cnpj)) return false;
|
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[]) => {
|
// Accepts any tax document with 5–30 alphanumeric characters (global).
|
||||||
const sum = base
|
const isValidDocument = (value: string): boolean => {
|
||||||
.split("")
|
const clean = value.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
.reduce((acc, current, index) => acc + Number(current) * weights[index], 0);
|
return clean.length >= 5 && clean.length <= 30;
|
||||||
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}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isHttpsUrl = (value: string) => /^https:\/\/.+/i.test(value);
|
const isHttpsUrl = (value: string) => /^https:\/\/.+/i.test(value);
|
||||||
|
|
@ -125,8 +121,8 @@ const contentByLocale = {
|
||||||
employeeCount: "Número de empregados",
|
employeeCount: "Número de empregados",
|
||||||
foundedYear: "Ano de fundação",
|
foundedYear: "Ano de fundação",
|
||||||
foundedYearPlaceholder: "Ex: 2012",
|
foundedYearPlaceholder: "Ex: 2012",
|
||||||
cnpj: "CNPJ",
|
cnpj: "Documento fiscal",
|
||||||
cnpjPlaceholder: "00.000.000/0000-00",
|
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||||
companyDescription: "Descrição da empresa",
|
companyDescription: "Descrição da empresa",
|
||||||
companyDescPlaceholder: "Convide os candidatos a conhecer a organização...",
|
companyDescPlaceholder: "Convide os candidatos a conhecer a organização...",
|
||||||
applicationChannel: "Como receber candidaturas",
|
applicationChannel: "Como receber candidaturas",
|
||||||
|
|
@ -232,7 +228,7 @@ const contentByLocale = {
|
||||||
invalidPhone: "Informe um telefone com DDI válido. Exemplo: +55 11999998888",
|
invalidPhone: "Informe um telefone com DDI válido. Exemplo: +55 11999998888",
|
||||||
salaryFixedRequired: "Informe o salário fixo.",
|
salaryFixedRequired: "Informe o salário fixo.",
|
||||||
salaryRangeRequired: "Informe a faixa salarial (mínimo e máximo).",
|
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.",
|
billingRequired: "Preencha documento, país, endereço e e-mail de faturamento.",
|
||||||
billingEmailInvalid: "Informe um e-mail de faturamento válido.",
|
billingEmailInvalid: "Informe um e-mail de faturamento válido.",
|
||||||
termsRequired: "Você precisa aceitar as condições legais para continuar.",
|
termsRequired: "Você precisa aceitar as condições legais para continuar.",
|
||||||
|
|
@ -289,8 +285,8 @@ const contentByLocale = {
|
||||||
employeeCount: "Number of employees",
|
employeeCount: "Number of employees",
|
||||||
foundedYear: "Founded year",
|
foundedYear: "Founded year",
|
||||||
foundedYearPlaceholder: "e.g. 2012",
|
foundedYearPlaceholder: "e.g. 2012",
|
||||||
cnpj: "Tax ID",
|
cnpj: "Tax document",
|
||||||
cnpjPlaceholder: "00.000.000/0000-00",
|
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||||
companyDescription: "Company description",
|
companyDescription: "Company description",
|
||||||
companyDescPlaceholder: "Invite candidates to learn about the organization...",
|
companyDescPlaceholder: "Invite candidates to learn about the organization...",
|
||||||
applicationChannel: "How to receive applications",
|
applicationChannel: "How to receive applications",
|
||||||
|
|
@ -396,7 +392,7 @@ const contentByLocale = {
|
||||||
invalidPhone: "Please enter a valid phone with country code. Example: +1 5551234567",
|
invalidPhone: "Please enter a valid phone with country code. Example: +1 5551234567",
|
||||||
salaryFixedRequired: "Please enter the fixed salary.",
|
salaryFixedRequired: "Please enter the fixed salary.",
|
||||||
salaryRangeRequired: "Please enter the salary range (min and max).",
|
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.",
|
billingRequired: "Please fill in document, country, address and billing email.",
|
||||||
billingEmailInvalid: "Please enter a valid billing email.",
|
billingEmailInvalid: "Please enter a valid billing email.",
|
||||||
termsRequired: "You must accept the legal terms to continue.",
|
termsRequired: "You must accept the legal terms to continue.",
|
||||||
|
|
@ -453,8 +449,8 @@ const contentByLocale = {
|
||||||
employeeCount: "Número de empleados",
|
employeeCount: "Número de empleados",
|
||||||
foundedYear: "Año de fundación",
|
foundedYear: "Año de fundación",
|
||||||
foundedYearPlaceholder: "Ej: 2012",
|
foundedYearPlaceholder: "Ej: 2012",
|
||||||
cnpj: "ID Fiscal",
|
cnpj: "Documento fiscal",
|
||||||
cnpjPlaceholder: "00.000.000/0000-00",
|
cnpjPlaceholder: "CNPJ, EIN, VAT...",
|
||||||
companyDescription: "Descripción de la empresa",
|
companyDescription: "Descripción de la empresa",
|
||||||
companyDescPlaceholder: "Invite a los candidatos a conocer la organización...",
|
companyDescPlaceholder: "Invite a los candidatos a conocer la organización...",
|
||||||
applicationChannel: "Cómo recibir candidaturas",
|
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",
|
invalidPhone: "Ingrese un teléfono con código de país válido. Ejemplo: +34 612345678",
|
||||||
salaryFixedRequired: "Ingrese el salario fijo.",
|
salaryFixedRequired: "Ingrese el salario fijo.",
|
||||||
salaryRangeRequired: "Ingrese el rango salarial (mínimo y máximo).",
|
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.",
|
billingRequired: "Complete documento, país, dirección y email de facturación.",
|
||||||
billingEmailInvalid: "Ingrese un email de facturación válido.",
|
billingEmailInvalid: "Ingrese un email de facturación válido.",
|
||||||
termsRequired: "Debe aceptar las condiciones legales para continuar.",
|
termsRequired: "Debe aceptar las condiciones legales para continuar.",
|
||||||
|
|
@ -739,8 +735,8 @@ export default function PostJobPage() {
|
||||||
toast.error(c.errors.salaryRangeRequired);
|
toast.error(c.errors.salaryRangeRequired);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (company.document && !isValidCNPJ(company.document)) {
|
if (company.document && !isValidDocument(company.document)) {
|
||||||
toast.error(c.errors.invalidCnpj);
|
toast.error(c.errors.invalidDocument);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -796,7 +792,7 @@ export default function PostJobPage() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const apiBase = process.env.NEXT_PUBLIC_API_URL || "";
|
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 contactFullName = [billing.contactName, billing.contactLastName].filter(Boolean).join(" ");
|
||||||
|
|
||||||
const registerRes = await fetch(`${apiBase}/api/v1/auth/register/company`, {
|
const registerRes = await fetch(`${apiBase}/api/v1/auth/register/company`, {
|
||||||
|
|
@ -806,9 +802,9 @@ export default function PostJobPage() {
|
||||||
companyName: company.name,
|
companyName: company.name,
|
||||||
email: billing.contactEmail,
|
email: billing.contactEmail,
|
||||||
contact: contactFullName || null,
|
contact: contactFullName || null,
|
||||||
document: cleanDigits(company.document) || null,
|
document: company.document.replace(/[^a-zA-Z0-9]/g, "") || null,
|
||||||
password: billing.password,
|
password: billing.password,
|
||||||
phone: billingPhone ? `+${billingPhone}` : null,
|
phone: billingPhone || null,
|
||||||
website: company.website || null,
|
website: company.website || null,
|
||||||
employeeCount: company.employeeCount || null,
|
employeeCount: company.employeeCount || null,
|
||||||
foundedYear: company.foundedYear ? Number(company.foundedYear) : null,
|
foundedYear: company.foundedYear ? Number(company.foundedYear) : null,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue