[PRD] Qontak CDP | Customers/Notes | Mention User in Notes — Adj: add @mention + notify
| Field | Value |
|---|---|
| PM | PM Qontak |
| PRD Version | 1.6 |
| Status | DRAFT |
| PRD Type | ADJUSTMENT |
| Epic | TF-3184 |
| Squad | CDP Squad |
| RFC Link | N/A — to be created |
| Figma Master | TBD — mention picker + mention chip + mention preview card |
| Anchor | No — standalone adjustment to the existing CDP Notes feature |
| Labels | epic:qontak-cdp | module:customers | feature:notes-mention |
| Last Updated | 2026-06-25 |
Table of Contents
- HEADER BLOCK
- 2. Adjustment Context
- 3. One-liner + Problem
- 4. Target Users + Persona Context
- 5. Non-Goals
- 6. Constraints
- 7. Feature Changes
- 8. API & Webhook Behavior
- 9. System Flow + User Stories + ACs
- 10. Rollout
- 11. Observability
- 12. Success Metrics
- 13. Dependencies
- 14. Key Decisions + Alternatives Rejected
- 15. Open Questions
- Appendix A — Grounded Code References
- PRD CHANGELOG
2. Adjustment Context
| Field | Detail |
|---|---|
| Parent feature | CDP Notes (existing) — single-CRUD notes on a contact, served by contact-service under /iag/v1/contacts/{contact_id}/notes and rendered in the customer-fe Notes panel. |
| Parent Anchor PRD | N/A — CDP Notes shipped without an anchor; this adjustment is standalone. |
| Adjustment scope | Add the ability to @mention a user inside a CDP note, and notify the mentioned user. Mention is stored as a resolved tag keyed to the user's SSO UUID (Decision D-1, "Option B"), validated against company membership, and delivered via the Qontak Unified Notification Service. |
| Parent PRD scope preserved | Existing note create/read/update/delete, attachments, ownership, and permission behavior are unchanged. Notes without mentions behave exactly as today. |
| Reason for adjustment | Legacy CRM person notes already support @mention + email notification (note.rb:94-133); CDP notes have no mention capability at any layer. Customers migrating to Qontak One lose a workflow they rely on (tagging a teammate to follow up on a contact). This closes that parity gap. |
3. One-liner + Problem
One-liner: Let an agent @mention a teammate inside a CDP contact note and have that teammate notified, so collaboration on a customer record happens in-context — at parity with legacy CRM.
Problem:
CDP Notes today is a plain content + attachments record. There is no mention support in any layer: contact-service note code has no mention parsing/storage (grep for mention across the notes service/handler/repo returns nothing; create validates length only), and the customer-fe Notes components (NoteInput.vue, NotesList.vue) have no mention picker or mention rendering.
Legacy CRM, by contrast, has a full mention feature: notes parse <a data-user-id="{id}"> anchors (note.rb:98-105), validate that mentioned users are in the creator's team (note.rb:122-133), and notify each mentioned user (creates a Crm::Notification and fires an email via MentionWorker, note.rb:44-61). Agents migrating to Qontak One therefore lose the ability to loop a teammate into a customer note — a daily collaboration workflow. They work around it by messaging the teammate outside the product (chat/WhatsApp), which breaks the audit trail and context.
4. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — CS / Sales Agent | Agent working a contact in CDP | Tag a teammate on a note so they pick up / are aware of a follow-up on this contact | CDP notes can't mention anyone; the teammate never gets notified | Copies the contact link into an external chat and pings the teammate manually |
| Secondary — Mentioned teammate | Agent/Supervisor who gets mentioned | Get notified with a direct link back to the exact contact note | No notification exists; they only find out if they happen to open the contact | Relies on the mentioner to message them out-of-band |
Scope Changes
Engineering surfaces this PRD touches (controlled vocab). Kept in sync with the scope_changes frontmatter above.
- Backend —
contact-service: store mentioned-user SSO UUIDs on notes, validate company membership, fire notifications via the Qontak Unified Notification Service; the mention-anchor parser must accept both the webdata-user-id/data-mentionform and the mobile<a href=".../users/{id}/edit_user">form and normalize to SSO UUID (D-9); mention typeahead resolved via Launchpad. - Frontend —
qontak-customer-fe:@-mention typeahead inMpRichTextEditor(feasibility spike — OQ-7), DOMPurifyADD_ATTRfordata-mention/data-user-id, mention chip rendering + notification surfacing; a mention preview card on the notes list (hover web / tap mobile) that resolves the mentioned user's name/email/staff level/team(s) via Launchpad, with an inactive / not-found state (Figma 15091-119388 / 15091-448947). - Mobile —
mobile-qontak-crm: mention compose already ships (MpMentionToolbarButtonX(isCdp:true)+ CDP member picker,mention_toolbar_botton.dart) but emits an href anchor, not an SSO-UUIDdata-user-idanchor (encoding/identity gap — OQ-10/OQ-12); note rendering strips HTML (note_screen.dart:264stripHtml()), so a mention chip is net-new (OQ-11); One Notification V2 already defines amentioncategory but the tap-through has no CDP route — mention notifications must route viaextra.origin=external_urlor a new native route (D-10, OQ-5). Tapping a rendered mention chip opens the mention preview card (identity + inactive/not-found state) — net-new alongside the chip. - Design — Figma for the mention picker/typeahead and the mention chip + notification UI (web + mobile).
5. Non-Goals
- No migration of legacy CRM mentions — historical CRM notes carry
data-user-idreferencing CRM integer user IDs; their handling (strip-to-text) is owned by the separate Legacy CRM Notes → CDP migration PRD, not this one. - No new notification surface — this reuses the existing Qontak Unified Notification Service + Notification Center; it does not build a notification center.
- No mention of non-users — only Qontak One users in the same company can be mentioned; no mentioning of contacts, teams, or external emails.
- No rich-text overhaul — this adds the mention token only; it does not change the rest of the note editor's formatting capabilities.
- No notification-preference management — v1 has no digest, snooze, mute, or per-user notification settings. A mentioned user is notified at most once per note (idempotent — re-mentioning the same user on edit does not re-notify; see D-4/D-11), and nothing more.
- No cross-company mention — mention candidates are scoped to the note's company (
company_sso_id); no cross-tenant mentions. - No
qontak-mobile-chatnotification — the mention notification is delivered only in the CRM mobile app (mobile-qontak-crm, via One Notification V2), not the chat mobile app (qontak-mobile-chat).
6. Constraints
| Constraint | Value |
|---|---|
| Feature flag | cdp_notes_mention_enabled | default: OFF — enabled per company by Ops. |
| Platform | Both web (qontak-customer-fe) and CRM mobile (mobile-qontak-crm). Compose/render mentions on both; notifications via the Unified Notification Service (Notification Center on web; One Notification V2 / FCM on mobile, gated by flag_one_notification). Grounded asymmetry (v1.2): mobile compose already ships a mention button + picker, whereas web compose feasibility is the open item (OQ-7) — see the mobile-specific constraint rows below. The mention notification is delivered on mobile-qontak-crm only (the CRM mobile app) — not qontak-mobile-chat (the chat mobile app). |
| Mobile mention encoding (grounded) | Mobile (detail_note_screen.dart) composes in MpTextEditor (Quill) and emits mentions as <a href="../../../users/{id}/edit_user">@Name</a> (mention_toolbar_botton.dart:246-251) — not the web <a data-user-id="{sso_uuid}" data-mention="true"> form. The backend mention parser (CHG-003) must accept both forms (D-9), and the mobile {id} must resolve to the user's SSO UUID (the picker loads GetListMemberCdp — confirm it yields SSO UUID, not a member/CRM id — OQ-10). Alternatively change the mobile editor to emit the data-attribute anchor (the Quill→HTML codec does not support data-* today — OQ-12). |
| Mobile mention rendering (grounded) | Mobile has no HTML renderer (no flutter_html): note lists render via stripHtml() (note_screen.dart:264) → a mention shows as plain text; reopening a note reuses the Quill editor → plain link, and data-* attributes are lost on the Quill round-trip. A styled mention chip on mobile is net-new (OQ-11). |
| Mobile notification routing (grounded) | The One Notification V2 tap handler (notification_item_v2_mixin.dart:92-151) routes only by extra.origin in detailModuleRouteMapping (deal/contact/lead/task/company/expense/external_url) — it ignores click_action_url unless origin = external_url. There is no customers/Contact360/CDP origin or native CDP-contact-note route (qontak_app_route.dart:133-141). A CDP mention notification must therefore set extra.origin = external_url (opens the web click_action_url in a browser) or add a native CDP-contact origin+route; otherwise it hits the "cannot redirect" toast (D-10, OQ-5). |
| Identity | Mentions are keyed to the user's SSO UUID (not CRM integer IDs). |
| Mention candidate scope | Active users in the same company (company_sso_id), sourced from Launchpad GET /iag/v1/users?query={term} (UserListParam.Query does ILIKE search on full_name + email). (Team-scoped narrowing is a future option — OQ-2.) FE must: debounce keystrokes before querying; handle paginated response; display full_name (null-safe — sql.NullString) and avatar (null-safe — sql.NullString); show "No matching people" on empty result. |
| Launchpad not-found semantics (grounded) | GET /iag/v1/users returns a generic 404 / not-found when a user can't be resolved — covering both a wrong keyword / no match and a deleted user. Launchpad has no per-user flag to differentiate a deleted user from a non-existent one. The mention picker and server-side validation both treat not-found as "not available": the picker shows "No matching people"; validation drops/rejects the mention. The system cannot show a distinct "user was deleted" vs "no match" message today — see OQ-13. |
| Performance | Mention typeahead suggestions ≤ 500ms P95. Note create with mentions ≤ 2s P95 (notification dispatch is async — must not block the note write). |
| Note content limit | CDP enforces a 10,000-character limit on note content (contact_notes_service.go:272). Each mention anchor (<a data-user-id="[36-char UUID]" data-mention="true">@Name</a>) adds ~80 characters of markup. Maximum mentions per note: TBD — see OQ-4; must ensure combined markup does not push the note over 10,000 characters. FE must surface a validation error when the limit is approached. |
| Read/write scope | Write: agents with customers_customernotes_add can compose and save mentions. Read: agents with customers_customernotes_view can view mention chips. The server enforces mention validation regardless of the client. |
| Notification delivery | Qontak Unified Notification Service POST /api/v1/notifications/chat (internal X-Api-Key), one call per newly-mentioned user keyed by recipient sso_id. Partially grounded (v1.2): mobile One Notification V2 already defines notif_type = general(1) (notif_type_enum.dart:2) and notif_category = mention('2') (notif_category.dart); confirm the web Notification Center + service use the same values — (OQ-1). organization_id in the notification payload = Launchpad company.id (UUID); contact-service must map its company_sso_id (string) to the Launchpad UUID — see OQ-8. On mobile, delivery is mobile-qontak-crm only (One Notification V2) — not qontak-mobile-chat. |
| Security | Note content is stored HTML; the mention anchor (data-user-id) must pass server-side sanitization on write (today there is none — content is only DOMPurify-sanitized at FE render). DOMPurify at FE render must be explicitly configured with ADD_ATTR: ['data-user-id', 'data-mention'] — without this option, DOMPurify's default strips all data-* attributes, destroying mention identity silently. Mention validation must reject SSO UUIDs not in the company. |
| FE editor feasibility | NoteInput.vue uses MpRichTextEditor (Mekari Pixel 3 component); editorOptions is a hardcoded fixed toolbar array (NoteInput.vue:155). Whether MpRichTextEditor supports custom extensions (a mention @ trigger + user picker) is unverified — this must be confirmed as a discovery spike before the RFC. If it does not support extensions, the FE requires either a Pixel 3 component update or a custom Tiptap/Quill wrapper. (See OQ-7.) |
| Async notification pattern | There is no existing async notification infrastructure in the contact-service notes write path today (zero events/Kafka on note create). Adding async dispatch-with-retry is a net-new pattern for this service — scope includes adding the async fire-and-forget call with exponential backoff to the note service layer. |
| Backward compat | Notes without mentions behave exactly as today; existing notes are unaffected. |
| Plan scope | Same plans that have CDP Notes — Growth and Enterprise. Not Starter. |
7. Feature Changes
CHG-001 — Mention token in the note editor (Web + Mobile)
| Element | Before | After |
|---|---|---|
Note composer — web (NoteInput.vue) | Plain content editor; no @ affordance | Typing @ opens a typeahead picker of company users (name + avatar) sourced from Launchpad; selecting one inserts a mention chip backed by the user's SSO UUID (web editor extension feasibility — OQ-7) |
Note composer — mobile (detail_note_screen.dart) | Grounded: a mention button already exists — MpMentionToolbarButtonX(isCdp:true) (detail_note_screen.dart:728-733) opens a CDP member picker bottom-sheet (GetListMemberCdp); but it inserts a plain Quill link (<a href=".../users/{id}/edit_user">), not a data-user-id anchor | Reuse the existing mobile mention button, but align its output to the SSO-UUID anchor the backend parses — either change the insert to emit data-user-id/data-mention (Quill codec gap — OQ-12) or have the backend accept the mobile href form (D-9); the mobile {id} must resolve to the SSO UUID (OQ-10) |
| Stored note content | HTML content, no mention markup | HTML content may contain mention anchors — web <a data-user-id="{sso_uuid}" data-mention="true">@Name</a> and/or the mobile <a href=".../users/{id}/edit_user">@Name</a> form; backend normalizes both to mentioned_user_ids SSO UUIDs (D-9) |
| Note composer — edit mode (note already contains ≥1 mention) | (no mention feature today) | The @ trigger (web) / mention button (mobile) is enabled on edit (same affordance as create — CRM parses mentions identically on create and update, no branch). The agent can re-mention an already-mentioned user and can add new mentions; existing mention chips are editable. Matches CRM note behavior (note.rb). Re-mentioning the same user does not re-notify them (notification is idempotent per note+user — D-11). |
CHG-002 — Mention chip rendering (Web + Mobile)
| Element | Before | After |
|---|---|---|
Note rendering — web (NotesList.vue via DOMPurify) | sanitizeHtml() calls DOMPurify.sanitize(value) with no options (NotesList.vue:103-106) — DOMPurify's default strips all data-* attributes, so any mention anchor stored in the note would have data-user-id/data-mention silently removed, rendering as a plain <a>@Name</a> with no identity. | DOMPurify config extended with ADD_ATTR: ['data-user-id', 'data-mention'] so mention anchors survive sanitization with their identity intact; renders a styled, non-editable @Name chip. |
Note rendering — mobile (note_screen.dart / note_item.dart) | Grounded: no HTML renderer (no flutter_html); note lists render content via stripHtml() (note_screen.dart:264) → a mention anchor degrades to plain text "@Name"; reopening a note uses the Quill editor → plain link and data-* is lost on round-trip. | A styled, non-editable mention chip on mobile is net-new — either add an HTML/markdown renderer with chip support, or accept degraded display (plain text in list / plain link in editor) for v1 (decision — OQ-11). |
CHG-003 — Mention persistence + notification (contact-service)
| Element | Before | After |
|---|---|---|
ContactNote model | content + attachments + owner only | adds mentioned_user_ids []string (SSO UUIDs), populated by parsing mention anchors on create/update |
| Note create | length validation only (contact_notes_service.go:272, max 10,000 chars) | parse mention anchors from HTML — accepting both the web data-user-id form and the mobile href=".../users/{id}/edit_user" form, normalizing each to an SSO UUID (D-9) → validate each SSO UUID is an active company user via Launchpad (batched: one GET /iag/v1/users?query= per company, filter client-side; or individual lookups if batch endpoint unavailable — see OQ-6) → server-side sanitize HTML → persist note + mentioned_user_ids → async dispatch notification per newly-mentioned user |
| Note update (edit allows re-mention + add — D-11) | no mention diff logic | on update, re-parse all mention anchors from the edited HTML (both forms, D-9) → validate → refresh mentioned_user_ids → async-dispatch a notification only to users not already notified for this note (idempotent per (note_id, sso_id), mirroring CRM's find_or_create_by + sent_at guard). So newly-added mentions on edit are notified; re-mentioning an already-notified user is not re-notified. (Like CRM, the per-note notified set persists — removing then re-adding the same user does not re-notify; OQ-14.) |
| Notification dispatch (net-new async pattern) | no async side-effects in the notes write path | after note is committed, fire-and-forget call to POST /api/v1/notifications/chat per new mention; organization_id in the payload = Launchpad company.id UUID (contact-service maps company_sso_id string → UUID via a lookup or stores it — see OQ-8); retry up to 3× with exponential backoff; on final failure log cdp_note_mention_notify_failed; note write is unaffected |
CHG-004 — Mention preview card on the notes list (Web + Mobile)
| Element | Before | After |
|---|---|---|
Mention chip interaction (web NotesList.vue / mobile note list) | A rendered mention chip is static — hovering/tapping it shows nothing; the reader can't tell who @Name actually is. | Hovering (web) / tapping (mobile) a mention chip opens a preview card that resolves the mentioned user from mentioned_user_ids via Launchpad and shows name, email, staff level, and team(s) (Figma 15091-119388). Read-only; no new write path. |
| Inactive / not-found user | (no preview today) | If the mentioned user is deactivated or unresolvable (Launchpad returns a generic 404 with no deleted-vs-nonexistent flag — same constraint as OQ-13), the card shows an inactive / not-found state (Figma 15091-448947): an inactive-avatar indicator + last-known name; it never errors the note view. |
| Data source | — | Launchpad company-scoped user lookup keyed by the mention's SSO UUID → {full_name, email, avatar, staff_level, teams[]}. |
8. API & Webhook Behavior
| # | Behavior | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | List mentionable users (typeahead) | Launchpad users | Agent types @ + character(s) in the composer (FE debounces before calling) | FE calls GET /iag/v1/users?query={term} (Launchpad UserListParam.Query does ILIKE on full_name + email); FE filters/paginates client-side; returns {sso_id, full_name (null-safe), avatar (null-safe)} for the picker; displays full_name ?? email if name is null | Launchpad unavailable: picker shows "Couldn't load people — try again"; agent can still save the note without a mention; cdp_note_mention_picker_failed logged |
| 2 | Create/update note with mentions | ContactNote (+ mentioned_user_ids) in CDP | Agent saves a note containing mention anchors (web data-user-id form or mobile href=".../users/{id}/edit_user" form) | Create: parse anchors (both forms → normalize to SSO UUID, D-9) → validate SSO UUIDs against active company users → sanitize HTML (server-side) → persist note + mentioned_user_ids → async-dispatch notification per mentioned user. Update (edit allows re-mention + add — D-11): re-parse + validate + sanitize the edited content → refresh mentioned_user_ids → async-dispatch a notification only to users not yet notified for this note (idempotent per (note_id, sso_id), CRM-aligned): newly-added mentions notify; re-mentions of already-notified users do not. | Invalid sso_id: reject (422) or drop-and-warn per OQ-3. Content > 10,000 chars (incl. markup): reject with 422 NOTE_TOO_LONG. Sanitization failure: note not saved. |
| 3 | Dispatch mention notification | Notification (Unified Notification Service) | Async, after note with new mentions is committed | contact-service calls POST /api/v1/notifications/chat (X-Api-Key) per newly-mentioned user: sso_id = recipient, title = "{Author} mentioned you in a note", description = "On contact {Contact Name}", a CTA that opens the customer details page for the contact (exact URL/deep-link is an engineering implementation detail — not prescribed here; mobile on mobile-qontak-crm only — D-7/D-10/OQ-5), organization_id = Launchpad company.id UUID (mapped from company_sso_id — OQ-8), notif_type/notif_category/event_type = TBD — must be confirmed with Notification Platform squad before RFC (OQ-1), skip_fcm = false | Notification Service 5xx/timeout: retry with backoff (max 3); on final failure log cdp_note_mention_notify_failed — the note remains saved (notification is best-effort, non-blocking) |
| 4 | Mentioned user opens notification | — | Recipient clicks the notification in the Notification Center (web) / One Notification V2 on mobile-qontak-crm (mobile) | Web: opens the customer details page for the contact. Mobile (mobile-qontak-crm only): the V2 tap handler (notification_item_v2_mixin.dart:92-151) routes only when extra.origin = external_url or a mapped origin, and has no native CDP-contact route — so the mention notification opens the customer details page via origin = external_url (or a native CDP route is added) — D-10. Notification marked read via existing PUT /api/v1/notifications/mark_as_read/{id} | Target invalid (note/contact deleted): route to the contact page; if contact also deleted, show "Contact no longer available." Mobile unmapped origin: if origin is neither external_url nor a mapped origin, the app shows the "cannot redirect" toast — must be avoided via D-10. Private notes: if the note has Private permission, the mentioned user may reach a 403 when clicking — confirm interaction with note privacy model before Beta (OQ-9) |
9. System Flow + User Stories + ACs
9.1 System Flow
- Agent opens a contact in CDP and starts a note; types
@. - FE calls Launchpad to list/search active company users; shows a typeahead (name + avatar).
- Agent selects a user → a mention chip (backed by SSO UUID) is inserted into the note HTML.
- Agent saves. contact-service parses mention anchors, validates each SSO UUID is an active company user, sanitizes the HTML, stores the note +
mentioned_user_ids. - On note create or edit, contact-service re-parses the mentions and asynchronously calls the Unified Notification Service (
POST /api/v1/notifications/chat) for each mentioned user not yet notified for this note (idempotent per(note_id, sso_id), CRM-aligned — D-11), with the recipientsso_idand a CTA that opens the customer details page. - The mentioned user sees the notification in the Notification Center (web) / One Notification V2 on
mobile-qontak-crm(mobile); clicking it opens the customer details page; the notification is marked read. - Failure branches: Launchpad down → picker error, note still savable without mention; invalid mention → rejected/dropped per OQ-3; Notification Service down → retried then logged, note unaffected.
📊 System Flow — CDP Notes @Mention
sequenceDiagram
participant Agent
participant FE as customer-fe (Notes)
participant LP as qontak-launchpad
participant CS as contact-service
participant NS as Unified Notification Service
participant Mentioned as Mentioned User
Agent->>FE: type "@" + query
FE->>LP: GET /iag/v1/users (search company users)
LP-->>FE: [{sso_id, full_name, avatar}]
Agent->>FE: select user -> insert mention chip (data-user-id = sso_uuid)
Agent->>FE: Save note
FE->>CS: POST /iag/v1/contacts/{id}/notes (HTML with mention anchors)
CS->>CS: parse mentions, validate sso_ids (company), sanitize HTML, persist + mentioned_user_ids
alt invalid mention sso_id
CS-->>FE: 422 INVALID_MENTION (or drop+warn per OQ-3)
else valid
CS-->>FE: 201 note created
CS->>NS: POST /api/v1/notifications/chat (per not-yet-notified mention on create/edit, sso_id + CTA to customer details page)
alt notify fails (5xx/timeout)
CS->>NS: retry x3 (backoff)
CS-->>CS: log cdp_note_mention_notify_failed (note stays saved)
else notify ok
NS-->>Mentioned: Notification Center (web) / One Notification V2 (mobile-qontak-crm)
Mentioned->>FE: click -> open customer details page
end
end
9.2 User Stories
| User Story | Importance | Mockup | Technical Notes | Acceptance Criteria |
|---|---|---|---|---|
| [NOTE-MENTION-S01] — Mention a teammate in a note (Web) As a CS/Sales Agent, I want to type @ and pick a teammate inside a CDP note, so that I can tag them on this contact. | Must Have | Figma: https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=15089-301155&t=hMokh6s1yCfL47E9-0 | Data Fields: • mention candidates — source: Launchpad GET /iag/v1/users (company-scoped), {sso_id, full_name, avatar}• mention anchor data-user-id = SSO UUID — source: picker selection• note (HTML) — source: composerBefore-After Behavior: Before: the composer has no @ affordance. After: typing @ opens a company-user typeahead; selecting inserts a mention chip backed by SSO UUID. | — Happy Path — • AC-1: Given flag ON and the composer focused, when the agent types @bu, then a typeahead lists active company users matching "bu" with name + avatar within 500ms P95.• AC-2: Given the typeahead is open, when the agent selects a user, then a mention chip @Name is inserted and the underlying anchor carries that user's SSO UUID.• AC-3: Given a note with one or more mention chips, when the agent saves, then the note persists and mentioned_user_ids contains the selected SSO UUIDs.• AC-4 (Edit allows re-mention + add — D-11): Given an existing note that contains ≥1 mention, when the agent edits it, then the @ mention affordance is enabled (same as create) — the agent can re-mention an already-mentioned user and can add new mentions, and existing mention chips are editable. On save, a notification is sent only to users not already notified for this note (newly-added → notified; re-mention of an already-notified user → not re-notified), matching CRM.— Error / Unhappy Path — • ERR-1: Given the agent types @, when Launchpad is unavailable, then the picker shows "Couldn't load people — try again," and the agent can still save the note without a mention; event cdp_note_mention_picker_failed logged.• ERR-2 (Not found — wrong keyword or deleted user): Given the agent types a query that matches no active company user — whether a typo / wrong keyword or a since-deleted user — when Launchpad returns its generic 404 / empty result, then the picker shows the "No matching people" not-found state. Launchpad has no per-user flag to distinguish a deleted user from a non-existent one, so both render the same message (OQ-13). — Permission Model — • CAN: any agent who can create CDP notes (existing customers_customernotes_add).• CANNOT: users without note-create permission — @ affordance not shown.— UI States — • Loading: typeahead spinner while querying Launchpad. • Empty: "No matching people." • Error: "Couldn't load people — try again." • Success: matching users listed with avatars. |
| [NOTE-MENTION-S02] — Mention a teammate in a note (Mobile) As a CS/Sales Agent on Qontak One mobile, I want to mention a teammate on a contact note from my phone, so that I can tag them without switching to desktop. | Must Have | Figma (Mobile): https://www.figma.com/design/7RVU7m4Un6d65yKcZa89xq/Mobile-CRM---Customer-contacts---Company?node-id=6020-24006&t=s7svkcJyCH7Cfr8m-0 | Data Fields: same as S01 (Launchpad-sourced candidates, SSO UUID anchor). Grounded (v1.2): a mention affordance already ships — MpMentionToolbarButtonX(isCdp:true) (detail_note_screen.dart:728-733) opens a CDP member picker (GetListMemberCdp). The gap is encoding (it inserts <a href=".../users/{id}/edit_user">, not an SSO-UUID data-user-id anchor — OQ-10/OQ-12) and rendering (note list stripHtml() → no chip — OQ-11). NOT a feasibility risk like web's OQ-7.Before-After Behavior: Before: mobile has a mention button that inserts a plain link not parsed by the backend, and renders mentions as stripped plain text. After: the same button inserts an anchor the backend resolves to mentioned_user_ids; mention displays per OQ-11. | — Happy Path — • AC-1: Given flag ON on mobile, when the agent taps the mention button and selects a user, then a mention is inserted whose anchor resolves to that user's SSO UUID (not a CRM/member id — OQ-10). • AC-2: Given a selection, when the agent saves, then the note persists with the mention and mentioned_user_ids set — identical server behavior to web (backend accepts the mobile anchor form, D-9).• AC-3: Given a saved note containing a mention, when it is shown in the mobile note list, then the mention renders per the OQ-11 decision (chip or graceful plain-text fallback) — never broken/raw HTML. • AC-4 (Edit allows re-mention + add — D-11): Given an existing note that contains ≥1 mention, when the agent edits it on mobile-qontak-crm, then the mention button is enabled — the agent can re-mention an already-mentioned user and add new mentions; on save, only users not already notified for this note are notified (CRM-aligned), identical server behavior to web.— Error / Unhappy Path — • ERR-1: Given the device is offline, when the agent opens the picker, then it shows "People list unavailable — check your connection"; note still savable without a mention. — UI States — • Loading: list skeleton. • Empty: "No matching people." • Error: offline message. • Success: list rendered. |
| [NOTE-MENTION-S03] — Mentioned user is notified (Web & Mobile) As a mentioned teammate, I want a notification that opens the customer details page, so that I can act on it without hunting for context. | Must Have | Figma: N/A — uses Notification Center (web) / One Notification V2 on mobile-qontak-crm (mobile). | Data Fields (notification body): • sso_id (recipient), title/description, a CTA that opens the customer details page (exact URL/deep-link is an implementation detail — not prescribed here), organization_id, notif_type = general(1)/notif_category = mention('2') (grounded — already defined on mobile)Grounded (v1.2) — mobile routing ( mobile-qontak-crm only): One Notification V2 already has a mention category, but the tap handler (notification_item_v2_mixin.dart:92-151) routes only when extra.origin = external_url or a mapped origin, and has no native CDP-contact route (qontak_app_route.dart:133-141). So the mention notification opens the customer details page via origin = external_url (or a native route — D-10), else it shows the "cannot redirect" toast. Mobile delivery is mobile-qontak-crm only — not qontak-mobile-chat.Before-After Behavior: Before: mentioning is impossible, so no notification. After: each mentioned user (at note create) receives a Unified Notification Service notification that opens the customer details page (web Notification Center + mobile-qontak-crm One Notification V2 only — not qontak-mobile-chat). | — Happy Path — • AC-1: Given a note is created with a mention of user X, when the write commits, then contact-service calls POST /api/v1/notifications/chat with X's sso_id, notif_category = mention, and a CTA that opens the customer details page (async; does not block the note write).• AC-2: Given the notification is delivered, when X clicks it on web, then they land on the customer details page and the notification is marked read. • AC-2b: Given the notification is delivered on mobile-qontak-crm, when X taps it, then it opens the customer details page (via origin=external_url or a native route — D-10) and is marked read; it must not dead-end on the "cannot redirect" toast. (Not delivered on qontak-mobile-chat.)• AC-3 (Edit notifies new mentions only — D-11): Given an existing note with mentions is edited, when it saves, then a notification is sent only to newly-added mentions (users not already notified for this note); re-mentioning an already-notified user sends no new notification (idempotent per (note_id, sso_id), mirroring CRM). A user is notified at most once per note.— Error / Unhappy Path — • ERR-1: Given the Notification Service returns 5xx/timeout, when dispatch fails, then contact-service retries up to 3× with backoff; on final failure it logs cdp_note_mention_notify_failed and the note remains saved.• ERR-2: Given X mentions themselves, when the note saves, then no self-notification is sent. — Permission Model — • CAN: Notification Center marks the notification as read when the recipient clicks it via PUT /api/v1/notifications/mark_as_read/{id}.• CANNOT: The agent who wrote the mention cannot unsend, retract, or delete the notification after it has been dispatched. Notifications are permanent delivery records; the note itself can be edited/deleted, but already-sent notifications are not recalled. • Unauthorized: N/A — notifications are delivered to the recipient's own notification stream. — UI States — • Loading: N/A (async dispatch). • Empty: N/A. • Success: notification appears in Notification Center / as push. • Error: silent to author (note saved); failure visible only in logs/metrics. |
| [NOTE-MENTION-S04] — Only valid company users can be mentioned As a System, I want to validate mentions against company membership, so that notes can't reference or notify invalid/cross-tenant users. | Must Have | Figma: N/A | Data Fields: mentioned_user_ids validated against Launchpad active company users.Before-After Behavior: Before: no mention validation. After: every mention SSO UUID is validated server-side against the company's active users before persist + notify. | — Happy Path — • AC-1: Given a note with mention SSO UUIDs all belonging to active company users, when saved, then all are stored in mentioned_user_ids and notified.— Error / Unhappy Path — • ERR-1: Given a mention SSO UUID that is not an active user in the note's company, when saved, then the system rejects with 422 INVALID_MENTION (or drops the mention and warns — per OQ-3); no notification is sent to that ID.• ERR-2: Given note HTML contains a crafted/mismatched data-user-id (XSS / injection attempt), when saved, then server-side sanitization strips disallowed markup and only validated mention anchors persist.• ERR-3 (Mentioned user not found / deleted): Given a mention SSO UUID that Launchpad cannot resolve — because the user was deleted or the id is otherwise invalid — when validating on save, then Launchpad returns a generic 404 (no deleted-vs-non-existent flag); the system treats it as not a valid active user and rejects/drops the mention per OQ-3 (same path as ERR-1). It cannot distinguish a deleted user from an invalid one (OQ-13). — Permission Model — • CAN: contact-service (server-side enforcement). • CANNOT: client — FE validation is convenience only; the server is authoritative. |
| [NOTE-MENTION-S05] — Preview a mentioned user on the notes list As a CS/Sales Agent, when I hover (web) or tap (mobile) a mention chip on a saved note, I want a preview card showing that teammate's identity — name, email, staff level, and team(s) — so that I know exactly who was tagged without leaving the notes view. | Should Have | Figma (Happy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=15091-119388&t=JTiXXfN4S3wAoCTt-4 Figma (Unhappy Path — inactive / not found): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=15091-448947&t=JTiXXfN4S3wAoCTt-4 | Data Fields (preview card): • sso_id — source: the note's stored mention anchor / mentioned_user_ids• full_name, email, avatar — source: Launchpad user lookup (company-scoped)• staff_level (e.g. "Staff"), teams[] (e.g. "Sales, CRM") — source: LaunchpadBefore-After Behavior: Before: a mention chip on a saved note is just highlighted text — hovering/tapping shows nothing. After: hovering (web) / tapping (mobile) the chip opens a preview card resolving the mentioned user's name, email, staff level, and team(s). | — Happy Path — • AC-1: Given a saved note containing a mention chip and the mentioned user is an active company user, when the agent hovers (web) / taps (mobile) the chip, then a preview card shows the user's name, email, staff level, and team(s) (per Figma 15091-119388). • AC-2: Given the mentioned user belongs to multiple teams, when the card renders, then all teams are listed (e.g. "Sales, CRM"). • AC-3: Given the preview card is open, when the agent moves the cursor away (web) / dismisses (mobile), then the card closes and the note stays readable. — Error / Unhappy Path — • ERR-1 (Inactive / not found): Given the mentioned user is deactivated or can no longer be found in the company directory, when the agent hovers/taps the chip, then the preview card shows the inactive / not-found state (per Figma 15091-448947) — an inactive-user indicator on the avatar with last-known name + any resolvable detail — and does not error out. (Launchpad returns a generic 404 with no deleted-vs-nonexistent flag — same constraint as OQ-13.) • ERR-2 (Lookup fails): Given the Launchpad lookup times out or returns 5xx, when the agent hovers/taps the chip, then the card shows a graceful fallback (name only / "Couldn't load details") and the note remains readable. — Permission Model — • CAN: any agent who can view the note (the preview reads company-scoped directory data only). — UI States — • Loading: skeleton/spinner in the card while resolving the user. • Empty / Not-found: inactive-or-not-found card state (Figma 15091-448947). • Error: "Couldn't load details." • Success: full preview card (name, email, staff level, team(s)). |
| [NOTE-MENTION-S06-NEG] — No cross-company / non-user mentions (Guard Rail — from Non-Goals) As an agent, when I attempt to mention a user outside my company or a non-user entity, then the mention is not created or notified. | Guard Rail | — | — | • NEG-1: Given a mention SSO UUID from another company, when the note saves, then the mention is rejected/dropped and no cross-tenant notification is sent. • NEG-2: Given the flag is OFF for the company, when the agent types @, then no mention picker appears and notes behave exactly as today. |
Dependencies: S03 depends on the Unified Notification Service (Section 13); S01/S02 depend on a Launchpad user-search source; S01/S02 depend on MpRichTextEditor supporting a custom mention extension (OQ-7); S05 (preview card) depends on a Launchpad user-detail lookup (name/email/staff level/team(s)) + an inactive/not-found state.
🧪 Test Coverage Matrix — [NOTE-MENTION-S01]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | AC-1 covers typeahead response time; ⚠️ QA: note with max mentions (at 10,000-char limit), picker with 0 results, picker with 100+ results (pagination) |
| State transitions | ✅ defined | AC-2 (chip inserted), AC-3 (saved with mentions), ERR-1 (Launchpad down) |
| Data validation | ⚠️ partial | AC-2 covers SSO UUID in anchor; ⚠️ QA: special characters in @ query, emoji in user names, null full_name display |
| Concurrency | ⚠️ TBD | ⚠️ QA: two agents concurrently creating notes with mentions on the same contact (edit cannot add mentions — D-11) |
| Network/timeout | ✅ defined | ERR-1 covers Launchpad unavailable; note savable without mention |
🧪 Test Coverage Matrix — [NOTE-MENTION-S02]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | ⚠️ QA: offline → picker unavailable; slow connection > 500ms typeahead threshold |
| State transitions | ✅ defined | AC-1/AC-2 cover same server enforcement as web; ERR-1 covers offline |
| Data validation | ✅ defined | Identical server-side path as S01 (same contact-service validation) |
| Concurrency | ⚠️ TBD | ⚠️ QA: mobile + web agent concurrently creating mentions on the same contact (edit cannot add mentions — D-11) |
| Network/timeout | ✅ defined | ERR-1 offline message |
🧪 Test Coverage Matrix — [NOTE-MENTION-S03]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | Need: note with 0 mentions (no notify), 1, and many mentions (fan-out limit) — ⚠️ QA: define max mentions per note |
| State transitions | ✅ defined | AC-3 covers add vs unchanged/removed mention on update |
| Data validation | ✅ defined | S04 ERR-2 covers crafted data-user-id / sanitization |
| Concurrency | ⚠️ TBD | ⚠️ QA: two agents concurrently creating notes with different mentions (edit cannot add mentions — D-11) |
| Network/timeout | ✅ defined | ERR-1 covers Notification Service 5xx/timeout + retry |
🧪 Test Coverage Matrix — [NOTE-MENTION-S04]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | ERR-1/ERR-2 cover invalid UUID and XSS; ⚠️ QA: note with 0 mentions (no validation triggered), mention of a deactivated user (status ≠ active), mention of user from same company but different role |
| State transitions | ✅ defined | ERR-1 (invalid → reject/drop), ERR-2 (crafted HTML → strip) |
| Data validation | ✅ defined | ERR-2 covers crafted data-user-id; ⚠️ QA: malformed UUID format (not a valid UUID) |
| Concurrency | ⚠️ TBD | ⚠️ QA: two concurrent saves of the same note with conflicting mention sets |
| Network/timeout | ⚠️ TBD | ⚠️ QA: Launchpad times out during mention validation on note save — what happens? |
10. Rollout
| Field | Detail |
|---|---|
| Flag | cdp_notes_mention_enabled | default: OFF, enabled per company by Ops. |
| Stage 1 — Internal QA | Enable on a QA company; verify picker, validation, notification deep-link on web + mobile. |
| Stage 2 — Closed Beta | 3–5 companies (ideally migrated-from-CRM accounts that used mentions). Monitor notify success rate + latency. |
| Stage 3 — GA | Progressive per-company enable. |
| Backward compat | Notes without mentions unchanged; existing notes unaffected. |
11. Observability
| Event Name | Trigger | Properties |
|---|---|---|
cdp_note_mention_added | A note is created or edited with ≥1 mention (edit may add/re-mention — D-11) | note_id, contact_id, company_sso_id, mention_count, new_mention_count, author_sso_id, event (create/edit) |
cdp_note_mention_notify_sent | Notification dispatched per mention | note_id, recipient_sso_id, notification_id |
cdp_note_mention_notify_failed | Notification dispatch failed after retries | note_id, recipient_sso_id, reason, retry_count |
cdp_note_mention_invalid | A mention SSO UUID failed company validation | note_id, company_sso_id, invalid_sso_id, action (rejected/dropped) |
cdp_note_mention_picker_failed | Launchpad user-list call failed in the composer | company_sso_id, platform |
Dashboard owner: CDP Squad. Alerts: cdp_note_mention_notify_failed rate > 5% of mentions → Slack #cdp-ops; picker failure rate > 10% → investigate Launchpad dependency. Cadence: weekly review for the first 4 weeks post-GA; investigate immediately if notify success < 95%.
12. Success Metrics
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Adoption — mention usage | % of active companies (flag ON) with ≥1 note mention within 30 days | 0 (capability doesn't exist) | ≥ 30% of beta companies within 30 days of GA |
| ⭐ Notification reliability | notify_sent / (notify_sent + notify_failed) | N/A | ≥ 99% |
| Engagement — click-through | mentioned-notification clicks / notifications sent | N/A | ≥ 50% within 7 days |
| Performance — typeahead | P95 latency of mention typeahead | N/A | ≤ 500ms |
13. Dependencies
| Dependency | Owning Team | Deliverable Needed | Blocking? |
|---|---|---|---|
Qontak Unified Notification Service — POST /api/v1/notifications/chat | Notification/Platform | Stable endpoint + X-Api-Key for contact-service; recipient by sso_id; CTA/deep-link support to open the customer details page (exact URL format not prescribed — D-7) | YES |
| Notification Center on Qontak Unified Component | Platform / FE Platform | Renders the mention notification on web; defines notif_type/notif_category taxonomy to use | YES |
| RFC One Notification (mobile) | Mobile Squad | Deliver the CDP-note mention notification in mobile-qontak-crm only (One Notification V2) — not qontak-mobile-chat. Routing to open the customer details page: set extra.origin=external_url or add a native CDP-contact origin+route in detailModuleRouteMapping (notification_item_v2_mixin.dart + qontak_app_route.dart). mention/general categories already exist (notif_category.dart / notif_type_enum.dart) | YES (mobile tap-through) |
mobile-qontak-crm mention compose + render | Mobile Squad / CDP | Align the existing MpMentionToolbarButtonX(isCdp:true) output to the SSO-UUID anchor (OQ-10/OQ-12); mention-chip rendering or graceful fallback in the note list (no flutter_html today — OQ-11) | YES (mobile parity) |
| Launchpad user-search source | Launchpad | A company-scoped active-user list/search for the typeahead (GET /iag/v1/users exists; a search/paginated variant preferred for typeahead) | YES |
| contact-service note schema change | CDP Squad | mentioned_user_ids field + dual-form mention-anchor parser (web data-user-id + mobile href, D-9) + server-side HTML sanitization on the note write path | YES |
| customer-fe editor + renderer | CDP Squad / FE | Mention picker in the composer (OQ-7 spike); mention-chip rendering; DOMPurify allowlist for data-user-id | YES |
📊 Dependency Graph — CDP Notes @Mention
graph LR
F[CDP Notes @Mention] -->|BLOCKING| NS[Unified Notification Service]
F -->|BLOCKING| NC[Notification Center web component]
F -->|BLOCKING| LP[Launchpad user search]
F -->|BLOCKING| CS[contact-service: mentioned_user_ids + sanitize]
F -->|BLOCKING - OQ-7 spike| FE[customer-fe: picker + chip + DOMPurify ADD_ATTR]
F -->|mobile - OQ-10/11/12| MOBC[mobile: SSO-UUID anchor + chip render]
F -->|mobile - D-10/OQ-5| MOBN[mobile: notif route - external_url or native]
14. Key Decisions + Alternatives Rejected
14a — Decisions Made
All decisions dated 2026-06-03 (grounded code review).
| ID | Decision | Rationale (grounded) |
|---|---|---|
| D-1 | Option B — store mention as a resolved tag keyed to SSO UUID, with notification. | Mirrors the proven CRM mention model (note.rb) but uses CDP's identity (SSO UUID, not CRM int). The stored mentioned_user_ids is a stable, queryable key for notification + future features. |
| D-2 | Deliver via the Qontak Unified Notification Service, not a CDP-local mechanism. | The service already exists (POST /api/v1/notifications/chat, recipient by sso_id, deep-link click_action_url) with a Notification Center (web) and a mobile RFC — no need to build a notification channel. |
| D-3 | Mention candidates = active users in the same company (Launchpad GET /iag/v1/users?query= ILIKE search on full_name + email). | CDP tenancy is company-scoped; company-wide is the simplest correct default. Team-scoping (CRM's rule) is deferred to OQ-2 to avoid coupling to the team-permission initiative. |
| D-4 | Notify on create and edit, but idempotently — each user at most once per note; async; non-blocking. | Mirrors CRM (note.rb after_commit :save_notification fires on create and update; Crm::Notification.find_or_create_by on (note, user, Email) + a sent_at guard mean a newly-added mention is emailed once and re-mentions are deduped). CDP dispatches per save to mentioned users not yet notified for this note (tracked per (note_id, sso_id)). Async fire-and-forget keeps note-write latency within the 2s SLA. |
| D-5 | Server-side sanitize + validate on write. | Today HTML is only DOMPurify-sanitized at FE render (NotesList.vue:103-106 — no options, strips data-* by default). Storing mention HTML requires server-side sanitization AND FE DOMPurify must add ADD_ATTR: ['data-user-id', 'data-mention']. |
| D-6 | Notifications are permanent delivery records — the sender cannot retract them. | No retract/recall mechanism exists in the Unified Notification Service; the note itself can be edited or deleted, but already-dispatched notifications are not recalled. This is the explicit Q2 position for Story S03. |
| D-7 | The notification CTA opens the customer details page for the contact. The exact URL/deep-link format is not prescribed by this PRD — it is an engineering implementation detail. | The PM requirement is only that the recipient lands on the customer's details page (where the note lives); the precise URL/route is left to engineering. Mobile destination is mobile-qontak-crm only (D-10). |
| D-8 | Async notification dispatch is a net-new pattern in the notes service. | Zero events/Kafka exist in the notes write path today (contact_notes_service.go). Adding async fire-and-forget with backoff is new scope — not a trivial "call an endpoint." Engineering must design the async dispatch layer as part of CHG-003. |
| D-9 | The backend mention parser accepts BOTH anchor forms (web data-user-id/data-mention and mobile href=".../users/{id}/edit_user") and normalizes both to an SSO UUID. | Grounded: web's planned anchor and mobile's already-shipped MpMentionToolbarButtonX emit different markup (mention_toolbar_botton.dart:246-251). A parser keyed only on data-user-id would silently miss every mobile-authored mention. A single dual-form parser is the minimal correct backend contract. |
| D-10 | On mobile-qontak-crm, the mention notification routes via extra.origin = external_url to open the customer details page for v1; a native CDP-contact route is deferred. Delivered on mobile-qontak-crm only — not qontak-mobile-chat. | Grounded: the One Notification V2 tap handler (notification_item_v2_mixin.dart:92-151) routes only when origin = external_url or a mapped origin, and there is no native CDP-contact route (qontak_app_route.dart:133-141). external_url is the minimal path to a working tap-through; a native deep-link is a later enhancement (OQ-5). |
| D-11 | Edit allows re-mention + add new mentions (follows CRM). Editing a note keeps the mention affordance enabled — the agent can re-mention an already-mentioned user and add new mentions; existing chips are editable. Notification is idempotent per (note_id, sso_id): newly-added mentions notify; re-mentioning an already-notified user does not re-notify. | Grounded in CRM (app/models/crm/note.rb): mention parsing is identical on create and update (no branch), and notification fires after_commit on both — but Crm::Notification.find_or_create_by on (note, user, Email) + a sent_at guard mean each user is notified at most once per note. This reverses the earlier v1.3 "freeze on edit" at the PM's request, to match CRM (see screenshot — the CRM Edit Notes modal keeps @mention enabled). Caveat (matches CRM): the per-note notified set persists, so removing then re-adding the same user does not re-notify (OQ-14). |
14b — Alternatives Rejected
All rejections dated 2026-06-03.
| Alternative | Why Rejected |
|---|---|
| Keep mention as plain text (no notification) | Doesn't achieve parity; no notify = no collaboration value. |
| Store CRM-style integer user IDs | CDP identity is SSO UUID; integer IDs don't resolve in CDP. |
| Build a CDP-local notification/email path | The Unified Notification Service already covers web + mobile; duplicating it is wasted scope. |
Resolve mention names live only (no stored mentioned_user_ids) | Notification fan-out and any future "notes mentioning me" view need a stored, queryable key. |
| Team-scoped candidates in v1 | Couples this to the new flat multi-team model; deferred to keep scope tight (OQ-2). |
| DOMPurify default (no options) for rendering mention chips | DOMPurify strips all data-* attributes by default (NotesList.vue:103-106), silently destroying data-user-id/data-mention. Must use ADD_ATTR. |
15. Open Questions
| # | Type | Question | Mitigation / Default | Owner | Deadline |
|---|---|---|---|---|---|
| OQ-1 | Open Question | Which notif_type / notif_category / event_type values should a CDP note-mention use? Partially grounded (v1.2): mobile already defines notif_type = general(1) (notif_type_enum.dart:2) + notif_category = mention('2') (notif_category.dart) — confirm the web Notification Center + service accept the same values for a contact-note mention. | Default to general/mention (mobile-grounded); confirm web/service parity with Notification Platform squad before RFC. | CDP + Notification Platform | 2026-06-25 |
| OQ-2 | Open Question | Should mention candidates be company-wide (v1 default) or team-scoped (CRM precedent, note.rb:122-133)? If team-scoped, it requires the new Launchpad team membership API (same dependency as the team-permission PRD). | Default: company-wide for v1. | PM | 2026-06-25 |
| OQ-3 | Decision | Invalid mention SSO UUID on save → reject the whole save (422) or drop the mention + warn? | Recommended: drop-and-warn (mentioned_user_ids excludes the invalid ID; note saves; FE shows a warning chip) so a typo doesn't block the note. | PM + Eng | 2026-06-25 |
| OQ-4 | Decision | Max mentions per note (fan-out cap to bound notification volume + stay within 10,000-char limit)? | Recommended: 10 mentions max (each anchor ~80 chars = ~800 chars markup; well within the 10,000-char budget; bounded notification fan-out). | PM + Eng | 2026-06-25 |
| OQ-5 | Risk | How does the mention notification open the customer details page on mobile-qontak-crm? Grounded: the One Notification V2 tap handler routes only when extra.origin = external_url or a mapped origin, and there is no native CDP-contact route (notification_item_v2_mixin.dart:92-151, qontak_app_route.dart:133-141). | Mitigation (D-10): ship v1 with origin = external_url opening the customer details page (exact URL is an eng detail — D-7); confirm with Mobile Squad before closed beta; a native CDP-contact route is a later enhancement. This is for mobile-qontak-crm only — not qontak-mobile-chat. Block mobile GA if neither path is confirmed. | Notification + Mobile | 2026-06-25 |
| OQ-6 | Open Question | Is GET /iag/v1/users?query={term} (existing UserListParam.Query ILIKE search, list_all.go:22) sufficient for the typeahead, or does Launchpad need to expose a dedicated search/autocomplete endpoint with tighter pagination? | Default: use existing ?query= param with FE debounce. Escalate to Launchpad if latency > 500ms P95 at company scale. | Launchpad | 2026-06-25 |
| OQ-7 | Risk | Does MpRichTextEditor (Mekari Pixel 3 component) support a custom @ mention trigger extension? editorOptions at NoteInput.vue:155 is a hardcoded fixed toolbar array — no extension slot is currently visible. | Mitigation: run a discovery spike (≤3 days) to confirm if MpRichTextEditor exposes a custom-extension API. If not, escalate to Pixel 3 team for an extension hook or plan a custom Tiptap/Quill wrapper. Block RFC until resolved. | CDP FE + Pixel 3 squad | 2026-06-25 |
| OQ-8 | Open Question | The Unified Notification Service payload requires organization_id (UUID). contact-service stores company_sso_id (string). Are these the same value or different identifiers? If different, contact-service needs a lookup or must store the Launchpad company UUID. | Confirm with Platform squad. If mapping is needed, add a company_uuid field to the note context or a one-time lookup at notification dispatch time. | CDP Eng + Platform | 2026-06-25 |
| OQ-9 | Open Question | NoteInput.vue:40-43 shows a "Private" note permission. If a note is Private (restricted visibility), should @mentions still trigger a notification? If yes, the mentioned user may receive a notification deep-link that returns 403 when they click it. | Proposed: if the note is Private, suppress mention notifications (or notify with a generic message that does not link to the note). Confirm with PM. | PM | 2026-06-25 |
| OQ-10 | Risk | Does the mobile CDP member picker (GetListMemberCdp, which inserts /users/{id}/edit_user) yield the user's SSO UUID, or a CRM/member id? Backend mention identity is SSO UUID — a mismatch needs a mapping. | Mitigation: confirm the id type in GetListMemberCdp/note_cdp during the discovery spike; if it is not SSO UUID, add a mobile-side resolution or a backend {member_id → sso_uuid} lookup. | Mobile + CDP Eng | 2026-06-25 |
| OQ-11 | Decision | Mobile note rendering has no HTML renderer (stripHtml() in the list; Quill in the editor). For v1, build a styled mention chip on mobile, or accept a graceful plain-text fallback (never raw HTML)? | Recommended: graceful plain-text fallback for v1 (display @Name as text); defer a styled chip (would need an HTML/markdown renderer — no flutter_html today). | PM + Mobile | 2026-06-25 |
| OQ-12 | Decision | Align mention encoding by changing the mobile editor to emit a data-user-id/data-mention anchor (the Quill→HTML codec does not support data-* today), or by having the backend dual-parse both forms (D-9)? | Recommended: backend dual-parse (D-9) for v1 — no mobile editor/codec change needed; revisit a unified anchor later. | CDP Eng + Mobile | 2026-06-25 |
| OQ-13 | Open Question | Launchpad's user lookup returns a generic 404 for any not-found user (wrong keyword or deleted) — there is no per-user flag to differentiate a deleted user from a non-existent one. Should we (a) accept the generic not-found (show "No matching people" everywhere), or (b) ask Launchpad to expose a deleted/status flag so the UI can show a distinct "user no longer available" message (e.g. for a previously-mentioned user later deleted)? | Default (a): treat all not-found as "No matching people"; revisit (b) with the Launchpad team if differentiating deleted users becomes a real need. | PM + Launchpad | 2026-06-25 |
| OQ-14 | Decision | CRM never re-notifies a user once notified for a note — even if the mention is removed and later re-added (the Crm::Notification row persists). Should CDP match CRM exactly (append-only per-note notified set, no re-notify on remove→re-add), or reset so a removed-then-re-added mention notifies again? | Default: match CRM — append-only notified set keyed (note_id, sso_id); no re-notify on remove→re-add. Revisit if users report missed re-mentions. | PM + Eng | 2026-06-25 |
Appendix A — Grounded Code References
CRM mention (qontak.com) — the parity reference
mention_peopleparses<a data-user-id>anchors (and legacy/users/{id}/edit_user) —app/models/crm/note.rb:98-105.has_mention?—app/models/crm/note.rb:94-95.- Team-scoped validation ("users not in your team") —
app/models/crm/note.rb:122-133. - Notify on mention:
save_notificationcreatesCrm::Notification+send_email→MentionWorker.perform_async—app/models/crm/note.rb:44-61;app/workers/mention_worker.rb. - Composer uses TinyMCE
tinimce-mention—app/views/crm/shared/_note.html.erb:456.
CDP backend (contact-service) — mention ABSENT today
- No
mentionlogic in notes service/handler/repo (grep returns nothing); create validates length only —internal/app/service/contact_notes/contact_notes_service.go:268-274. - 10,000-character limit on note content:
contact_notes_service.go:272—if len(req.Note) > 10000 { return errors.New("note content exceeds maximum length of 10,000 characters") }. SetDefaults()stamps timestamps; no mention fields on the model —internal/app/repository/contact_notes/base.go.- Zero async events/Kafka in the notes write path today — adding async notification dispatch is a net-new pattern for this service.
CDP frontend (customer-fe) — mention ABSENT today
NoteInput.vueusesMpRichTextEditor(Mekari Pixel 3 component);editorOptionsatNoteInput.vue:155is a hardcoded fixed toolbar array (['heading'], ['bold', 'italic', 'underline', 'strike']...); no custom extension slot is visible. WhetherMpRichTextEditorsupports a mention extension is unverified — see OQ-7.sanitizeHtml()inNotesList.vue:103-106callsDOMPurify.sanitize(value)with NO options — DOMPurify's default strips alldata-*attributes, sodata-user-id/data-mentionwould be silently removed. Must useADD_ATTR: ['data-user-id', 'data-mention'].- Author shown via
<MpAvatar :name="note.owner_name" />—NotesList.vue:20.
Launchpad (user source)
GET /iag/v1/usersListAll —internal/server/rest_router.go:84.UserListParamhasQuery string(schemaquery) that does ILIKE search onfull_name+email—db/query/users.sql:115((users.full_name ILIKE @query::text OR users.email ILIKE @query::text)). Endpoint is paginated; FE must handle pagination.User.FullNameissql.NullString(may be null — displayfull_name ?? email).User.Avatarissql.NullString(may be null — FE must null-check). —internal/app/repository/models.go:209-244.GET /iag/v1/teams/{id}/members—:123. Users carrysso_id,full_name,avatar,status.
Notification (Unified Notification Service — from reference docs)
POST /api/v1/notifications/chat(X-Api-Key), body incl.sso_id,title/description,click_action,click_action_url,event_type,notif_type,notif_category,organization_id,skip_fcm. Mark-as-read:PUT /api/v1/notifications/mark_as_read/{id},PUT /api/v1/notifications/mark_all_as_read.
CDP mobile (mobile-qontak-crm) — grounded v1.2
- Compose already ships. CDP note editor
features/crm_note/lib/src/presentation/screens/detail_note/detail_note_screen.dartusesMpTextEditor(Quill); the custom toolbar's first button isMpMentionToolbarButtonX(icon: …textEditorMention, controller, isCdp: widget.argument.isCdp)—detail_note_screen.dart:728-733. WhenisCdp == trueit opens a CDP member picker bottom-sheet viaGetListMemberCdpBloc/GetListMemberCdp—features/qontak_custom_form/lib/src/presentation/widgets/text_editor/toolbar/mention_toolbar_botton.dart:12,72-80,330-403. - Encoding gap. The mention is inserted as a plain Quill
LinkAttributewith href'../../../users/{id}/edit_user'(mention_toolbar_botton.dart:122-142,246-251,398-403);deltaToHtmlthen yields<a href=".../users/{id}/edit_user">@Name</a>— not the webdata-user-id/data-mentionanchor. Note content field isnote(String) on the CDP request —features/crm_note/lib/src/data/models/remote/note_cdp_request/note_cdp_request.dart:18; create/edit serialize viaTextAreaFieldHelper.deltaToHtml—detail_note_screen.dart:1252,1348. - Render gap. No HTML renderer in
crm_note(noflutter_html/markdown). Note list shows(noteItem.note ?? '').stripHtml()—features/crm_note/lib/src/presentation/screens/note/note_screen.dart:264via plainTextinnote_item.dart:51; reopening a note returns to the Quill editor (htmlToJson,detail_note_screen.dart:329) where a link renders plain anddata-*is lost on round-trip. - Notification taxonomy exists.
NotifCategory/NotifCategoryV2definemention = '2'—features/crm_misc/lib/src/util/notif_category.dart;NotifTypeEnumdefinesgeneral(1)(andapproval(2)) —features/crm_misc/lib/src/config/constant/enum/notif_type_enum.dart:2. - Tap-through gap.
notification_item_v2_mixin.dart:92-151(_navigateToItem/_isValidOrigin) routes only origins present inQontakAppRoute.detailModuleRouteMapping(deal,contact,lead,task,company,expense,external_url) —qontak_app_route.dart:133-141; it opensclick_action_urlonly fororigin == external_url(:134-139), else shows the "cannot redirect" toast (:119-126). Nocustomers/Contact360/CDP origin or native CDP-contact-note route exists. Notifications gated byflag_one_notification(default OFF) + profileuseQontakOneNotif.
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 1.6 | 2026-06-25 | Add mention preview card story | S6 (Scope Changes), S7 (CHG-004), S9 (S05 + guard-rail renumber), S13 | UPDATED | Added NOTE-MENTION-S05 — Preview a mentioned user on the notes list: hover (web) / tap (mobile) a mention chip → a preview card resolving the mentioned user's name, email, staff level, team(s) via Launchpad (Figma 15091-119388), incl. an inactive / not-found state (Figma 15091-448947 — same generic-404 constraint as OQ-13). Added CHG-004 (preview card render + data source), extended Scope Changes (Frontend + Mobile preview card), added the S05 dependency line, and renumbered the guard rail NOTE-MENTION-S05-NEG → S06-NEG to keep numbering sequential (no Jira tickets exist yet — sync is off). |
| 1.5 | 2026-06-23 | Edit re-mention behavior (grounded) | S3 (Non-Goals), S7 (CHG-001/003), S8 (API#2), S9 (flow + S01/S02/S03), S11, S14 (D-4/D-11), S15 | UPDATED | Reversed the v1.3 "freeze on edit" — edit now allows re-mention + add new mention, matching CRM. Grounded in qontak.com app/models/crm/note.rb: mention parsing is identical on create and update (no branch), notification fires after_commit on both, and Crm::Notification.find_or_create_by on (note, user, Email) + a sent_at guard make notification idempotent per (note_id, sso_id) (newly-added mentions notify; re-mentioning an already-notified user does not re-notify; a user is notified at most once per note). Updated: CHG-001 edit-mode row (affordance enabled on edit), CHG-003 update row (re-parse + notify newly-added only), API#2 update path, system-flow step 5 + mermaid, S01/S02 AC-4 + S03 AC-3 (edit allows re-mention + add; idempotent notify), D-4 (notify on create and edit, idempotent), D-11 rewritten (edit allows re-mention + add — reverses v1.3 freeze), Non-Goal #5 (→ no notification-preference management), observability cdp_note_mention_added (create or edit; +new_mention_count/event). Added OQ-14 (remove→re-add re-notify policy — default match CRM). |
| 1.4 | 2026-06-23 | Not-found user scenario | S6 (Constraints), S8 (S01/S04), S15 | UPDATED | Added the user-not-found scenario grounded in Launchpad behavior: GET /iag/v1/users returns a generic 404 for any unresolved user — a wrong keyword / no match and a deleted user are indistinguishable (Launchpad has no per-user deleted flag). Added a Constraints row ("Launchpad not-found semantics"); S01 ERR-2 (typeahead: typo or deleted → "No matching people"); S04 ERR-3 (validation: a mentioned SSO UUID for a deleted/invalid user → generic 404 → rejected/dropped per OQ-3, cannot tell deleted from invalid); and OQ-13 (accept the generic 404 vs. ask Launchpad for a deleted/status flag). |
| 1.3 | 2026-06-22 | Behavior adjustments | S3 (Non-Goals), S6, S7, S8 (S01/S02/S03), S9, S11, S13, S14, S15 | UPDATED | Three PM adjustments. (1) Notification CTA → customer details page: removed the explicit URL format (https://{host}/customers/{contact_id}?tab=...) throughout (API#3/#4, S03 title/data/ACs, system flow, D-7, OQ-5, deps); the CTA just opens the customer details page — exact URL/deep-link is an engineering detail. (2) Mobile scope = mobile-qontak-crm only: the mention notification is delivered in the CRM mobile app (One Notification V2) only, not qontak-mobile-chat — clarified in Constraints (Platform + Notification delivery), new Non-Goal #7, S03, Dependencies (RFC row), D-10. (3) Edit freezes mentions (new D-11, Google-Chat-style): editing a note that contains mentions disables the mention affordance (no re-mention, no new mention; existing chips read-only); mentioned_user_ids preserved unchanged on update; no edit-time notification (removed the update-diff logic). Updated CHG-001 (new edit-mode row), CHG-003 (update row), API#2 update path, S01/S02 AC-4, S03 AC-3, D-4 (notify on create only), Non-Goal #5, observability cdp_note_mention_added trigger. |
| 1.2 | 2026-06-18 | Mobile gap (grounded) | Scope Changes, S6, S7, S8, S9 (S02/S03), S13, S14, S15, Appendix A | UPDATED | Mobile-grounded adjustment against mobile-qontak-crm. Scope Changes + frontmatter: added Mobile. Corrected the prior assumption that mobile has no mention: compose already ships (MpMentionToolbarButtonX(isCdp:true) + CDP member picker) — real gaps are (a) encoding (mobile emits <a href=".../users/{id}/edit_user">, not the SSO-UUID data-user-id anchor), (b) rendering (note list stripHtml() → no chip; no flutter_html), (c) notif tap-through (V2 handler ignores click_action_url unless origin=external_url; no CDP route). Constraints: rewrote Platform row + added 3 grounded mobile rows; Notification delivery now grounded (general(1)/mention('2') exist on mobile). CHG-001/002 split web vs mobile rows; CHG-003 + API#2 dual-form parser; API#4 mobile routing. S02 corrected (mention button exists; AC-1/2/3 on SSO-UUID anchor + render); S03 retitled (Web & Mobile) + AC-2b mobile tap-through. Dependencies: refined mobile RFC row + added mobile compose/render row; dependency graph split mobile nodes. Decisions: added D-9 (dual-form parser), D-10 (route via external_url). OQ-1 partially resolved (general/mention grounded); OQ-5 grounded (external_url vs native route); added OQ-10 (mobile id = SSO UUID?), OQ-11 (mobile chip vs plain-text), OQ-12 (mobile codec vs backend dual-parse); all OQ deadlines → 2026-06-25. Appendix A: added grounded CDP-mobile references. |
| 1.1 | 2026-06-03 | Score fixes (11 grounded gaps) | S4, S5, S6, S7, S8, S12, S13, S14, S15 | UPDATED | Constraints: added 10k-char limit + max-mentions constraint + MpRichTextEditor feasibility risk + read/write role scope + Launchpad typeahead ?query= spec + null handling for FullName/Avatar + explicit async scope. CHG-002: specified DOMPurify ADD_ATTR: ['data-user-id','data-mention'] (default strips data-* silently). CHG-003: added update-diff logic, validation batching strategy, async pattern as net-new scope, organization_id mapping. API Behavior 3: specified click_action_url format + notif_type TBD + org_id mapping. S03 CANNOT block + 4th UI state added (Q2 fix). Test Coverage Matrices added for S01, S02, S04. Key Decisions: all dated + D-6 (notifications not retractable), D-7 (click_action_url), D-8 (async net-new). Open Questions: all deadlines → 2026-06-17; OQ-5 mitigation explicit; OQ-7 (MpRichTextEditor feasibility spike), OQ-8 (org_id mapping), OQ-9 (Private note + mention) added. |
| 1.0 | 2026-06-03 | Grounded draft | All | CREATED | NEW adjustment PRD: add @mention + notify to CDP Notes (Option B — SSO-UUID-keyed mention tag). Grounded in CRM mention (qontak.com note.rb), CDP gaps (contact-service + customer-fe), Launchpad user source, and the Qontak Unified Notification Service. |