feat(portal): customer ticket comments (PMS-449 phase 1) #339

Merged
YousifShkara merged 1 commit from feat/PMS-449-portal-ticket-comments into main 2026-06-23 11:19:36 +02:00
Owner

Adds GET / POST /api/v1/portal/tickets/{id}/notes, the missing leg of the customer comment loop. A portal contact can now follow up on an open ticket without having to email the helpdesk and have an agent re-paste the reply into the ticket; the comment lands directly on the same ticket_notes track agents use, so the existing notifications dispatcher fans it out and the agent sees it on their normal queue.

Schema: a new ticket_notes.created_by_contact_id UUID NULL REFERENCES contacts(id) ON DELETE SET NULL plus a partial index on (tenant_id, created_by_contact_id) WHERE created_by_contact_id IS NOT NULL. created_by_id stays NOT NULL: portal-originated notes set it to the tenant's fallback admin/manager (mirrors create_portal_ticket's attribution pattern) and populate created_by_contact_id with the real author. That keeps the existing mokosh_types::tickets::TicketNote.created_by_id: Uuid shape intact - making it Optional would ripple across every consumer for marginal value when the SPA can already tell "Customer:" vs "Agent:" from note_type='public' + a non-null created_by_contact_id.

Guarantees, all pinned by tests/portal_ticket_notes.rs:

  • the portal feed filters server-side on note_type='public', so internal / resolution / time_entry agent back-channel never leaks to the customer;
  • a contact cannot read OR write a ticket in another company within the same tenant - both surfaces 404 (not 403), so the response never confirms the existence of another company's ticket;
  • the DB row carries created_by_contact_id equal to the authenticated contact, so downstream UI / reporting can attribute the comment correctly;
  • list ordering is ascending by created_at so the agent + customer comments render as a thread.

Phase 2 follow-ups, tracked under PMS-449:

  • expose created_by_contact_id on TicketNoteResponse (currently lossy in the wire shape; the SPA distinguishes from created_by_name + heuristics for now);
  • portal attachment upload (POST a file alongside the comment);
  • the agent UI's "show me all comments from this customer" feed, which the new index lights up cheaply.

#PMS-449

Adds `GET` / `POST /api/v1/portal/tickets/{id}/notes`, the missing leg of the customer comment loop. A portal contact can now follow up on an open ticket without having to email the helpdesk and have an agent re-paste the reply into the ticket; the comment lands directly on the same `ticket_notes` track agents use, so the existing notifications dispatcher fans it out and the agent sees it on their normal queue. Schema: a new `ticket_notes.created_by_contact_id UUID NULL REFERENCES contacts(id) ON DELETE SET NULL` plus a partial index on `(tenant_id, created_by_contact_id) WHERE created_by_contact_id IS NOT NULL`. `created_by_id` stays NOT NULL: portal-originated notes set it to the tenant's fallback admin/manager (mirrors `create_portal_ticket`'s attribution pattern) and populate `created_by_contact_id` with the real author. That keeps the existing `mokosh_types::tickets::TicketNote.created_by_id: Uuid` shape intact - making it Optional would ripple across every consumer for marginal value when the SPA can already tell "Customer:" vs "Agent:" from `note_type='public'` + a non-null `created_by_contact_id`. Guarantees, all pinned by `tests/portal_ticket_notes.rs`: - the portal feed filters server-side on `note_type='public'`, so `internal` / `resolution` / `time_entry` agent back-channel never leaks to the customer; - a contact cannot read OR write a ticket in another company within the same tenant - both surfaces 404 (not 403), so the response never confirms the existence of another company's ticket; - the DB row carries `created_by_contact_id` equal to the authenticated contact, so downstream UI / reporting can attribute the comment correctly; - list ordering is ascending by `created_at` so the agent + customer comments render as a thread. Phase 2 follow-ups, tracked under PMS-449: - expose `created_by_contact_id` on `TicketNoteResponse` (currently lossy in the wire shape; the SPA distinguishes from `created_by_name` + heuristics for now); - portal attachment upload (POST a file alongside the comment); - the agent UI's "show me all comments from this customer" feed, which the new index lights up cheaply. #PMS-449
feat(portal): customer ticket comments (PMS-449 phase 1)
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 58s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m45s
Integration / integration tests (pull_request) Successful in 8m30s
Create release / Create release from merged PR (pull_request) Successful in 1s
f655561625
Adds `GET` / `POST /api/v1/portal/tickets/{id}/notes`, the missing leg of the customer comment loop. A portal contact can now follow up on an open ticket without having to email the helpdesk and have an agent re-paste the reply into the ticket; the comment lands directly on the same `ticket_notes` track agents use, so the existing notifications dispatcher fans it out and the agent sees it on their normal queue.

Schema: a new `ticket_notes.created_by_contact_id UUID NULL REFERENCES contacts(id) ON DELETE SET NULL` plus a partial index on `(tenant_id, created_by_contact_id) WHERE created_by_contact_id IS NOT NULL`. `created_by_id` stays NOT NULL: portal-originated notes set it to the tenant's fallback admin/manager (mirrors `create_portal_ticket`'s attribution pattern) and populate `created_by_contact_id` with the real author. That keeps the existing `mokosh_types:🎟️:TicketNote.created_by_id: Uuid` shape intact - making it Optional would ripple across every consumer for marginal value when the SPA can already tell "Customer:" vs "Agent:" from `note_type='public'` + a non-null `created_by_contact_id`.

Guarantees, all pinned by `tests/portal_ticket_notes.rs`:
- the portal feed filters server-side on `note_type='public'`, so `internal` / `resolution` / `time_entry` agent back-channel never leaks to the customer;
- a contact cannot read OR write a ticket in another company within the same tenant - both surfaces 404 (not 403), so the response never confirms the existence of another company's ticket;
- the DB row carries `created_by_contact_id` equal to the authenticated contact, so downstream UI / reporting can attribute the comment correctly;
- list ordering is ascending by `created_at` so the agent + customer comments render as a thread.

Phase 2 follow-ups, tracked under PMS-449:
- expose `created_by_contact_id` on `TicketNoteResponse` (currently lossy in the wire shape; the SPA distinguishes from `created_by_name` + heuristics for now);
- portal attachment upload (POST a file alongside the comment);
- the agent UI's "show me all comments from this customer" feed, which the new index lights up cheaply.

#PMS-449
YousifShkara deleted branch feat/PMS-449-portal-ticket-comments 2026-06-23 11:19:36 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!339
No description provided.