Skip to main content

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 — reason when 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 reads not 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

FieldValueNotes
StatusIDEAHuman 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.
DRIZhelia AlifaSingle accountable owner. Per-task staffing (Jovi — Frontend) lives in delivery/.
TeamcdpAdvisory squad slug from PRD / initiative README.
Author(s)Jovi (CDP Frontend)Primary author.
ReviewersCDP tech lead; CRM/Omnichannel tech leadCRM owns the embed contract + auth — their review gates the contract assumptions.
Approver(s)CDP engineering lead; infosecInfosec must approve the iframe sandbox + postMessage origin model.
Submitted Date2026-06-30ISO-8601.
Last Updated2026-06-30ISO-8601; bump on every material edit.
Target Release2026-Q3Quarter (gated on CRM dependencies).
Target Quarter2026-Q3Advisory, from PRD / initiative README.
Deliverynot yet handed to deliveryPointer to delivery/ once handed off.
Related../prds/prd-create-ticket-embeddable-web.md · ../prds/prd-create-ticket-from-cdp-anchor.mdSUPPORT PRD (web) + anchor PRD.
DiscussionTBD — CDP squad Slack

Type: frontend Sub-type: new-feature

Sections at a Glance

  1. Overview (incl. §1 Design References — Figma, design system version, design QA)
  2. Technical Design (Repo Reading Guide → mermaid architecture → UI contracts → Asset Inventory)
  3. High-Availability & Security
  4. Backwards Compatibility and Rollout Plan (incl. §4 Agent Execution Plan + Verification & Rollback Recipe)
  5. Concern, Questions, or Known Limitations
  6. Comment logs
  7. 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_INITTICKET_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 (roomId is always null for CDP — no room context).

Assumptions

  1. CRM exposes /embed/ticket/create on the same CRM origin already used by the Deal embed (config.CRM_V3_EMBED_URL) — so CDP reuses that config key.
  2. The CRM ticket embed implements the typed postMessage contract from the CRM FE RFC (version: 1, origin-validated), not the legacy {msg} string.
  3. The embed authenticates itself from the host session (per BE RFC: direct JWT Bearer, not a token URL param) — so CDP does not append a token query param the way AssociatedDeals.vue does. (See OQ-2 — unconfirmed for Qontak One SSO.)
  4. CDP passes contact context via EMBED_INIT postMessage, keyed on qontakCustomerId (the route contactId), not via URL params.

Dependencies

DependencyOwnerAvailabilityBlocking?
CDP-reusable embed at /embed/ticket/create (accepts EMBED_INIT from a non-chat host)CRM/Omnichannelneeds-building — OUT of CRM Phase 1YES (OQ-1)
Qontak One auth resolution (v2.8 JWT vs Mekari SSO in the iframe)CRM + Platformblocked — unconfirmedYES (OQ-2)
Typed postMessage contract frozen (EMBED_INIT / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_CLOSE / EMBED_RESIZE)CRM Frontendneeds-buildingYES (OQ contract)
CDP embed_source (e.g. embed-web-cdp) added to BE ALLOWED_EMBED_SOURCESCRM Backendneeds-buildingYES (for TCKT-S05)
Ticket-create permission key for the CDP entryCRM + CDPneeds-confirmingYES (OQ-4)
config.CRM_V3_EMBED_URL (CRM embed base URL)CDP (exists)existsuseCustomConfigno
POST /v1/leads/{contactId}/tickets/{ticketId} (fallback associate)CDP (exists)exists — used in AssociatedTickets.vueno

Design References (frontend-specific — required)

PRD-named surfaceFigma / design linkFrame nameDesign system versionDesign QA contactNotes
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)TBDMirror the Deal "+" popover (AssociatedDeals.vue MpPopover with "Associate existing" / "Create new deal").
Embed panel (loading / error / success states)n/a — design pendingTBD@mekari/pixel3@1.0.10-dev.0TBDMirror 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 idRFC sectionComponent / file
TCKT-S01/AC-1, TCKT-S01/AC-2, TCKT-S01/ERR-1§2.A, §2.C, §4.C #1–#3AssociatedTickets.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 #4TicketEmbedPanel.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–#5TicketEmbedPanel.vue (message handler), AssociatedTickets.vue (refresh)
TCKT-S04/AC-1, TCKT-S04/AC-2, TCKT-S04/ERR-1§2.4, §4.C #6AssociatedTickets.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 Scopen/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 #2AssociatedTickets.vue (permission + flag gate)

Reverse (RFC → PRD AC):

New component / RFC decisionPRD composite AC id driving it
TicketEmbedPanel.vue (new sandboxed iframe panel)TCKT-S01/AC-2
Typed origin+version-validated message handlerTCKT-S03/AC-1, TCKT-S03/ERR-1
EMBED_INIT sender keyed on qontakCustomerIdTCKT-S02/AC-1
Permission + feature-flag gate on the entry pointTCKT-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 surfaceConsumerRequired reads (BE endpoint)Required writes (BE endpoint)Status surface
Tickets section (AssociatedTickets.vue)webGET /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 embedn/a — create owned by CRM via postMessage contractEMBED_READY / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_RESIZE messages

Role Coverage

PRD roleUI surface visibilityAction buttons enabledAuth scope expected from BENotes
Support / CS Agent (primary)Sees "Create Ticket" entry iff ticket-create permission + flag ON"Create Ticket" enabledTicket-create permission key (OQ-4); is_enabled === true via UserStore.permissionsSame gate pattern as Notes.vue canAddNotes.
Sales / CRM Admin (secondary)Same as aboveSameSameNo role-specific divergence in CDP.
User without permission / flag OFFEntry not rendered; associate-existing onlyn/an/aGuard rail TCKT-S07-NEG.

PRD Section Coverage

PRD section #TitleWhere covered
HEADER BLOCKMetadataMetadata table (this RFC)
2SUPPORT Context§1 Overview + Dependencies
3One-liner + Problem§1 Overview
4Target Users + PersonaDetail 1.A Role Coverage
5Non-Goals§1 Out of Scope
Scope ChangesFrontend/Design/Backend(conditional)§1 Dependencies, §2.D, §4.C #6
6Constraints§1 Assumptions, §3 (sandbox/origin), §4.A (flag)
7New Features (entry point + embed)§2.A, §2.C, §4.C
8API & Webhook Behavior (postMessage contract)§2.2 sequence, §2.4, §2.A
9System Flow + Stories + ACsDetail 1.A, Detail 1.C
10Rollout§4 Rollout Strategy
11Observability§3 Monitoring & Alerting
12Success Metrics§1 Success Criteria, §3
13Dependencies§1 Dependencies, §5
14Key Decisions + AlternativesDetail 1.B
15Open 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

DecisionChosen optionAlternatives rejectedWhy rejected
D-1: Consume CRM embed, build nothing CDP-nativeReuse /embed/ticket/create iframeBuild a native CDP ticket formDuplicates CRM logic; perpetuates the parity-lag the anchor exists to remove.
D-2: Use the typed versioned postMessage contractEMBED_INITTICKET_CREATED/…, version:1, origin-validatedMirror the shipped Deal {msg} string contractThe 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 qontakCustomerIdpostMessage payloadtoken + 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_sourceCRM adds embed-web-cdp to the allow-listCDP sends a source field client-sideField is data_source; client value is overwritten server-side. CDP cannot influence it.
D-5: Degrade gracefully to associate-existingFeature is additive; entry hidden when embed unavailableBlock the Tickets section on embed availabilityThe associate path already exists and must never regress.
D-6: New component TicketEmbedPanel.vue vs inline in AssociatedTickets.vueExtract a child componentInline 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 idStory titleLayer scopeChanges (concrete FE artifacts + cross-layer refs)Composite AC idsAcceptance criteria (verifiable)RFC anchors
TCKT-S01Open the Create-Ticket embed from CDPFE-onlyAssociatedTickets.vue: add MpPopover "+" menu ("Associate existing" / "Create Ticket") mirroring AssociatedDeals.vue L35–53 • new TicketEmbedPanel.vue (sandboxed iframe) • isShowCreateTicket toggleTCKT-S01/AC-1, AC-2, ERR-1Vitest: 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-S02Auto-fill the customer in the embedFE-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 postingTCKT-S02/AC-1, AC-2, ERR-1, ERR-2Vitest: 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-S03Confirm creation + refresh on TICKET_CREATEDFE-onlyTicketEmbedPanel.vue: window message handler validating event.origin + payload.version === 1; emit created / close / error / resizeAssociatedTickets.vue: on createdfetchWithReset() + customerStore.markContactUpdated('tickets') + success toastTCKT-S03/AC-1, AC-2, AC-3, ERR-1, ERR-2Vitest: 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-S04Created ticket associated to the customerFE + 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 refreshTCKT-S04/AC-1, AC-2, ERR-1Vitest (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-S05data_source labels the ticket as CDP-originatedCross-squadn/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-1n/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-S06Pipeline + custom layout inherited from CRMCross-squadn/a — CRM-owned (FE RFC useTicketPipeline / useTicketLayout); CDP only hosts the iframe.TCKT-S06/AC-1, AC-2n/a — covered in CRM FE RFC§1 Out of Scope
TCKT-S07-NEGNo create without permission / flagFE-only + ConfigAssociatedTickets.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 trueTCKT-S07-NEG/NEG-1, NEG-2Vitest: 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 RFC per 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

PathWhy the agent reads itWhat pattern it teaches
features/customers/detail/components/AssociatedDeals.vueThe shipped embed-in-detail pattern to mirrorisShowCreateDeal toggle + iframe panel + createDealUrl computed + handleCreateDealMessage origin-validated listener + MpPopover add-menu + mount/unmount window.addEventListener('message')
features/customers/detail/components/AssociatedTickets.vueThe file to extendExisting Tickets section: useTicketStore, fetchTicketsAssociated, associate POST/DELETE /v1/leads/{contactId}/tickets/{id}, customerStore.markContactUpdated('tickets'), toastNotify
features/customers/store/TicketStore.tsThe store to refresh after createfetchTicketsAssociated(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.vueThe permission-gate patterncanAddNotes computed: storeToRefs(useUserStore())Object.values(permissions.value)[0].permissions.find(p => p.name === KEY)?.is_enabled === true
features/customers/detail/components/Notes/constants.tsWhere permission keys are definedexport const ADD_NOTES_PERMISSION = 'customers_customernotes_add' — new ticket-create key goes in a sibling constants file
common/store/FeatureFlagStore.tsHow feature flags gate UIuseFeatureFlagStore().featureFlags[code]; codes added to FeatureFlags + DEFAULT_FEATURE_FLAGS; resolved as enabled && enabled_for_company from GET /v1/feature_flags
common/composables/useMixpanel.tsAnalytics conventionuseMixpanel().track('[Qontak One] [Customer] <Action>', props)
common/composables/useCustomConfig.tsConfig accessconst { config } = useCustomConfig(); config.CRM_V3_EMBED_URL, config.CUSTOMER_360_CRM_URL
common/constants/auth.tsSession token keyTOKEN_KEY = 'global_sso_token' (read only if OQ-2 forces a token param)
features/customers/detail/components/Notes/Notes.spec.tsThe test patternvitest + @testing-library/vue + msw + @pinia/testing createTestingPinia + permission ref mocking

Patterns to Follow (and where to find them)

ConcernPattern in repoReference fileDeviation in this RFC?
State managementPinia setup stores (storeToRefs)features/customers/store/TicketStore.ts, UserStore.tsnone
Folder conventionFeature-scoped components under features/customers/detail/components/AssociatedDeals.vuenone — TicketEmbedPanel.vue lands beside AssociatedTickets.vue
StylingPixel3 design system + css() pandaAssociatedDeals.vue (imports from @mekari/pixel3)none
Error / toast / retrytoastNotify({position,variant,title})utils/toast used in AssociatedTickets.vuenone
iframe embedsandboxed <iframe> + origin-validated window message listenerAssociatedDeals.vue L9–19, L287–310, L368–373yes — typed version:1 payload + EMBED_INIT sender, replacing the legacy {msg} string + URL-param contract (D-2, D-3)
Feature flaguseFeatureFlagStore().featureFlags[code]features/customers/detail/components/MenuSidebar.vue L36–37none
AnalyticsuseMixpanel().track(...)features/customers/detail/views/DetailPage.vue L90–102none

Reading Order for the Agent

  1. AssociatedDeals.vue — the embed pattern to mirror (and the contract to supersede).
  2. AssociatedTickets.vue — the file you extend; learn its store + associate flow.
  3. TicketStore.tsfetchTicketsAssociated (the refresh you call after create).
  4. Notes.vue + Notes/constants.ts — the permission-gate pattern + where keys live.
  5. FeatureFlagStore.ts — how to add + read a new flag code.
  6. useMixpanel.ts — analytics event convention.
  7. useCustomConfig.ts + common/constants/auth.ts — config + session token keys.
  8. Notes.spec.ts — the vitest + testing-library + msw + pinia test harness.

Source Verification (anti-hallucination — required)

Anchor / pattern / contractVerified byEvidence
AssociatedDeals.vue embed patternreadcreateDealUrl 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 targetreadassociate POST /v1/leads/${contactId}/tickets/${ticketId} L182; ticketStore.fetchTicketsAssociated L241; customerStore.markContactUpdated('tickets') L202; toastNotify L208
TicketStore.fetchTicketsAssociatedreaddeclared L168; appendIfExists(params,'qontak_customer_id',contactId) L153; GET /v1/tickets?... on config.CUSTOMER_360_CRM_URL L179
Permission gate patternreadNotes.vue canAddNotes L83–97 (permission?.is_enabled === true L96); key ADD_NOTES_PERMISSION = 'customers_customernotes_add' in Notes/constants.ts L27
FeatureFlagStorereadFeatureFlags 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 conventionreadtrack(trackerEvent, options) L39; event names '[Qontak One] [Customer] …' (e.g. DetailPage.vue L102)
Config keysread / grepuseCustomConfig.ts returns config; config.CRM_V3_EMBED_URL + config.CUSTOMER_360_CRM_URL referenced across repo (grep of config.[A-Z_]+)
TOKEN_KEYreadcommon/constants/auth.ts: const TOKEN_KEY = 'global_sso_token'
Test harnessreadNotes.spec.ts L1–14: vitest, @testing-library/vue, msw server, @pinia/testing createTestingPinia, @testing-library/user-event
Build/test commandsreadpackage.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 reusabilityNOT verifiable in this repoCRM-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 / componentImplementing fileReuse vs newDesign tokens usedDeviation 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 abovepending — 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

MethodPathStatusContract authorityNotes
GET/v1/tickets?qontak_customer_id=… (via TicketStore.fetchTicketsAssociated)existsTicketStore.ts L168–179reused — the post-create refresh. baseURL: config.CUSTOMER_360_CRM_URL.
POST/v1/leads/{contactId}/tickets/{ticketId}existsAssociatedTickets.vue L182reusedfallback associate only, used iff CRM does not auto-associate (OQ-6).
— (iframe)${config.CRM_V3_EMBED_URL}/embed/ticket/createneeds-building / blockedCRM FE RFCnew-with-justification: a new CRM route reused by a non-chat host; CDP cannot build it. OUT of CRM Phase 1 (OQ-1).
postMessageEMBED_INIT / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_CLOSE / EMBED_RESIZEneeds-building / blockedCRM FE RFCnew-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 mirrors AssociatedDeals.vue create 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 in TicketStore (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 a title="Create Ticket".

AssociatedTickets.vue (extend):

  • Add MpPopover "+" menu (mirroring AssociatedDeals.vue L35–53): items "Associate existing" (existing handleShowAssociateExisting) + "Create Ticket" (new handleShowCreateTicket), the latter rendered only when canCreateTicket.
  • Add canCreateTicket computed (permission + flag) and isShowCreateTicket ref.
  • 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 created event 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

SurfaceLoadingEmptyErrorPartialSuccess
Embed panel (TicketEmbedPanel.vue)Spinner/skeleton until EMBED_READY (or embedReadyTimeoutMs)n/a — panel always renders the CRM form once readyiframe load fail or TICKET_CREATE_ERROR → inline error + retry + associate-existing hintn/a — single embed, no partialTICKET_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" toastexisting infinite-scroll load-morerefreshed 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 of Notes/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 to FeatureFlags + DEFAULT_FEATURE_FLAGS).
  • Files explicitly NOT touched: AssociatedDeals.vue (reference only); the associate-existing flow in AssociatedTickets.vue (must not regress); TicketStore.ts write 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

EntityState field / event consumedDefault valuesSource endpoint / eventStale-tolerance window
Embed panellocal state machine (hiddenloadingreadysuccess/error)hiddenpostMessage events from CRM embedn/a — live, event-driven
Associated tickets listassociatedTicketData + ticketStore.totalData[], 0GET /v1/tickets (re-fetched on created)refreshed immediately after TICKET_CREATED

Detail 2.F — Asset Inventory

Asset nameTypeSourceFormat & sizesPath in repo
ticket.png (empty state — existing)imageexisting in repoPNGassets/images/ticket.png (reused, unchanged)
Create Ticket menu iconiconPixel3 MpIcon (add) — reuse Deal patternDS 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 NameTriggerProperties
cdp_ticket_embed_openedCreate-Ticket embed openedcompany_sso_id, contact_id, qontak_customer_id, user_id
cdp_ticket_embed_createdTICKET_CREATED received + validatedcontact_id, ticket_id, pipeline_id, data_source
cdp_ticket_embed_create_errorTICKET_CREATE_ERROR receivedcontact_id, error_code
cdp_ticket_embed_associate_failedFallback associate call failed (OQ-6)contact_id, ticket_id, reason
cdp_ticket_embed_init_droppedEMBED_INIT blocked by origin guardcompany_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 the event payload 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_error rate > 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_INIT to 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).origin and payload.version === 1 before any side effect (mirrors AssociatedDeals.vue L292; tightened with the version check). Failing either → no-op + cdp_ticket_embed_init_dropped.
    • Outbound EMBED_INIT is posted to the exact CRM origin string, never '*' (carries contact PII).
    • Clickjacking / sandbox — iframe uses sandbox="allow-forms allow-scripts allow-same-origin" and referrerpolicy="strict-origin-when-cross-origin" (exact attributes from AssociatedDeals.vue L15–16).
  • Auth token storage: unchanged — global_sso_token httpOnly-style cookie read via useCookie(TOKEN_KEY). Per D-3 the ticket embed authenticates itself (no token URL 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:// (mirror AssociatedDeals.vue L167).
  • PII handling: contact PII transits postMessage to a validated origin only; not persisted by CDP.

Detail 3.A — Failure Mode Catalog

API call401403404429500Timeout (s)OfflineRetry mechanism
iframe load /embed/ticket/createn/a (iframe)n/aerror staten/aerror state10s no EMBED_READY → errorerror state; associate-existing availableuser "retry" re-mounts iframe
TICKET_CREATE_ERROR (from embed)shown inlineshown inlineshown inlineshown inlineshown inlinen/an/apanel stays open; user resubmits in embed
fallback POST /v1/leads/{contactId}/tickets/{ticketId}toast + logtoast + logtoast + logtoast + logtoast + logdefault $customFetchtoastlog 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 embedonUnmounted removes the message listener (mirror AssociatedDeals.vue L372).

Detail 3.B — Error Message Catalog

Error codeUser-facing message (i18n key)SurfaceUser-facing?
embed_load_failed"Couldn't load the ticket form. Try again or use Associate existing."inline (panel)yes
embed_ready_timeoutsame as aboveinline (panel)yes
TICKET_CREATE_ERROR (passthrough)embed-provided errorMessage, else genericinline (panel)yes
associate_failed"Ticket created but couldn't be linked. Retry?"toastyes
origin_dropped(none — silent no-op)n/ano

Detail 3.C — Accessibility

  • WCAG AA.
  • Keyboard: "+" popover navigable; opening the panel moves focus into it; back-arrow / Esc returns focus to the trigger.
  • Focus management: trap focus within the panel header controls; the iframe owns its internal focus.
  • ARIA: iframe title; error state uses role="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 to FeatureFlagStore (enabled && enabled_for_company), default OFF. Layered under CRM's unify_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_error rate > 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 / flagTypeDefaultRequiredProvisioner
cdp_create_ticket_embed (feature flag code; name TBD OQ-7)booleanfalseyesCDP feature-flag service (/v1/feature_flags)
config.CRM_V3_EMBED_URLstring (URL)existingyes (reused)existing runtime config (__CUSTOM_CONFIG__)
config.CUSTOMER_360_CRM_URLstring (URL)existingyes (reused)existing runtime config

Detail 4.B — Test Plan (commands sourced from package.json)

LayerCommand (source)What it must prove
Unit/componentpnpm 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
Coveragepnpm test:coverage (script test:coverage: "vitest run --coverage")coverage does not regress
Lintpnpm lint (script lint: "eslint .")no lint errors
Buildpnpm build (script build: "nuxt build && node scripts/fix-federation-remote-entry.mjs")app compiles with the new component
E2En/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)
Typecheckn/a — no standalone typecheck script; nuxt build performs type checking

Detail 4.C — Agent Execution Plan

OrderChunkFiles to modify/createCommands to runAcceptance criteria (verifiable)
1Scaffold TicketEmbedPanel.vue (iframe + state machine, no contract yet)create features/customers/detail/components/TicketEmbedPanel.vuepnpm lint · pnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.tsComponent renders a sandboxed iframe with src ending /embed/ticket/create; loading state shown by default
2Permission + flag gate + add the flag codemodify AssociatedTickets.vue (add canCreateTicket), common/store/FeatureFlagStore.ts (add flag code + default), create ticket-create permission constantpnpm vitest run features/customers/detail/components/AssociatedTicketsVitest: entry hidden when permission disabled OR flag OFF; visible only when both true (TCKT-S07-NEG/NEG-1, NEG-2)
3Wire the "+" menu item + open/close panelmodify AssociatedTickets.vue (MpPopover item "Create Ticket", isShowCreateTicket, back-arrow)pnpm vitest run features/customers/detail/components/AssociatedTickets · pnpm lintClicking "Create Ticket" opens panel; back-arrow closes; associate-existing item still works (TCKT-S01/AC-1, AC-2)
4Implement the typed postMessage handler + EMBED_INIT sendermodify 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.tsVitest: 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/*)
5Parent success handling (refresh + toast + analytics)modify AssociatedTickets.vue (on created: fetchWithReset + markContactUpdated('tickets') + toast + useMixpanel().track)pnpm vitest run features/customers/detail/components/AssociatedTicketsVitest: valid createdfetchTicketsAssociated re-invoked, success toast fired, cdp_ticket_embed_created tracked (TCKT-S03/AC-1)
6Optional 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/AssociatedTicketsVitest: fallback path issues one POST with ticketId from payload; failure logs cdp_ticket_embed_associate_failed (TCKT-S04/AC-2, ERR-1)
7Final verificationpnpm lint · pnpm test:coverage · pnpm buildAll 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):
    1. pnpm lint
    2. pnpm vitest run features/customers/detail/components/TicketEmbedPanel.spec.ts
    3. pnpm vitest run features/customers/detail/components/AssociatedTickets
    4. pnpm build
  • Post-deploy verification signals:
    • Mixpanel: cdp_ticket_embed_opened > 0 and cdp_ticket_embed_created > 0 on the QA company within first session.
    • Mixpanel: cdp_ticket_embed_create_error rate < 10%; cdp_ticket_embed_associate_failed < 5%.
    • Datadog RUM: no spike in iframe-load errors / cdp_ticket_embed_init_dropped.
  • Rollback recipe:
    1. Toggle the cdp_create_ticket_embed flag OFF for the affected company (entry hides immediately, no deploy).
    2. If a code defect, revert the PR introducing TicketEmbedPanel.vue + AssociatedTickets.vue changes.
    3. Confirm cdp_ticket_embed_create_error returns to baseline and the associate-existing flow still works (its tests pass) within 15 min.

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 postMessage payload field names (§2.A) are taken from the PRD, not a frozen CRM contract. CRM must freeze EMBED_INIT / TICKET_CREATED / TICKET_CREATE_ERROR / EMBED_CLOSE / EMBED_RESIZE before chunk 4. This RFC's §2.A block is provisional.
  • OQ-3: CDP embed_source (embed-web-cdp) added to BE ALLOWED_EMBED_SOURCES (else data_source coerced to embed-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.vue permission-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

DateComment(s) FromAction Item(s)
2026-06-30rfc-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.vue pattern, 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/create reusability 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_source allow-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-reviewer for a second-pass score after the blocking OQs close and §7 flips to yes.