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
209 lines
20 KiB
Markdown
209 lines
20 KiB
Markdown
# Scratchpad — Inbound Email Sender Routing to Boards
|
|
|
|
- Plan slug: `inbound-email-sender-routing-to-boards`
|
|
- Created: `2026-02-25`
|
|
|
|
## 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-25) Revised plan ownership from provider-level sender-rule table to client/contact-owned routing configuration.
|
|
- (2026-02-25) Destination target stays as `inbound_ticket_defaults_id` (profile-level), not board-only override.
|
|
- (2026-02-25) Precedence for new-ticket creation: contact override -> client destination -> provider default.
|
|
- (2026-02-25) Existing-ticket reply flows remain unchanged; no rerouting for threaded replies.
|
|
- (2026-02-25) `F001` completed as a plan-model checkpoint: implementation work proceeds only with client/contact-owned routing and does not introduce provider-level sender-rule tables.
|
|
- (2026-02-25) Shared resolver `resolveEffectiveInboundTicketDefaults` is the single precedence source for contact override / contact-client default / provider fallback used by both in-app and runtime context paths.
|
|
- (2026-02-25) Domain-matched client routing now contributes to destination resolution via the same shared resolver (`client_default_from_domain` before provider fallback).
|
|
- (2026-02-25) Destination IDs from contacts/clients are runtime-validated against `inbound_ticket_defaults` with `(tenant, id, is_active=true)` before use; invalid/inactive mappings now warn and fallback.
|
|
|
|
## Discoveries / Constraints
|
|
|
|
- (2026-02-25) Existing sender-domain-to-client mapping already exists and is explicit via `client_inbound_email_domains`.
|
|
- (2026-02-25) `processInboundEmailInApp` currently sets ticket board from provider defaults (`defaults.board_id`) and does not vary by sender.
|
|
- (2026-02-25) `resolve_inbound_ticket_context` currently resolves target client/contact/location only; it does not yet compute sender-based destination defaults.
|
|
- (2026-02-25) `ClientDetails` already includes inbound-domain and default-contact controls, making client-level destination a natural extension.
|
|
- (2026-02-25) `clients` currently had no dedicated inbound destination field; persisted client destination now starts with nullable `clients.inbound_ticket_defaults_id`.
|
|
- (2026-02-25) `contacts` currently had no dedicated inbound destination override; persisted contact override now starts with nullable `contacts.inbound_ticket_defaults_id`.
|
|
- (2026-02-25) Added explicit tenant-scoped indexes for lookup safety/perf:
|
|
- `idx_clients_tenant_inbound_ticket_defaults`
|
|
- `idx_contacts_tenant_inbound_ticket_defaults`
|
|
- (2026-02-25) Local worktree currently has no installed npm dependencies (`npm ls vitest --depth=0` is empty), so targeted Vitest runs fail at startup before test execution.
|
|
|
|
## Commands / Runbooks
|
|
|
|
- (2026-02-25) Read current inbound processing and context resolution:
|
|
- `rg -n "processInboundEmailInApp|resolve_inbound_ticket_context|resolveInboundTicketDefaults" shared/services/email shared/workflow`
|
|
- (2026-02-25) Read existing domain-match plans and tests:
|
|
- `ee/docs/plans/2026-02-13-inbound-email-domain-matching-default-contact/`
|
|
- `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`
|
|
- `server/src/test/integration/resolveInboundTicketContext.domainFallback.integration.test.ts`
|
|
- (2026-02-25) Scaffold and validate plan:
|
|
- `python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/scaffold_plan.py "Inbound Email Sender Routing to Boards" --slug inbound-email-sender-routing-to-boards`
|
|
- `python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/validate_plan.py ee/docs/plans/2026-02-25-inbound-email-sender-routing-to-boards`
|
|
- (2026-02-25) Add client destination schema migration:
|
|
- `server/migrations/20260225120000_add_client_inbound_ticket_defaults_id.cjs`
|
|
- (2026-02-25) Add contact override schema migration:
|
|
- `server/migrations/20260225120500_add_contact_inbound_ticket_defaults_id.cjs`
|
|
- (2026-02-25) Add lookup index migration:
|
|
- `server/migrations/20260225121000_add_inbound_ticket_defaults_lookup_indexes.cjs`
|
|
- (2026-02-25) Targeted unit tests attempted for shared/runtime email routing changes:
|
|
- `npx vitest run shared/services/email/__tests__/processInboundEmailInApp.test.ts shared/services/email/__tests__/processInboundEmailInApp.additionalPaths.test.ts shared/workflow/runtime/actions/__tests__/registerEmailWorkflowActions.contactAuthorship.test.ts`
|
|
- Blocker in local env: Vitest startup fails because `dotenv` package is missing from active node_modules resolution path.
|
|
- (2026-02-25) Targeted migration integration test attempted:
|
|
- `cd server && npx vitest run src/test/integration/inboundTicketDestinationMigrations.integration.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `vitest` package is not installed in this worktree.
|
|
- (2026-02-25) Targeted in-app routing integration test attempted:
|
|
- `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `vitest` package is not installed in this worktree.
|
|
- (2026-02-25) Targeted workflow destination routing integration test attempted:
|
|
- `cd server && npx vitest run src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `vitest` package is not installed in this worktree.
|
|
- (2026-02-25) Targeted resolver unit test attempted:
|
|
- `npx vitest run shared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `dotenv` package is missing from active root node_modules resolution path.
|
|
- (2026-02-25) Targeted clients actions unit test attempted:
|
|
- `npx vitest run packages/clients/src/actions/inboundTicketDestinationActions.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `dotenv` package is missing from active root node_modules resolution path.
|
|
- (2026-02-25) Targeted ClientDetails inbound destination wiring test attempted:
|
|
- `npx vitest run packages/clients/src/components/clients/ClientDetails.inboundDestination.wiring.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `dotenv` package is missing from active root node_modules resolution path.
|
|
- (2026-02-25) Targeted ContactDetails inbound destination wiring test attempted:
|
|
- `npx vitest run packages/clients/src/components/contacts/ContactDetails.inboundDestination.wiring.test.ts --reporter=dot`
|
|
- Blocker in local env: Vitest startup fails because `dotenv` package is missing from active root node_modules resolution path.
|
|
|
|
## Links / References
|
|
|
|
- In-app inbound pipeline:
|
|
- `shared/services/email/processInboundEmailInApp.ts`
|
|
- Workflow action registration and context resolution:
|
|
- `shared/workflow/runtime/actions/registerEmailWorkflowActions.ts`
|
|
- Shared email workflow actions:
|
|
- `shared/workflow/actions/emailWorkflowActions.ts`
|
|
- Existing client inbound domain actions:
|
|
- `packages/clients/src/actions/clientInboundEmailDomainActions.ts`
|
|
- New inbound destination actions:
|
|
- `packages/clients/src/actions/inboundTicketDestinationActions.ts`
|
|
- Client details UI:
|
|
- `packages/clients/src/components/clients/ClientDetails.tsx`
|
|
- Contact UI surfaces:
|
|
- `packages/clients/src/components/contacts/*`
|
|
|
|
## Open Questions
|
|
|
|
- Should client/contact destination be global, or optionally provider-specific for tenants with multiple inbound mailboxes?
|
|
- Should UI block selecting inactive defaults profiles, or allow save and rely on runtime fallback?
|
|
- Should contact override be shown only when contact has a valid email value?
|
|
|
|
## Implementation Log
|
|
|
|
- (2026-02-25) Marked `F001` implemented in `features.json` and committed as the first checklist checkpoint.
|
|
- (2026-02-25) Completed `F002` by adding migration `20260225120000_add_client_inbound_ticket_defaults_id.cjs` to persist `clients.inbound_ticket_defaults_id`.
|
|
- (2026-02-25) Completed `F003` by adding migration `20260225120500_add_contact_inbound_ticket_defaults_id.cjs` to persist `contacts.inbound_ticket_defaults_id`.
|
|
- (2026-02-25) Completed `F004` by adding migration `20260225121000_add_inbound_ticket_defaults_lookup_indexes.cjs` with safe create/drop index behavior.
|
|
- (2026-02-25) Completed `F005` by adding shared resolver logic in `shared/workflow/actions/emailWorkflowActions.ts` and wiring it into:
|
|
- `shared/services/email/processInboundEmailInApp.ts`
|
|
- `shared/workflow/runtime/actions/registerEmailWorkflowActions.ts`
|
|
- (2026-02-25) Completed `F006` by passing domain-matched client context into the shared resolver in both in-app and workflow-runtime paths.
|
|
- (2026-02-25) Completed `F007` by enforcing tenant+active validation on contact/client destination IDs before applying defaults.
|
|
- (2026-02-25) Completed `F008` by using shared destination resolution in `processInboundEmailInApp` before new-ticket create, while preserving existing reply-token/thread branches.
|
|
- (2026-02-25) Completed `F009` by using the same shared destination resolver in `resolve_inbound_ticket_context` (runtime v2 action registry) for parity with in-app processing.
|
|
- (2026-02-25) Completed `F010` by keeping reply-token/thread-header branches untouched; only new-ticket destination selection path was changed.
|
|
- (2026-02-25) Completed `F011` by retaining existing sender identity matching semantics:
|
|
- exact contact remains the only source for comment authorship
|
|
- domain matching remains explicit via `client_inbound_email_domains`
|
|
- (2026-02-25) Completed `F012` by adding server actions for inbound destination option reads and client destination updates with explicit client permission checks.
|
|
- (2026-02-25) Completed `F013` by adding contact destination update action with explicit contact permission checks and tenant-scoped destination validation.
|
|
- (2026-02-25) Completed `F014` by adding an inbound ticket destination selector to `ClientDetails` with clear-to-provider-default behavior.
|
|
- (2026-02-25) Completed `F015` by adding optional contact override selectors to:
|
|
- `packages/clients/src/components/contacts/ContactDetails.tsx`
|
|
- `packages/clients/src/components/contacts/ContactDetailsEdit.tsx`
|
|
- (2026-02-25) Completed `F016` by adding explicit precedence helper text in both client and contact destination controls:
|
|
- Contact override -> Client destination -> Provider default
|
|
- (2026-02-25) Completed `F017` by emitting structured resolution logs:
|
|
- destination source (`contact_override`, `client_default_from_contact`, `client_default_from_domain`, `provider_default`)
|
|
- fallback warnings with configured invalid destination IDs and fallback reason
|
|
- (2026-02-25) Completed `F018` by adding in-app integration cases in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts` for:
|
|
- exact contact override destination routing
|
|
- exact contact using client destination
|
|
- domain-matched client destination
|
|
- unmatched sender using provider destination
|
|
- (2026-02-25) Completed `F019` by adding workflow-runtime integration coverage in:
|
|
- `server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts`
|
|
- includes direct parity assertion against `processInboundEmailInApp` for identical sender/provider input
|
|
- (2026-02-25) Completed `F020` by extending regression coverage:
|
|
- explicit board-unchanged assertions on reply-token and thread-header reply flows
|
|
- existing idempotency tests for duplicate reply/new-email processing remain in place and continue to validate dedupe behavior
|
|
- (2026-02-25) Completed `T001` by adding migration integration coverage in `server/src/test/integration/inboundTicketDestinationMigrations.integration.test.ts` to assert `clients.inbound_ticket_defaults_id` exists, is nullable, and is UUID-typed.
|
|
- (2026-02-25) Completed `T002` by extending the same migration integration suite to assert `contacts.inbound_ticket_defaults_id` exists, is nullable, and is UUID-typed.
|
|
- (2026-02-25) Completed `T003` by adding index coverage in the same migration integration suite, asserting:
|
|
- `idx_clients_tenant_inbound_ticket_defaults` on `clients(tenant, inbound_ticket_defaults_id)`
|
|
- `idx_contacts_tenant_inbound_ticket_defaults` on `contacts(tenant, inbound_ticket_defaults_id)`
|
|
- (2026-02-25) Completed `T004` by adding rollback-path coverage in the same migration integration suite:
|
|
- executes migration `down` functions inside a transaction and asserts both columns/indexes are removed
|
|
- executes migration `up` functions inside the same transaction and asserts both columns/indexes are restored
|
|
- rolls back transaction to avoid mutating shared integration DB state
|
|
- (2026-02-25) Completed `T005` by adding shared resolver unit coverage in `shared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.ts` for precedence path:
|
|
- exact sender contact + contact override resolves `source=contact_override`
|
|
- returns contact override defaults and does not consult client defaults
|
|
- (2026-02-25) Completed `T006` by extending shared resolver unit coverage for precedence path:
|
|
- exact sender contact with no override resolves `source=client_default_from_contact`
|
|
- uses the matched contact's client `inbound_ticket_defaults_id`
|
|
- (2026-02-25) Completed `T007` by extending shared resolver unit coverage for domain path:
|
|
- when no exact contact is provided and domain client is matched, resolves `source=client_default_from_domain`
|
|
- returns domain client's active defaults
|
|
- (2026-02-25) Completed `T008` by extending shared resolver unit coverage for fallback path:
|
|
- when neither exact-contact nor domain destination applies, resolves `source=provider_default`
|
|
- returns provider defaults unchanged with no fallback reason
|
|
- (2026-02-25) Completed `T009` by extending shared resolver unit coverage for invalid override safety:
|
|
- invalid/inactive contact override destination falls back to `source=provider_default`
|
|
- emits `fallbackReason=invalid_or_inactive_contact_override`
|
|
- (2026-02-25) Completed `T010` by extending shared resolver unit coverage for invalid client destination safety:
|
|
- invalid/inactive client destination (from exact-contact client path) falls back to `source=provider_default`
|
|
- emits `fallbackReason=invalid_or_inactive_client_default_from_contact`
|
|
- (2026-02-25) Completed `T011` by confirming in-app integration coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Routing destination: exact sender contact override uses contact override defaults board` asserts board/client/contact routing from contact override defaults.
|
|
- (2026-02-25) Completed `T012` by confirming in-app integration coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Routing destination: exact sender without contact override uses contact's client destination defaults` asserts exact-contact fallback to client destination defaults.
|
|
- (2026-02-25) Completed `T013` by confirming in-app integration coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Routing destination: unknown sender + domain-matched client uses domain client destination defaults` asserts domain-matched unknown-contact routing.
|
|
- (2026-02-25) Completed `T014` by confirming in-app integration coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Unmatched sender: system follows the defined behavior without throwing` asserts provider-default board/client fallback for unmatched routing.
|
|
- (2026-02-25) Completed `T015` by confirming workflow integration coverage exists in `server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts`:
|
|
- test case `returns contact override destination outcome for exact sender` asserts `resolve_inbound_ticket_context` returns contact-override destination.
|
|
- (2026-02-25) Completed `T016` by confirming workflow integration coverage exists in `server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts`:
|
|
- test case `returns client's destination outcome when exact sender has no contact override` asserts workflow action returns client-default destination.
|
|
- (2026-02-25) Completed `T017` by confirming workflow integration coverage exists in `server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts`:
|
|
- test case `returns domain-matched client destination outcome when sender is unknown contact` asserts domain-matched destination resolution.
|
|
- (2026-02-25) Completed `T018` by confirming workflow/in-app parity coverage exists in `server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts`:
|
|
- test case `matches in-app destination selection for the same sender/provider input` compares runtime action output with `processInboundEmailInApp` ticket results.
|
|
- (2026-02-25) Completed `T019` by confirming reply-token regression coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Reply threading: reply token resolves ticket and creates exactly 1 new comment` now asserts ticket board remains unchanged after reply threading.
|
|
- (2026-02-25) Completed `T020` by confirming thread-header regression coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Reply threading: thread headers resolve ticket and create exactly 1 new comment` now asserts ticket board remains unchanged after thread-header matching.
|
|
- (2026-02-25) Completed `T021` by confirming exact-contact matching regression coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Contact match: sender email is normalized from display-name format` asserts contact/client IDs still resolve from exact sender match semantics.
|
|
- (2026-02-25) Completed `T022` by confirming explicit-domain matching regression coverage exists in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- test case `Domain fallback: does not match by domain unless the domain is explicitly configured` asserts no inferred domain ownership from contact records.
|
|
- (2026-02-25) Completed `T023` by adding `packages/clients/src/actions/inboundTicketDestinationActions.test.ts` coverage for client destination actions:
|
|
- rejects updates without `client:update` permission
|
|
- rejects cross-tenant destination IDs via tenant-scoped `inbound_ticket_defaults` validation
|
|
- (2026-02-25) Completed `T024` by extending `packages/clients/src/actions/inboundTicketDestinationActions.test.ts` coverage for contact destination actions:
|
|
- rejects updates without `contact:update` permission
|
|
- rejects cross-tenant destination IDs via tenant-scoped `inbound_ticket_defaults` validation
|
|
- (2026-02-25) Completed `T025` by adding `packages/clients/src/components/clients/ClientDetails.inboundDestination.wiring.test.ts`:
|
|
- verifies client UI destination select wiring (`value`, `allowClear`, `onValueChange`)
|
|
- verifies persistence path calls `updateClient(...)` and that server-side client update normalizes cleared empty strings to `null`
|
|
- (2026-02-25) Completed `T026` by:
|
|
- extending `packages/clients/src/actions/contact-actions/contactActions.tsx` to persist `inbound_ticket_defaults_id` updates, normalize clears to `null`, and tenant-validate destination IDs
|
|
- adding `packages/clients/src/components/contacts/ContactDetails.inboundDestination.wiring.test.ts` to verify contact UI set/clear wiring and save-path persistence through `updateContact(...)`
|
|
- (2026-02-25) Completed `T027` by extending `shared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.ts`:
|
|
- asserts `console.warn` metadata includes `source=contact_override` and `fallback=provider_default` for invalid override fallbacks
|
|
- asserts terminal `console.debug` metadata includes `source=provider_default` with `fallbackReason=invalid_or_inactive_contact_override`
|
|
- (2026-02-25) Completed `T028` by extending `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
|
|
- added routed idempotency case `Idempotency: replay same routed contact-override email does not create duplicate routed tickets`
|
|
- asserts first run creates routed ticket on override board and second run dedupes, preserving single ticket/comment under sender-based routing
|