# 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.