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

264 lines
13 KiB
Markdown

# Scratchpad — Ticket Response Source
## Request
User asks for a new feature to show how a ticket response was received:
- Client Portal
- Inbound Email processing system
Targeted surfaces:
- `packages/tickets/src/components/ticket/TicketDetails.tsx`
- `packages/client-portal/src/components/tickets/TicketDetails.tsx`
## Discoveries
- Inbound email processing currently creates comments through:
- `shared/services/email/processInboundEmailInApp.ts`
- `shared/workflow/actions/emailWorkflowActions.ts#createCommentFromEmail`
- Inbound email comments already include rich `metadata.email` content and are authored as client/contact style comments.
- Client portal comments are inserted directly in:
- `packages/client-portal/src/actions/client-portal-actions/client-tickets.ts#addClientTicketComment`
- These currently do not set `metadata.responseSource`.
- `comments.metadata` JSONB exists (migration `20250917150000_add_metadata_to_comments.cjs`), so MVP can avoid schema migration.
- `IComment` type currently does not expose `metadata`, which makes source-based UI logic awkward/unsafe.
## Proposed MVP Strategy
1. Source tagging:
- Write `metadata.responseSource=client_portal` for client portal comments.
- Write `metadata.responseSource=inbound_email` for inbound email comments (in shared email comment creation path).
2. Source derivation:
- Add shared utility to compute latest customer response source from conversation comments.
- Prefer explicit metadata; fallback heuristics for older records.
3. UI:
- Show source indicator in both ticket details surfaces near response-state context.
## Why This Approach
- No DB migration required.
- Works for Google, Microsoft, and IMAP because it centralizes inbound tagging at shared email comment creation.
- Backward compatible with legacy comments through heuristics.
## Risks / Gaps
- Historical records may be ambiguous if metadata is missing.
- Provider-specific labels depend on consistent provider metadata presence.
## Open Decisions To Confirm
1. Show source only when `response_state=awaiting_internal`, or whenever latest customer response source is known?
2. Generic inbound label vs provider-specific label in UI.
3. ~~Ticket-level indicator only vs ticket-level + per-comment badges.~~ **Resolved:** per-comment badges only (no ticket-level indicator).
4. Do we need backfill for older comments in this phase?
## Implementation Log
### F001 — Canonical response source values
- Added canonical constants/types in `packages/types/src/interfaces/comment.interface.ts`:
- `COMMENT_RESPONSE_SOURCES.CLIENT_PORTAL = "client_portal"`
- `COMMENT_RESPONSE_SOURCES.INBOUND_EMAIL = "inbound_email"`
- `CommentResponseSource` union derived from those constants.
- Rationale: single source of truth for source values shared by UI/action logic and tests.
### F002 — Shared comment metadata typing
- Extended `packages/types/src/interfaces/comment.interface.ts`:
- Added `InboundEmailProviderType = "google" | "microsoft" | "imap"`.
- Added `CommentMetadataEmail` and `CommentMetadata` with safe loose typing.
- Added `IComment.metadata?: CommentMetadata | null`.
- Added optional normalized `IComment.response_source?: CommentResponseSource`.
- Rationale: unblock UI/action logic from using `any` for source resolution while staying backward-compatible with existing metadata shapes.
### F003 — Client portal writes response source
- Updated `packages/client-portal/src/actions/client-portal-actions/client-tickets.ts#addClientTicketComment` to persist:
- `metadata.responseSource = "client_portal"` on inserted comments.
- Implementation uses canonical constants from `@alga-psa/types` (`COMMENT_RESPONSE_SOURCES.CLIENT_PORTAL`) to avoid string drift.
### F004 — Inbound email writes response source
- Added shared metadata normalizer in `shared/workflow/actions/emailWorkflowActions.ts`:
- `buildInboundEmailCommentMetadata(...)`
- `normalizeInboundEmailProvider(...)`
- `createCommentFromEmail` now always persists `metadata.responseSource = "inbound_email"` via the shared metadata builder before calling `TicketModel.createComment`.
- Rationale: centralize inbound source tagging in one path used by Google/Microsoft/IMAP ingestion to avoid per-caller drift.
### F005 — Inbound metadata carries provider type
- `buildInboundEmailCommentMetadata` now normalizes provider type to `google|microsoft|imap` when present.
- Persisted provider detail is written on `metadata.email.provider` and `metadata.email.providerType`.
- Updated `shared/services/email/processInboundEmailInApp.ts` to pass `metadata.email.provider = emailData.provider` in all inbound comment creation branches.
### F006 — Shared source derivation utility
- Added `packages/tickets/src/lib/responseSource.ts` with:
- `getCommentResponseSource(comment)`
- `getLatestCustomerResponseSource(conversations)`
- Exported utility from `packages/tickets/src/lib/index.ts` for use in both MSP and client-portal ticket detail surfaces.
### F007 — Internal exclusion + explicit precedence
- `getLatestCustomerResponseSource` now only evaluates customer-visible comments (`!is_internal`, `author_type in {client,contact}`).
- `getCommentResponseSource` resolves explicit metadata first:
- `metadata.responseSource`
- `response_source` fallback field
- Heuristics are only applied if explicit source is absent.
### F008 — Legacy inbound fallback
- Added fallback in `getCommentResponseSource`:
- if `comment.metadata.email` exists and explicit source is absent, infer `inbound_email`.
- This supports historical comments that predate `metadata.responseSource`.
### F009 — Legacy client-portal fallback
- Added fallback in `getCommentResponseSource`:
- if explicit source is absent and comment is `author_type=client` with `user_id`, infer `client_portal`.
- This keeps older client-authored comments source-identifiable without backfill migration.
### F010 / F011 — Per-comment response source badge
- Moved from ticket-level indicator to per-comment badge.
- Reusable UI component `packages/tickets/src/components/ResponseSourceBadge.tsx` is unchanged.
- Updated `packages/tickets/src/components/ticket/CommentItem.tsx`:
- derives source per-comment with `getCommentResponseSource(conversation)`.
- renders `ResponseSourceBadge` inline with internal/resolution badges next to author name.
- Removed ticket-level badge from both:
- `packages/tickets/src/components/ticket/TicketDetails.tsx` (MSP)
- `packages/client-portal/src/components/tickets/TicketDetails.tsx` (client portal)
- Since CommentItem is shared, badge automatically appears in both MSP and client portal views.
### F012 — i18n labels
- Added English locale keys:
- `server/public/locales/en/clientPortal.json``tickets.responseSource.clientPortal|inboundEmail`
- `server/public/locales/en/common.json``tickets.responseSource.clientPortal|inboundEmail`
- Both ticket detail screens now resolve labels through i18n keys with safe English fallbacks.
### F013 — Hide when unresolved
- Both TicketDetails screens render the response source indicator conditionally:
- only when `getLatestCustomerResponseSource(...)` returns a non-null source.
- No placeholder/error UI is shown when source cannot be determined.
### F014 — Schema-light implementation
- Confirmed implementation uses existing `comments.metadata` JSONB only.
- No migration files were added/modified for this workstream.
### F015 — Shared inbound path coverage
- Google/Microsoft/IMAP inbound flows all route through `createCommentFromEmail` in:
- `shared/services/email/processInboundEmailInApp.ts`
- Since source/provider tagging is centralized in `shared/workflow/actions/emailWorkflowActions.ts#createCommentFromEmail`, all three providers now share the same metadata behavior.
### F016 — Response-state behavior remains unchanged
- This implementation only adds metadata writes and UI read/display logic.
- No updates were made to response-state transition logic (`awaiting_client` / `awaiting_internal`) or ticket state machine code.
## Test Log
### T001 — Client portal metadata write
- Added test: `packages/client-portal/src/actions/client-portal-actions/client-tickets.responseSource.test.ts`
- Asserts `addClientTicketComment` inserts `metadata.responseSource = "client_portal"`.
- Also introduced `server/vitest.config.ts` alias for `@alga-psa/analytics` to support importing client-portal action modules in Vitest.
- Validation command:
- `npx vitest run packages/client-portal/src/actions/client-portal-actions/client-tickets.responseSource.test.ts shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts packages/tickets/src/lib/__tests__/responseSource.test.ts packages/tickets/src/components/ResponseSourceBadge.render.test.tsx packages/tickets/src/lib/__tests__/responseSourceLocales.test.ts packages/types/src/interfaces/comment.interface.typecheck.test.ts --config server/vitest.config.ts --coverage.enabled false`
### T002
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: asserts `createCommentFromEmail` passes `metadata.responseSource=inbound_email` to shared comment creation.
### T003
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: asserts provider normalization writes `metadata.email.provider/providerType` when available.
### T004
- Covered by `packages/types/src/interfaces/comment.interface.typecheck.test.ts`: validates `IComment` accepts metadata + normalized source fields.
### T005
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: explicit inbound metadata on latest eligible comment resolves to `inbound_email`.
### T006
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: explicit client portal metadata on latest eligible comment resolves to `client_portal`.
### T007
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: internal comments are ignored during source selection.
### T008
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: fallback infers `inbound_email` from legacy `metadata.email`.
### T009
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: fallback infers `client_portal` for legacy client comment with `user_id`.
### T010
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: returns `null` when no eligible customer source can be resolved.
### T011
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: per-comment badge renders `Received via Client Portal` for portal-sourced comment.
### T012
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: per-comment badge renders `Received via Inbound Email` for email-sourced comment.
### T013
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: per-comment badge renders client_portal via legacy fallback (client with user_id).
### T014
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: per-comment badge renders inbound_email via legacy fallback (metadata.email present).
### T015
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: unresolved source (internal comment) produces no badge markup.
### T016
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: legacy comments without metadata remain valid inputs and resolve safely without schema changes.
### T017
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: google inbound provider path resolves/persists `inbound_email`.
### T018
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: microsoft inbound provider path resolves/persists `inbound_email`.
### T019
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: imap inbound provider path resolves/persists `inbound_email`.
### T020
- Covered by `shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts`: create-comment-from-email path remains non-internal/non-resolution and additive to response-state behavior.
### T021
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: new portal comment renders client_portal badge immediately.
### T022
- Covered by `packages/tickets/src/components/ResponseSourceBadge.render.test.tsx`: new inbound email comment renders inbound_email badge immediately.
### T023
- Covered by `packages/tickets/src/lib/__tests__/responseSourceLocales.test.ts`: English locale keys resolve and existing ticket response-state keys remain present.
### T024
- Covered by `packages/tickets/src/lib/__tests__/responseSource.test.ts`: mixed legacy stream picks latest eligible customer response source correctly.