Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
12 KiB
PRD — Multiple Contact Phone Numbers
- Slug:
multiple-contact-phone-numbers - Date:
2026-03-09 - Status: Draft
Summary
Replace the single contacts.phone_number field with a normalized multi-number model that supports one or more phone numbers per contact, fixed canonical types, tenant-scoped custom type suggestions, and an explicit default number. Update the contact creation/editing/import flows and the main dependent GUI surfaces so the new model is first-class everywhere the current scalar phone number is shown or edited.
Problem
Contacts currently expose only one scalar phone_number, which is too limited for real-world usage. MSP users need to store multiple numbers per client contact, distinguish those numbers by type, and choose which number should be treated as the default. The current shape also makes integrations like Entra sync lossy because multiple external phone values are collapsed into one field.
Goals
- Support multiple phone numbers on a contact with an explicit default number.
- Keep a small canonical type set:
work,mobile,home,fax,other. - Support tenant-scoped custom phone type labels with autocomplete/deduplication behavior similar to tags.
- Replace all contact-domain create/edit/read code paths so they work against the normalized model instead of
contacts.phone_number. - Update the key GUI surfaces that display or edit contact phone numbers.
- Preserve practical list/search behavior by defining a default-phone display rule and searchable normalized phone values.
- Map Entra mobile/business phones into the new contact phone model instead of discarding extra values.
Non-goals
- Refactoring client company phone fields, client location phone fields, tenant support phone fields, or portal profile phone fields in this plan.
- Introducing an admin-managed settings UI for custom phone type definitions.
- Shipping a long-lived compatibility layer that keeps
contacts.phone_numberas an application field after the cutover. - Designing a generalized reusable multi-phone component for every entity in the product; this plan only requires the contact-domain surfaces.
- Adding metrics, feature flags, or operational rollout tooling beyond normal migration sequencing and validation.
Users and Primary Flows
Primary users
- MSP staff managing contacts in the contact directory or client detail screens.
- MSP staff creating contacts quickly from the contacts area or while creating a client.
- MSP staff viewing a contact phone from ticket and related-detail screens.
- Admins/operators relying on contact import and Entra synchronization.
Primary flows
- Open a contact in the contacts area, add multiple phone numbers, assign canonical or custom types, and choose exactly one default.
- Create a new contact from quick-add and enter one or more phone numbers before save.
- Create a client with an inline primary contact and capture multiple phone numbers for that new contact.
- View contacts in list/table form and see the default phone number rendered consistently.
- Search contacts by any stored phone number, not just the default one.
- Import contacts from CSV with one primary/default phone number in v1, then manage additional numbers in the UI after import.
- Sync a contact from Entra and retain both mobile and business phone values where present.
UX / UI Notes
Contact authoring/editing surfaces in scope
packages/clients/src/components/contacts/ContactDetails.tsxpackages/clients/src/components/contacts/ContactDetailsEdit.tsxpackages/clients/src/components/contacts/ContactDetailsView.tsxpackages/clients/src/components/contacts/QuickAddContact.tsxpackages/clients/src/components/clients/QuickAddClient.tsx(inline contact subsection)
Contact display surfaces in scope
packages/clients/src/components/contacts/Contacts.tsxpackages/clients/src/components/contacts/ClientContactsList.tsxpackages/tickets/src/components/ticket/TicketProperties.tsxpackages/clients/src/components/interactions/InteractionDetails.tsxviaContactDetailsView
Contact import surfaces in scope
packages/clients/src/components/contacts/ContactsImportDialog.tsx- Export/query behavior in
packages/clients/src/actions/contact-actions/contactActions.tsx
UX assumptions
- Contact forms should use a repeater/list UI for phone numbers with add/remove controls.
- Each phone row should let the user:
- enter a phone number
- pick a canonical type or choose/create a custom type
- mark the row as default
- When numbers exist, the form must enforce exactly one default.
- List/table/detail surfaces that previously showed one
phone_numbershould render the default phone number only. - Where useful, the default phone label/type may be shown as a badge or secondary text, but v1 does not require a redesign of list layouts.
- CSV import/export remains intentionally simpler than the full UI:
- v1 import/export handles one default phone number and its type
- multi-number bulk CSV shape is out of scope for this first cut
Requirements
Functional Requirements
FR-001Add a normalized contact phone storage model that supports multiple phone rows per contact.FR-002Add a tenant-scoped custom phone type definition store for suggestion/deduplication of custom labels.FR-003Keep canonical phone types fixed towork,mobile,home,fax, andother.FR-004Require at most one default phone number per contact and exactly one default when a contact has at least one phone number.FR-005Preserve display order for multiple phone numbers.FR-006Store a searchable normalized phone value per phone row in addition to the displayed phone string.FR-007Backfill existingcontacts.phone_numbervalues into the new normalized phone table as defaultworknumbers unless empty.FR-008Replace contact create and update contracts so they accept the new phone collection instead of the old scalar field.FR-009Replace contact read contracts so callers receive ordered phone rows and can derive the default number without readingcontacts.phone_number.FR-010Update contact validation so phone collection writes validate number format, type shape, default uniqueness, and custom type deduplication.FR-011UpdateContactDetailsto edit multiple phone numbers, custom types, and default selection.FR-012UpdateContactDetailsEditandContactDetailsViewto render the normalized contact phone model.FR-013UpdateQuickAddContactto capture the normalized contact phone model.FR-014Update the inline contact section inQuickAddClientto capture the normalized contact phone model.FR-015Update contacts list surfaces to display the default phone number in place of the old scalar phone.FR-016Update contact search/filter behavior so phone search matches any stored contact phone number.FR-017Update contact list sort behavior so phone sorting is based on the derived default phone number.FR-018Update ticket/contact-related display surfaces to show the derived default contact phone number.FR-019Update contact CSV import/export behavior to map a single imported/exported phone into the normalized model as the default number.FR-020Update workflow/domain event payloads and API/service schemas that currently emit or validatephone_number.FR-021Update Entra sync so mobile and business phones are mapped into the normalized contact phone model instead of collapsing to one scalar.FR-022Remove application reliance oncontacts.phone_numberafter the new model is in use.FR-023Drop the legacycontacts.phone_numbercolumn in a follow-up migration after code has cut over.
Non-functional Requirements
NFR-001Migration sequencing must be operationally safe: add/backfill new tables before the code release that stops readingcontacts.phone_number, then drop the old column afterward.NFR-002Contact writes touching phone numbers must remain transactional so child rows and the parent contact cannot drift.NFR-003Existing contacts with no phone number must remain valid and should simply hydrate with an empty phone list.NFR-004Custom phone type deduplication must be case-insensitive at the tenant scope.NFR-005Searchability of phone numbers must not depend on formatting characters in the stored display value.
Data / API / Integrations
Proposed tables
contact_phone_type_definitions
tenantcontact_phone_type_idlabelnormalized_labelcreated_atupdated_at- unique on
(tenant, normalized_label)
contact_phone_numbers
tenantcontact_phone_number_idcontact_name_idphone_numbernormalized_phone_numbercanonical_typenullablecustom_phone_type_idnullableis_defaultdisplay_ordercreated_atupdated_at
Table constraints
- Exactly one of
canonical_typeorcustom_phone_type_idmust be present. canonical_typeis constrained towork | mobile | home | fax | other.- Foreign key from
contact_phone_numberstocontacts. - Foreign key from
contact_phone_numberstocontact_phone_type_definitionswhen custom type is used. - Partial uniqueness for one default per contact.
Type/API shape
- Replace scalar
phone_numberon contact DTOs with aphone_numberscollection. - Each phone row should expose:
contact_phone_number_idphone_numbernormalized_phone_numbercanonical_typecustom_typeis_defaultdisplay_order
- Contact list/query responses may additionally expose a derived
default_phone_numberanddefault_phone_typefor convenience.
Import/export shape
- CSV import v1 continues to accept one phone column and maps it to a single default
workphone unless a type column is added in the same effort. - CSV export v1 emits the default phone number only.
Entra mapping assumption
mobilePhonemaps to canonicalmobile.businessPhones[]map to canonicalwork.- If one or more business phones exist, the first business phone is the default.
- Otherwise the first mobile phone becomes the default.
Security / Permissions
- No new permission model is introduced.
- Existing contact create/update/read permissions continue to govern all affected flows.
- Custom phone type creation is implicit within existing contact edit/create permissions; no separate admin permission is added.
Observability
- No new observability scope is included in v1 beyond normal migration logging, validation errors, and test coverage.
Rollout / Migration
- Migration A:
- create
contact_phone_type_definitions - create
contact_phone_numbers - backfill existing
contacts.phone_numberintocontact_phone_numbers - keep
contacts.phone_numberin place temporarily for deploy safety
- create
- Code release:
- move all contact-domain reads/writes/UI to
phone_numbers - stop reading or writing
contacts.phone_number
- move all contact-domain reads/writes/UI to
- Migration B:
- drop
contacts.phone_number - remove any remaining schema/index references to the old scalar field
- drop
This is still a breaking cutover at the application contract level, but the database rollout is intentionally split to avoid coupling schema removal to the same deploy that introduces the new readers/writers.
Open Questions
- Whether v1 CSV import/export should add an explicit phone-type column or stay with default
workonly. - Whether contact list/table responses should expose derived
default_phone_numberfields from the server for convenience or let each caller derive them fromphone_numbers. - Whether the first version of the phone-number editor should be a contact-only local component or a reusable shared UI component.
Acceptance Criteria (Definition of Done)
- MSP users can add, remove, type, reorder, and default multiple phone numbers on a contact.
- MSP users can choose canonical phone types or enter a new custom type that becomes a tenant-scoped suggestion for future contacts.
- Contact detail, quick-add, and inline client-contact creation flows all work against the new model.
- Contact list and ticket display surfaces render the derived default phone consistently.
- Contact search finds a contact by any stored phone number.
- Existing scalar phone values are migrated into the normalized model.
- Entra sync preserves both mobile and business phone information in the normalized contact phone model.
- The application no longer relies on
contacts.phone_number, and the legacy column can be removed cleanly.