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
13 KiB
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.tsxpackages/client-portal/src/components/tickets/TicketDetails.tsx
Discoveries
- Inbound email processing currently creates comments through:
shared/services/email/processInboundEmailInApp.tsshared/workflow/actions/emailWorkflowActions.ts#createCommentFromEmail
- Inbound email comments already include rich
metadata.emailcontent 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.metadataJSONB exists (migration20250917150000_add_metadata_to_comments.cjs), so MVP can avoid schema migration.ICommenttype currently does not exposemetadata, which makes source-based UI logic awkward/unsafe.
Proposed MVP Strategy
- Source tagging:
- Write
metadata.responseSource=client_portalfor client portal comments. - Write
metadata.responseSource=inbound_emailfor inbound email comments (in shared email comment creation path).
- Write
- Source derivation:
- Add shared utility to compute latest customer response source from conversation comments.
- Prefer explicit metadata; fallback heuristics for older records.
- 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
- Show source only when
response_state=awaiting_internal, or whenever latest customer response source is known? - Generic inbound label vs provider-specific label in UI.
Ticket-level indicator only vs ticket-level + per-comment badges.Resolved: per-comment badges only (no ticket-level indicator).- 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"CommentResponseSourceunion 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
CommentMetadataEmailandCommentMetadatawith safe loose typing. - Added
IComment.metadata?: CommentMetadata | null.
- Added
- Added optional normalized
IComment.response_source?: CommentResponseSource. - Rationale: unblock UI/action logic from using
anyfor 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#addClientTicketCommentto 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(...)
createCommentFromEmailnow always persistsmetadata.responseSource = "inbound_email"via the shared metadata builder before callingTicketModel.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
buildInboundEmailCommentMetadatanow normalizes provider type togoogle|microsoft|imapwhen present.- Persisted provider detail is written on
metadata.email.providerandmetadata.email.providerType. - Updated
shared/services/email/processInboundEmailInApp.tsto passmetadata.email.provider = emailData.providerin all inbound comment creation branches.
F006 — Shared source derivation utility
- Added
packages/tickets/src/lib/responseSource.tswith:getCommentResponseSource(comment)getLatestCustomerResponseSource(conversations)
- Exported utility from
packages/tickets/src/lib/index.tsfor use in both MSP and client-portal ticket detail surfaces.
F007 — Internal exclusion + explicit precedence
getLatestCustomerResponseSourcenow only evaluates customer-visible comments (!is_internal,author_type in {client,contact}).getCommentResponseSourceresolves explicit metadata first:metadata.responseSourceresponse_sourcefallback field
- Heuristics are only applied if explicit source is absent.
F008 — Legacy inbound fallback
- Added fallback in
getCommentResponseSource:- if
comment.metadata.emailexists and explicit source is absent, inferinbound_email.
- if
- 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=clientwithuser_id, inferclient_portal.
- if explicit source is absent and comment is
- 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.tsxis unchanged. - Updated
packages/tickets/src/components/ticket/CommentItem.tsx:- derives source per-comment with
getCommentResponseSource(conversation). - renders
ResponseSourceBadgeinline with internal/resolution badges next to author name.
- derives source per-comment with
- 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|inboundEmailserver/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.
- only when
- No placeholder/error UI is shown when source cannot be determined.
F014 — Schema-light implementation
- Confirmed implementation uses existing
comments.metadataJSONB only. - No migration files were added/modified for this workstream.
F015 — Shared inbound path coverage
- Google/Microsoft/IMAP inbound flows all route through
createCommentFromEmailin: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
addClientTicketCommentinsertsmetadata.responseSource = "client_portal". - Also introduced
server/vitest.config.tsalias for@alga-psa/analyticsto 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: assertscreateCommentFromEmailpassesmetadata.responseSource=inbound_emailto shared comment creation.
T003
- Covered by
shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts: asserts provider normalization writesmetadata.email.provider/providerTypewhen available.
T004
- Covered by
packages/types/src/interfaces/comment.interface.typecheck.test.ts: validatesICommentaccepts metadata + normalized source fields.
T005
- Covered by
packages/tickets/src/lib/__tests__/responseSource.test.ts: explicit inbound metadata on latest eligible comment resolves toinbound_email.
T006
- Covered by
packages/tickets/src/lib/__tests__/responseSource.test.ts: explicit client portal metadata on latest eligible comment resolves toclient_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 infersinbound_emailfrom legacymetadata.email.
T009
- Covered by
packages/tickets/src/lib/__tests__/responseSource.test.ts: fallback infersclient_portalfor legacy client comment withuser_id.
T010
- Covered by
packages/tickets/src/lib/__tests__/responseSource.test.ts: returnsnullwhen no eligible customer source can be resolved.
T011
- Covered by
packages/tickets/src/components/ResponseSourceBadge.render.test.tsx: per-comment badge rendersReceived via Client Portalfor portal-sourced comment.
T012
- Covered by
packages/tickets/src/components/ResponseSourceBadge.render.test.tsx: per-comment badge rendersReceived via Inbound Emailfor 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/persistsinbound_email.
T018
- Covered by
shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts: microsoft inbound provider path resolves/persistsinbound_email.
T019
- Covered by
shared/workflow/actions/__tests__/emailWorkflowActions.responseSource.test.ts: imap inbound provider path resolves/persistsinbound_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.