* fix(contato): normalizar e-mail e telefone no formulário público de briefing
SPEC.md §12.3 exige normalização de e-mail e telefone no formulário
público, mas o ContactBriefingRequest só validava, sem normalizar
(MAN-104).
E-mail: aplica trim + lowercase em prepareForValidation — evita
duplicatas triviais ("Maria@X.com" vs "maria@x.com") e mantém a
detecção de duplicidade por hash consistente.
Telefone: normaliza para o formato brasileiro legível
"(DD) 9XXXX-XXXX"/"(DD) XXXX-XXXX", extraído para o value object
App\Domain\Contact\BrazilianPhoneNumber por concentrar lógica de
decisão (contagem de dígitos, remoção de DDI 55) que merece teste
isolado. Optou-se pelo formato com máscara em vez de dígitos puros
porque o Mailable ContactBriefing e seus templates (html/text) apenas
imprimem o valor do campo "Telefone/WhatsApp" em uma tabela/lista,
sem link "tel:" nem formatação na view — "(11) 98888-7777" é o que
fica legível para quem recebe o briefing por e-mail, enquanto
"11988887777" é opaco de bater o olho.
Para não descartar informação que o destinatário precisa, o
normalizador só reformata quando a string é composta exclusivamente
por caracteres de telefone; anotações como "(WhatsApp)" ou "falar com
João" junto do número são preservadas como estão. Entradas que não
batem com 10/11 dígitos (após remover DDI) também são preservadas,
apenas com espaços internos colapsados.
Honeypot, rate limit e aceite de privacidade não foram tocados.
Cobertura: teste de unidade para o value object (formatos válidos,
DDI, anotações, formatos não reconhecidos) e teste de feature
provando que uma submissão com e-mail maiúsculo/padded e telefone
"sujo" chega normalizada nos Mailables ContactBriefing e
ContactBriefingConfirmation.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): evitar fabricar DDD/9º dígito em números não brasileiros
O normalizador aceitava qualquer string de 10 ou 11 dígitos como se
fosse um telefone brasileiro, sem checar se os dois primeiros dígitos
formam um DDD plausível (11-99, nunca com zero em nenhuma posição) ou
se um número de 11 dígitos tem o 9º dígito obrigatório do celular.
Isso fazia números estrangeiros como '2025551234' (EUA) virarem
'(20) 2555-1234' — um número brasileiro plausível, porém inventado,
que destrói silenciosamente o contato real na tabela do e-mail de
briefing. Um '+55' explícito sem DDD (ex.: '+55 98888-7777') também
era lido como DDD 55 em vez de número incompleto.
Passa a validar a forma do DDD e o 9º dígito do celular antes de
formatar, preservando o texto original quando a checagem falha —
mesmo comportamento já usado para números com contagem de dígitos
fora do padrão. Mantém DDD 55 (Rio Grande do Sul) funcionando
normalmente quando o DDD é digitado de fato.
Limitação residual conhecida e aceita: DDDs americanos que colidem
estruturalmente com um DDD brasileiro válido (ex. '2125551234', área
212 de Nova York, bate com DDD 21) continuam sendo formatados como
brasileiros — não há como distinguir os dois casos só pela forma dos
dígitos. Da mesma forma, um '55' sem o '+' explícito (ex.
'55988887777') é ambiguamente tratado como DDD 55 real, já que nada
no texto indica se é código de país ou área.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
95 lines
3.4 KiB
PHP
95 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Domain\Contact;
|
|
|
|
use App\Domain\Contact\BrazilianPhoneNumber;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use Tests\TestCase;
|
|
|
|
class BrazilianPhoneNumberTest extends TestCase
|
|
{
|
|
/**
|
|
* @return iterable<string, array{string, string}>
|
|
*/
|
|
public static function normalizableNumbers(): iterable
|
|
{
|
|
yield 'mobile with punctuation' => ['(11) 98888-7777', '(11) 98888-7777'];
|
|
yield 'mobile digits only' => ['11988887777', '(11) 98888-7777'];
|
|
yield 'mobile with spaces and dashes' => ['11 98888 7777', '(11) 98888-7777'];
|
|
yield 'mobile with +55 country code' => ['+55 11 98888-7777', '(11) 98888-7777'];
|
|
yield 'mobile with bare 55 country code' => ['5511988887777', '(11) 98888-7777'];
|
|
yield 'landline digits only' => ['1133334444', '(11) 3333-4444'];
|
|
yield 'landline with punctuation' => ['(11) 3333-4444', '(11) 3333-4444'];
|
|
yield 'padded with surrounding whitespace' => [" 11 98888-7777 \n", '(11) 98888-7777'];
|
|
}
|
|
|
|
#[DataProvider('normalizableNumbers')]
|
|
public function test_it_normalizes_recognizable_brazilian_numbers(string $raw, string $expected): void
|
|
{
|
|
$this->assertSame($expected, BrazilianPhoneNumber::normalize($raw));
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{string}>
|
|
*/
|
|
public static function foreignOrImplausibleNumbers(): iterable
|
|
{
|
|
// 10-digit US number: DDD "20" has a '0' in the second position,
|
|
// which no real Brazilian area code has.
|
|
yield 'us number without country code' => ['2025551234'];
|
|
|
|
// 11-digit US number with leading '1': DDD "12" is plausible, but
|
|
// the third digit is '0', not the mandatory mobile '9'.
|
|
yield 'us number with leading 1' => ['12025551234'];
|
|
|
|
// 10-digit number with a DDD starting in '0', which cannot occur.
|
|
yield 'ten digits with leading zero ddd' => ['0212345678'];
|
|
|
|
// Explicit "+55" leaves only 9 digits behind — a mobile subscriber
|
|
// number with no area code, not DDD 55.
|
|
yield 'explicit country code missing ddd' => ['+55 98888-7777'];
|
|
}
|
|
|
|
#[DataProvider('foreignOrImplausibleNumbers')]
|
|
public function test_it_preserves_numbers_that_are_not_plausibly_brazilian(string $raw): void
|
|
{
|
|
$this->assertSame($raw, BrazilianPhoneNumber::normalize($raw));
|
|
}
|
|
|
|
public function test_it_still_formats_a_genuine_ddd_55_number(): void
|
|
{
|
|
// DDD 55 (Rio Grande do Sul) is a real area code and must not be
|
|
// confused with the "+55" country code prefix handling above.
|
|
$this->assertSame(
|
|
'(55) 98888-7777',
|
|
BrazilianPhoneNumber::normalize('(55) 98888-7777')
|
|
);
|
|
}
|
|
|
|
public function test_it_preserves_unrecognized_shapes_instead_of_discarding_information(): void
|
|
{
|
|
$this->assertSame(
|
|
'+44 20 7946 0958',
|
|
BrazilianPhoneNumber::normalize(' +44 20 7946 0958 ')
|
|
);
|
|
}
|
|
|
|
public function test_it_collapses_internal_whitespace_for_unrecognized_shapes(): void
|
|
{
|
|
$this->assertSame(
|
|
'ramal 123',
|
|
BrazilianPhoneNumber::normalize("ramal 123\n")
|
|
);
|
|
}
|
|
|
|
public function test_it_preserves_annotations_next_to_a_recognizable_number(): void
|
|
{
|
|
$this->assertSame(
|
|
'11 98888-7777 (WhatsApp)',
|
|
BrazilianPhoneNumber::normalize('11 98888-7777 (WhatsApp)')
|
|
);
|
|
}
|
|
}
|