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
182 lines
7.8 KiB
Markdown
182 lines
7.8 KiB
Markdown
# PRD — Inbound Email Sender Routing to Boards
|
|
|
|
- Slug: `inbound-email-sender-routing-to-boards`
|
|
- Date: `2026-02-25`
|
|
- Status: Draft
|
|
|
|
## Summary
|
|
|
|
Revise routing ownership to live on clients and contacts instead of provider-level sender-rule tables.
|
|
|
|
The system should resolve an effective inbound destination from:
|
|
- contact override (exact sender match),
|
|
- then client default destination (exact contact client or domain-matched client),
|
|
- then provider default inbound settings.
|
|
|
|
Destination should resolve to an Inbound Ticket Defaults profile (not just `board_id`) so board/status/priority/category/client/location remain coherent.
|
|
|
|
## Problem
|
|
|
|
Today, new inbound-email tickets always use one provider-level defaults profile and one board.
|
|
|
|
Current sender matching logic only influences `client_id`/`contact_id`:
|
|
- exact contact email match,
|
|
- explicit client domain mapping (`client_inbound_email_domains`).
|
|
|
|
Because board/defaults are not resolved from client/contact ownership, tickets from known client domains still land on a generic board and require manual rerouting.
|
|
|
|
## Goals
|
|
|
|
1. Let each client define a default inbound destination profile.
|
|
2. Let each contact optionally define a destination override that takes precedence over client default.
|
|
3. Reuse existing client domain mapping so unknown contacts from a known client domain route to that client's destination.
|
|
4. Preserve deterministic precedence and safe fallback to provider defaults.
|
|
5. Keep existing reply-thread behavior unchanged.
|
|
|
|
## Non-goals
|
|
|
|
1. Provider-level sender-rule table UI in this phase.
|
|
2. Wildcard/regex sender matching.
|
|
3. Routing existing ticket replies to other boards.
|
|
4. Auto-provisioning client/contact routing from historical mail.
|
|
|
|
## Users and Primary Flows
|
|
|
|
### Personas
|
|
- Admin/dispatcher configuring client and contact behavior.
|
|
- Helpdesk operators expecting inbound tickets to land on the correct board.
|
|
|
|
### Flow A — Exact contact with contact override
|
|
1. New email arrives from `invoices@customer.com`.
|
|
2. Sender matches a contact by exact email.
|
|
3. Contact has `inbound_ticket_defaults_id` override.
|
|
4. Ticket uses contact override defaults profile.
|
|
|
|
### Flow B — Exact contact without override
|
|
1. New email arrives from `tech@customer.com`.
|
|
2. Sender matches a contact by exact email.
|
|
3. Contact has no override; contact's client has default destination.
|
|
4. Ticket uses client default destination profile.
|
|
|
|
### Flow C — Domain-matched client (unknown contact)
|
|
1. New email sender is not an exact contact.
|
|
2. Domain matches explicit `client_inbound_email_domains` record.
|
|
3. Matched client has default destination.
|
|
4. Ticket uses client default destination profile.
|
|
|
|
### Flow D — No contact/domain client routing match
|
|
1. New email arrives from unconfigured sender/domain.
|
|
2. Ticket uses provider default inbound settings.
|
|
|
|
### Flow E — Existing ticket reply
|
|
1. Email is matched by reply token/thread headers.
|
|
2. System appends comment to existing ticket.
|
|
3. No client/contact routing for board/defaults is evaluated.
|
|
|
|
## UX / UI Notes
|
|
|
|
### Client screen (`ClientDetails`)
|
|
- Keep existing Inbound email domains management.
|
|
- Add Inbound ticket destination picker for Inbound Ticket Defaults.
|
|
- Helper text: "Used for inbound senders that map to this client and have no contact override."
|
|
|
|
### Contact screen
|
|
- Add optional Inbound ticket destination override picker.
|
|
- Helper text: "If set, this overrides the client destination for this exact sender contact."
|
|
|
|
### Resolution help text
|
|
- Display precedence in both screens:
|
|
- Contact override -> Client destination -> Provider default.
|
|
|
|
## Requirements
|
|
|
|
### Functional Requirements
|
|
|
|
1. Add client-level inbound destination setting:
|
|
- `clients.inbound_ticket_defaults_id` (nullable UUID), or equivalent persisted client property.
|
|
2. Add contact-level destination override:
|
|
- `contacts.inbound_ticket_defaults_id` (nullable UUID), or equivalent persisted contact property.
|
|
3. Resolution for new-ticket creation must use this precedence:
|
|
- exact contact sender + contact override,
|
|
- else exact contact sender + contact's client destination,
|
|
- else domain-matched client destination,
|
|
- else provider default inbound settings.
|
|
4. Existing ticket reply/comment paths remain unchanged and do not re-evaluate destination.
|
|
5. Existing exact-contact and domain-to-client matching behavior remains intact.
|
|
6. Validate resolved destination belongs to tenant and is active; otherwise fallback to provider default.
|
|
7. Add server actions/API updates to read/write client/contact destination settings with existing permissions.
|
|
8. Update client and contact UI surfaces to set/clear these values.
|
|
9. When no client/contact destination is configured, behavior remains identical to current provider-default flow.
|
|
|
|
### Non-functional Requirements
|
|
|
|
1. No degradation of inbound processing throughput.
|
|
2. No cross-tenant routing leakage.
|
|
3. No dedupe/idempotency regression.
|
|
|
|
## Data / API / Integrations
|
|
|
|
### Approach Options
|
|
1. Provider-level sender rule table (previous draft).
|
|
2. Client/contact-owned destination config (recommended).
|
|
3. Board-only fields on client/contact.
|
|
|
|
Recommendation rationale:
|
|
- Client and contact screens are the natural ownership point.
|
|
- Existing domain mapping already maps senders to client identity.
|
|
- Using inbound defaults profile IDs avoids board-only mismatches.
|
|
|
|
### Proposed Data Model
|
|
- Reuse: `client_inbound_email_domains` (already present).
|
|
- Add:
|
|
- `clients.inbound_ticket_defaults_id` nullable.
|
|
- `contacts.inbound_ticket_defaults_id` nullable.
|
|
- Add indexes for read path:
|
|
- `(tenant, inbound_ticket_defaults_id)` on clients.
|
|
- `(tenant, inbound_ticket_defaults_id)` on contacts.
|
|
|
|
### Resolution Integration Points
|
|
- `shared/services/email/processInboundEmailInApp.ts`
|
|
- resolve effective defaults before ticket creation.
|
|
- `shared/workflow/runtime/actions/registerEmailWorkflowActions.ts`
|
|
- extend `resolve_inbound_ticket_context` to return effective defaults using same precedence.
|
|
- `shared/workflow/actions/emailWorkflowActions.ts`
|
|
- add shared resolver helper(s) used by both paths.
|
|
- UI/actions:
|
|
- `packages/clients/src/components/clients/ClientDetails.tsx`
|
|
- contact edit/create surfaces in `packages/clients/src/components/contacts/*`
|
|
- related actions in `packages/clients/src/actions/*`
|
|
|
|
## Security / Permissions
|
|
|
|
1. Client destination updates require existing client update permission.
|
|
2. Contact override updates require existing contact update permission.
|
|
3. Resolver validates destination defaults are in the same tenant.
|
|
|
|
## Observability
|
|
|
|
1. Add structured debug logs for destination resolution source:
|
|
- `contact_override`, `client_default_from_contact`, `client_default_from_domain`, `provider_default`.
|
|
2. Warn when configured client/contact destination is invalid or inactive and fallback is applied.
|
|
|
|
## Rollout / Migration
|
|
|
|
1. Add migration(s) for client/contact destination fields (or equivalent persisted schema).
|
|
2. No backfill required.
|
|
3. Tenants without new config continue on provider defaults unchanged.
|
|
|
|
## Open Questions
|
|
|
|
1. Should client/contact destination be global, or optionally provider-specific when a tenant has multiple inbound mailboxes?
|
|
2. Should we block saving client/contact destination when target defaults are inactive, or allow and fallback at runtime?
|
|
3. Should contact override be shown only for contacts with valid email addresses?
|
|
|
|
## Acceptance Criteria (Definition of Done)
|
|
|
|
1. Exact-sender contact with contact override creates new ticket using contact destination defaults.
|
|
2. Exact-sender contact without override uses client destination defaults.
|
|
3. Unknown sender with domain-matched client uses client destination defaults.
|
|
4. Unmatched sender uses provider default inbound settings.
|
|
5. Existing-ticket reply flows are unchanged.
|
|
6. In-app inbound and workflow-runtime paths produce identical destination resolution for the same sender/provider input.
|