Coverage for src/ai_lls_lib/core/verifier.py: 100%
38 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 17:06 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 17:06 +0000
1"""
2Phone verification logic - checks line type and DNC status
3"""
5from datetime import UTC, datetime
7import phonenumbers
8from aws_lambda_powertools import Logger
10from ..providers import ExternalAPIProvider, VerificationProvider
11from .cache import DynamoDBCache
12from .models import LineType, PhoneVerification, VerificationSource
14logger = Logger()
17class PhoneVerifier:
18 """Verifies phone numbers for line type and DNC status"""
20 def __init__(
21 self, cache: DynamoDBCache | None = None, provider: VerificationProvider | None = None
22 ):
23 """
24 Initialize phone verifier.
26 Args:
27 cache: Optional DynamoDB cache for storing results
28 provider: Verification provider (defaults to ExternalAPIProvider)
29 """
30 self.cache = cache
31 self.provider = provider or ExternalAPIProvider()
32 logger.debug("PhoneVerifier initialized")
34 def normalize_phone(self, phone: str) -> str:
35 """Normalize phone to E.164 format"""
36 try:
37 # Parse with US as default country
38 parsed = phonenumbers.parse(phone, "US")
39 if not phonenumbers.is_valid_number(parsed):
40 raise ValueError(f"Invalid phone number: {phone}")
42 # Format as E.164
43 return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
44 except Exception as e:
45 # Callers classify invalid rows themselves (BulkProcessor logs
46 # them at WARNING), so there is no ERROR record here.
47 raise ValueError(f"Invalid phone format: {phone}") from e
49 def verify(self, phone: str) -> PhoneVerification:
50 """Verify phone number for line type and DNC status"""
51 normalized = self.normalize_phone(phone)
53 # Check cache first if available
54 if self.cache:
55 cached = self.cache.get(normalized)
56 if cached:
57 return cached
59 # Use provider to verify
60 provider_result = self.provider.verify_phone(normalized)
62 result = PhoneVerification(
63 phone_number=normalized,
64 line_type=provider_result.line_type,
65 dnc=provider_result.dnc,
66 known_litigator=provider_result.known_litigator,
67 cached=False,
68 verified_at=datetime.now(UTC),
69 source=VerificationSource.API,
70 )
72 # Store in cache if available
73 if self.cache:
74 try:
75 self.cache.set(normalized, result)
76 except Exception as e:
77 logger.warning(f"Failed to cache result: {e}")
78 # Continue without caching - don't fail the verification
80 return result
82 def _check_line_type(self, phone: str) -> LineType:
83 """
84 Check line type (for backwards compatibility with CLI).
85 Delegates to provider.
86 """
87 return self.provider.verify_phone(phone).line_type
89 def _check_dnc(self, phone: str) -> bool:
90 """
91 Check DNC status (for backwards compatibility with CLI).
92 Delegates to provider.
93 """
94 return self.provider.verify_phone(phone).dnc