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

7.2 KiB

Scratchpad — Inbound Email Domain Matching + Client Default Contact

  • Plan slug: inbound-email-domain-matching-default-contact
  • Created: 2026-02-13

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-02-13) Store the “client default contact” using existing clients.properties.primary_contact_id (and optional primary_contact_name) to avoid a DB migration for this phase.
  • (2026-02-13) Superseded: Domain-to-client matching derived from scanning contacts.email domains.
  • (2026-02-13) New requirement: Admin must explicitly configure inbound email domains per client; enforce uniqueness per tenant; do not attempt domain matching unless the domain is configured.

Discoveries / Constraints

  • (2026-02-13) In-app inbound email new-ticket creation happens in shared/services/email/processInboundEmailInApp.ts and:
    • Finds a contact strictly by exact normalized email (findContactByEmail).
    • If no exact contact match, only attempts domain matching via explicit domain mappings (client_inbound_email_domains).
    • If no explicit mapping exists for the sender domain, falls back to inbound ticket defaults client_id and leaves contact_id null.
  • (2026-02-13) Updated in-app inbound email new-ticket creation (shared/services/email/processInboundEmailInApp.ts) to:
    • Prefer exact contact match (existing behavior).
    • Else attempt unique client match by sender domain, and apply the client's validated properties.primary_contact_id as ticket contact when available.
    • Keep comment author attribution as system unless there is an exact sender email contact match (avoids implying the default contact authored the email).
    • When the resolved client differs from the inbound defaults client, do not apply defaults.location_id to avoid cross-client location mismatch.
  • (2026-02-13) Client "default contact" storage is already supported end-to-end via client properties:
    • Types: packages/types/src/interfaces/client.interfaces.ts includes properties.primary_contact_id and properties.primary_contact_name.
    • Client-side validation: packages/clients/src/schemas/client.schema.ts includes primary_contact_id and primary_contact_name.
    • Server API schema: server/src/lib/api/schemas/client.ts allows properties.primary_contact_id (uuid) + primary_contact_name.
    • Client update persistence merges properties into clients.properties: packages/clients/src/actions/clientActions.ts.
  • (2026-02-13) Added shared helper extractEmailDomain() in shared/lib/email/addressUtils.ts which normalizes via normalizeEmailAddress() then returns substring after @ (lowercased).
  • (2026-02-13) Superseded implementation: contact-derived domain matching (findUniqueClientIdByContactEmailDomain) was removed in favor of explicit domain mappings.
  • (2026-02-13) Implemented findClientIdByInboundEmailDomain(domain, tenant) in shared/workflow/actions/emailWorkflowActions.ts:
    • Looks up client_inbound_email_domains by lower(domain) in the tenant.
    • Returns a client_id only when a mapping exists; otherwise returns null.
  • (2026-02-13) Added migration creating client_inbound_email_domains with unique (tenant, lower(domain)): server/migrations/20260213180500_create_client_inbound_email_domains.cjs.
  • (2026-02-13) Added server actions for managing inbound email domains:
    • packages/clients/src/actions/clientInboundEmailDomainActions.ts
  • (2026-02-13) Implemented findValidClientPrimaryContactId(clientId, tenant) in shared/workflow/actions/emailWorkflowActions.ts to safely apply clients.properties.primary_contact_id only when it references an active contact belonging to the client.
  • (2026-02-13) The workflow runtime action resolve_inbound_ticket_context in shared/workflow/runtime/actions/registerEmailWorkflowActions.ts contains similar “find contact by email, else defaults” logic and should be kept in parity if workflows still invoke it.
  • (2026-02-13) Updated shared/workflow/runtime/actions/registerEmailWorkflowActions.ts (resolve_inbound_ticket_context) to match in-app resolution using explicit domain mappings:
    • Exact contact match first.
    • Else configured domain-to-client match and optional validated client default contact.
    • targetLocationId is null when resolved client differs from defaults client.
  • (2026-02-13) Added inbound-email integration coverage for domain fallback in server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:
    • Configured domain match sets ticket client_id.
    • Default contact applied when configured on client properties.
    • No explicit mapping => no domain match (falls back to defaults), even if contacts exist with that domain.
    • Location_id cleared when resolved client differs from defaults.
  • (2026-02-13) There is existing “billing contact” (clients.billing_contact_id) UI in packages/clients/src/components/clients/BillingConfigForm.tsx, but it is billing-specific and not suitable as the inbound-email default contact.
  • (2026-02-13) Added "Default contact" picker to the client details screen: packages/clients/src/components/clients/ClientDetails.tsx persists properties.primary_contact_id and properties.primary_contact_name.
  • (2026-02-13) Client default contact picker supports clearing by selecting "None" (empties both primary_contact_id and primary_contact_name).
  • (2026-02-13) Added Playwright coverage for default contact persistence: ee/server/src/__tests__/integration/client-default-contact.playwright.test.ts.
  • (2026-02-13) Added Playwright coverage for clearing the default contact: ee/server/src/__tests__/integration/client-default-contact.playwright.test.ts.

Commands / Runbooks

  • (2026-02-13) Code search:
    • rg -n "resolve_inbound_ticket_context|processInboundEmailInApp|findContactByEmail" -S
  • (2026-02-13) Unit tests:
    • shared/lib/email/addressUtils.test.ts covers extractEmailDomain() normalization + domain extraction cases.
    • shared/workflow/actions/emailWorkflowActions.inboundDomainLookup.test.ts covers findClientIdByInboundEmailDomain() lookup + normalization behavior.
  • shared/services/email/processInboundEmailInApp.ts (new-ticket path)
  • shared/workflow/actions/emailWorkflowActions.ts (findContactByEmail)
  • shared/workflow/runtime/actions/registerEmailWorkflowActions.ts (resolve_inbound_ticket_context)
  • packages/clients/src/components/clients/ClientDetails.tsx (client screen)
  • packages/types/src/interfaces/client.interfaces.ts (IClient.properties.primary_contact_id)
  • server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts (inbound email integration coverage)
  • server/src/test/integration/resolveInboundTicketContext.domainFallback.integration.test.ts (workflow action parity coverage)

Open Questions

  • Exact-only domain matching vs subdomain handling (it.acme.com vs acme.com).
  • Should comment author attribution change when ticket contact is set via default contact (domain fallback)?
  • Do we want an admin helper that suggests domains based on contacts, while still requiring explicit confirmation?