feat(notes): expose created_by_contact_id on note DTO + contact-notes feed (PMS-468) #350
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!350
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-468-portal-notes-attachments"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PMS-449 phase 1 stamped
ticket_notes.created_by_contact_idon portal-originated comments but did NOT surface the column on the wire shape. The SPA distinguished Customer vs Agent comments by sniffingcreated_by_nameagainst the contacts table - lossy enough that a deleted contact mis-rendered as Agent and a contact whose display name matched a user's first+last name collided. This PR exposes the column directly and adds the agent-facing "all comments from this contact" feed that the new partial index on(tenant_id, created_by_contact_id)was sized for.What landed:
mokosh_types::tickets::TicketNoteandTicketNoteResponsegain anOption<Uuid> created_by_contact_idfield. OnTicketNoteResponseit's#[serde(skip_serializing_if = "Option::is_none")]so existing agent-originated payloads stay byte-identical and only portal-originated rows carry the new key.The
TicketNoteRowFromRow target picks up the column; the three SELECT call sites intickets::service(get_note,get_ticket_notes,list_portal_ticket_notes) hydrate it. The four consumer call sites that buildTicketNoteResponse(tickets::routes::get_ticket_notes / add_note,portal::routes::list_ticket_notes / create_ticket_note) thread it through.New
TicketService::list_notes_by_contact(tenant_id, contact_id, pagination)queries the(tenant_id, created_by_contact_id) WHERE created_by_contact_id IS NOT NULLpartial index added in migration 069. Returnsnote_type='public'rows only - internal / resolution / time_entry notes are agent-side abstractions and don't belong on a customer-comment feed.New endpoint
GET /api/v1/contacts/{contact_id}/notes. Mounted via a sibling routercontact_notes_routes(ticket_service)so the path matches the SPA's contact-detail URL while the handler stays in the tickets module that owns the notes surface. Auth =RequireAuth(any authenticated user); tenant-scoped throughuser.tenant()in the service call so a cross-tenant UUID guess returns an empty page, not the other tenant's rows.Follow-up filed as PMS-483 for the deferred attachment-upload slice of PMS-468; agent and portal upload surfaces have to land together because no agent-side attachment endpoints exist yet either.
Integration tests at
tests/contact_notes_feed.rs:note_dto_carries_contact_attribution: a portal-originated note round-trips through the agent list endpoint withcreated_by_contact_idpopulated; an agent-originated note on the same ticket leaves the field absent from the serialised JSON.contact_notes_feed_lists_all_public_from_contact: two portal notes from the same contact across two different tickets land in the feed; an agent note on one of those tickets stays out.contact_notes_feed_is_tenant_scoped: a tenant-A admin querying a tenant-B contact's notes feed gets an empty page, never the cross-tenant rows.#PMS-468
PMS-449 phase 1 stamped `ticket_notes.created_by_contact_id` on portal-originated comments but did NOT surface the column on the wire shape. The SPA distinguished Customer vs Agent comments by sniffing `created_by_name` against the contacts table - lossy enough that a deleted contact mis-rendered as Agent and a contact whose display name matched a user's first+last name collided. This PR exposes the column directly and adds the agent-facing "all comments from this contact" feed that the new partial index on `(tenant_id, created_by_contact_id)` was sized for. What landed: * `mokosh_types:🎟️:TicketNote` and `TicketNoteResponse` gain an `Option<Uuid> created_by_contact_id` field. On `TicketNoteResponse` it's `#[serde(skip_serializing_if = "Option::is_none")]` so existing agent-originated payloads stay byte-identical and only portal-originated rows carry the new key. * The `TicketNoteRow` FromRow target picks up the column; the three SELECT call sites in `tickets::service` (`get_note`, `get_ticket_notes`, `list_portal_ticket_notes`) hydrate it. The four consumer call sites that build `TicketNoteResponse` (`tickets::routes::get_ticket_notes / add_note`, `portal::routes::list_ticket_notes / create_ticket_note`) thread it through. * New `TicketService::list_notes_by_contact(tenant_id, contact_id, pagination)` queries the `(tenant_id, created_by_contact_id) WHERE created_by_contact_id IS NOT NULL` partial index added in migration 069. Returns `note_type='public'` rows only - internal / resolution / time_entry notes are agent-side abstractions and don't belong on a customer-comment feed. * New endpoint `GET /api/v1/contacts/{contact_id}/notes`. Mounted via a sibling router `contact_notes_routes(ticket_service)` so the path matches the SPA's contact-detail URL while the handler stays in the tickets module that owns the notes surface. Auth = `RequireAuth` (any authenticated user); tenant-scoped through `user.tenant()` in the service call so a cross-tenant UUID guess returns an empty page, not the other tenant's rows. * Follow-up filed as PMS-483 for the deferred attachment-upload slice of PMS-468; agent and portal upload surfaces have to land together because no agent-side attachment endpoints exist yet either. Integration tests at `tests/contact_notes_feed.rs`: * `note_dto_carries_contact_attribution`: a portal-originated note round-trips through the agent list endpoint with `created_by_contact_id` populated; an agent-originated note on the same ticket leaves the field absent from the serialised JSON. * `contact_notes_feed_lists_all_public_from_contact`: two portal notes from the same contact across two different tickets land in the feed; an agent note on one of those tickets stays out. * `contact_notes_feed_is_tenant_scoped`: a tenant-A admin querying a tenant-B contact's notes feed gets an empty page, never the cross-tenant rows. #PMS-468