Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
23 KiB
23 KiB
Scratchpad — Multiple Contact Phone Numbers
- Plan slug:
multiple-contact-phone-numbers - Created:
2026-03-09
What This Is
Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing this plan.
Prefer short bullets. Append new entries as you learn things, and also update earlier notes when a decision changes or an open question is resolved.
Decisions
- (2026-03-09) Canonical contact phone types are
work,mobile,home,fax, andother. - (2026-03-09) The application contract is a breaking cutover: contact APIs/types/UI should move to
phone_numbersrather than keep a long-lived scalar compatibility field. - (2026-03-09) The storage model should be normalized instead of JSON on
contacts. - (2026-03-09) Custom phone types should behave like tags: tenant-scoped reusable suggestions created on demand, with normalization-based deduplication.
- (2026-03-09) List/detail/ticket surfaces should display the derived default phone rather than attempt to render every phone row in summary views.
- (2026-03-09) Migration A is implemented as one additive schema file that creates both normalized phone tables, backfills scalar contact phones, and intentionally leaves
contacts.phone_numberin place for deploy safety. Rationale: it satisfies the rollout sequencing requirement without coupling the later cutover/drop step to the initial schema release. - (2026-03-09)
contact_phone_numbers.normalized_phone_numberis implemented as a generated stored column derived fromphone_numberinstead of an app-populated plain text field. Rationale: it guarantees searchable normalized digits for every insert/update path, including direct SQL fixtures and future services that have not been cut over yet. - (2026-03-09) Phone-row write logic is centralized in
shared/models/contactModel.tsinstead of being duplicated acrossContactService, client actions, CSV import, and later Entra sync. Rationale: one transactional helper surface keeps default enforcement, custom-type reuse, and read hydration consistent. - (2026-03-09) Contact read/query paths now expose
default_phone_numberanddefault_phone_typeconvenience fields in addition to the orderedphone_numbersarray. Rationale: summary surfaces and sort/search code need a stable derived default without reimplementing that derivation everywhere. - (2026-03-09) The first UI slice uses a contact-domain-local
ContactPhoneNumbersEditorrather than extracting a global multi-entity phone component. Rationale: the PRD scope is contact-only, and a local editor let the form behavior converge before broader reuse decisions.
Discoveries / Constraints
- (2026-03-09)
contacts.phone_numberis still assumed broadly across shared types, server interfaces, API schemas, contact actions, query actions, CSV import/export, list tables, detail views, ticket properties, and Entra sync. - (2026-03-09) Main contact GUI surfaces in scope include:
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/contacts/Contacts.tsxpackages/clients/src/components/contacts/ClientContactsList.tsxpackages/clients/src/components/contacts/ContactsImportDialog.tsxpackages/clients/src/components/clients/QuickAddClient.tsxpackages/tickets/src/components/ticket/TicketProperties.tsx
- (2026-03-09) Contact query actions currently sort and project directly on
contacts.phone_number, so query behavior needs an explicit default-phone derivation rule after normalization. - (2026-03-09) Contact workflow/domain events and API schemas currently still emit/validate scalar phone fields (
phoneNumber,phone_number). - (2026-03-09) Entra sync currently collapses
mobilePhoneandbusinessPhones[0]into one scalarphone_number; the new model should preserve more than one external number. - (2026-03-09) Existing repo migration tests commonly use file-content contract assertions rather than spinning up a database for every migration case; the first phone migration coverage follows that pattern in
server/src/test/unit/migrations/contactPhoneNumbersMigration.test.ts. - (2026-03-09) This worktree’s
.env.localtestpoints atlocalhost:5438, but the active local Postgres for integration tests is the Docker container exposed onlocalhost:55433withpostgres/app_userpasswords fromsecrets/postgres_passwordandsecrets/db_password_server(postpass123). - (2026-03-09)
shared/vitest.config.tsonly discovers tests underservices/**/*.test.tsand**/__tests__/**/*.test.ts, so shared validation tests for this work need to live inshared/**/__tests__/. - (2026-03-09) The existing shared workflow builder tests import
buildWorkflowPayloadthrough a package re-export that resolves the published@alga-psa/event-schemasentry. In this worktree, the reliable local path ispackages/event-schemas/src/schemas/workflowEventPublishHelpers.ts. - (2026-03-09)
server/vitest.config.tsneeded local source aliases for@alga-psa/clientsand@alga-psa/user-compositionso server-side Vitest contract tests could import unbuilt package source files directly from the monorepo. - (2026-03-09)
npx tsc -p shared/tsconfig.json --noEmitstill fails because the shared TypeScript program pulls inpackages/event-schemas/src/schemas/workflowEventPublishHelpers.tsoutside its configuredrootDir; that pre-existing config issue is unrelated to the contact-phone cutover.
Commands / Runbooks
- (2026-03-09) Find contact phone usage in contact UI and actions:
rg -n "phone_number|PhoneInput|ContactDetails|QuickAddContact|ContactsImportDialog|ClientContactsList" packages/clients/src --glob '!**/node_modules/**'
- (2026-03-09) Find wider GUI/contact display usage:
rg -n "phone_number|Phone Number|Phone" packages/clients/src/components packages/tickets/src/components ee/server/src --glob '!**/node_modules/**'
- (2026-03-09) Find API/service/event usage:
rg -n "CONTACT_CREATED|CONTACT_UPDATED|phoneNumber|phone_number" packages server ee --glob '!**/node_modules/**'
- (2026-03-09) Validate the new migration contract suite:
cd server && npx vitest run src/test/unit/migrations/contactPhoneNumbersMigration.test.ts
- (2026-03-09) Quick syntax-load check for the new migration:
node -e "require('./server/migrations/20260309120000_create_contact_phone_numbers_schema.cjs'); console.log('migration-load-ok')"
- (2026-03-09) Run the DB-backed normalized phone storage test against the live local Postgres container:
cd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactPhoneNumbers.integration.test.ts --coverage=false
- (2026-03-09) Type-check the backend cutover slice:
npx tsc -p shared/tsconfig.json --noEmitnpx tsc -p server/tsconfig.json --noEmitnpx tsc -p packages/types/tsconfig.json --noEmitnpx tsc -p packages/clients/tsconfig.json --noEmitnpx tsc -p packages/event-schemas/tsconfig.json --noEmit
- (2026-03-09) Run the backend contract tests for normalized contact phones:
cd packages/types && npx vitest run src/contact-phone.typecheck.test.tsnpx vitest run --config shared/vitest.config.ts shared/models/__tests__/contactModel.test.ts shared/workflow/streams/domainEventBuilders/__tests__/contactEventBuilders.test.tscd server && npx vitest run src/test/unit/validation/contactPhoneSchemas.test.ts --coverage=falsecd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactModelPhoneNumbers.integration.test.ts --coverage=false
- (2026-03-09) Run the contact UI normalized-phone tests:
npx tsc -p packages/clients/tsconfig.json --noEmitcd server && npx vitest run src/test/unit/contacts/ContactPhoneNumbersEditor.test.tsx src/test/unit/contacts/ContactDetailsSave.contract.test.ts src/test/unit/contacts/ContactDetailsPhoneNumbers.contract.test.ts src/test/unit/contacts/QuickAddContact.phoneNumbers.test.tsx src/test/unit/contacts/QuickAddClient.phoneNumbers.test.tsx src/test/unit/contacts/ContactPhoneDisplay.contract.test.ts --coverage=false
- (2026-03-09) Run the contact search/sort integration coverage:
cd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactServicePhoneSearch.integration.test.ts --coverage=false
- (2026-03-09) Run the contact CSV normalized-phone integration coverage:
cd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactCsvPhoneImportExport.integration.test.ts --coverage=false
- (2026-03-09) Run the Entra normalized-phone coverage:
npx tsc -p ee/server/tsconfig.json --noEmitcd ee/server && npx vitest run src/__tests__/unit/entraContactFieldSync.test.ts src/__tests__/unit/entraContactReconciler.test.ts --coverage=false
- (2026-03-09) Run the contact helper/seed normalized-phone regression:
cd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactTestHelpersPhoneRows.integration.test.ts --coverage=false
- (2026-03-09) Validate the post-cutover server slice and Migration B coverage:
npx tsc -p server/tsconfig.json --noEmitcd server && DB_PORT=55433 DB_PASSWORD_ADMIN=postpass123 DB_PASSWORD_SERVER=postpass123 DB_USER_ADMIN=postgres DB_USER_SERVER=app_user npx vitest run src/test/integration/contactPhoneColumnCutover.integration.test.ts src/test/unit/migrations/contactPhoneNumbersCutoverMigration.test.ts --coverage=false
Links / References
- Contact type definition:
packages/types/src/interfaces/contact.interfaces.ts - Contact actions:
packages/clients/src/actions/contact-actions/contactActions.tsx - Contact query actions:
packages/clients/src/actions/queryActions.ts - API contact schemas:
server/src/lib/api/schemas/contact.ts - Initial contacts schema:
server/migrations/202409071803_initial_schema.cjs - Contact details screen explicitly called out by user:
packages/clients/src/components/contacts/ContactDetails.tsx
Open Questions
- Should v1 CSV import/export add an explicit phone type column, or should import/export remain single-default-phone only?
- Should the server expose derived
default_phone_numberconvenience fields on list responses, or should callers derive them fromphone_numbers? - Should the new multi-phone editor be contact-local first, or extracted immediately into a shared UI component?
Completed Items
- (2026-03-09) Completed
F001,F002,F004, andF005withserver/migrations/20260309120000_create_contact_phone_numbers_schema.cjs.- Added
contact_phone_type_definitionswith tenant-scoped uniquenormalized_labeland a DB check that stored normalized labels are lower-trimmed. - Added
contact_phone_numberswith canonical/custom type exclusivity, canonical type constraint, per-contact default uniqueness, display ordering, and tenant/contact lookup indexes. - Backfilled non-empty legacy
contacts.phone_numbervalues into defaultworkphone rows while retaining the legacy column for the later cutover/drop sequence.
- Added
- (2026-03-09) Completed
T001throughT005withserver/src/test/unit/migrations/contactPhoneNumbersMigration.test.ts.- Coverage asserts the migration contract for custom type deduplication, phone-row type exclusivity, default uniqueness, and the scalar-phone backfill rules.
- (2026-03-09) Completed
F003by switchingcontact_phone_numbers.normalized_phone_numberto a generated stored column inserver/migrations/20260309120000_create_contact_phone_numbers_schema.cjs.- Searchable normalized digits are now derived by the database from the display phone value, which avoids drift between formatted and normalized storage.
- (2026-03-09) Completed
T006withserver/src/test/integration/contactPhoneNumbers.integration.test.ts.- The integration test inserts a formatted phone row and verifies the stored/generated normalized digits can be queried without punctuation.
- (2026-03-09) Completed
F007,F008,F009,F011, andF021by cutting the backend contracts over to normalized contact phones.shared/interfaces/contact.interfaces.ts,packages/types/src/interfaces/contact.interfaces.ts, andserver/src/interfaces/contact.interfaces.tsxnow exposephone_numbersplus derived default-phone convenience fields instead of a scalarphone_number.shared/models/contactModel.tsnow validates canonical/custom phone rows, enforces exactly one default, auto-creates/reuses tenant-scoped custom type definitions, replaces child phone rows transactionally, and hydrates ordered phone rows on reads.server/src/lib/api/services/ContactService.ts,packages/clients/src/actions/contact-actions/contactActions.tsx, andpackages/clients/src/actions/queryActions.tsnow create/read/update contacts through the normalized phone model and derive default-phone search/sort/export behavior from child rows.server/src/lib/api/schemas/contact.ts,shared/workflow/runtime/schemas/crmEventSchemas.ts,packages/event-schemas/src/schemas/domain/crmEventSchemas.ts, andshared/workflow/streams/domainEventBuilders/contactEventBuilders.tsnow validate and emit normalized phone payloads.
- (2026-03-09) Completed
T007throughT015,T030,T031, andT032.packages/types/src/contact-phone.typecheck.test.tsverifies the exported contact create/read types acceptphone_numberscollections and reject legacy scalar-only create payloads.shared/models/__tests__/contactModel.test.tsverifies create validation rejects duplicate defaults and missing defaults while accepting mixed canonical/custom rows.server/src/test/unit/validation/contactPhoneSchemas.test.tsverifies contact API schemas validatephone_numberscollections and response payloads with derived default fields.server/src/test/integration/contactModelPhoneNumbers.integration.test.tsverifies DB-backed custom-type reuse, transactional create/update behavior, rollback on failed child writes, and ordered read hydration.shared/workflow/streams/domainEventBuilders/__tests__/contactEventBuilders.test.tsnow assertsCONTACT_CREATEDandCONTACT_UPDATEDpayloads carry normalized phone data rather than scalar-only phone fields.
- (2026-03-09) Completed
F010,F012,F013,F014,F015,F016, andF018by wiring the contact-facing UI and display surfaces to normalized phone rows.packages/clients/src/components/contacts/ContactPhoneNumbersEditor.tsxprovides the shared repeater UI for add/remove/reorder/default behavior, canonical vs. custom type selection, suggestion datalists, and normalized validation feedback.packages/clients/src/components/contacts/ContactDetails.tsx,packages/clients/src/components/contacts/ContactDetailsEdit.tsx,packages/clients/src/components/contacts/QuickAddContact.tsx, andpackages/clients/src/components/clients/QuickAddClient.tsxnow read and submitphone_numberscollections instead of a scalar phone field.packages/clients/src/components/contacts/ContactDetailsView.tsx,packages/clients/src/components/contacts/Contacts.tsx,packages/clients/src/components/contacts/ClientContactsList.tsx, andpackages/tickets/src/components/ticket/TicketProperties.tsxnow render the derived default contact phone from normalized rows.packages/clients/src/actions/contact-actions/contactActions.tsxnow exposeslistContactPhoneTypeSuggestions, andserver/vitest.config.tsnow resolves the package-source aliases needed for these UI contract tests.
- (2026-03-09) Completed
T016throughT023andT026.server/src/test/unit/contacts/ContactPhoneNumbersEditor.test.tsxverifies multi-row editor behavior for custom types, explicit default selection, and invalid duplicate-default states.server/src/test/unit/contacts/ContactDetailsSave.contract.test.tsandserver/src/test/unit/contacts/ContactDetailsPhoneNumbers.contract.test.tsverify the contact detail screens bindphone_numbersinto the shared editor, validate before save, and render normalized rows instead of scalarphone_number.server/src/test/unit/contacts/QuickAddContact.phoneNumbers.test.tsxandserver/src/test/unit/contacts/QuickAddClient.phoneNumbers.test.tsxverify the quick-add flows submit normalized phone collections with the expected default row.server/src/test/unit/contacts/ContactPhoneDisplay.contract.test.tsverifies contacts lists and ticket properties derive their displayed contact phone fromdefault_phone_numberor the default normalized child row.
- (2026-03-09) Completed
F017with DB-backed coverage inserver/src/test/integration/contactServicePhoneSearch.integration.test.ts.server/src/lib/api/services/ContactService.tsnow searches contact phone matches through any normalized child phone row, including non-default rows, and sortsphone_numberlist views by the derived default row.packages/clients/src/actions/queryActions.tskeeps client-side list sorting aligned with the same derived-default-phone rule after hydrated contacts are returned.
- (2026-03-09) Completed
T024andT025withserver/src/test/integration/contactServicePhoneSearch.integration.test.ts.- The integration suite verifies
ContactService.search()returns a contact when only a secondary phone row matches the query digits. - The integration suite verifies
ContactService.list()sorts bydefault_phone_numberrather than a non-default child phone row.
- The integration suite verifies
- (2026-03-09) Completed
F019andF020by finishing the CSV import/export cutover for the normalized phone model.packages/clients/src/actions/contact-actions/contactActions.tsxnow maps CSVphone_numbervalues into one default canonicalworkphone row on import, exports the derived default phone value, and normalizes optional CSV text fields so omittedrole/notesvalues do not fail contact validation.packages/clients/src/components/contacts/ContactsImportDialog.tsxnow labels the CSV phone column as the default phone number and explicitly tells users that v1 CSV import/export handles one default phone per contact.
- (2026-03-09) Completed
T027,T028, andT029withserver/src/test/integration/contactCsvPhoneImportExport.integration.test.ts.- The integration suite verifies importing one CSV phone column creates one default normalized
workphone row. - The integration suite verifies contact CSV export emits the derived default phone instead of depending on a legacy scalar field.
- The same suite contract-checks the import dialog copy for the single-default-phone CSV rule.
- The integration suite verifies importing one CSV phone column creates one default normalized
- (2026-03-09) Completed
F022by mapping Entra contact phones into normalized contact phone rows.ee/server/src/lib/integrations/entra/sync/contactFieldSync.tsnow buildsphone_numberscollections frombusinessPhones[]andmobilePhoneinstead of returning a scalarphone_numberpatch.ee/server/src/lib/integrations/entra/sync/contactReconciler.tsnow creates Entra contacts withphone_numbersand routes linked-contact phone updates throughContactModel.updateContact(...)inside the same transaction rather than trying to update child rows via the rawcontactstable.
- (2026-03-09) Completed
T033andT034withee/server/src/__tests__/unit/entraContactFieldSync.test.tsandee/server/src/__tests__/unit/entraContactReconciler.test.ts.- The Entra field-sync tests verify
mobilePhonebecomes canonicalmobile,businessPhones[]become canonicalwork, and the first business phone wins default precedence over mobile. - The Entra reconciler tests verify the create path passes normalized
phone_numbersintoContactModel.createContact(...)and linked-contact phone sync usesContactModel.updateContact(...)with the same normalized mapping.
- The Entra field-sync tests verify
- (2026-03-09) Completed
F023by updating contact-facing seeds, factories, and E2E helpers to create normalized phone rows.server/seeds/dev/05_contacts.cjsnow inserts contacts without the legacy scalar field and seeds one defaultcontact_phone_numbersrow per contact.server/src/test/e2e/factories/contact.factory.tsandserver/src/test/e2e/utils/contactTestDataFactory.tsnow create contacts throughContactModel.createContact(...), which leavescontacts.phone_numberempty while writing normalized child rows.server/src/test/e2e/api/contacts.e2e.test.tsandserver/src/test/e2e/utils/clientTestData.tsnow use the normalizedphone_numbersshape instead of scalar contact phones in their generated contact payloads.
- (2026-03-09) Completed
T035withserver/src/test/integration/contactTestHelpersPhoneRows.integration.test.ts.- The integration suite verifies the E2E contact factory and contact test data helper leave
contacts.phone_numbernull while creating defaultcontact_phone_numbersrows. - The same suite contract-checks the dev contacts seed for normalized default phone inserts.
- The integration suite verifies the E2E contact factory and contact test data helper leave
- (2026-03-09) Completed
F024by removing the remaining application-level reads and writes ofcontacts.phone_number.shared/ticketClients/contacts.ts,shared/services/emailService.ts, andshared/workflow/actions/emailWorkflowActions.tsnow hydrate contact phones throughContactModeland derive the default phone from normalized child rows instead of selecting the legacy scalar field.packages/integrations/src/actions/clientLookupActions.ts,packages/integrations/src/actions/email-actions/emailActions.ts,packages/ui/src/components/ContactPickerDialog.tsx,server/src/lib/api/services/TicketService.ts, andserver/src/lib/eventBus/subscribers/ticketEmailSubscriber.tsnow read or display the default contact phone fromcontact_phone_numbers.packages/clients/src/actions/contact-actions/contactActions.tsxno longer falls back to writing scalarphone_numbervalues in the main contact create/update flows after the normalized model cutover.
- (2026-03-09) Completed
T036withserver/src/test/integration/contactPhoneColumnCutover.integration.test.ts.- The DB-backed suite drops
contacts.phone_numberagainst the live test schema and verifiesContactService.create(...),update(...), andgetById(...)continue to work through normalized phone rows only.
- The DB-backed suite drops
- (2026-03-09) Completed
F006withserver/migrations/20260309183000_drop_contacts_phone_number_column.cjs.- Migration B now drops
contacts.phone_numberonce the app no longer depends on it, and restores the column on rollback so the rollout remains reversible.
- Migration B now drops
- (2026-03-09) Completed
T037withserver/src/test/integration/contactPhoneColumnCutover.integration.test.tsandserver/src/test/unit/migrations/contactPhoneNumbersCutoverMigration.test.ts.- The migration contract test asserts the new migration explicitly drops and restores
contacts.phone_number. - The integration coverage verifies contact create/update/read flows still pass after applying Migration B and before rolling it back.
- The migration contract test asserts the new migration explicitly drops and restores