Diseño definitivo: Phone E.164 + 5 campos nuevos + entidad SupplierContact[] con multi-contacto. La investigación (glosario, problema, industria, opciones evaluadas) vive en research-supplier-phone.md; la decisión en ADR-0102; el control en @qiiub/ui (<PhoneInput>, #435).
✅ Plan B + C juntos en este sprint: normalizar Phone a E.164 + agregar 5 columnas (Notes, Address2, TaxNumber, Terms, Website) + crear entidad SupplierContact con multi-contacto (modelo Square / Shopify B2B).
🏛️ Modelo B (híbrido): Supplier.Phone se conserva como centralita de la empresa. Supplier.ContactName y Supplier.Email se eliminan (pertenecen a la persona, no a la empresa). Cada SupplierContact tiene su propio Phone + PhoneExt.
📞 Extensión telefónica: columna SupplierContact.PhoneExt varchar(10). El campo Notes queda libre para notas genéricas (delivery, terms, alternativos).
📦 Tareas: se dividen en dos task files separados (-backend.md y -frontend.md) compartiendo este HTML como source-of-truth visual. PR backend primero, frontend consume la API.
Punto clave que motiva el diseño: en Puerto Rico nadie marca +1 para llamadas locales. La gente escribe 787-555-1234 y listo. El +1 es transparente — solo existe en el almacenamiento, no en la pantalla.
No agregamos columna PhoneExt en este sprint. La UI tiene un solo campo Teléfono (E.164 detrás) y un nuevo campo Notes de texto libre donde el usuario escribe la extensión y cualquier otra info útil del proveedor (días de delivery, terms, instrucciones, contactos alternos).
Si después un merchant pide buscar/dedupe por extensión, agregamos la columna estructurada. En dev solo es schema change — los seeds se regeneran.
Un campo Notes en Supplier es estándar en ERPs serios (SAP MM-LFA1-MEMO, NetSuite, Dynamics). Resolvemos extensión y el caso real de "necesito recordar X de este proveedor".
Un solo campo. Sin dropdown de país. Sin +1. El usuario teclea 7875551234 y el frontend lo formatea como (787) 555-1234 en vivo.
(787) 555-1234
7875551234
+17875551234
+17875551234
El +1 se agrega en el frontend justo antes de enviar a la API. El usuario nunca lo ve.
La banderita es casi invisible por defecto (un chip pequeño a la izquierda con 🇵🇷). Solo si el usuario hace click expande un dropdown con los 200 países. Es la misma UI que usa react-phone-number-input.
→ DB: +17875551234
→ DB: Phone=+17875551234, Notes="ext. 5102 (Wanda)"
→ DB: +525512345678
Desde que el usuario teclea hasta lo que se ve en la página de detalle.
+1.Notes = "ext. 5102"Supplier.Phone varchar(16)Supplier.Notes varchar(500)Dos cambios al schema: modificar Supplier y crear nueva entidad SupplierContact. Una sola migración EF Core.
El drawer ahora tiene dos zonas claras: arriba la información del proveedor (la empresa), abajo el array de contactos (personas). El usuario puede crear un Supplier sin contactos y agregarlos después.
Notar: Wanda y AR comparten el mismo phone (la centralita) con extensiones distintas. César tiene celular directo (sin ext). Eso es el modelo B funcionando: Supplier.Phone es la centralita compartida, cada SupplierContact puede o no compartirla.
Estamos en pure dev. Las únicas filas Supplier.Phone que existen vienen de S04_CategoriesAndSuppliersSeed.cs (Faker con formato "(###) ###-####") y cualquier dev DB local.
+1 + 10 dígitos, sin formato).Phone 30→16).No se necesita script de data migration, tabla DataMigrationIssue, parser tolerante, ni reporte. Eso aplicaría post-launch cuando haya datos de merchants reales — pero eso es problema de otro sprint.
El trabajo se divide en dos task files con el mismo prefijo de fecha. El HTML completo y el research son compartidos como source-of-truth visual / documental. Backend mergea primero; frontend consume la API ya disponible.
2026-05-21-supplier-phone-and-schema-backend.md
Supplier, create SupplierContactSupplier, crear SupplierContact : BaseEntity, IHasPublicId(MerchantId, SupplierId)libphonenumber-csharpBeValidE164SupplierContact por supplierSupplierContact2026-05-21-supplier-phone-and-schema-frontend.md
react-phone-number-input, libphonenumber-js<PhoneInput> en @qiiub/ui con tokens del design systemSupplier, SupplierContactuseSuppliers, useSupplierContactsSupplierDrawer.tsx refactor: dos zonas (empresa + contactos)<SupplierContactList> con add/remove rows<SupplierContactRow> con Phone + Ext inlineSupplierDetailPage.tsx: secciones empresa + contactos| Modelo | Supplier.Phone | SupplierContact.Phone | Estado |
|---|---|---|---|
| A — Square strict | ❌ no existe | ✓ todo aquí | Descartado |
| B — Híbrido | ✓ centralita | ✓ phone + ext por persona | Elegido |
| C — Tipos | ❌ no existe | ✓ con campo Type | Descartado |
Patrón de referencia interno: Qiiub ya tiene Account ↔ AccountContact — reusar las convenciones (composite FK, soft delete, audit cols, RowVersion). Estamos en pure dev — schema change + seed regen, no migración de datos.
El tel: link de cada contacto incluye su extensión con pausa (tel:+17877652929,,6645) — el móvil del usuario marca la centralita, espera, y luego marca la extensión automáticamente.
Después de cuestionar el supuesto, busqué evidencia concreta de prevalencia por tipo de entidad. La realidad:
| Entidad | ¿Usa ext? | Evidencia |
|---|---|---|
| Supplier (B2B) | SÍ, común | Distribuidores, mayoristas, reguladores. Ejemplo PR real: Board of Pharmacy 787-765-2929 ext 6645. Sales teams monitorean "switchboard rate" como métrica. |
| Customer retail (B2C) | NO típicamente | Shopper individual = celular. POS de cell phone stores (Lightspeed, CellSmart) no mencionan ext. |
| Customer empresarial | A veces | Cuenta comercial (farmacia que vende a una clínica). Pero ahí la persona específica suele ser un Contact. |
| Contact (persona dentro de Customer) | SÍ, común | Por eso Salesforce y HubSpot ponen ext en Contact, no en Account. La persona específica tiene extensión, no la empresa. |
Implicación para Qiiub: Supplier.PhoneExt sí vale la pena. Customer.PhoneExt probablemente no — el customer típico de un POS retail es B2C. Si en el futuro agregamos Contact (persona dentro de un Customer empresarial), ahí sí va PhoneExt.
Mismo schema. Si quieres campo aparte para fax, lo agregamos como Fax varchar(16) separado (también E.164). RMH/GP tienen Fax como campo independiente — vale la pena considerarlo.
No los distinguimos en almacenamiento. libphonenumber-csharp puede decirte el tipo (FIXED_LINE, MOBILE) en runtime si algún día necesitamos saber para SMS vs llamada.
El endpoint de import bulk usa el mismo validador BeValidE164. Líneas no-parseables se marcan en el reporte de errores del import, no rompen toda la carga. Esto aplica cuando el import exista — no es parte del sprint actual.
libphonenumber-csharp (NuGet, MIT, port oficial de Google ~5MB) + react-phone-number-input (~30kB gz, ya usa el design system de inputs). Ambos mantenidos activamente, sin issues de seguridad conocidos.
varchar(16) (alineado con Stripe, Shopify, Twilio).+1 visible, auto-formato local PR/US. Chip de país pequeño con dropdown al hacer click para internacional."PR" en parser libphonenumber.Supplier.Phone = centralita (E.164). SupplierContact.Phone + PhoneExt por persona. Supplier.ContactName y Supplier.Email se eliminan.SupplierContact.PhoneExt varchar(10). Notes vuelve a ser notas genéricas.Notes (1000), Address2 (60), TaxNumber (30), Terms (50), Website (255). Todos nullable.SupplierContact (Name, Title, Email, Phone, PhoneExt, Ordinal, IsPrimary, IsActive + base entity cols).Empezar con Supplier como piloto (es donde los devs notaron el problema). ADR + EF Core migration + seeder update + frontend en una sola PR. Si funciona, replicar a Customer y demás entidades con Phone. Recordar: estamos en pure dev — no hay datos de producción que mover, solo schema changes y seed regen.
Fuentes: ITU-T E.164 · ITU-T E.123 · Microsoft Learn (Dynamics GP — Phone_Number Dexterity type) · Stripe API docs · Shopify Customer API · Square Vendors API · Twilio Phone Validation Best Practices · GitHub catamphetamine/react-phone-number-input · GitHub twcclegg/libphonenumber-csharp.