← All memos
Jun 15, 2026salesgrowthplatformResponded

Corrupt intake phone numbers reach identity and the Sales snapshot and break sales-ordering close; please validate phones at the intake boundary

Tagsintake, identity, data-quality, phone, sales-ordering-close

Validate phone numbers at the intake boundary

What happened

An operator could not complete a sales-ordering close. The final step returned "Square customer is missing payer email or phone after customer sync." That message originates in Revenue's close endpoint and Sales surfaced it verbatim. The cause was not a missing phone. The lead had a phone, but it was corrupt: a doubled US country code that reduced to a national number with an invalid area code (it rendered in the operator UI as "(191) 891-4989", and the area code "191" is not a valid NANP area code). Square rejected it at customer sync, so the synced customer had no usable phone and no email, and the close failed.

The important part for Growth and Platform: one corrupt value from a single intake reached two separate stores.

  1. Platform Person.phone_normalized, which the close reads through the operator identity contact. This is the value Revenue forwards to Square.
  2. Sales LeadIntakeSnapshot.phone, the mirror that drives the workbench display.

Because they are independent copies, correcting one does not fix the other. The operator fixed the Platform record (which unblocks the close), and the Sales display still showed the bad number until the snapshot was corrected separately.

Root cause

Nothing on the write path rejects a phone whose national form is not a real NANP number. Platform's normalizePhone in modules/person/normalize.ts correctly prepends a 1 to a clean 10-digit number, but it passes an already-malformed input straight through to storage. The intake capture that seeds both stores does the same. So a doubled country code, or any number with an invalid area code or exchange, is stored as-is and only fails much later, at Square, with an opaque message.

Compounding this: phone is immutable downstream by the intake-amendment contract. Sales applies Growth's intake.amended events but is explicitly forbidden from mutating phone or email. So once a bad phone lands, there is no supported in-app path to correct it on the Sales side, which is why a one-off data edit was needed.

What Sales has already done

These shipped in the Sales repo and are pending deploy. They reduce blast radius but do not fix the source.

A pre-flight payer-contact check now runs in the sales-ordering close before any soft-hold is created. It validates the email, normalizes the phone to E.164, and rejects any number whose national form is not a valid NANP number. It drops an invalid value so Revenue only ever receives well-formed contact, and when neither a valid phone nor email is present it returns a clear operator message ("correct the phone number or add an email before booking") instead of the cryptic Square error after the fact. A bad phone no longer blocks a close that a valid email could fund, and it no longer leaves an orphaned soft-hold.

A guarded, dry-run-by-default script corrects a single LeadIntakeSnapshot.phone when a value was already corrupted, validating the replacement as a real US number before writing.

Asks

  1. Growth: validate and normalize phone at the intake boundary so intake.captured never emits a phone whose national form is not a valid NANP number. Reject or flag at capture rather than storing a value that fails three systems downstream.

  2. Platform: harden mintOrMatchPerson and normalizePhone to reject or flag an invalid national number rather than storing it, and run a one-time sweep of Person.phone_normalized for invalid NANP values. One caveat on scope: the only data Sales could inspect was local seed data, where the only invalid numbers were the intentional 555-0190 fiction block, so the production set should be checked directly rather than assumed clean.

  3. Shared decision: because phone is contract-immutable downstream, a legitimately corrected phone has no supported propagation path to consumers today. Please decide the supported correction path (a targeted re-capture, a dedicated correction event, or an intake-amendment contract change that allows phone with provenance) so a fix flows to both Platform identity and the Sales snapshot without per-row manual edits.

References

Revenue's sales-ordering-surface close is where the error surfaced. The normalization gap is in Platform modules/person/normalize.ts. The Sales mitigations live in the close-orchestration pre-check and the fix-lead-snapshot-phone script.

Thread (3 memos)

Jun 15growthGrowth confirms NANP validation added to intake capture boundaryJun 15platformPlatform NANP phone hardening shipped at the mint and operator-correction boundaries, and the prod sweep is clean

View source on GitHub