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
| Area | Before | Now |
|---|---|---|
| Object scope | Deal-focused | Both Deal AND Ticket |
| Creator label | "Qontak System" | "Bot" or "AI" (creation-source-aware) |
| Scope structure | One block | Split per system: CRM / Omnichannel / Chatbot |
| Integration strategy | Vague | Explicit: create/update = API · delete = webhook |
| Ticket happy-path flow | Absent | Full 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)
| # | Claim | Verdict | Key Evidence |
|---|---|---|---|
| C1 | Calls CRM deal & ticket create APIs | ✅ Confirmed | execute.rb:18-25 — POST /api/v3.1/deals & /api/v3.1/tickets; legacy path via send_api_integration.rb |
| C2 | Must add room_id to payload — absent today | ✅ Confirmed absent | @room_id used only for Room lookup (execute.rb:75) and HTTP client logging (execute.rb:302). Never injected into request body. |
| C3 | Must add created_by_bot flag — absent today | ✅ Confirmed absent | Zero matches for bot, is_bot, actor, creator_type in node execution path |
| C4 | Source auto-fill: empty→channel, provided→keep | ✅ Confirmed absent | No channel→source mapping exists. room.channel is available (room.rb:20,42) but never used to derive a CRM source value. Entirely net-new. |
| C5 | Must pass contact_id for deal contact association | ⚠️ Partial | Deal 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. |
| C6 | Per-flow source value must not be overridden | ✅ Confirmed | crm_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. |
| C7 | Agentic AI uses same execution path as chatbot | ✅ Confirmed | action_execute.rb:144-155 → ActionExecutorFactory → MekariQontakCrm::Execute. No separate Agentic AI CRM path exists anywhere. |
| C8 | NodeRegistry defines what params the bot sends | ✅ Confirmed (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)
| # | Claim | Verdict | Key Evidence |
|---|---|---|---|
| R1 | Deal create API accepts room_id | ✅ Confirmed | Accepted as channel_integration_room_id. v3dot1/deals.rb:75, v4/deals.rb:92, schema.rb:1392 |
| R2 | Deal create API accepts contact_id | ⚠️ Partial | No contact_id. Accepts crm_person_id and crm_lead_ids[]. v3dot1/deals.rb:899, v4/deals.rb:659 |
| R3 | Deal create API accepts bot/AI creator flag | ❌ Not found | No created_by_bot, actor_type in deal_params. creator_id must resolve to a real User. |
| R4 | Ticket create API accepts room_id | ✅ Confirmed (via service) | Set in Ticket::Create service (ticket/create.rb:46), not in strong params. schema.rb:4941 |
| R5 | Ticket create API accepts bot/AI creator flag | ❌ Not found | data_source IS permitted by ticket_params (v3dot1/tickets.rb:528) but immediately overwritten to "open-api-v3" (line 61). Callers cannot control it. |
| R6 | Display creator as "Bot" or "AI" in activity log | ❌ Contradicted | audit.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. |
| R7 | data_source distinguishes Chatbot vs Agentic AI | ❌ Contradicted | Both 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. |
| R8 | CRM sends deal delete webhook to Omnichannel | ❌ Not found | Deal destroy fires Crm::WebhookSender::ShouldSendWebhookService → generic customer webhooks only. Does not call OMNICHANNEL_TICKET_WEBHOOK. deal.rb:1630-1640 |
| R9 | CRM sends ticket delete webhook to Omnichannel | ✅ Confirmed (feature-flag gated) | ticket.rb:617-620 → Crm::WebhookSender::Omnichannel::SendWebhookWorker. Gated by Feature.enabled_globally('ticket_deletion_webhook'). Sends to ENV['OMNICHANNEL_TICKET_WEBHOOK']. |
| R10 | CRM calls Omnichannel API on deal/ticket create | ❌ Not found | No outbound HTTP to Omnichannel on any create/update path in deal or ticket controllers. Net-new. |
| R11 | Chat history endpoint exists | ✅ Confirmed | GET /api/v4/deals/:id/chat-history → ChatHistory::GetChatHistory → fetches up to 100 messages from Qiscus or ChatPanel. chat_history/get_chat_history.rb |
| R12 | Room navigation deeplink from timeline | ❌ Not found | Chat-history endpoint returns { room_id, type, contents }. No navigation URL field anywhere. |
| R13 | Source auto-fill from channel if empty | ⚠️ Partial | Exists only in Hub::Ticket::V2::DealBuilder (internal webhook path, deal_builder.rb:83-91). Open API create path has zero source auto-fill. |
| R14 | 1 room = 1 deal; second create → latest wins | ❌ Contradicted | DB unique index channel_integration_room_unique_key on (channel_integration_id, channel_integration_room_id). Second create → ActiveRecord::RecordNotUnique → 422 error (v3dot1/deals.rb:1409). No replacement logic exists. |
Chat Panel (/hub-core + /hub-worker)
Note:
hub-workercontains zero CRM logic — only billing/WhatsApp/auto-resolve workers. All CRM logic is inhub-core.hub-serviceis not available in this workspace; HTTP routes live there.
| # | Claim | Verdict | Key Evidence |
|---|---|---|---|
| H1 | Ticket create webhook accepts room_id + 3 params | ⚠️ Partial / Mislabeled | Crm::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. |
| H2 | Deal create webhook: external_id, external_url, room_id | ✅ Confirmed | deal_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" | ❌ Contradicted | Ticket 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. |
| H4 | Dedicated CRM→Omnichannel create/update API exists | ❌ Not found | hub-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. |
| H5 | Actor type mechanism to filter CRM→Omnichannel events | ❌ Not found | notification_receiver.rb filters only by event_type string and action. Zero actor/source/initiator filtering anywhere in the webhook pipeline. |
| H6 | Deal doesn't store actor/initiator | ✅ Confirmed | qontak_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. |
| H7 | 1 room = 1 deal = 1 ticket | ✅ Confirmed | Unique 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]. |
| H7a | Second deal for same room → latest shown | ❌ Contradicted | deal_receiver.rb:49: return failure('Room deal already exist!') — new deal is rejected, not replaced. |
| H8 | Deal/ticket preview: Name, ID, Stage, Pipeline | ⚠️ Partial | Fields 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. |
| H9 | CRM deal deletion clears Omnichannel preview | ✅ Confirmed | deal_receiver.rb:41-47 soft-deletes QontakCrmObject. get_deal.rb returns 410 Gone. Preview clears. |
| H10 | CRM ticket deletion clears Omnichannel preview | ✅ Confirmed | ticket_receiver.rb:30-35 soft-deletes RoomTicket. Same 410 pattern. |
| H11 | hub-worker contains CRM logic | ❌ Contradicted | hub-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:
- Accept new flag param on deal/ticket create (CRM API layer)
- Store it — new
data_sourceenum values ("chatbot"/"agentic_ai") or a new column 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 theOMNICHANNEL_TICKET_WEBHOOKendpoint)
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.create→DealWebhookReceiverWorker→deal_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 — ifcrm_source_nameis absent, no fallback occurs - The chatbot sends no
sourceparam 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
| Task | What to change |
|---|---|
Add room_id to deal & ticket create body | Inject channel_integration_room_id from @room_id in process_body |
Add creator_flag param | Inject "bot" or "agentic_ai" into body for both deal and ticket create action types |
| Extend contact association to ticket create | Remove 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 NodeRegistry | Add room_id and creator_flag entries (with destination: 'body') to node_registries DB rows for both node types |
CRM Backend (qontak.com)
| Task | What to change |
|---|---|
Accept creator_flag on deal create | Add 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 create | Add param to ticket_params (v3dot1/tickets.rb); stop unconditionally overwriting data_source to "open-api-v3" (line 61) |
| Display "Bot"/"AI" in activity log | Update audit.rb:mapping_who (lines 1824-1842) — add branch reading the new flag/column to return "Bot" or "AI" |
| Build deal delete webhook → Omnichannel | Add 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 APIs | New 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 path | Add 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 logic | Decision-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 log | On 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.
| Task | What to change |
|---|---|
| Build embedded Deal/Ticket preview component | New 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 section | CRM 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 errors | CRM embedded component handles agents without CRM permissions; Omnichannel has no additional error handling responsibility |
| Receive and react to CRM refresh events | Embedded component re-renders on create, update, and delete events triggered by Omnichannel from CRM API calls |
| Creator badge ("Bot" / "AI") on deal/ticket detail | Render 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.
| Task | What to change |
|---|---|
| Provide Deal & Ticket section/tab container | Add the section/tab shell in the conversation room UI; host the CRM embedded component inside it |
| Feature flag | Define and gate the section/tab visibility per account (default OFF). CRM embedded component is not separately flagged |
| Trigger embedded component refresh | On receiving create/update/delete events from CRM Backend, signal the embedded component to re-render |
Chat Panel (hub-core + hub-service)
| Task | What to change |
|---|---|
| Add actor/initiator to deal storage | Migration: 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 endpoint | hub-service: expose POST endpoint calling Deal::UserCreateDirect interactor (currently unrouted — H4) |
| Wire ticket create to HTTP endpoint | hub-service: confirm existing route for Ticket::UserCreate; verify callable from CRM |
| "Latest wins" replace logic | deal_receiver.rb:49: replace return failure('Room deal already exist!') with soft-delete-and-replace if that decision is made |
| Actor/source filtering | notification_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:
- Chatbot must send
room_id+creator_flag→ unblocks CRM storage and creator label - CRM Backend must call Omnichannel create API → unblocks embedded component refresh
- CRM Backend must send deal delete event → unblocks preview clearing for deals (ticket delete already exists, feature-flagged)
- hub-service must expose deal create HTTP endpoint → unblocks CRM → Omnichannel create event flow
- 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.