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
7.2 KiB
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 optionalprimary_contact_name) to avoid a DB migration for this phase. - (2026-02-13) Superseded: Domain-to-client matching derived from scanning
contacts.emaildomains. - (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.tsand:- 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_idand leavescontact_idnull.
- Finds a contact strictly by exact normalized email (
- (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_idas ticket contact when available. - Keep comment author attribution as
systemunless 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_idto 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.tsincludesproperties.primary_contact_idandproperties.primary_contact_name. - Client-side validation:
packages/clients/src/schemas/client.schema.tsincludesprimary_contact_idandprimary_contact_name. - Server API schema:
server/src/lib/api/schemas/client.tsallowsproperties.primary_contact_id(uuid) +primary_contact_name. - Client update persistence merges
propertiesintoclients.properties:packages/clients/src/actions/clientActions.ts.
- Types:
- (2026-02-13) Added shared helper
extractEmailDomain()inshared/lib/email/addressUtils.tswhich normalizes vianormalizeEmailAddress()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)inshared/workflow/actions/emailWorkflowActions.ts:- Looks up
client_inbound_email_domainsbylower(domain)in the tenant. - Returns a client_id only when a mapping exists; otherwise returns null.
- Looks up
- (2026-02-13) Added migration creating
client_inbound_email_domainswith 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)inshared/workflow/actions/emailWorkflowActions.tsto safely applyclients.properties.primary_contact_idonly when it references an active contact belonging to the client. - (2026-02-13) The workflow runtime action
resolve_inbound_ticket_contextinshared/workflow/runtime/actions/registerEmailWorkflowActions.tscontains 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.
targetLocationIdis 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 inpackages/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.tsxpersistsproperties.primary_contact_idandproperties.primary_contact_name. - (2026-02-13) Client default contact picker supports clearing by selecting "None" (empties both
primary_contact_idandprimary_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.tscoversextractEmailDomain()normalization + domain extraction cases.shared/workflow/actions/emailWorkflowActions.inboundDomainLookup.test.tscoversfindClientIdByInboundEmailDomain()lookup + normalization behavior.
Links / References
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.comvsacme.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?