Skip to main content

[PRD] Qontak CDP | Customers/Notes | Mention User in Notes — Adj: add @mention + notify

FieldValue
PMPM Qontak
PRD Version1.6
StatusDRAFT
PRD TypeADJUSTMENT
EpicTF-3184
SquadCDP Squad
RFC LinkN/A — to be created
Figma MasterTBD — mention picker + mention chip + mention preview card
AnchorNo — standalone adjustment to the existing CDP Notes feature
Labelsepic:qontak-cdp | module:customers | feature:notes-mention
Last Updated2026-06-25

Table of Contents


2. Adjustment Context

FieldDetail
Parent featureCDP 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 PRDN/A — CDP Notes shipped without an anchor; this adjustment is standalone.
Adjustment scopeAdd 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 preservedExisting note create/read/update/delete, attachments, ownership, and permission behavior are unchanged. Notes without mentions behave exactly as today.
Reason for adjustmentLegacy 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

PersonaRoleGoalPainWorkaround
Primary — CS / Sales AgentAgent working a contact in CDPTag a teammate on a note so they pick up / are aware of a follow-up on this contactCDP notes can't mention anyone; the teammate never gets notifiedCopies the contact link into an external chat and pings the teammate manually
Secondary — Mentioned teammateAgent/Supervisor who gets mentionedGet notified with a direct link back to the exact contact noteNo notification exists; they only find out if they happen to open the contactRelies 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.

  • Backendcontact-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 web data-user-id/data-mention form and the mobile <a href=".../users/{id}/edit_user"> form and normalize to SSO UUID (D-9); mention typeahead resolved via Launchpad.
  • Frontendqontak-customer-fe: @-mention typeahead in MpRichTextEditor (feasibility spike — OQ-7), DOMPurify ADD_ATTR for data-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).
  • Mobilemobile-qontak-crm: mention compose already ships (MpMentionToolbarButtonX(isCdp:true) + CDP member picker, mention_toolbar_botton.dart) but emits an href anchor, not an SSO-UUID data-user-id anchor (encoding/identity gap — OQ-10/OQ-12); note rendering strips HTML (note_screen.dart:264 stripHtml()), so a mention chip is net-new (OQ-11); One Notification V2 already defines a mention category but the tap-through has no CDP route — mention notifications must route via extra.origin=external_url or 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

  1. No migration of legacy CRM mentions — historical CRM notes carry data-user-id referencing CRM integer user IDs; their handling (strip-to-text) is owned by the separate Legacy CRM Notes → CDP migration PRD, not this one.
  2. No new notification surface — this reuses the existing Qontak Unified Notification Service + Notification Center; it does not build a notification center.
  3. No mention of non-users — only Qontak One users in the same company can be mentioned; no mentioning of contacts, teams, or external emails.
  4. No rich-text overhaul — this adds the mention token only; it does not change the rest of the note editor's formatting capabilities.
  5. 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.
  6. No cross-company mention — mention candidates are scoped to the note's company (company_sso_id); no cross-tenant mentions.
  7. No qontak-mobile-chat notification — 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

ConstraintValue
Feature flagcdp_notes_mention_enabled | default: OFF — enabled per company by Ops.
PlatformBoth 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).
IdentityMentions are keyed to the user's SSO UUID (not CRM integer IDs).
Mention candidate scopeActive 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.
PerformanceMention typeahead suggestions ≤ 500ms P95. Note create with mentions ≤ 2s P95 (notification dispatch is async — must not block the note write).
Note content limitCDP 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 scopeWrite: 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 deliveryQontak 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.
SecurityNote 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 feasibilityNoteInput.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 patternThere 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 compatNotes without mentions behave exactly as today; existing notes are unaffected.
Plan scopeSame plans that have CDP Notes — Growth and Enterprise. Not Starter.

7. Feature Changes

CHG-001 — Mention token in the note editor (Web + Mobile)

ElementBeforeAfter
Note composer — web (NoteInput.vue)Plain content editor; no @ affordanceTyping @ 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 existsMpMentionToolbarButtonX(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 anchorReuse 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 contentHTML content, no mention markupHTML 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)

ElementBeforeAfter
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)

ElementBeforeAfter
ContactNote modelcontent + attachments + owner onlyadds mentioned_user_ids []string (SSO UUIDs), populated by parsing mention anchors on create/update
Note createlength 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 logicon 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 pathafter 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)

ElementBeforeAfter
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 sourceLaunchpad company-scoped user lookup keyed by the mention's SSO UUID → {full_name, email, avatar, staff_level, teams[]}.

8. API & Webhook Behavior

#BehaviorEntity AffectedTriggered ByExpected BehaviorFailure Behavior
1List mentionable users (typeahead)Launchpad usersAgent 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 nullLaunchpad unavailable: picker shows "Couldn't load people — try again"; agent can still save the note without a mention; cdp_note_mention_picker_failed logged
2Create/update note with mentionsContactNote (+ mentioned_user_ids) in CDPAgent 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.
3Dispatch mention notificationNotification (Unified Notification Service)Async, after note with new mentions is committedcontact-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 = falseNotification 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)
4Mentioned user opens notificationRecipient 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

  1. Agent opens a contact in CDP and starts a note; types @.
  2. FE calls Launchpad to list/search active company users; shows a typeahead (name + avatar).
  3. Agent selects a user → a mention chip (backed by SSO UUID) is inserted into the note HTML.
  4. 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.
  5. 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 recipient sso_id and a CTA that opens the customer details page.
  6. 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.
  7. 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 StoryImportanceMockupTechnical NotesAcceptance 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 HaveFigma: 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: composer

Before-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 HaveFigma (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 shipsMpMentionToolbarButtonX(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 HaveFigma: 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 HaveFigma: 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 HaveFigma (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: Launchpad

Before-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]

DimensionCoverageNotes
Boundary values⚠️ partialAC-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✅ definedAC-2 (chip inserted), AC-3 (saved with mentions), ERR-1 (Launchpad down)
Data validation⚠️ partialAC-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✅ definedERR-1 covers Launchpad unavailable; note savable without mention

🧪 Test Coverage Matrix — [NOTE-MENTION-S02]

DimensionCoverageNotes
Boundary values⚠️ partial⚠️ QA: offline → picker unavailable; slow connection > 500ms typeahead threshold
State transitions✅ definedAC-1/AC-2 cover same server enforcement as web; ERR-1 covers offline
Data validation✅ definedIdentical 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✅ definedERR-1 offline message

🧪 Test Coverage Matrix — [NOTE-MENTION-S03]

DimensionCoverageNotes
Boundary values⚠️ partialNeed: note with 0 mentions (no notify), 1, and many mentions (fan-out limit) — ⚠️ QA: define max mentions per note
State transitions✅ definedAC-3 covers add vs unchanged/removed mention on update
Data validation✅ definedS04 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✅ definedERR-1 covers Notification Service 5xx/timeout + retry

🧪 Test Coverage Matrix — [NOTE-MENTION-S04]

DimensionCoverageNotes
Boundary values⚠️ partialERR-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✅ definedERR-1 (invalid → reject/drop), ERR-2 (crafted HTML → strip)
Data validation✅ definedERR-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

FieldDetail
Flagcdp_notes_mention_enabled | default: OFF, enabled per company by Ops.
Stage 1 — Internal QAEnable on a QA company; verify picker, validation, notification deep-link on web + mobile.
Stage 2 — Closed Beta3–5 companies (ideally migrated-from-CRM accounts that used mentions). Monitor notify success rate + latency.
Stage 3 — GAProgressive per-company enable.
Backward compatNotes without mentions unchanged; existing notes unaffected.

11. Observability

Event NameTriggerProperties
cdp_note_mention_addedA 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_sentNotification dispatched per mentionnote_id, recipient_sso_id, notification_id
cdp_note_mention_notify_failedNotification dispatch failed after retriesnote_id, recipient_sso_id, reason, retry_count
cdp_note_mention_invalidA mention SSO UUID failed company validationnote_id, company_sso_id, invalid_sso_id, action (rejected/dropped)
cdp_note_mention_picker_failedLaunchpad user-list call failed in the composercompany_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

MetricDefinitionBaselineTarget
⭐ Adoption — mention usage% of active companies (flag ON) with ≥1 note mention within 30 days0 (capability doesn't exist)≥ 30% of beta companies within 30 days of GA
⭐ Notification reliabilitynotify_sent / (notify_sent + notify_failed)N/A≥ 99%
Engagement — click-throughmentioned-notification clicks / notifications sentN/A≥ 50% within 7 days
Performance — typeaheadP95 latency of mention typeaheadN/A≤ 500ms

13. Dependencies

DependencyOwning TeamDeliverable NeededBlocking?
Qontak Unified Notification ServicePOST /api/v1/notifications/chatNotification/PlatformStable 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 ComponentPlatform / FE PlatformRenders the mention notification on web; defines notif_type/notif_category taxonomy to useYES
RFC One Notification (mobile)Mobile SquadDeliver 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 + renderMobile Squad / CDPAlign 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 sourceLaunchpadA 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 changeCDP Squadmentioned_user_ids field + dual-form mention-anchor parser (web data-user-id + mobile href, D-9) + server-side HTML sanitization on the note write pathYES
customer-fe editor + rendererCDP Squad / FEMention picker in the composer (OQ-7 spike); mention-chip rendering; DOMPurify allowlist for data-user-idYES

📊 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).

IDDecisionRationale (grounded)
D-1Option 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-2Deliver 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-3Mention 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-4Notify 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-5Server-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-6Notifications 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-7The 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-8Async 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-9The 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-10On 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-11Edit 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.

AlternativeWhy Rejected
Keep mention as plain text (no notification)Doesn't achieve parity; no notify = no collaboration value.
Store CRM-style integer user IDsCDP identity is SSO UUID; integer IDs don't resolve in CDP.
Build a CDP-local notification/email pathThe 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 v1Couples this to the new flat multi-team model; deferred to keep scope tight (OQ-2).
DOMPurify default (no options) for rendering mention chipsDOMPurify strips all data-* attributes by default (NotesList.vue:103-106), silently destroying data-user-id/data-mention. Must use ADD_ATTR.

15. Open Questions

#TypeQuestionMitigation / DefaultOwnerDeadline
OQ-1Open QuestionWhich 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 Platform2026-06-25
OQ-2Open QuestionShould 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.PM2026-06-25
OQ-3DecisionInvalid 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 + Eng2026-06-25
OQ-4DecisionMax 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 + Eng2026-06-25
OQ-5RiskHow 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 + Mobile2026-06-25
OQ-6Open QuestionIs 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.Launchpad2026-06-25
OQ-7RiskDoes 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 squad2026-06-25
OQ-8Open QuestionThe 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 + Platform2026-06-25
OQ-9Open QuestionNoteInput.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.PM2026-06-25
OQ-10RiskDoes 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 Eng2026-06-25
OQ-11DecisionMobile 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 + Mobile2026-06-25
OQ-12DecisionAlign 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 + Mobile2026-06-25
OQ-13Open QuestionLaunchpad'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 + Launchpad2026-06-25
OQ-14DecisionCRM 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 + Eng2026-06-25

Appendix A — Grounded Code References

CRM mention (qontak.com) — the parity reference

  • mention_people parses <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_notification creates Crm::Notification + send_emailMentionWorker.perform_asyncapp/models/crm/note.rb:44-61; app/workers/mention_worker.rb.
  • Composer uses TinyMCE tinimce-mentionapp/views/crm/shared/_note.html.erb:456.

CDP backend (contact-service) — mention ABSENT today

  • No mention logic 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:272if 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.vue uses MpRichTextEditor (Mekari Pixel 3 component); editorOptions at NoteInput.vue:155 is a hardcoded fixed toolbar array (['heading'], ['bold', 'italic', 'underline', 'strike']...); no custom extension slot is visible. Whether MpRichTextEditor supports a mention extension is unverified — see OQ-7.
  • sanitizeHtml() in NotesList.vue:103-106 calls DOMPurify.sanitize(value) with NO options — DOMPurify's default strips all data-* attributes, so data-user-id/data-mention would be silently removed. Must use ADD_ATTR: ['data-user-id', 'data-mention'].
  • Author shown via <MpAvatar :name="note.owner_name" />NotesList.vue:20.

Launchpad (user source)

  • GET /iag/v1/users ListAll — internal/server/rest_router.go:84. UserListParam has Query string (schema query) that does ILIKE search on full_name + emaildb/query/users.sql:115 ((users.full_name ILIKE @query::text OR users.email ILIKE @query::text)). Endpoint is paginated; FE must handle pagination.
  • User.FullName is sql.NullString (may be null — display full_name ?? email). User.Avatar is sql.NullString (may be null — FE must null-check). — internal/app/repository/models.go:209-244.
  • GET /iag/v1/teams/{id}/members:123. Users carry sso_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.dart uses MpTextEditor (Quill); the custom toolbar's first button is MpMentionToolbarButtonX(icon: …textEditorMention, controller, isCdp: widget.argument.isCdp)detail_note_screen.dart:728-733. When isCdp == true it opens a CDP member picker bottom-sheet via GetListMemberCdpBloc/GetListMemberCdpfeatures/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 LinkAttribute with href '../../../users/{id}/edit_user' (mention_toolbar_botton.dart:122-142,246-251,398-403); deltaToHtml then yields <a href=".../users/{id}/edit_user">@Name</a>not the web data-user-id/data-mention anchor. Note content field is note (String) on the CDP request — features/crm_note/lib/src/data/models/remote/note_cdp_request/note_cdp_request.dart:18; create/edit serialize via TextAreaFieldHelper.deltaToHtmldetail_note_screen.dart:1252,1348.
  • Render gap. No HTML renderer in crm_note (no flutter_html/markdown). Note list shows (noteItem.note ?? '').stripHtml()features/crm_note/lib/src/presentation/screens/note/note_screen.dart:264 via plain Text in note_item.dart:51; reopening a note returns to the Quill editor (htmlToJson, detail_note_screen.dart:329) where a link renders plain and data-* is lost on round-trip.
  • Notification taxonomy exists. NotifCategory/NotifCategoryV2 define mention = '2'features/crm_misc/lib/src/util/notif_category.dart; NotifTypeEnum defines general(1) (and approval(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 in QontakAppRoute.detailModuleRouteMapping (deal,contact,lead,task,company,expense,external_url) — qontak_app_route.dart:133-141; it opens click_action_url only for origin == external_url (:134-139), else shows the "cannot redirect" toast (:119-126). No customers/Contact360/CDP origin or native CDP-contact-note route exists. Notifications gated by flag_one_notification (default OFF) + profile useQontakOneNotif.

PRD CHANGELOG

VersionDateBySectionTypeSummary
1.62026-06-25Add mention preview card storyS6 (Scope Changes), S7 (CHG-004), S9 (S05 + guard-rail renumber), S13UPDATEDAdded 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.52026-06-23Edit 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), S15UPDATEDReversed 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.42026-06-23Not-found user scenarioS6 (Constraints), S8 (S01/S04), S15UPDATEDAdded 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.32026-06-22Behavior adjustmentsS3 (Non-Goals), S6, S7, S8 (S01/S02/S03), S9, S11, S13, S14, S15UPDATEDThree 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.22026-06-18Mobile gap (grounded)Scope Changes, S6, S7, S8, S9 (S02/S03), S13, S14, S15, Appendix AUPDATEDMobile-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.12026-06-03Score fixes (11 grounded gaps)S4, S5, S6, S7, S8, S12, S13, S14, S15UPDATEDConstraints: 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.02026-06-03Grounded draftAllCREATEDNEW 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.