1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! The SMS channel's targeting port (hand-authored, user-owned; see
//! `metaphor.codegen.yaml`).
//!
//! The email channel resolves its external bridge targets through
//! [`crate::application::service::mailing_write_service::TargetRecipientResolver`]
//! — one host-composed adapter per `target_model`, declaratively keyed, no
//! cargo edge onto the target module. This file is the PHONE-channel twin of
//! that seam: the same declarative composition, over canonical E.164 numbers.
//!
//! Why a separate trait and not a flag on the email resolver: the two channels
//! answer different questions. The email arm asks "who has an address to mail"
//! and dedups on the email; the phone arm asks "who has a sanitizable number
//! to text" and — critically — must report the leads it could NOT sanitize.
//! The upstream bridge this ports (website_crm_sms) gated SMS eligibility on
//! STRICT RAW-STRING equality between two independently written phone columns
//! (`lead.phone == visitor.mobile`), so any normalization drift between the
//! two writes silently disqualified every lead. The fix this port lands:
//! eligibility is the SANITIZER's verdict, computed once, canonically, at
//! targeting time — and a lead that fails it is COUNTED in the report, never
//! silently dropped. There is no raw-string comparison anywhere on this path.
//!
//! Composition status (recorded deliberately): the send walk that CONSUMES
//! this port — claim → resolve phones → claim-time suppression →
//! channel-correct trace mint → gateway enqueue through backbone-mail's
//! `SmsWriteService` → walk-complete marker — is tracked as DIT #231
//! ("Compose the SMS send walk in backbone-mailing"); until it lands, an
//! sms-type mailing still parks loudly at the mail walk's channel guard and
//! NOTHING mints from this port on the sweep path. What ships here is the
//! targeting contract itself: the port the walk will call, the report shape
//! its outcome must surface, and the channel-correct mint verbs
//! (`TraceRepository::mint_trace_channel*`) its traces mint through — so the
//! walk composes onto a proven surface instead of growing one.
use cratePhoneRecipient;
use crateCompiledDomain;
/// One targeting pass's answer: the send set plus the VISIBLE exclusion
/// counts. `excluded_invalid_phone` is the anti-silent-disqualification
/// counter — the send walk logs it, counts it in its sweep outcome, and a
/// future provider leg can surface it per mailing. A targeting pass that
/// returns only the send set would re-create the upstream defect (every
/// drift silently narrowing the audience).
/// One EXTERNAL target's phone resolver — the phone-channel twin of
/// [`crate::application::service::mailing_write_service::TargetRecipientResolver`].
///
/// The host composes ONE resolver per phone-bearing bridge target (e.g.
/// `crm_lead`), keyed by the `target_model` string it serves. The
/// implementation MUST:
///
/// - resolve under each company's fence (one transaction per company with
/// `bind_company_on`) — the concatenated send set may span companies but
/// every row must have entered through exactly one company's policy;
/// - sanitize through backbone-mail's public `sanitize_candidates` walk and
/// emit the FORMATTER-MINTED `E164Number` (never a raw string) — the
/// canonical-equality guarantees downstream (suppression, STOP matching)
/// hold only if every number on this path is canonical;
/// - count non-sanitizable rows into `excluded_invalid_phone` instead of
/// dropping them;
/// - dedup across companies by CANONICAL number (the send grain), mirroring
/// the email resolvers' cross-company dedup.