RFC: Embeddable Create-Ticket Form in CDP Customer Detail (Web)
Document Conventions (do not remove)
This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. Mark sections
N/A — reasonwhen truly inapplicable rather than deleting them.It is agent-execution-ready: the §1 Design References, §2 Repo Reading Guide (Detail 2.0), mermaid diagrams, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 Ready for agent execution: yes.
Delivery & project management live elsewhere. This RFC is the technical artifact only — no staffing, effort, timeline, or rollout schedule. Those live in the initiative's
delivery/folder. Until handed to delivery, the Delivery row readsnot yet handed to delivery.The YAML frontmatter is the machine-readable index; the Metadata table is the human-readable governance record. Both must agree on every shared field.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | Human label IDEA; YAML status: carries the remapped linter enum draft. This RFC cannot reach RFC/AGREED until the blocking CRM dependencies (§5 OQ-1/OQ-2/OQ-3) are closed. |
| DRI | Zhelia Alifa | Single accountable owner. Per-task staffing (Jovi — Frontend) lives in delivery/. |
| Team | cdp | Advisory squad slug from PRD / initiative README. |
| Author(s) | Jovi (CDP Frontend) | Primary author. |
| Reviewers | CDP tech lead; CRM/Omnichannel tech lead | CRM owns the embed contract + auth — their review gates the contract assumptions. |
| Approver(s) | CDP engineering lead; infosec | Infosec must approve the iframe sandbox + postMessage origin model. |
| Submitted Date | 2026-06-30 | ISO-8601. |
| Last Updated | 2026-06-30 | ISO-8601; bump on every material edit. |
| Target Release | 2026-Q3 | Quarter (gated on CRM dependencies). |
| Target Quarter | 2026-Q3 | Advisory, from PRD / initiative README. |
| Delivery | not yet handed to delivery | Pointer to delivery/ once handed off. |
| Related | ../prds/prd-create-ticket-embeddable-web.md · ../prds/prd-create-ticket-from-cdp-anchor.md | SUPPORT PRD (web) + anchor PRD. |
| Discussion | TBD — CDP squad Slack | — |
Type: frontend Sub-type: new-feature
Sections at a Glance
- Overview (incl. §1 Design References — Figma, design system version, design QA)
- Technical Design (Repo Reading Guide → mermaid architecture → UI contracts → Asset Inventory)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan (incl. §4 Agent Execution Plan + Verification & Rollback Recipe)
- Concern, Questions, or Known Limitations
- Comment logs
- Ready for agent execution
1. Overview
CDP's Customer Detail → Tickets section
(features/customers/detail/components/AssociatedTickets.vue) can today only
associate an existing ticket to a customer (POST /v1/leads/{contactId}/tickets/{ticketId}).
There is no way to create a new ticket without leaving the customer record for
the Ticket Dashboard / Omnichannel. The sibling Deal flow already embeds a
CRM-owned create form (AssociatedDeals.vue → iframe /embed/deals/create/), so
the experience is asymmetric.
This RFC specifies the CDP frontend integration that embeds the CRM-owned
Create-Ticket form (/embed/ticket/create) inside the Tickets section — mirroring
the shipped Deal embed pattern, but consuming the new typed, versioned
postMessage contract (EMBED_INIT ⇄ TICKET_CREATED / TICKET_CREATE_ERROR
/ EMBED_CLOSE / EMBED_RESIZE) defined by the CRM FE RFC — not the legacy
{msg} string contract the Deal embed uses today.
CDP builds no ticket form, no pipeline/layout logic, and no create API — all
CRM-owned. CDP's scope is: the entry point, the iframe panel, the origin-validated
message handler, the EMBED_INIT sender, the post-create list refresh, an optional
fallback association call, observability, and the permission + feature-flag gate.
Success Criteria
- Create reliability
created / (created + create_error)≥ 99% (PRD §12). - Association success
associated / created≥ 99% (PRD §12). - Parity: ticket-create available on the same customer-detail surface where deal-create already is (100% of CDP customer-detail surfaces).
- Zero regression to the existing associate-existing flow (it is untouched).
Out of Scope
- Building the ticket form / pipeline selection / custom-layout rendering (CRM-owned).
- The create API (
POST /api/mobile/v2.8/tickets) — CRM-owned. - Mobile (sibling SUPPORT PRD
prd-create-ticket-mobile.md). - Ticket viewing/editing/history in CDP (anchor Phase 2/3).
- Any change to the existing "Associate existing ticket" flow.
- Chat/room linking (
roomIdis alwaysnullfor CDP — no room context).
Related Documents
- PRD (web, SUPPORT):
../prds/prd-create-ticket-embeddable-web.md - Anchor PRD:
../prds/prd-create-ticket-from-cdp-anchor.md - CRM Omnichannel BE RFC (Phase 1): https://jurnal.atlassian.net/wiki/spaces/QON/pages/51107070211 — reviewed: defines
data_source/embed_sourceallow-list + JWT Bearer auth; marks CDP reuse OUT of Phase 1. - CRM Omnichannel FE RFC (Phase 1): https://jurnal.atlassian.net/wiki/spaces/QON/pages/51112771612 — reviewed: defines the typed versioned
postMessagecontract +EMBED_INITcontact transport; marks CDP reuse OUT of Phase 1. - Reference implementation in repo:
AssociatedDeals.vue(shipped Deal embed — legacy contract; pattern reused, contract superseded).
Assumptions
- CRM exposes
/embed/ticket/createon the same CRM origin already used by the Deal embed (config.CRM_V3_EMBED_URL) — so CDP reuses that config key. - The CRM ticket embed implements the typed
postMessagecontract from the CRM FE RFC (version: 1, origin-validated), not the legacy{msg}string. - The embed authenticates itself from the host session (per BE RFC: direct
JWT Bearer, not a
tokenURL param) — so CDP does not append atokenquery param the wayAssociatedDeals.vuedoes. (See OQ-2 — unconfirmed for Qontak One SSO.) - CDP passes contact context via
EMBED_INITpostMessage, keyed onqontakCustomerId(the routecontactId), not via URL params.
Dependencies
| Dependency | Owner | Availability | Blocking? |
|---|---|---|---|
CDP-reusable embed at /embed/ticket/create (accepts EMBED_INIT from a non-chat host) | CRM/Omnichannel | needs-building — OUT of CRM Phase 1 | YES (OQ-1) |
| Qontak One auth resolution (v2.8 JWT vs Mekari SSO in the iframe) | CRM + Platform | blocked — unconfirmed | YES (OQ-2) |
Typed postMessage contract frozen (EMBED_INIT / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_CLOSE / EMBED_RESIZE) | CRM Frontend | needs-building | YES (OQ contract) |
CDP embed_source (e.g. embed-web-cdp) added to BE ALLOWED_EMBED_SOURCES | CRM Backend | needs-building | YES (for TCKT-S05) |
| Ticket-create permission key for the CDP entry | CRM + CDP | needs-confirming | YES (OQ-4) |
config.CRM_V3_EMBED_URL (CRM embed base URL) | CDP (exists) | exists — useCustomConfig | no |
POST /v1/leads/{contactId}/tickets/{ticketId} (fallback associate) | CDP (exists) | exists — used in AssociatedTickets.vue | no |
Design References (frontend-specific — required)
| PRD-named surface | Figma / design link | Frame name | Design system version | Design QA contact | Notes |
|---|---|---|---|---|---|
| CDP Create-Ticket entry point (Tickets section header menu) | n/a — design pending (PRD Figma Master = TBD) | TBD | @mekari/pixel3@1.0.10-dev.0 (from package.json) | TBD | Mirror the Deal "+" popover (AssociatedDeals.vue MpPopover with "Associate existing" / "Create new deal"). |
| Embed panel (loading / error / success states) | n/a — design pending | TBD | @mekari/pixel3@1.0.10-dev.0 | TBD | Mirror the Deal create panel (back-arrow header + sandboxed iframe). Per PRD OQ-8 default = inline panel like Deal. |
Frames are missing for both surfaces → this is a blocker recorded in §5 (OQ-8 / design pending). The UI shell is fully specified by the in-repo Deal embed pattern, so the panel/entry-point chunks (4.C #1–#3) can proceed against that pattern; pixel-level polish waits on Figma.
Detail 1.A — PRD Traceability Matrix
Forward (PRD AC → RFC):
| PRD composite AC id | RFC section | Component / file |
|---|---|---|
TCKT-S01/AC-1, TCKT-S01/AC-2, TCKT-S01/ERR-1 | §2.A, §2.C, §4.C #1–#3 | AssociatedTickets.vue, TicketEmbedPanel.vue |
TCKT-S02/AC-1, TCKT-S02/AC-2, TCKT-S02/ERR-1, TCKT-S02/ERR-2 | §2.A, §2.2 (sequence), §4.C #4 | TicketEmbedPanel.vue (EMBED_INIT sender) |
TCKT-S03/AC-1, TCKT-S03/AC-2, TCKT-S03/AC-3, TCKT-S03/ERR-1, TCKT-S03/ERR-2 | §2.2, §3.A, §4.C #4–#5 | TicketEmbedPanel.vue (message handler), AssociatedTickets.vue (refresh) |
TCKT-S04/AC-1, TCKT-S04/AC-2, TCKT-S04/ERR-1 | §2.4, §4.C #6 | AssociatedTickets.vue (fallback associate) |
TCKT-S05/AC-1, TCKT-S05/ERR-1 | §2.4, §5 (cross-squad) | n/a — CRM BE allow-list; CDP cannot set data_source |
TCKT-S06/AC-1, TCKT-S06/AC-2 | §1 Out of Scope | n/a — CRM-owned; CDP only hosts the iframe |
TCKT-S07-NEG/NEG-1, TCKT-S07-NEG/NEG-2 | §2.D (branch), §4.A, §4.C #2 | AssociatedTickets.vue (permission + flag gate) |
Reverse (RFC → PRD AC):
| New component / RFC decision | PRD composite AC id driving it |
|---|---|
TicketEmbedPanel.vue (new sandboxed iframe panel) | TCKT-S01/AC-2 |
| Typed origin+version-validated message handler | TCKT-S03/AC-1, TCKT-S03/ERR-1 |
EMBED_INIT sender keyed on qontakCustomerId | TCKT-S02/AC-1 |
| Permission + feature-flag gate on the entry point | TCKT-S07-NEG/NEG-1, TCKT-S07-NEG/NEG-2 |
Optional fallback POST /v1/leads/{contactId}/tickets/{ticketId} | TCKT-S04/AC-2 |
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE endpoint) | Required writes (BE endpoint) | Status surface |
|---|---|---|---|---|
Tickets section (AssociatedTickets.vue) | web | GET /v1/tickets?qontak_customer_id=… (existing, TicketStore.fetchTicketsAssociated) | POST /v1/leads/{contactId}/tickets/{ticketId} (existing — fallback associate only) | Refreshed list after TICKET_CREATED; toast |
Embed panel (TicketEmbedPanel.vue) | web (iframe host) | n/a — content served by CRM embed | n/a — create owned by CRM via postMessage contract | EMBED_READY / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_RESIZE messages |
Role Coverage
| PRD role | UI surface visibility | Action buttons enabled | Auth scope expected from BE | Notes |
|---|---|---|---|---|
| Support / CS Agent (primary) | Sees "Create Ticket" entry iff ticket-create permission + flag ON | "Create Ticket" enabled | Ticket-create permission key (OQ-4); is_enabled === true via UserStore.permissions | Same gate pattern as Notes.vue canAddNotes. |
| Sales / CRM Admin (secondary) | Same as above | Same | Same | No role-specific divergence in CDP. |
| User without permission / flag OFF | Entry not rendered; associate-existing only | n/a | n/a | Guard rail TCKT-S07-NEG. |
PRD Section Coverage
| PRD section # | Title | Where covered |
|---|---|---|
| HEADER BLOCK | Metadata | Metadata table (this RFC) |
| 2 | SUPPORT Context | §1 Overview + Dependencies |
| 3 | One-liner + Problem | §1 Overview |
| 4 | Target Users + Persona | Detail 1.A Role Coverage |
| 5 | Non-Goals | §1 Out of Scope |
| Scope Changes | Frontend/Design/Backend(conditional) | §1 Dependencies, §2.D, §4.C #6 |
| 6 | Constraints | §1 Assumptions, §3 (sandbox/origin), §4.A (flag) |
| 7 | New Features (entry point + embed) | §2.A, §2.C, §4.C |
| 8 | API & Webhook Behavior (postMessage contract) | §2.2 sequence, §2.4, §2.A |
| 9 | System Flow + Stories + ACs | Detail 1.A, Detail 1.C |
| 10 | Rollout | §4 Rollout Strategy |
| 11 | Observability | §3 Monitoring & Alerting |
| 12 | Success Metrics | §1 Success Criteria, §3 |
| 13 | Dependencies | §1 Dependencies, §5 |
| 14 | Key Decisions + Alternatives | Detail 1.B |
| 15 | Open Questions | §5 |
The PRD's §8 ACs are FE-shaped (no DDL); this is a frontend RFC consuming a cross-squad contract, so there is no PRD-to-Schema derivation — n/a, no CDP-owned persistence.
Detail 1.B — Decisions Closed
| Decision | Chosen option | Alternatives rejected | Why rejected |
|---|---|---|---|
| D-1: Consume CRM embed, build nothing CDP-native | Reuse /embed/ticket/create iframe | Build a native CDP ticket form | Duplicates CRM logic; perpetuates the parity-lag the anchor exists to remove. |
D-2: Use the typed versioned postMessage contract | EMBED_INIT ⇄ TICKET_CREATED/…, version:1, origin-validated | Mirror the shipped Deal {msg} string contract | The CRM FE RFC defines a typed payload; {msg} would not interoperate with the new Phase-1 component. |
D-3: Pass contact via EMBED_INIT keyed on qontakCustomerId | postMessage payload | token + customer_id URL params (as Deal does) | BE RFC uses direct JWT Bearer (no token param); FE RFC passes contact via EMBED_INIT. |
D-4: data_source set server-side from an allow-listed embed_source | CRM adds embed-web-cdp to the allow-list | CDP sends a source field client-side | Field is data_source; client value is overwritten server-side. CDP cannot influence it. |
| D-5: Degrade gracefully to associate-existing | Feature is additive; entry hidden when embed unavailable | Block the Tickets section on embed availability | The associate path already exists and must never regress. |
D-6: New component TicketEmbedPanel.vue vs inline in AssociatedTickets.vue | Extract a child component | Inline iframe + handler in AssociatedTickets.vue (as Deal does) | The typed handler + state machine is heavier than Deal's; isolating it keeps AssociatedTickets.vue testable and the message handler unit-testable. no alternative seriously blocking — reversible. |
Detail 1.C — Per-Story Change Map
| Story id | Story title | Layer scope | Changes (concrete FE artifacts + cross-layer refs) | Composite AC ids | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|---|
TCKT-S01 | Open the Create-Ticket embed from CDP | FE-only | • AssociatedTickets.vue: add MpPopover "+" menu ("Associate existing" / "Create Ticket") mirroring AssociatedDeals.vue L35–53 • new TicketEmbedPanel.vue (sandboxed iframe) • isShowCreateTicket toggle | TCKT-S01/AC-1, AC-2, ERR-1 | Vitest: clicking "Create Ticket" sets isShowCreateTicket true and renders iframe with src ending /embed/ticket/create; loading state shown until EMBED_READY; on iframe error event, inline error renders and associate-existing button still present | §2.A · §2.C · §4.C #1–#3 |
TCKT-S02 | Auto-fill the customer in the embed | FE-only (consumer = CRM, cross-squad/blocked) | • TicketEmbedPanel.vue: on EMBED_READY, postMessage(EMBED_INIT, allowedOrigin) with {version:1, roomId:null, qontakCustomerId, contactName, contactPhone, contactEmail, contactAccountUniqId, channelType, locale} • origin guard before posting | TCKT-S02/AC-1, AC-2, ERR-1, ERR-2 | Vitest: on receiving EMBED_READY, iframe.contentWindow.postMessage called once with payload qontakCustomerId === route contactId and target origin === new URL(CRM_V3_EMBED_URL).origin; not called for a spoofed origin | §2.A · §2.2 · §4.C #4 |
TCKT-S03 | Confirm creation + refresh on TICKET_CREATED | FE-only | • TicketEmbedPanel.vue: window message handler validating event.origin + payload.version === 1; emit created / close / error / resize • AssociatedTickets.vue: on created → fetchWithReset() + customerStore.markContactUpdated('tickets') + success toast | TCKT-S03/AC-1, AC-2, AC-3, ERR-1, ERR-2 | Vitest: valid TICKET_CREATED → panel closes, fetchTicketsAssociated re-invoked, toastNotify success called; EMBED_RESIZE sets iframe height; spoofed origin / wrong version → handler is a no-op; TICKET_CREATE_ERROR → inline error, panel stays open | §2.2 · §3.A · §4.C #4–#5 |
TCKT-S04 | Created ticket associated to the customer | FE + BE existing (conditional — OQ-6) | • AssociatedTickets.vue: if CRM does not auto-associate, call POST /v1/leads/{contactId}/tickets/{ticketId} (baseURL: config.CUSTOMER_360_CRM_URL) using ticketId from TICKET_CREATED, then refresh | TCKT-S04/AC-1, AC-2, ERR-1 | Vitest (auto-assoc path): no extra POST, refreshed list shows ticket. Vitest (fallback path): one POST to /v1/leads/{contactId}/tickets/{ticketId}; on failure, cdp_ticket_embed_associate_failed logged + retry offered | §2.4 · §4.C #6 |
TCKT-S05 | data_source labels the ticket as CDP-originated | Cross-squad | n/a — CRM Backend owns ALLOWED_EMBED_SOURCES; CDP cannot set data_source. CDP's only dependency is that CRM adds embed-web-cdp (OQ-3). | TCKT-S05/AC-1, ERR-1 | n/a — verified on CRM side; CDP asserts only that the embed is loaded from CDP (analytics cdp_ticket_embed_opened) | §2.4 · §5 OQ-3 |
TCKT-S06 | Pipeline + custom layout inherited from CRM | Cross-squad | n/a — CRM-owned (FE RFC useTicketPipeline / useTicketLayout); CDP only hosts the iframe. | TCKT-S06/AC-1, AC-2 | n/a — covered in CRM FE RFC | §1 Out of Scope |
TCKT-S07-NEG | No create without permission / flag | FE-only + Config | • AssociatedTickets.vue: canCreateTicket computed (permission via UserStore.permissions, same pattern as Notes.vue canAddNotes) AND flag via useFeatureFlagStore().featureFlags['<cdp ticket embed flag>'] • entry rendered only when both true | TCKT-S07-NEG/NEG-1, NEG-2 | Vitest: permission disabled → entry not in DOM; flag OFF → entry not in DOM, associate-existing unaffected | §2.D · §4.A · §4.C #2 |
Coverage: all 7 PRD stories appear exactly once. S05/S06 are cross-squad (CRM-owned) and marked
n/a — covered in CRM RFCper the FE→cross-squad rule.
2. Technical Design
Detail 2.0 — Repo Reading Guide (read this first)
Repo Map (mermaid)
flowchart LR
subgraph detail["features/customers/detail/components"]
at["AssociatedTickets.vue (extend)"]
panel["TicketEmbedPanel.vue (new)"]
tc["TicketsCard.vue"]
aet["AssociatedExistingTickets.vue"]
deals["AssociatedDeals.vue (reference)"]
end
subgraph store["features/customers/store"]
ts["TicketStore.ts"]
us["UserStore.ts"]
cs["CustomerStore.ts"]
end
subgraph common["common"]
cfg["composables/useCustomConfig.ts"]
mp["composables/useMixpanel.ts"]
ff["store/FeatureFlagStore.ts"]
end
ext(["CRM embed iframe: /embed/ticket/create"])
at --> panel
at --> tc
at --> aet
at --> ts
at --> us
at --> cs
at --> mp
at --> ff
panel --> cfg
panel --> ext
deals -. pattern .-> panel
Existing Code Anchors
| Path | Why the agent reads it | What pattern it teaches |
|---|---|---|
features/customers/detail/components/AssociatedDeals.vue | The shipped embed-in-detail pattern to mirror | isShowCreateDeal toggle + iframe panel + createDealUrl computed + handleCreateDealMessage origin-validated listener + MpPopover add-menu + mount/unmount window.addEventListener('message') |
features/customers/detail/components/AssociatedTickets.vue | The file to extend | Existing Tickets section: useTicketStore, fetchTicketsAssociated, associate POST/DELETE /v1/leads/{contactId}/tickets/{id}, customerStore.markContactUpdated('tickets'), toastNotify |
features/customers/store/TicketStore.ts | The store to refresh after create | fetchTicketsAssociated(contactId, {page,limit,query,firstFetch}) (L168) builds qontak_customer_id query (L153), calls GET /v1/tickets on config.CUSTOMER_360_CRM_URL |
features/customers/detail/components/Notes/Notes.vue | The permission-gate pattern | canAddNotes computed: storeToRefs(useUserStore()) → Object.values(permissions.value)[0].permissions.find(p => p.name === KEY)?.is_enabled === true |
features/customers/detail/components/Notes/constants.ts | Where permission keys are defined | export const ADD_NOTES_PERMISSION = 'customers_customernotes_add' — new ticket-create key goes in a sibling constants file |
common/store/FeatureFlagStore.ts | How feature flags gate UI | useFeatureFlagStore().featureFlags[code]; codes added to FeatureFlags + DEFAULT_FEATURE_FLAGS; resolved as enabled && enabled_for_company from GET /v1/feature_flags |
common/composables/useMixpanel.ts | Analytics convention | useMixpanel().track('[Qontak One] [Customer] <Action>', props) |
common/composables/useCustomConfig.ts | Config access | const { config } = useCustomConfig(); config.CRM_V3_EMBED_URL, config.CUSTOMER_360_CRM_URL |
common/constants/auth.ts | Session token key | TOKEN_KEY = 'global_sso_token' (read only if OQ-2 forces a token param) |
features/customers/detail/components/Notes/Notes.spec.ts | The test pattern | vitest + @testing-library/vue + msw + @pinia/testing createTestingPinia + permission ref mocking |
Patterns to Follow (and where to find them)
| Concern | Pattern in repo | Reference file | Deviation in this RFC? |
|---|---|---|---|
| State management | Pinia setup stores (storeToRefs) | features/customers/store/TicketStore.ts, UserStore.ts | none |
| Folder convention | Feature-scoped components under features/customers/detail/components/ | AssociatedDeals.vue | none — TicketEmbedPanel.vue lands beside AssociatedTickets.vue |
| Styling | Pixel3 design system + css() panda | AssociatedDeals.vue (imports from @mekari/pixel3) | none |
| Error / toast / retry | toastNotify({position,variant,title}) | utils/toast used in AssociatedTickets.vue | none |
| iframe embed | sandboxed <iframe> + origin-validated window message listener | AssociatedDeals.vue L9–19, L287–310, L368–373 | yes — typed version:1 payload + EMBED_INIT sender, replacing the legacy {msg} string + URL-param contract (D-2, D-3) |
| Feature flag | useFeatureFlagStore().featureFlags[code] | features/customers/detail/components/MenuSidebar.vue L36–37 | none |
| Analytics | useMixpanel().track(...) | features/customers/detail/views/DetailPage.vue L90–102 | none |
Reading Order for the Agent
AssociatedDeals.vue— the embed pattern to mirror (and the contract to supersede).AssociatedTickets.vue— the file you extend; learn its store + associate flow.TicketStore.ts—fetchTicketsAssociated(the refresh you call after create).Notes.vue+Notes/constants.ts— the permission-gate pattern + where keys live.FeatureFlagStore.ts— how to add + read a new flag code.useMixpanel.ts— analytics event convention.useCustomConfig.ts+common/constants/auth.ts— config + session token keys.Notes.spec.ts— the vitest + testing-library + msw + pinia test harness.
Source Verification (anti-hallucination — required)
| Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|
AssociatedDeals.vue embed pattern | read | createDealUrl computed at L162; handleCreateDealMessage origin check event.origin !== allowedOrigin at L292; {msg} handling 'Deal successfully created' at L301; listener add L368 / remove L372; iframe sandbox="allow-forms allow-scripts allow-same-origin" L15 |
AssociatedTickets.vue extend target | read | associate POST /v1/leads/${contactId}/tickets/${ticketId} L182; ticketStore.fetchTicketsAssociated L241; customerStore.markContactUpdated('tickets') L202; toastNotify L208 |
TicketStore.fetchTicketsAssociated | read | declared L168; appendIfExists(params,'qontak_customer_id',contactId) L153; GET /v1/tickets?... on config.CUSTOMER_360_CRM_URL L179 |
| Permission gate pattern | read | Notes.vue canAddNotes L83–97 (permission?.is_enabled === true L96); key ADD_NOTES_PERMISSION = 'customers_customernotes_add' in Notes/constants.ts L27 |
| FeatureFlagStore | read | FeatureFlags interface + DEFAULT_FEATURE_FLAGS (e.g. loyalty_integration); featureFlags[flag.code] = Boolean(flag.enabled && flag.enabled_for_company); GET /v1/feature_flags on config.CUSTOMER_360_URL; consumed MenuSidebar.vue L37 |
| useMixpanel convention | read | track(trackerEvent, options) L39; event names '[Qontak One] [Customer] …' (e.g. DetailPage.vue L102) |
| Config keys | read / grep | useCustomConfig.ts returns config; config.CRM_V3_EMBED_URL + config.CUSTOMER_360_CRM_URL referenced across repo (grep of config.[A-Z_]+) |
TOKEN_KEY | read | common/constants/auth.ts: const TOKEN_KEY = 'global_sso_token' |
| Test harness | read | Notes.spec.ts L1–14: vitest, @testing-library/vue, msw server, @pinia/testing createTestingPinia, @testing-library/user-event |
| Build/test commands | read | package.json scripts: test: "vitest", test:coverage: "vitest run --coverage", lint: "eslint .", build: "nuxt build && node scripts/fix-federation-remote-entry.mjs" |
CRM embed contract + auth + /embed/ticket/create reusability | NOT verifiable in this repo | CRM-owned; lives in qontak-customer-fe? No — CRM repo. → §5 OQ-1/OQ-2/contract. Do not implement the EMBED_INIT payload shape until CRM freezes it. |
Design ↔ Code Mapping (frontend-specific)
| Figma frame / component | Implementing file | Reuse vs new | Design tokens used | Deviation from design |
|---|---|---|---|---|
n/a — design pending (entry menu) | AssociatedTickets.vue (MpPopover) | extended (mirror AssociatedDeals.vue popover) | Pixel3 tokens (background.neutral, border.default, md/sm spacing — as in AssociatedDeals.vue) | pending — Figma TBD (OQ-8) |
n/a — design pending (embed panel) | TicketEmbedPanel.vue (new) | new (mirror Deal panel shell) | Pixel3 tokens as above | pending — Figma TBD (OQ-8) |
Both frames are design-pending (§5 OQ-8). The UI shell is fully determined by the in-repo Deal pattern, so chunks proceed against it; pixel polish + the "Create Ticket" menu copy/icon wait on Figma sign-off by the design QA contact.
Detail 2.1 — Architecture (mermaid)
Component diagram
flowchart TB
agent([Support Agent]) --> at["AssociatedTickets.vue"]
at --> gate{"canCreateTicket (permission AND flag)"}
gate -->|true| entry["Create Ticket menu item"]
gate -->|false| hidden["entry hidden — associate-existing only"]
entry --> panel["TicketEmbedPanel.vue"]
panel --> iframe["sandboxed iframe"]
iframe -->|postMessage| handler["message handler (origin + version)"]
handler -->|created| at
at --> ts[("TicketStore.fetchTicketsAssociated")]
at --> mp[["useMixpanel.track"]]
iframe --> crm(["CRM /embed/ticket/create"])
State machine (embed panel)
stateDiagram-v2
[*] --> hidden
hidden --> loading: click Create Ticket
loading --> ready: EMBED_READY received
loading --> error: iframe load fail or EMBED_READY timeout
ready --> success: TICKET_CREATED valid
ready --> error: TICKET_CREATE_ERROR
error --> loading: retry
ready --> hidden: EMBED_CLOSE or cancel
success --> hidden: panel torn down after refresh
success --> [*]
Detail 2.2 — Sequence (mermaid, happy + failure)
sequenceDiagram
actor Agent
participant CDP as AssociatedTickets.vue
participant Panel as TicketEmbedPanel.vue
participant Embed as CRM embed iframe
participant CRM as CRM API v2.8 tickets
Agent->>CDP: Click "Create Ticket"
CDP->>Panel: open (isShowCreateTicket true)
Panel->>Embed: load iframe (/embed/ticket/create)
Embed-->>Panel: postMessage EMBED_READY (version 1)
Panel->>Embed: postMessage EMBED_INIT (qontakCustomerId, contact, roomId null)
Note over Embed: CRM auto-fills Contact, renders pipeline and layout
Agent->>Embed: fill form and Submit
Embed->>CRM: POST /api/mobile/v2.8/tickets (embed true, embed_source)
alt success
CRM-->>Embed: 201 ticket
Embed-->>Panel: TICKET_CREATED (ticketId, ...)
Panel->>CDP: validate origin and version, emit created
CDP->>CDP: fetchWithReset, markContactUpdated, success toast
opt CRM did not auto-associate (OQ-6)
CDP->>CRM: POST /v1/leads/{contactId}/tickets/{ticketId}
end
else failure
CRM-->>Embed: 4xx or 5xx
Embed-->>Panel: TICKET_CREATE_ERROR (errorCode, errorMessage)
Panel->>CDP: emit error, panel stays open for retry
end
Detail 2.3 — Database Model
N/A — pure frontend RFC. No CDP-owned persistence. No localStorage/IndexedDB/cookies
introduced (the existing global_sso_token cookie is read-only and unchanged).
Detail 2.4 — APIs Consumed
| Method | Path | Status | Contract authority | Notes |
|---|---|---|---|---|
| GET | /v1/tickets?qontak_customer_id=… (via TicketStore.fetchTicketsAssociated) | exists | TicketStore.ts L168–179 | reused — the post-create refresh. baseURL: config.CUSTOMER_360_CRM_URL. |
| POST | /v1/leads/{contactId}/tickets/{ticketId} | exists | AssociatedTickets.vue L182 | reused — fallback associate only, used iff CRM does not auto-associate (OQ-6). |
| — (iframe) | ${config.CRM_V3_EMBED_URL}/embed/ticket/create | needs-building / blocked | CRM FE RFC | new-with-justification: a new CRM route reused by a non-chat host; CDP cannot build it. OUT of CRM Phase 1 (OQ-1). |
| postMessage | EMBED_INIT / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_CLOSE / EMBED_RESIZE | needs-building / blocked | CRM FE RFC | new-with-justification: typed version:1 contract; CDP implements to it but cannot freeze it. |
Detail 2.A — UI Contract
TicketEmbedPanel.vue (new):
- Figma frame URL:
n/a — design pending (OQ-8); shell mirrorsAssociatedDeals.vuecreate panel. - Implementation file path:
features/customers/detail/components/TicketEmbedPanel.vue - Props type:
interface TicketEmbedPanelProps {
// customer context posted to the embed on EMBED_READY
customerContext: {
qontakCustomerId: string // = route contactId
contactName?: string
contactPhone?: string
contactEmail?: string
contactAccountUniqId?: string
channelType?: string
locale?: string
}
embedReadyTimeoutMs?: number // default 10000 — fall to error if no EMBED_READY
}
- Emits:
created(payload:TicketCreatedPayload),close(reason),error(errorCode/message),resize(height). - State shape & ownership: local panel state machine (
hidden | loading | ready | error | success) owned by the component; ticket list state stays inTicketStore(parent refreshes). - Event payloads — postMessage contract (typed;
version: 1):
// CDP → embed
type EmbedInit = {
version: 1; roomId: null
qontakCustomerId: string
contactName?: string; contactPhone?: string; contactEmail?: string
contactAccountUniqId?: string; channelType?: string; locale?: string
}
// embed → CDP
type EmbedReady = { version: 1; type: 'EMBED_READY' }
type TicketCreated = {
version: 1; type: 'TICKET_CREATED'
ticketId: string; ticketName?: string; ticketSlug?: string
pipelineId?: string; stageId?: string; priorityId?: string
createdAt?: string; dueDate?: string | null; channelIntegrationRoomId?: string | null
}
type TicketCreateError = { version: 1; type: 'TICKET_CREATE_ERROR'; errorCode: string; errorMessage: string }
type EmbedClose = { version: 1; type: 'EMBED_CLOSE'; reason: 'user_cancel' | 'ticket_created' | 'error' }
type EmbedResize = { version: 1; type: 'EMBED_RESIZE'; height: number }
The exact field names above are taken from the PRD §8, which sources them from the CRM FE RFC. They are NOT yet verified against a frozen CRM contract (§5 OQ-contract). Treat this block as provisional until CRM signs off.
- Analytics events (Mixpanel, via
useMixpanel().track): see §3 Monitoring. - Conditional rendering:
loading→ spinner/skeleton;ready/success→ iframe;error→ inline error + retry + "use Associate existing". - A11y: focus moves into the panel on open;
Esc/ back-arrow returns focus to the "+" trigger; iframe has atitle="Create Ticket".
AssociatedTickets.vue (extend):
- Add MpPopover "+" menu (mirroring
AssociatedDeals.vueL35–53): items "Associate existing" (existinghandleShowAssociateExisting) + "Create Ticket" (newhandleShowCreateTicket), the latter rendered only whencanCreateTicket. - Add
canCreateTicketcomputed (permission + flag) andisShowCreateTicketref. - On panel
created:fetchWithReset()+customerStore.markContactUpdated('tickets')+ success toast (reuse existing helpers).
Detail 2.B — Data-Fetching Strategy
- Library: Pinia store action +
$customFetch(existing —useCustomFetch). No new data lib. - Cache key structure:
n/a— list refetch is imperative (fetchWithReset), as today. - TTL & refetch triggers: refetch on panel
createdevent and on the existing mount/search/intersection triggers (unchanged). - Stale-while-revalidate: no — explicit reset-then-fetch (matches Deal/ticket existing behavior).
- Optimistic updates: no — the list is refetched after a confirmed
TICKET_CREATED.
Detail 2.C — UI State Matrix
| Surface | Loading | Empty | Error | Partial | Success |
|---|---|---|---|---|---|
Embed panel (TicketEmbedPanel.vue) | Spinner/skeleton until EMBED_READY (or embedReadyTimeoutMs) | n/a — panel always renders the CRM form once ready | iframe load fail or TICKET_CREATE_ERROR → inline error + retry + associate-existing hint | n/a — single embed, no partial | TICKET_CREATED → close panel, list refreshes, success toast |
Tickets list (AssociatedTickets.vue, existing) | existing skeleton (isLoading) | existing "No tickets yet" empty state (L58–66) | existing "Failed to load tickets" toast | existing infinite-scroll load-more | refreshed list shows the new ticket |
Detail 2.D — Scope Boundaries
- Files to create:
features/customers/detail/components/TicketEmbedPanel.vue;features/customers/detail/components/TicketEmbedPanel.spec.ts; a constants entry for the ticket-create permission key (sibling ofNotes/constants.ts). - Files to modify:
features/customers/detail/components/AssociatedTickets.vue(add menu item + panel + handler wiring + gate);common/store/FeatureFlagStore.ts(add the CDP ticket-embed flag code toFeatureFlags+DEFAULT_FEATURE_FLAGS). - Files explicitly NOT touched:
AssociatedDeals.vue(reference only); the associate-existing flow inAssociatedTickets.vue(must not regress);TicketStore.tswrite paths (refresh uses the existing read action only). - Shared components touched: none beyond Pixel3 imports (MpPopover/MpFlex/etc., already used in
AssociatedDeals.vue).
Detail 2.E — State Surface Contract
| Entity | State field / event consumed | Default values | Source endpoint / event | Stale-tolerance window |
|---|---|---|---|---|
| Embed panel | local state machine (hidden→loading→ready→success/error) | hidden | postMessage events from CRM embed | n/a — live, event-driven |
| Associated tickets list | associatedTicketData + ticketStore.totalData | [], 0 | GET /v1/tickets (re-fetched on created) | refreshed immediately after TICKET_CREATED |
Detail 2.F — Asset Inventory
| Asset name | Type | Source | Format & sizes | Path in repo |
|---|---|---|---|---|
ticket.png (empty state — existing) | image | existing in repo | PNG | assets/images/ticket.png (reused, unchanged) |
| Create Ticket menu icon | icon | Pixel3 MpIcon (add) — reuse Deal pattern | DS icon | @mekari/pixel3 (no new asset) |
No net-new assets. If Figma (OQ-8) introduces a custom illustration for the embed states, add it here and flag for design review before merge.
3. High-Availability & Security
The feature is additive and gracefully degrading (D-5): if the embed is unavailable, not yet reusable for CDP, or fails to load, the Tickets section falls back to the existing associate-existing-only behavior. The "Create Ticket" entry is hidden entirely when the flag is OFF or the permission is absent, so the worst case is "no create button" — never a broken Tickets section.
Performance Requirement
- LCP / INP / CLS: the panel is lazy (rendered only on open); no impact on the Tickets section initial render. Targets inherit the app defaults (no new budget).
- Bundle size: one small SFC (
TicketEmbedPanel.vue) + handler logic; delta is negligible (no new dependency — reuses Pixel3,useCustomConfig, Pinia). - Code-splitting: component-level (panel only mounts when opened).
- Browser support: inherits the app matrix (Nuxt 4 / Vue 3 target).
- i18n / RTL: toast + menu copy via existing i18n; the embed content is CRM's.
Monitoring & Alerting (Mixpanel + Datadog RUM — both already in package.json)
| Event Name | Trigger | Properties |
|---|---|---|
cdp_ticket_embed_opened | Create-Ticket embed opened | company_sso_id, contact_id, qontak_customer_id, user_id |
cdp_ticket_embed_created | TICKET_CREATED received + validated | contact_id, ticket_id, pipeline_id, data_source |
cdp_ticket_embed_create_error | TICKET_CREATE_ERROR received | contact_id, error_code |
cdp_ticket_embed_associate_failed | Fallback associate call failed (OQ-6) | contact_id, ticket_id, reason |
cdp_ticket_embed_init_dropped | EMBED_INIT blocked by origin guard | company_sso_id, attempted_origin |
Naming note: PRD §11 uses snake_case event names. The repo's Mixpanel convention is
'[Qontak One] [Customer] <Action>'(useMixpanel.ts). Resolve at implementation: wrap the PRD's snake_case names as theeventpayload under a'[Qontak One] [Customer] Create Ticket Embed …'label, OR confirm the data team consumes the raw snake_case names. Recorded as a minor open item (§5).
- Error monitoring: route iframe-load failures + dropped-origin events to Datadog
RUM (
@datadog/browser-rum, already a dependency). - Alerts (from PRD §11):
cdp_ticket_embed_create_errorrate > 10% →#cdp-ops; associate-failed > 5% → investigate. Cadence weekly for first 4 weeks post-GA.
Logging
- FE log fields:
contact_id,ticket_id,error_code,attempted_origin,reason(no PII beyond ids already in the route). - PII: contact name/phone/email are sent only via
EMBED_INITto the validated CRM origin — never logged.
Security Implications
- Threat model: the primary surface is the cross-origin iframe + postMessage.
- postMessage spoofing — every inbound message is validated on both
event.origin === new URL(config.CRM_V3_EMBED_URL).originandpayload.version === 1before any side effect (mirrorsAssociatedDeals.vueL292; tightened with the version check). Failing either → no-op +cdp_ticket_embed_init_dropped. - Outbound
EMBED_INITis posted to the exact CRM origin string, never'*'(carries contact PII). - Clickjacking / sandbox — iframe uses
sandbox="allow-forms allow-scripts allow-same-origin"andreferrerpolicy="strict-origin-when-cross-origin"(exact attributes fromAssociatedDeals.vueL15–16).
- postMessage spoofing — every inbound message is validated on both
- Auth token storage: unchanged —
global_sso_tokenhttpOnly-style cookie read viauseCookie(TOKEN_KEY). Per D-3 the ticket embed authenticates itself (notokenURL param). OQ-2: if Qontak One SSO does not flow into the iframe, the create call 401s — this is the headline blocking risk. dangerouslySetInnerHTML/v-html: none.- HTTPS-only: the embed base URL is forced to
https://(mirrorAssociatedDeals.vueL167). - PII handling: contact PII transits postMessage to a validated origin only; not persisted by CDP.
Detail 3.A — Failure Mode Catalog
| API call | 401 | 403 | 404 | 429 | 500 | Timeout (s) | Offline | Retry mechanism |
|---|---|---|---|---|---|---|---|---|
iframe load /embed/ticket/create | n/a (iframe) | n/a | error state | n/a | error state | 10s no EMBED_READY → error | error state; associate-existing available | user "retry" re-mounts iframe |
TICKET_CREATE_ERROR (from embed) | shown inline | shown inline | shown inline | shown inline | shown inline | n/a | n/a | panel stays open; user resubmits in embed |
fallback POST /v1/leads/{contactId}/tickets/{ticketId} | toast + log | toast + log | toast + log | toast + log | toast + log | default $customFetch | toast | log cdp_ticket_embed_associate_failed + offer retry |
Narrative: rapid double-click on "Create Ticket" → guarded by isShowCreateTicket
(idempotent open). Duplicate TICKET_CREATED → handler debounced by panel
state (success is terminal; second message ignored). Navigation during embed
→ onUnmounted removes the message listener (mirror AssociatedDeals.vue L372).
Detail 3.B — Error Message Catalog
| Error code | User-facing message (i18n key) | Surface | User-facing? |
|---|---|---|---|
embed_load_failed | "Couldn't load the ticket form. Try again or use Associate existing." | inline (panel) | yes |
embed_ready_timeout | same as above | inline (panel) | yes |
TICKET_CREATE_ERROR (passthrough) | embed-provided errorMessage, else generic | inline (panel) | yes |
associate_failed | "Ticket created but couldn't be linked. Retry?" | toast | yes |
origin_dropped | (none — silent no-op) | n/a | no |
Detail 3.C — Accessibility
- WCAG AA.
- Keyboard: "+" popover navigable; opening the panel moves focus into it; back-arrow /
Escreturns focus to the trigger. - Focus management: trap focus within the panel header controls; the iframe owns its internal focus.
- ARIA: iframe
title; error state usesrole="alert". - Color contrast: Pixel3 tokens (DS-verified).
prefers-reduced-motion: no custom animation introduced.
4. Backwards Compatibility and Rollout Plan
Compatibility
- API contracts changed: none (CDP adds no endpoint; reuses existing read + associate).
- Saved client state: none changed.
- Old bundle / CDN: standard app deploy; no special invalidation.
- The associate-existing flow is byte-for-byte unchanged (regression-guarded by its existing tests).
Rollout Strategy
- Feature flag: a CDP-side flag (PRD OQ-7; name TBD with CRM — e.g.
cdp_create_ticket_embed) added toFeatureFlagStore(enabled && enabled_for_company), default OFF. Layered under CRM'sunify_ticket_omnichannel_view. - Stages (audience / go-no-go): Internal QA company → 3–5 Qontak One beta
companies (monitor create + association rate) → progressive per-company GA.
(Schedule + PICs live in
delivery/, not here.) - Stop conditions:
cdp_ticket_embed_create_errorrate > 10%, or associate-failed > 5%, or any origin-validation bypass. - Rollback: flag OFF → entry hidden, Tickets section reverts to associate-existing instantly (no deploy needed).
- Blast radius: worst case is the Tickets section header for flagged companies; the rest of Customer Detail is untouched.
- Dependency gate: cannot start before OQ-1 (CRM commits a CDP-reusable embed),
OQ-2 (auth), OQ-3 (
embed_source).
Detail 4.A — Configuration Contract
| Env var / build config / flag | Type | Default | Required | Provisioner |
|---|---|---|---|---|
cdp_create_ticket_embed (feature flag code; name TBD OQ-7) | boolean | false | yes | CDP feature-flag service (/v1/feature_flags) |
config.CRM_V3_EMBED_URL | string (URL) | existing | yes (reused) | existing runtime config (__CUSTOM_CONFIG__) |
config.CUSTOMER_360_CRM_URL | string (URL) | existing | yes (reused) | existing runtime config |
Detail 4.B — Test Plan (commands sourced from package.json)
| Layer | Command (source) | What it must prove |
|---|---|---|
| Unit/component | pnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.ts (script test: "vitest", package.json) | EMBED_INIT sent to correct origin only; origin+version validation; created/error/resize handling |
| Unit (regression) | pnpm vitest run features/customers/detail/components/AssociatedTickets (test) | permission/flag gate hides entry; associate-existing flow unchanged |
| Coverage | pnpm test:coverage (script test:coverage: "vitest run --coverage") | coverage does not regress |
| Lint | pnpm lint (script lint: "eslint .") | no lint errors |
| Build | pnpm build (script build: "nuxt build && node scripts/fix-federation-remote-entry.mjs") | app compiles with the new component |
| E2E | n/a — repo has e2e/ (Playwright) but the embed is cross-origin/CRM-owned; defer to CRM + the initiative's Lane-C E2E suite (QA-initiated) | — |
| Typecheck | n/a — no standalone typecheck script; nuxt build performs type checking | — |
Detail 4.C — Agent Execution Plan
| Order | Chunk | Files to modify/create | Commands to run | Acceptance criteria (verifiable) |
|---|---|---|---|---|
| 1 | Scaffold TicketEmbedPanel.vue (iframe + state machine, no contract yet) | create features/customers/detail/components/TicketEmbedPanel.vue | pnpm lint · pnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.ts | Component renders a sandboxed iframe with src ending /embed/ticket/create; loading state shown by default |
| 2 | Permission + flag gate + add the flag code | modify AssociatedTickets.vue (add canCreateTicket), common/store/FeatureFlagStore.ts (add flag code + default), create ticket-create permission constant | pnpm vitest run features/customers/detail/components/AssociatedTickets | Vitest: entry hidden when permission disabled OR flag OFF; visible only when both true (TCKT-S07-NEG/NEG-1, NEG-2) |
| 3 | Wire the "+" menu item + open/close panel | modify AssociatedTickets.vue (MpPopover item "Create Ticket", isShowCreateTicket, back-arrow) | pnpm vitest run features/customers/detail/components/AssociatedTickets · pnpm lint | Clicking "Create Ticket" opens panel; back-arrow closes; associate-existing item still works (TCKT-S01/AC-1, AC-2) |
| 4 | Implement the typed postMessage handler + EMBED_INIT sender | modify TicketEmbedPanel.vue (origin+version guard; send EMBED_INIT on EMBED_READY; handle TICKET_CREATED/ERROR/CLOSE/RESIZE) | pnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.ts | Vitest: EMBED_INIT posted once to new URL(CRM_V3_EMBED_URL).origin with qontakCustomerId === contactId; spoofed origin/wrong version → no-op; resize sets height (TCKT-S02/*, TCKT-S03/*) |
| 5 | Parent success handling (refresh + toast + analytics) | modify AssociatedTickets.vue (on created: fetchWithReset + markContactUpdated('tickets') + toast + useMixpanel().track) | pnpm vitest run features/customers/detail/components/AssociatedTickets | Vitest: valid created → fetchTicketsAssociated re-invoked, success toast fired, cdp_ticket_embed_created tracked (TCKT-S03/AC-1) |
| 6 | Optional fallback associate (gated on OQ-6) | modify AssociatedTickets.vue (if not auto-associated, POST /v1/leads/{contactId}/tickets/{ticketId}) | pnpm vitest run features/customers/detail/components/AssociatedTickets | Vitest: fallback path issues one POST with ticketId from payload; failure logs cdp_ticket_embed_associate_failed (TCKT-S04/AC-2, ERR-1) |
| 7 | Final verification | — | pnpm lint · pnpm test:coverage · pnpm build | All green; coverage not regressed; build passes |
Chunks 1–3 are not blocked (UI shell + gate use only in-repo facts). Chunks 4–6 are blocked on the CRM contract freeze + auth (OQ-1/OQ-2/contract) — the EMBED payload field names in §2.A are provisional until then.
Detail 4.D — Verification & Rollback Recipe
- Pre-merge verification commands (in order):
pnpm lintpnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.tspnpm vitest run features/customers/detail/components/AssociatedTicketspnpm build
- Post-deploy verification signals:
- Mixpanel:
cdp_ticket_embed_opened> 0 andcdp_ticket_embed_created> 0 on the QA company within first session. - Mixpanel:
cdp_ticket_embed_create_errorrate < 10%;cdp_ticket_embed_associate_failed< 5%. - Datadog RUM: no spike in iframe-load errors /
cdp_ticket_embed_init_dropped.
- Mixpanel:
- Rollback recipe:
- Toggle the
cdp_create_ticket_embedflag OFF for the affected company (entry hides immediately, no deploy). - If a code defect, revert the PR introducing
TicketEmbedPanel.vue+AssociatedTickets.vuechanges. - Confirm
cdp_ticket_embed_create_errorreturns to baseline and the associate-existing flow still works (its tests pass) within 15 min.
- Toggle the
5. Concern, Questions, or Known Limitations
Blocking (carried from PRD §15 — must close before RFC/AGREED):
- OQ-1 (blocking): CRM has not committed a CDP-reusable embed (non-chat host) to a phase — it is OUT of CRM Phase 1 in both RFCs. Without it, chunks 4–6 cannot be built. Owner: PM + CRM. Until then CDP ships nothing (or only chunks 1–3 behind an OFF flag).
- OQ-2 (blocking): Qontak One auth — does the embed authenticate in the iframe (v2.8 JWT vs Mekari SSO)? If not, the create call 401s. Owner: CRM + Platform.
- OQ-contract (blocking): The typed
postMessagepayload field names (§2.A) are taken from the PRD, not a frozen CRM contract. CRM must freezeEMBED_INIT/TICKET_CREATED/TICKET_CREATE_ERROR/EMBED_CLOSE/EMBED_RESIZEbefore chunk 4. This RFC's §2.A block is provisional. - OQ-3: CDP
embed_source(embed-web-cdp) added to BEALLOWED_EMBED_SOURCES(elsedata_sourcecoerced toembed-web-chat). Owner: CRM Backend. Gates TCKT-S05.
Non-blocking / to confirm:
- OQ-4: The exact ticket-create permission key for the CDP entry (CanCan-inherited;
unnamed). Mirror the
Notes.vuepermission-gate pattern once named. - OQ-6: Does CRM auto-associate the contact for the CDP path, or must CDP call the
fallback
POST /v1/leads/{contactId}/tickets/{ticketId}? Determines whether chunk 6 ships. - OQ-7: CDP feature-flag name + relation to CRM's
unify_ticket_omnichannel_view. - OQ-8: Figma frames for the entry point + panel are pending (design QA contact TBD).
- Analytics naming: PRD uses snake_case event names; repo convention is
'[Qontak One] [Customer] …'. Reconcile with the data team (see §3).
Known limitation: because the embed and its auth are CRM-owned, this RFC can fully
specify only the CDP host side; the create path's correctness depends on CRM's contract
and auth, which are not verifiable in qontak-customer-fe.
6. Comment logs
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-06-30 | rfc-starter (authoring) | Drafted from prd-create-ticket-embeddable-web.md, grounded against the live qontak-customer-fe repo (anchors verified — see Detail 2.0 Source Verification). All 4 mermaid blocks validated with mermaid.parse() (mermaid v11.16, headless via jsdom — mmdc can't launch Chromium in this sandbox); no parse errors. §7 = no — blocked on CRM OQ-1/OQ-2/contract. |
7. Ready for agent execution
Ready for agent execution: no
Chunks 1–3 (UI shell, permission/flag gate, "+" menu) are agent-executable today
— they depend only on verified in-repo facts. Chunks 4–6 (the typed postMessage
contract, EMBED_INIT, success/associate handling) are blocked. Missing gates:
- §1 Design References — Figma frames pending for both surfaces (OQ-8). (Mitigated:
the UI shell is fully determined by the in-repo
AssociatedDeals.vuepattern, so chunks 1–3 proceed; pixel polish waits on Figma.) - Detail 2.0 Source Verification — complete and green for all CDP-owned anchors;
the CRM embed contract + auth +
/embed/ticket/createreusability row is explicitly unverifiable here and moved to Open Questions (OQ-1/OQ-2/contract). - Detail 2.A UI Contract — typed, but the postMessage payload shape is provisional until CRM freezes the contract (OQ-contract).
- Blocking dependencies — OQ-1 (CRM CDP-reusable embed, OUT of Phase 1), OQ-2 (Qontak
One auth), OQ-3 (
embed_sourceallow-list) are open.
Flip to yes once CRM (a) commits the CDP-reusable embed to a phase, (b) confirms
Qontak One auth in the iframe, and (c) freezes the typed postMessage contract — at
which point §2.A stops being provisional and chunks 4–6 are executable.
Optional next step: hand to
rfc-reviewerfor a second-pass score after the blocking OQs close and §7 flips to yes.