Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

23 KiB
Raw Permalink Blame History

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, and other.
  • (2026-03-09) The application contract is a breaking cutover: contact APIs/types/UI should move to phone_numbers rather 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_number in 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_number is implemented as a generated stored column derived from phone_number instead 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.ts instead of being duplicated across ContactService, 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_number and default_phone_type convenience fields in addition to the ordered phone_numbers array. 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 ContactPhoneNumbersEditor rather 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_number is 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.tsx
    • packages/clients/src/components/contacts/ContactDetailsEdit.tsx
    • packages/clients/src/components/contacts/ContactDetailsView.tsx
    • packages/clients/src/components/contacts/QuickAddContact.tsx
    • packages/clients/src/components/contacts/Contacts.tsx
    • packages/clients/src/components/contacts/ClientContactsList.tsx
    • packages/clients/src/components/contacts/ContactsImportDialog.tsx
    • packages/clients/src/components/clients/QuickAddClient.tsx
    • packages/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 mobilePhone and businessPhones[0] into one scalar phone_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 worktrees .env.localtest points at localhost:5438, but the active local Postgres for integration tests is the Docker container exposed on localhost:55433 with postgres / app_user passwords from secrets/postgres_password and secrets/db_password_server (postpass123).
  • (2026-03-09) shared/vitest.config.ts only discovers tests under services/**/*.test.ts and **/__tests__/**/*.test.ts, so shared validation tests for this work need to live in shared/**/__tests__/.
  • (2026-03-09) The existing shared workflow builder tests import buildWorkflowPayload through a package re-export that resolves the published @alga-psa/event-schemas entry. In this worktree, the reliable local path is packages/event-schemas/src/schemas/workflowEventPublishHelpers.ts.
  • (2026-03-09) server/vitest.config.ts needed local source aliases for @alga-psa/clients and @alga-psa/user-composition so server-side Vitest contract tests could import unbuilt package source files directly from the monorepo.
  • (2026-03-09) npx tsc -p shared/tsconfig.json --noEmit still fails because the shared TypeScript program pulls in packages/event-schemas/src/schemas/workflowEventPublishHelpers.ts outside its configured rootDir; 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 --noEmit
    • npx tsc -p server/tsconfig.json --noEmit
    • npx tsc -p packages/types/tsconfig.json --noEmit
    • npx tsc -p packages/clients/tsconfig.json --noEmit
    • npx 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.ts
    • npx vitest run --config shared/vitest.config.ts shared/models/__tests__/contactModel.test.ts shared/workflow/streams/domainEventBuilders/__tests__/contactEventBuilders.test.ts
    • cd server && npx vitest run src/test/unit/validation/contactPhoneSchemas.test.ts --coverage=false
    • 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/contactModelPhoneNumbers.integration.test.ts --coverage=false
  • (2026-03-09) Run the contact UI normalized-phone tests:
    • npx tsc -p packages/clients/tsconfig.json --noEmit
    • cd 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 --noEmit
    • cd 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 --noEmit
    • 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/contactPhoneColumnCutover.integration.test.ts src/test/unit/migrations/contactPhoneNumbersCutoverMigration.test.ts --coverage=false
  • 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_number convenience fields on list responses, or should callers derive them from phone_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, and F005 with server/migrations/20260309120000_create_contact_phone_numbers_schema.cjs.
    • Added contact_phone_type_definitions with tenant-scoped unique normalized_label and a DB check that stored normalized labels are lower-trimmed.
    • Added contact_phone_numbers with canonical/custom type exclusivity, canonical type constraint, per-contact default uniqueness, display ordering, and tenant/contact lookup indexes.
    • Backfilled non-empty legacy contacts.phone_number values into default work phone rows while retaining the legacy column for the later cutover/drop sequence.
  • (2026-03-09) Completed T001 through T005 with server/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 F003 by switching contact_phone_numbers.normalized_phone_number to a generated stored column in server/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 T006 with server/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, and F021 by cutting the backend contracts over to normalized contact phones.
    • shared/interfaces/contact.interfaces.ts, packages/types/src/interfaces/contact.interfaces.ts, and server/src/interfaces/contact.interfaces.tsx now expose phone_numbers plus derived default-phone convenience fields instead of a scalar phone_number.
    • shared/models/contactModel.ts now 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, and packages/clients/src/actions/queryActions.ts now 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, and shared/workflow/streams/domainEventBuilders/contactEventBuilders.ts now validate and emit normalized phone payloads.
  • (2026-03-09) Completed T007 through T015, T030, T031, and T032.
    • packages/types/src/contact-phone.typecheck.test.ts verifies the exported contact create/read types accept phone_numbers collections and reject legacy scalar-only create payloads.
    • shared/models/__tests__/contactModel.test.ts verifies create validation rejects duplicate defaults and missing defaults while accepting mixed canonical/custom rows.
    • server/src/test/unit/validation/contactPhoneSchemas.test.ts verifies contact API schemas validate phone_numbers collections and response payloads with derived default fields.
    • server/src/test/integration/contactModelPhoneNumbers.integration.test.ts verifies 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.ts now asserts CONTACT_CREATED and CONTACT_UPDATED payloads carry normalized phone data rather than scalar-only phone fields.
  • (2026-03-09) Completed F010, F012, F013, F014, F015, F016, and F018 by wiring the contact-facing UI and display surfaces to normalized phone rows.
    • packages/clients/src/components/contacts/ContactPhoneNumbersEditor.tsx provides 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, and packages/clients/src/components/clients/QuickAddClient.tsx now read and submit phone_numbers collections 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, and packages/tickets/src/components/ticket/TicketProperties.tsx now render the derived default contact phone from normalized rows.
    • packages/clients/src/actions/contact-actions/contactActions.tsx now exposes listContactPhoneTypeSuggestions, and server/vitest.config.ts now resolves the package-source aliases needed for these UI contract tests.
  • (2026-03-09) Completed T016 through T023 and T026.
    • server/src/test/unit/contacts/ContactPhoneNumbersEditor.test.tsx verifies multi-row editor behavior for custom types, explicit default selection, and invalid duplicate-default states.
    • server/src/test/unit/contacts/ContactDetailsSave.contract.test.ts and server/src/test/unit/contacts/ContactDetailsPhoneNumbers.contract.test.ts verify the contact detail screens bind phone_numbers into the shared editor, validate before save, and render normalized rows instead of scalar phone_number.
    • server/src/test/unit/contacts/QuickAddContact.phoneNumbers.test.tsx and server/src/test/unit/contacts/QuickAddClient.phoneNumbers.test.tsx verify the quick-add flows submit normalized phone collections with the expected default row.
    • server/src/test/unit/contacts/ContactPhoneDisplay.contract.test.ts verifies contacts lists and ticket properties derive their displayed contact phone from default_phone_number or the default normalized child row.
  • (2026-03-09) Completed F017 with DB-backed coverage in server/src/test/integration/contactServicePhoneSearch.integration.test.ts.
    • server/src/lib/api/services/ContactService.ts now searches contact phone matches through any normalized child phone row, including non-default rows, and sorts phone_number list views by the derived default row.
    • packages/clients/src/actions/queryActions.ts keeps client-side list sorting aligned with the same derived-default-phone rule after hydrated contacts are returned.
  • (2026-03-09) Completed T024 and T025 with server/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 by default_phone_number rather than a non-default child phone row.
  • (2026-03-09) Completed F019 and F020 by finishing the CSV import/export cutover for the normalized phone model.
    • packages/clients/src/actions/contact-actions/contactActions.tsx now maps CSV phone_number values into one default canonical work phone row on import, exports the derived default phone value, and normalizes optional CSV text fields so omitted role/notes values do not fail contact validation.
    • packages/clients/src/components/contacts/ContactsImportDialog.tsx now 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, and T029 with server/src/test/integration/contactCsvPhoneImportExport.integration.test.ts.
    • The integration suite verifies importing one CSV phone column creates one default normalized work phone 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.
  • (2026-03-09) Completed F022 by mapping Entra contact phones into normalized contact phone rows.
    • ee/server/src/lib/integrations/entra/sync/contactFieldSync.ts now builds phone_numbers collections from businessPhones[] and mobilePhone instead of returning a scalar phone_number patch.
    • ee/server/src/lib/integrations/entra/sync/contactReconciler.ts now creates Entra contacts with phone_numbers and routes linked-contact phone updates through ContactModel.updateContact(...) inside the same transaction rather than trying to update child rows via the raw contacts table.
  • (2026-03-09) Completed T033 and T034 with ee/server/src/__tests__/unit/entraContactFieldSync.test.ts and ee/server/src/__tests__/unit/entraContactReconciler.test.ts.
    • The Entra field-sync tests verify mobilePhone becomes canonical mobile, businessPhones[] become canonical work, and the first business phone wins default precedence over mobile.
    • The Entra reconciler tests verify the create path passes normalized phone_numbers into ContactModel.createContact(...) and linked-contact phone sync uses ContactModel.updateContact(...) with the same normalized mapping.
  • (2026-03-09) Completed F023 by updating contact-facing seeds, factories, and E2E helpers to create normalized phone rows.
    • server/seeds/dev/05_contacts.cjs now inserts contacts without the legacy scalar field and seeds one default contact_phone_numbers row per contact.
    • server/src/test/e2e/factories/contact.factory.ts and server/src/test/e2e/utils/contactTestDataFactory.ts now create contacts through ContactModel.createContact(...), which leaves contacts.phone_number empty while writing normalized child rows.
    • server/src/test/e2e/api/contacts.e2e.test.ts and server/src/test/e2e/utils/clientTestData.ts now use the normalized phone_numbers shape instead of scalar contact phones in their generated contact payloads.
  • (2026-03-09) Completed T035 with server/src/test/integration/contactTestHelpersPhoneRows.integration.test.ts.
    • The integration suite verifies the E2E contact factory and contact test data helper leave contacts.phone_number null while creating default contact_phone_numbers rows.
    • The same suite contract-checks the dev contacts seed for normalized default phone inserts.
  • (2026-03-09) Completed F024 by removing the remaining application-level reads and writes of contacts.phone_number.
    • shared/ticketClients/contacts.ts, shared/services/emailService.ts, and shared/workflow/actions/emailWorkflowActions.ts now hydrate contact phones through ContactModel and 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, and server/src/lib/eventBus/subscribers/ticketEmailSubscriber.ts now read or display the default contact phone from contact_phone_numbers.
    • packages/clients/src/actions/contact-actions/contactActions.tsx no longer falls back to writing scalar phone_number values in the main contact create/update flows after the normalized model cutover.
  • (2026-03-09) Completed T036 with server/src/test/integration/contactPhoneColumnCutover.integration.test.ts.
    • The DB-backed suite drops contacts.phone_number against the live test schema and verifies ContactService.create(...), update(...), and getById(...) continue to work through normalized phone rows only.
  • (2026-03-09) Completed F006 with server/migrations/20260309183000_drop_contacts_phone_number_column.cjs.
    • Migration B now drops contacts.phone_number once the app no longer depends on it, and restores the column on rollback so the rollout remains reversible.
  • (2026-03-09) Completed T037 with server/src/test/integration/contactPhoneColumnCutover.integration.test.ts and server/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.