Skip to main content

Consistent Deal & Ticket Creation Experience

Pre-Grooming Code Grounding Report

Feature: Consistent Deal & Ticket Creation Experience PM: Alma Syafira | Squad: CRM | PRD Type: PHASE | Target: Q3'26 Grounding date: 2026-06-22 · Last updated: 2026-06-30 (v1.2 scope realignment) Worktrees: chatbot · qontak.com · hub-core · hub-worker


Part 1 — Intent & Objective

One-liner

Enable users to access a consistent deal & ticket experience regardless of whether the record is created manually, automatically, by chatbot, or by Agentic AI.

Core Problem

Four creation entry points exist (manual, auto-create, chatbot, Agentic AI). The first two produce a consistent experience; the latter two do not. Users cannot tell from a chat room whether a deal or ticket already exists, and records created by bots lack the contextual data available in manually created ones.

What the Form Proposes to Build

In the chat room (Omnichannel)

  • Display deal/ticket preview (Name, ID, Stage, Pipeline) once created by bot/AI
  • Allow navigation from the preview to the deal/ticket detail (new tab)

In the deal/ticket detail (CRM)

  • Auto-associate the conversation contact with the created record
  • Create a timeline log with chat history preview
  • Allow navigation from timeline back to the originating chat room
  • Display creator as "Bot" or "AI" (not the user who configured the flow)

Integration strategy

  • Create / Update: API-driven (CRM → Omnichannel and Chatbot → CRM)
  • Delete synchronization: webhook-driven (CRM emits, Omnichannel consumes)

What Changed vs. Previous Version

AreaBeforeNow
Object scopeDeal-focusedBoth Deal AND Ticket
Creator label"Qontak System""Bot" or "AI" (creation-source-aware)
Scope structureOne blockSplit per system: CRM / Omnichannel / Chatbot
Integration strategyVagueExplicit: create/update = API · delete = webhook
Ticket happy-path flowAbsentFull Section 5 flow added

Part 2 — Code Grounding

Method: Every claim verified against actual code. File:line references are exact. Verdict scale: ✅ Confirmed · ⚠️ Partial · ❌ Contradicted / Not Found


Chatbot (/chatbot)

#ClaimVerdictKey Evidence
C1Calls CRM deal & ticket create APIsConfirmedexecute.rb:18-25POST /api/v3.1/deals & /api/v3.1/tickets; legacy path via send_api_integration.rb
C2Must add room_id to payload — absent todayConfirmed absent@room_id used only for Room lookup (execute.rb:75) and HTTP client logging (execute.rb:302). Never injected into request body.
C3Must add created_by_bot flag — absent todayConfirmed absentZero matches for bot, is_bot, actor, creator_type in node execution path
C4Source auto-fill: empty→channel, provided→keepConfirmed absentNo channel→source mapping exists. room.channel is available (room.rb:20,42) but never used to derive a CRM source value. Entirely net-new.
C5Must pass contact_id for deal contact association⚠️ PartialDeal already sends crm_lead_ids via phone lookup (execute.rb:261-275). Ticket create has zero contact association — guard at execute.rb:74 is if @node_type == 'qontak_crm_deal_create' only.
C6Per-flow source value must not be overriddenConfirmedcrm_source_id/crm_source_name stored in api_body JSON per intent (update.rb:250). Sent verbatim to CRM. Legacy path only — node path has no equivalent.
C7Agentic AI uses same execution path as chatbotConfirmedaction_execute.rb:144-155ActionExecutorFactoryMekariQontakCrm::Execute. No separate Agentic AI CRM path exists anywhere.
C8NodeRegistry defines what params the bot sendsConfirmed (DB-driven)execute.rb:243-251 reads NodeRegistry.properties at runtime. Adding room_id / created_by_bot requires both a DB record update AND injection logic in execute.rb.

CRM (/qontak.com)

#ClaimVerdictKey Evidence
R1Deal create API accepts room_idConfirmedAccepted as channel_integration_room_id. v3dot1/deals.rb:75, v4/deals.rb:92, schema.rb:1392
R2Deal create API accepts contact_id⚠️ PartialNo contact_id. Accepts crm_person_id and crm_lead_ids[]. v3dot1/deals.rb:899, v4/deals.rb:659
R3Deal create API accepts bot/AI creator flagNot foundNo created_by_bot, actor_type in deal_params. creator_id must resolve to a real User.
R4Ticket create API accepts room_idConfirmed (via service)Set in Ticket::Create service (ticket/create.rb:46), not in strong params. schema.rb:4941
R5Ticket create API accepts bot/AI creator flagNot founddata_source IS permitted by ticket_params (v3dot1/tickets.rb:528) but immediately overwritten to "open-api-v3" (line 61). Callers cannot control it.
R6Display creator as "Bot" or "AI" in activity logContradictedaudit.rb:1824-1842 mapping_who only renders: User full name, "Qontak system", or "<deleted user>". data_source is never consulted in the timeline. Entirely net-new work.
R7data_source distinguishes Chatbot vs Agentic AIContradictedBoth would receive "open-api" today (v3dot1/deals.rb:73). No "chatbot" or "agentic_ai" enum value exists. Distinction requires the new flag from C3/R3.
R8CRM sends deal delete webhook to OmnichannelNot foundDeal destroy fires Crm::WebhookSender::ShouldSendWebhookService → generic customer webhooks only. Does not call OMNICHANNEL_TICKET_WEBHOOK. deal.rb:1630-1640
R9CRM sends ticket delete webhook to OmnichannelConfirmed (feature-flag gated)ticket.rb:617-620Crm::WebhookSender::Omnichannel::SendWebhookWorker. Gated by Feature.enabled_globally('ticket_deletion_webhook'). Sends to ENV['OMNICHANNEL_TICKET_WEBHOOK'].
R10CRM calls Omnichannel API on deal/ticket createNot foundNo outbound HTTP to Omnichannel on any create/update path in deal or ticket controllers. Net-new.
R11Chat history endpoint existsConfirmedGET /api/v4/deals/:id/chat-historyChatHistory::GetChatHistory → fetches up to 100 messages from Qiscus or ChatPanel. chat_history/get_chat_history.rb
R12Room navigation deeplink from timelineNot foundChat-history endpoint returns { room_id, type, contents }. No navigation URL field anywhere.
R13Source auto-fill from channel if empty⚠️ PartialExists only in Hub::Ticket::V2::DealBuilder (internal webhook path, deal_builder.rb:83-91). Open API create path has zero source auto-fill.
R141 room = 1 deal; second create → latest winsContradictedDB unique index channel_integration_room_unique_key on (channel_integration_id, channel_integration_room_id). Second create → ActiveRecord::RecordNotUnique422 error (v3dot1/deals.rb:1409). No replacement logic exists.

Chat Panel (/hub-core + /hub-worker)

Note: hub-worker contains zero CRM logic — only billing/WhatsApp/auto-resolve workers. All CRM logic is in hub-core. hub-service is not available in this workspace; HTTP routes live there.

#ClaimVerdictKey Evidence
H1Ticket create webhook accepts room_id + 3 params⚠️ Partial / MislabeledCrm::Interactors::Ticket::UserCreate (user_create.rb:4-13) accepts all 4 params, but is invoked via direct API call from hub-service — NOT through the webhook. The webhook receiver (ticket_receiver.rb:12-19) only handles delete.
H2Deal create webhook: external_id, external_url, room_idConfirmeddeal_receiver.rb:49-61 extracts payload.id → external_id, payload.slug → external_url, payload.channel_integration_room_id → room_id.
H3"Ticket create uses webhook; needs update"ContradictedTicket create is already API-driven via UserCreate interactor. Webhook receiver handles only delete. The form has this backwards — deal create is the webhook-driven one.
H4Dedicated CRM→Omnichannel create/update API existsNot foundhub-core has an empty routes.rb — it is a Rails Engine gem with no HTTP controllers. UserCreateDirect interactor exists but is not wired to any HTTP endpoint. Must be built in hub-service.
H5Actor type mechanism to filter CRM→Omnichannel eventsNot foundnotification_receiver.rb filters only by event_type string and action. Zero actor/source/initiator filtering anywhere in the webhook pipeline.
H6Deal doesn't store actor/initiatorConfirmedqontak_crm_objects schema: room_id, organization_id, external_id, external_url, object_type, deleted_at, timestamps. No actor fields. Contrast: room_tickets stores full created_by_id/type, updated_by_id/type, submitted_by_id/type.
H71 room = 1 deal = 1 ticketConfirmedUnique partial indexes: idx_room_deal on [room_id, organization_id, object_type] WHERE deleted_at IS NULL; room_tickets unique on [room_id, organization_id].
H7aSecond deal for same room → latest shownContradicteddeal_receiver.rb:49: return failure('Room deal already exist!') — new deal is rejected, not replaced.
H8Deal/ticket preview: Name, ID, Stage, Pipeline⚠️ PartialFields confirmed: deal_formatted_detail.rb:18-20 returns id, name, slug, crm_pipeline_name, crm_stage_name. But fetched live from CRM API on each request — only external_id stored locally.
H9CRM deal deletion clears Omnichannel previewConfirmeddeal_receiver.rb:41-47 soft-deletes QontakCrmObject. get_deal.rb returns 410 Gone. Preview clears.
H10CRM ticket deletion clears Omnichannel previewConfirmedticket_receiver.rb:30-35 soft-deletes RoomTicket. Same 410 pattern.
H11hub-worker contains CRM logicContradictedhub-worker has zero CRM code. All CRM logic is exclusively in hub-core.

Part 3 — Critical Corrections for Grooming

These 6 corrections must be resolved before the PRD is written. Each represents a gap between the form's assumption and what the code actually does.


❗ Correction 1 — "Latest wins" doesn't exist — it will 422

Form says: If Agentic AI creates a deal when auto-create already fired, Omnichannel shows the latest one.

Code says: The DB unique index channel_integration_room_unique_key causes a 422 on the second create. Neither hub-core nor qontak.com has soft-delete-and-replace logic.

Decision required: Implement soft-delete-and-replace (delete existing deal → create new) or document the 422 as intentional product behavior.


❗ Correction 2 — "Bot"/"AI" creator label is 3 layers of net-new work

Form says: "CRM uses the flag to display the author as Bot in the activity log."

Code says: audit.rb:1824-1842 mapping_who only branches on User, "Qontak system", or "<deleted user>". data_source is never read by the timeline renderer.

Work required:

  1. Accept new flag param on deal/ticket create (CRM API layer)
  2. Store it — new data_source enum values ("chatbot" / "agentic_ai") or a new column
  3. audit.rb:mapping_who — add branch to render "Bot" or "AI" based on stored value

❗ Correction 3 — Deal delete webhook to Omnichannel does NOT exist

Form says: "Send deal/ticket deletion webhooks back to Omnichannel."

Code says:

  • Ticket delete → Omnichannel ✅ (exists, feature-flagged at ticket.rb:617-620)
  • Deal delete → Omnichannel ❌ (uses generic customer webhook via ShouldSendWebhookService, NOT the OMNICHANNEL_TICKET_WEBHOOK endpoint)

Work required: Build deal delete → Omnichannel sender, mirroring the existing ticket path.


❗ Correction 4 — The ticket / deal webhook claim is backwards

Form says: "Omnichannel needs to update ticket integration to support ticket creation events, as the current implementation uses webhook for create & ticket."

Code says:

  • Ticket create is already API-driven via Crm::Interactors::Ticket::UserCreate. The webhook receiver only handles delete. No change needed on the ticket side.
  • Deal create is the webhook-driven flow (qontak.crm.deal.createDealWebhookReceiverWorkerdeal_receiver.rb). If the strategy shifts to "create = API", the deal path needs the new dedicated API endpoint in hub-service — not the ticket path.

❗ Correction 5 — Source auto-fill is net-new for the API create path

Form says (under CRM scope): "Source auto-fill based on conversation channel."

Code says:

  • Source auto-fill exists only in Hub::Ticket::V2::DealBuilder (the internal webhook path, deal_builder.rb:83-91)
  • The CRM Open API create path (POST /api/v3.1/deals) has zero auto-fill logic — if crm_source_name is absent, no fallback occurs
  • The chatbot sends no source param today (confirmed in C4)

Decision required: Where does auto-fill live — in the chatbot (send a derived source), or in CRM's API create path (server-side fallback)?


❗ Correction 6 — Ticket contact association is completely missing in chatbot

Form says: Chatbot/Agentic AI should associate contact with created ticket.

Code says: execute.rb:74 guards contact enrichment with if @node_type == 'qontak_crm_deal_create' — the ticket create path (qontak_crm_ticket_create) gets zero contact association. This is a full gap separate from the deal path.


Part 4 — Real Work Decomposition

v1.2 Scope Realignment (2026-06-30): CRM now owns the full embedded Deal/Ticket component — all preview, navigation, empty state, permissions, and business logic rendered inside the Omnichannel Deal & Ticket section. Omnichannel owns only the section/tab container shell and the feature flag. This shifts the preview/navigation frontend tasks from Omnichannel to CRM Frontend. All code grounding findings (Part 2) remain unchanged — they are code facts, not scope decisions.


Chatbot

Core file: app/core/repositories/node_executions/nodes/mekari_qontak_crm/execute.rb

TaskWhat to change
Add room_id to deal & ticket create bodyInject channel_integration_room_id from @room_id in process_body
Add creator_flag paramInject "bot" or "agentic_ai" into body for both deal and ticket create action types
Extend contact association to ticket createRemove if @node_type == 'qontak_crm_deal_create' guard (execute.rb:74) or extract to shared method; ticket create currently gets zero contact enrichment
Register new params in NodeRegistryAdd room_id and creator_flag entries (with destination: 'body') to node_registries DB rows for both node types

CRM Backend (qontak.com)

TaskWhat to change
Accept creator_flag on deal createAdd param to deal_params in v3dot1/deals.rb + v4/deals.rb; store as new data_source enum value or dedicated column
Accept creator_flag on ticket createAdd param to ticket_params (v3dot1/tickets.rb); stop unconditionally overwriting data_source to "open-api-v3" (line 61)
Display "Bot"/"AI" in activity logUpdate audit.rb:mapping_who (lines 1824-1842) — add branch reading the new flag/column to return "Bot" or "AI"
Build deal delete webhook → OmnichannelAdd after_destroy on Crm::Deal mirroring ticket.rb:617-620; create Deal::Delete omnichannel sender (currently deal destroy only fires generic customer webhooks via ShouldSendWebhookService)
Build CRM → Omnichannel create/update/delete event APIsNew outbound HTTP calls on deal/ticket create, update, and delete to dedicated Omnichannel endpoint (net-new — no such calls exist today, confirmed at R10)
Source auto-fill on API create pathAdd fallback: if crm_source_name blank → derive from channel context. Currently zero auto-fill on Open API path (R13 — only exists in internal webhook DealBuilder)
"Latest wins" replace logicDecision-dependent: implement soft-delete-and-replace on deal create (overcoming DB unique constraint R14 / H7a) or document the 422 as intentional and gate the UX accordingly
Chat history retrieval + timeline logOn deal/ticket create with room_id: call Chat Panel API → create BotCreationTimelineEntry with deeplink + creator badge. Net-new — no timeline entry for bot/AI records exists today (R6, R12)

CRM Frontend (qontak.com)

These tasks are net-new as of v1.2 — previously assumed to be Omnichannel Frontend's responsibility.

TaskWhat to change
Build embedded Deal/Ticket preview componentNew CRM-owned component rendered inside Omnichannel's Deal & Ticket section/tab. Displays: Deal Name (clickable), Deal ID, Stage, Pipeline / Ticket Name, Ticket ID, Status
Handle empty state within the sectionCRM embedded component renders empty state when no deal/ticket exists for the room
Handle navigation → deal/ticket detail (new tab)CRM embedded component owns the click handler; opens external_url in new tab
Handle 403 / permission errorsCRM embedded component handles agents without CRM permissions; Omnichannel has no additional error handling responsibility
Receive and react to CRM refresh eventsEmbedded component re-renders on create, update, and delete events triggered by Omnichannel from CRM API calls
Creator badge ("Bot" / "AI") on deal/ticket detailRender creator_flag value in Creator field on deal/ticket detail page

Omnichannel Frontend (hub-service / Omnichannel)

Scope reduced in v1.2 to container + flag only.

TaskWhat to change
Provide Deal & Ticket section/tab containerAdd the section/tab shell in the conversation room UI; host the CRM embedded component inside it
Feature flagDefine and gate the section/tab visibility per account (default OFF). CRM embedded component is not separately flagged
Trigger embedded component refreshOn receiving create/update/delete events from CRM Backend, signal the embedded component to re-render

Chat Panel (hub-core + hub-service)

TaskWhat to change
Add actor/initiator to deal storageMigration: add created_by_id, created_by_type to qontak_crm_objects (currently absent — confirmed H6; room_tickets already has these columns)
Wire deal create to HTTP endpointhub-service: expose POST endpoint calling Deal::UserCreateDirect interactor (currently unrouted — H4)
Wire ticket create to HTTP endpointhub-service: confirm existing route for Ticket::UserCreate; verify callable from CRM
"Latest wins" replace logicdeal_receiver.rb:49: replace return failure('Room deal already exist!') with soft-delete-and-replace if that decision is made
Actor/source filteringnotification_receiver.rb: add filter so only bot/AI-originated events (creator_flag present) trigger the CRM embedded component refresh signal; currently filters only by event_type + action (H5)

Part 5 — Dependency Map (Updated for v1.2)

Chatbot / Agentic AI
└─ sends room_id + creator_flag + contact_id


CRM Backend (qontak.com)
├─ stores room_id, creator_flag, contact association
├─ renders "Bot"/"AI" in activity log (audit.rb)
├─ builds timeline log entry (chat history + deeplink)
├─ calls Omnichannel API: create event ────────────────┐
├─ calls Omnichannel API: update event ────────────────┤
└─ calls Omnichannel API: delete event ────────────────┤

Omnichannel Backend (hub-service + hub-core)
├─ stores deal/ticket association per room
├─ signals CRM embedded component to refresh
└─ clears association on deletion event


CRM Frontend (embedded component)
├─ renders deal/ticket preview (Name, ID, Stage, Pipeline)
├─ renders empty state
├─ handles navigation → detail (new tab)
├─ handles 403 / permission errors
└─ displays creator badge ("Bot" / "AI")

Omnichannel Frontend
└─ provides section/tab container + feature flag

Blocking chain:

  1. Chatbot must send room_id + creator_flag → unblocks CRM storage and creator label
  2. CRM Backend must call Omnichannel create API → unblocks embedded component refresh
  3. CRM Backend must send deal delete event → unblocks preview clearing for deals (ticket delete already exists, feature-flagged)
  4. hub-service must expose deal create HTTP endpoint → unblocks CRM → Omnichannel create event flow
  5. CRM Frontend embedded component must be built → unblocks agent-visible preview (this is the critical frontend path in v1.2)

Generated by Claude Code against actual worktree code at /Users/mekari/Documents/repo_qontak. No assumptions made — all claims verified with file:line references. Parts 4–5 updated 2026-06-30 to reflect v1.2 CRM/Omnichannel ownership realignment.