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

13 KiB

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.jsontickets.responseSource.clientPortal|inboundEmail
    • server/public/locales/en/common.jsontickets.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.