Skip to main content

RFC: Consistent Deal & Ticket Creation Experience — Phase 1: Bot & AI Creation Parity

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. It is also agent-execution-ready: §1 PRD-to-Schema Derivation, §2 Repo Reading Guide (Detail 2.0), mermaid diagrams, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 flips to yes.

The YAML frontmatter is the machine-readable index. The metadata table below is the human-readable governance record. Status vocab mapping (documents-repo linter): table IDEA ⇄ frontmatter draft; RFCin-review; AGREEDapproved.

Metadata

FieldValueNotes
StatusIDEAfrontmatter draft; not yet review-ready (see §7)
Typebackendsub_type: enhancement (extends an existing webhook contract)
TitleConsistent Deal & Ticket Creation Experience — Phase 1: Bot & AI Creation Parity
Owner (DRI)Alma Syafira (PM)CRM Squad owns BE; crm-fe-v3 owns CRM UI embed
Authorsrfc-starter (agent draft)from PRD v1.1
ReviewersCRM Eng lead, Omnichannel Eng lead, Chatbot Squad, Agentic AI Squadcross-squad
ApproversCRM Tech Lead, InfoSec approver [REQUIRED]infosec sign-off mandatory
Submitted date2026-06-30ISO-8601
Last updated2026-06-30refinement r2 — added Chatbot BE (chatbot) producer repo
Target release2026-Q3per PRD
Related documentsPRD: Phase 1 Bot & AI Creation Parity (see frontmatter related) · ANCHOR: Create Deal in CRM via Chatbot UI · ANCHOR: AI Agent Commercialization
Discussion[REQUIRED: Slack channel/thread]

Sections at a Glance

  1. Overview — Problem, PRD-to-Schema Derivation, Decisions Closed index (1.B), Per-Story Change Map (1.C), coverage matrices (1.A)
  2. Technical Design — Infra Topology, Repo Reading Guide (2.0), ADRs, Data Model (2.3), APIs (2.4), Sequence diagrams, State diagrams, Branch catalog
  3. High-Availability & Security — auth (company token), authorization matrix, multi-tenancy, failure/idempotency
  4. Backwards Compatibility & Rollout — flag contract, Agent Execution Plan (4.C), Verification & Rollback Recipe
  5. Concerns / Open Questions — severity-tagged blockers
  6. Comment logs
  7. Ready for agent execution — gate marker

1. Overview

1.0 Problem & Scope

Deals and tickets created by Chatbot or Agentic AI today do not reach parity with manually-created records: the Omnichannel infobar keeps showing the "Create Deal" CTA, the CRM record has no conversation contact / origin context, and there is no timeline entry explaining how the record originated. This RFC makes the qontak.com CRM backend emit the create/update/delete signals and build the origin context so the existing Omnichannel + CRM UIs render parity.

In scope (BE, qontak.com — MAIN): persist creator_flag; emit create + update + delete events to Omnichannel for bot/AI records that carry a channel_integration_room_id; auto-associate the conversation contact (reuse); auto-create a chat-history timeline note; generate a room deeplink for that note. Cross-service (read-only context, NOT written here): chatbot (Chatbot BE) is the upstream producer — its CRM node executor already calls POST /api/v3.1/deals|tickets and must add channel_integration_room_id, contact_id/lead-ids, and creator_flag to that request body (ADR-7); hub-core (Omnichannel-be) must accept the new update (deal) and create/update (ticket) receiver branches; hub-chat (Omnichannel-fe) renders the preview; crm-fe-v3 renders the timeline entry + "Bot"/"AI" creator label (UI embed is pre-baked to crm-fe-v3 — ADR-1). Non-goals: multiple deals/tickets per room, manual/auto-create flow changes, historical backfill, source granularity beyond "Bot"/"AI", chatbot qualification logic, mobile (PRD §4).

1.A Coverage Matrices

PRD Section Coverage

PRD §TitleCovered in RFC
1One-liner + Problem§1.0
2If we don't ship§1.0 (motivation)
3Personas§3 authz matrix (Sales Agent / Manager)
4Non-Goals§1.0 Non-goals · §2 Branch Catalog (guard rails)
5Constraints§3 (auth, perf, retention) · §4 (flag)
5.1Data Lifecycle§2.3 Per-Status Lifecycle (note 1y retention, room overwrite)
6Feature Changes (CHG-001/002)§2.4 APIs (preview events) · ADR-4 (creator label)
7New Feature (Timeline log)§2.3 (Crm::Note reuse) · §2.4 row 8 · ADR-5
8API & Webhook Behavior (#1–9)§2.4 Outbound/Inbound tables
9System Flow + Stories + ACs§2 sequence diagrams · 1.C Per-Story Change Map
10Rollout§4 Rollout
11Observability§4 Observability
12Success Metrics§4 Observability (metric sources)
13Launch Plan & Stage Gates§4 Rollout
14Dependencies§3 Responsibility Boundary Matrix
15Key Decisions§2 ADRs (mapped)
16Open Questions§5 (severity-tagged)

UI / Consumer Surface Coverage

SurfaceRepoRead endpoint / data sourceNotes
Conversation infobar deal previewhub-chat DealDetails.vuehub-core QontakCrmObject (external_id,external_url)display fields source = OQ-1
Conversation infobar ticket previewhub-chat TicketDetails.vuehub-core RoomTicket (name,external_status,external_url)RoomTicket already stores display fields
"Create Deal" CTA (empty state)hub-chat TicketingCrmDeal.vue:220unchangedNon-Goal #2 guard rail
CRM deal timeline entrycrm-fe-v3 Deals/Detail/DealDetailTimeline.vueqontak.com Crm::Note (HubChannel types)new note auto-created on bot/AI create
CRM ticket timeline entrycrm-fe-v3 Tickets/Detail/ActivityTimeline.vueqontak.com Crm::Notesame
Creator field "Bot"/"AI"crm-fe-v3 types/deals/api-types.ts:61 (creator_name)qontak.com creator_flag (new)label mapping FE-side

Role Coverage

RoleCapabilityEnforcement
Sales/CS Agentview preview (Omnichannel inbox access, even w/o CRM perm); click → CRM 403 if no CRM permhub-core room access + CRM authz on detail
Sales Managerview creator label + timeline; auditCRM deal/ticket view authz
System (Chatbot / Agentic AI)create record w/ room_id,contact_id,creator_flagcompany-token authenticated create API

1.B Decisions Closed (index → §2 ADRs)

#DecisionStatusADR
1CRM UI embed (timeline/creator label) handled by crm-fe-v3chosen — pre-baked by author — see inputADR-1
2Reuse existing CRM→hub-core webhook pipeline rather than build a net-new "dedicated Omnichannel API"chosenADR-2
3Emit update events (not just create/delete) for preview freshnesschosen (PRD 15a)ADR-3
4Persist bot/AI actor via new creator_flag column (not creator_id/data_source)chosen (pending author confirm — OQ-3)ADR-4
5Chat-history timeline = lightweight reference + room deeplink via existing Crm::Note HubChannel types + ChatHistory::GetChatHistory, asyncchosen (PRD 15a)ADR-5
6Latest-wins, ordered by CRM creation timestampchosen (PRD 15a; E1)ADR-6
7Chatbot BE extends its existing CRM node executor body (not a new node) to carry room_id/contact_id/creator_flagchosen (cross-service — Chatbot Squad owns)ADR-7

1.C Per-Story Change Map

StoryLayer scopeChangesAcceptance criteria (verbatim AC IDs)RFC anchors
CDTC-S01 Preview in chat roomCross-squadchatbot: add room_id/creator_flag to create body · qontak.com: emit create/update/delete webhook for bot/AI deal+ticket w/ room_id · hub-core: add deal update + ticket create/update receiver branches · hub-chat: renderAC-1, AC-2, AC-3, AC-4, ERR-1, ERR-2§2.4 Inbound(chatbot)/Outbound/Inbound · ADR-2,3,6,7 · §4.C C2–C4
CDTC-S02 Navigate room→detail (new tab)FE-only (hub-chat)uses external_url from payloadAC-1, AC-2, ERR-1§2.4 (external_url) · n/a BE — covered in hub-chat
CDTC-S03 Auto-associate contactCross-squad (chatbot + qontak.com)chatbot: sends contact via crm_lead_ids phone lookup (OQ-10 vs contact_id) · qontak.com: reuse associate_hub_contactAC-1, AC-2, ERR-1, ERR-2chatbot execute.rb:261 · create_service.rb:378 · §4.C C1 verify · OQ-10
CDTC-S04 Timeline log + chat previewBE + FEqontak.com: auto-create Crm::Note HubChannel + room deeplink async · crm-fe-v3: renderAC-1, AC-2, ERR-1, ERR-2ADR-5 · §2.3 · §4.C C5
CDTC-S05 Navigate timeline→roomBE + FEqontak.com: generate room_deeplink (OQ-4) · crm-fe-v3: render linkAC-1, AC-2, ERR-1§2.4 · §5 OQ-4
CDTC-S06 Creator label "Bot"/"AI"Cross-squad (chatbot + qontak.com + FE)chatbot/AI: send creator_flag in create body · qontak.com: store+expose creator_flag · crm-fe-v3: map to "Bot"/"AI"AC-1, AC-2, AC-3ADR-4, ADR-7 · chatbot execute.rb · §2.3
CDTC-NEG-01 Single per room (latest wins)Runtime / behaviorenforce at hub-core unique index + CRM ts orderingNEG-1ADR-6 · §2.3
CDTC-NEG-02 Manual/auto flow unchangedConfig / behaviorno change to manual create pathNEG-2§2 Branch Catalog
CDTC-NEG-03 Source value preservedBE-onlydo not override existing crm_sourceNEG-3§2 Branch Catalog

2. Technical Design

2.1 Infrastructure Topology

flowchart TB
subgraph clients[Actors]
AGENT[Sales/CS Agent browser]
end
subgraph bot[chatbot — Chatbot BE]
BFLOW[Flow / node executor<br/>mekari_qontak_crm/execute.rb]
BHTTP[CrmHttpClient<br/>company-token auth + 401 refresh]
end
AI[Agentic AI / Airene<br/>producer — repo TBD]
LB[(ALB / nginx ingress)]
subgraph crm[qontak.com — CRM BE - MAIN]
WEB[Rails web pods]
SK[Sidekiq workers<br/>queues: integrations, room_interaction]
REDIS[(Redis<br/>webhook config cache)]
DBP[(Postgres primary)]
DBR[(Postgres replica)]
end
subgraph hub[hub-core — Omnichannel BE]
HWEB[Rails web pods]
HSK[Sidekiq: write_log_crm_webhook]
HDB[(Postgres: qontak_crm_objects / room_tickets / rooms)]
ES[(Elasticsearch<br/>message history)]
end
subgraph fe[Frontends]
HUBFE[hub-chat - Nuxt 4]
CRMFE[crm-fe-v3 - Nuxt 4]
end
BFLOW --> BHTTP
BHTTP -->|POST /api/v3.1/deals,/tickets| LB
AI -.->|POST create deal/ticket| LB
LB --> WEB
WEB --> DBP
WEB --> SK
SK -->|webhook config| REDIS
SK -->|HTTPS webhook eventType=qontak.crm.deal.*| HWEB
SK -->|GET chat history /api/open/v1/messages/rooms/:id| HWEB
HWEB --> HSK --> HDB
HWEB --> ES
AGENT --> HUBFE -->|read preview| HWEB
AGENT --> CRMFE -->|read timeline/creator| WEB
WEB -->|reads| DBR

Per-service responsibility & third-party connections

ServiceUse cases (this RFC)Internal calls (owner)External / third-party
chatbot (Chatbot BE)build create body w/ room_id/contact_id/creator_flag; call CRM deal/ticket create API; phone→lead lookup for contactqontak.com CRM REST /api/v3.1/* (CRM); SSO company-token refresh (CRM)qontak.com CRM API, Postgres, Sidekiq, Rollbar
qontak.com (CRM BE)persist creator_flag, associate contact, emit webhook events, build timeline note + deeplinkhub-core webhook receive (Omnichannel), hub-core message API (Omnichannel)Postgres, Redis, Sidekiq
hub-core (Omnichannel BE)receive create/update/delete → upsert QontakCrmObject/RoomTicket; serve message historyPostgres, Elasticsearch, Sidekiq
hub-chat (Omnichannel FE)render infobar preview, new-tab navhub-core read APIs
crm-fe-v3 (CRM FE)render timeline entry, "Bot"/"AI" creator label, room deeplinkqontak.com /api/mobile/v2.8

2.0 Repo Reading Guide (repo as documentation)

Repo Map (slice this RFC touches)

flowchart LR
subgraph CB[BE: chatbot READ-ONLY]
CEX[node_executions/.../mekari_qontak_crm/execute.rb<br/>add room_id/contact_id/creator_flag to body]
CHC[crm_http_client.rb<br/>company-token]
end
subgraph CRM[BE: qontak.com]
DS[crm/deals/create_service.rb<br/>modified]
TC[ticket/create.rb<br/>modified]
SSW[crm/webhook_sender/should_send_webhook_service.rb<br/>read]
PB[webhook/entity/deal,ticket base+update<br/>modified]
NOTE[crm/note.rb<br/>read]
GCH[chat_history/get_chat_history.rb<br/>read/extend]
NW[NEW: timeline-note builder worker<br/>new]
MIG[db/migrate add creator_flag<br/>new]
end
subgraph HUB[BE: hub-core READ-ONLY]
DR[crm/.../webhook/deal_receiver.rb<br/>add update branch]
TR[crm/.../webhook/ticket_receiver.rb<br/>add create/update]
QCO[models/qontak_crm_object.rb]
NR[interactors/.../notification_receiver.rb]
end
subgraph FE[FE READ-ONLY]
HD[hub-chat DealDetails.vue/TicketDetails.vue]
CT[crm-fe-v3 DealDetailTimeline.vue]
end
CEX --> CHC
CHC -.HTTPS POST /api/v3.1/deals.-> DS
DS --> MIG
DS --> NW --> NOTE
NW --> GCH
DS --> PB --> SSW
SSW -.HTTPS.-> NR --> DR --> QCO
NR --> TR
QCO --> HD
NOTE --> CT

Existing Code Anchors (read before writing)

ConcernRepo:path:lineWhat to learn
Chatbot CRM node executor (producer)chatbot app/core/repositories/node_executions/nodes/mekari_qontak_crm/execute.rb:18,74,261ACTION_TYPE_MAPPINGPOST /api/v3.1/deals / /api/v3.1/tickets (:18-25); body built from configured/AI args (process_body); enrich_body_for_deal_create adds crm_lead_ids via phone lookup (:261-275) — no channel_integration_room_id/creator_flag set today
Chatbot → CRM HTTP client (producer auth)chatbot app/core/repositories/node_resources/mekari_qontak_crm/crm_http_client.rb:107,143,150company-token credential __qontak_crm_company_token (:107-113); 401 → refresh via POST /api/v3.1/teams/company_token + X-Crm-Api-Key (:143-151)
Deal create serviceqontak.com app/services/crm/deals/create_service.rb:6,28,97,378#call shape; channel_integration_room_id already set (:97); associate_hub_contact already creates crm_people_deals (:378)
Ticket create serviceqontak.com app/services/ticket/create.rb:1,16,107hash result; associate_hub_contact (:107)
Deal modelqontak.com app/models/crm/deal.rb:150,157,2257creator is a User FK (:150); resolve_room_chat after_commit hub hook pattern (:2257)
Schema (deals)qontak.com db/schema.rb:1363,1367,1392,1394creator_id, crm_source_id, channel_integration_room_id, data_source default "web"
Schema (tickets)qontak.com db/schema.rb:4941,4956,4961creator_id, channel_integration_room_id, data_source
Webhook payload entityqontak.com app/services/webhook/entity/deal/base.rb:18,20,21exposes slug, channel_integration_room_id, creator_id/creator_name, stage/pipeline already
Webhook config gateqontak.com app/services/crm/webhook_sender/should_send_webhook_service.rb:33-44per-team { 'Crm::Deal' => {create,update,delete} } cache + DB gate
Omnichannel ticket senderqontak.com app/services/crm/webhook_sender/omnichannel/webhook_sender_service.rb:12,34,62-63ENV['OMNICHANNEL_TICKET_WEBHOOK']; hardcodes event_type:'ticket', action:'delete'
Event type formatqontak.com app/services/webhook/payload_builder.rb:52"qontak.crm.#{type}.#{action}"
Chat history retrievalqontak.com app/services/chat_history/get_chat_history.rb:11,64NOTE_TYPES HubChannel; Hub::ChatService::Messages::Message#load_message(token,room,1,100)
Timeline modelqontak.com app/models/crm/note.rbCrm::Note STI; HubChannel subtypes back timeline
Hub deal receiverhub-core app/apps/crm/repositories/webhook/deal_receiver.rb:20,27,50only create/delete; requires room; one-per-room guard
Hub ticket receiverhub-core app/apps/crm/repositories/webhook/ticket_receiver.rb:14only delete
Hub dispatchhub-core app/apps/crm/interactors/webhook/notification_receiver.rb:19,32,42routes by eventType (deal) vs event_type:'ticket'
Hub CRM object modelhub-core app/core/domains/models/qontak_crm_object.rbstores external_id,external_url,object_type,room_idno name/stage/pipeline
Hub room ticket modelhub-core app/core/domains/models/room_ticket.rbstores name,description,external_status,external_url
Migration dialectqontak.com db/migrate/20260619000001_add_live_location_config_to_teams.rbRails 5.2 Postgres add_column style
Worker patternqontak.com app/workers/crm/integrations/deal/resolve_room_worker.rbthin Sidekiq wrapper → service; queue: :integrations

Patterns to Follow

ConcernReference file (opened)Rule
Chatbot CRM call (producer)chatbot app/core/repositories/node_executions/nodes/mekari_qontak_crm/execute.rbextend process_body/enrich_body_for_deal_create to add fields; keep destination-based arg extraction
Service objectapp/services/crm/deals/create_service.rbkeyword-arg init, #call, hash result
Background workerapp/workers/crm/integrations/deal/resolve_room_worker.rblook up by id, delegate to service, idempotent
Webhook emission gateapp/services/crm/webhook_sender/should_send_webhook_service.rbper-team config + Redis cache
Webhook payloadapp/services/webhook/entity/deal/base.rbGrape::Entity expose; add fields here
Timeline noteapp/services/chat_history/get_chat_history.rb + Crm::Note HubChannel typesreuse existing note STI types
Migrationdb/migrate/20260619000001_add_live_location_config_to_teams.rbActiveRecord::Migration[5.2], Postgres

Source Verification

ItemEvidence (verified)
chatbot calls CRM deal/ticket create APIchatbot execute.rb:19,21 ACTION_TYPE_MAPPINGPOST /api/v3.1/deals, POST /api/v3.1/tickets
chatbot omits room_id/creator_flag todaychatbot execute.rb:74-79,261-275 — deal body built from configured/AI args + crm_lead_ids only; no channel_integration_room_id/creator_flag written → confirms Chatbot Squad payload work (Risk #7) is real
chatbot contact assoc via lead idschatbot execute.rb:269-273 GET /api/v3.1/contacts?phone=body['crm_lead_ids'] (array) — field differs from PRD contact_id → OQ-10
company token (producer side)chatbot crm_http_client.rb:107-113 credential __qontak_crm_company_token; :143-151 refresh POST /api/v3.1/teams/company_token + X-Crm-Api-KeyPRD assumption #5 verified from producer side
creator is User FKcrm/deal.rb:150 belongs_to :creator, class_name: 'User' — confirms no string actor today (drives ADR-4)
room id column existsdb/schema.rb:1392 t.string "channel_integration_room_id"
room id set at createcreate_service.rb:97 deal.channel_integration_room_id = params[:channel_integration_room_id]
contact auto-assoc existscreate_service.rb:378 deal.crm_people_deals.create!(crm_person_id: lead_id)
webhook entity exposes room+stage+pipelinewebhook/entity/deal/base.rb:20,28-29,56-61
deal receiver rejects updatehub-core deal_receiver.rb:20 unless %w[create delete].include?(@action)
ticket receiver delete-onlyhub-core ticket_receiver.rb:14 if @webhook_attr.action == 'delete'
QontakCrmObject lacks display fieldshub-core qontak_crm_object.rb exposes external_id/external_url/object_type/room_id only
company-token mapping existsomnichannel sender uses @team.external_company_id (webhook_sender_service.rb:64,69); hub-core finds org by company_id (deal_receiver.rb:35) — PRD assumption #5 verified
message history APIchat_history/get_chat_history.rb:64Hub::ChatService::Messages::Message/api/open/v1/messages/rooms/:idPRD assumption #6 verified
deeplink helperNOT FOUND — no web room deeplink builder located → OQ-4

Reading Order for the Agent

Cross-service producer (read first for context, do not edit here): chatbot app/core/repositories/node_executions/nodes/mekari_qontak_crm/execute.rb + crm_http_client.rb — how the bot/AI create request is assembled and authenticated.

  1. app/services/crm/deals/create_service.rb
  2. app/services/ticket/create.rb
  3. app/services/crm/webhook_sender/should_send_webhook_service.rb
  4. app/services/crm/webhook_sender/omnichannel/webhook_sender_service.rb
  5. app/services/webhook/entity/deal/base.rb
  6. app/services/chat_history/get_chat_history.rb + app/models/crm/note.rb
  7. hub-core app/apps/crm/interactors/webhook/notification_receiver.rb
  8. hub-core app/apps/crm/repositories/webhook/deal_receiver.rb + ticket_receiver.rb
  9. hub-core app/core/domains/models/qontak_crm_object.rb + room_ticket.rb
  10. db/migrate/20260619000001_add_live_location_config_to_teams.rb

2.2 Architecture Decision Records

ADR-1 — CRM UI embed handled by crm-fe-v3

  • Context: Timeline entry, creator label, and room deeplink must render in the CRM detail page.
  • Options: (a) crm-fe-v3 (pre-baked); (b) legacy Nuxt-2 crm repo.
  • Decision: crm-fe-v3. Rationale: pre-baked by author — see input. (crm-fe-v3 already ports Deals/Tickets per AGENTS.md rollout matrix; timeline component exists Deals/Detail/DealDetailTimeline.vue.)
  • Consequences: BE must expose creator_flag + timeline note fields on the /api/mobile/v2.8 CRM read endpoints crm-fe-v3 consumes.
  • Reversibility: High (FE-only swap).

ADR-2 — Reuse existing CRM→hub-core webhook pipeline (not a net-new API)

  • Context: PRD §8 describes "dedicated Omnichannel deal/ticket creation API". A working pipeline already exists: ShouldSendWebhookServiceSendWebhookWorkerWebhook::Sender (HTTPS, MAX_RETRIES=3) → hub-core NotificationReceiverDealReceiver/TicketReceiverQontakCrmObject/RoomTicket.
  • Options: (a) reuse/extend the existing webhook contract; (b) build a parallel dedicated endpoint.
  • Decision: (a) reuse/extend. Rationale: create+delete already function end-to-end (deal_receiver.rb:49); the pipeline already carries channel_integration_room_id and slug (entity/deal/base.rb:20), has retry + logging, and is company-token authenticated. A parallel API would duplicate auth, retry, and logging for no benefit.
  • Consequences: the "new API" in PRD §8 is realized as new event actions on the existing contract, plus new hub-core receiver branches. Per-team webhook config (Crm::Webhook) gating must be made automatic for bot/AI records (see OQ-5).
  • Reversibility: Medium (contract additions are additive/back-compat).

ADR-3 — Emit update events for preview freshness

  • Context: PRD AC-4 (CDTC-S01) requires the preview to reflect latest Name/Stage/Pipeline/Status. VALID_WEBHOOK_ACTIONS already includes update (crm/webhook.rb:13) and Webhook::Entity::Deal::Update/Ticket::Update entities exist, but hub-core DealReceiver rejects update (:20) and TicketReceiver handles only delete.
  • Options: (a) emit update + add hub-core update branch that upserts; (b) FE live-fetches from CRM on render (no update event).
  • Decision: (a) — conditional on OQ-1. Rationale: PRD explicitly chose update sync (15a). If OQ-1 shows hub-chat already live-fetches display fields from CRM, (b) may suffice for deals and update events become a cache-invalidation signal only.
  • Consequences: hub-core DealReceiver gains an update branch; QontakCrmObject may need display columns iff not live-fetched (OQ-1). RoomTicket already stores display fields.
  • Reversibility: Medium.

ADR-4 — Persist bot/AI actor via new creator_flag column

  • Context: PRD payload carries creator_flag ("bot"|"agentic_ai"); CHG-002 wants the Creator field to read "Bot"/"AI". creator_id is a User FK (deal.rb:150, ticket.rb:60); data_source is transport-level (web/mobile/open-api, schema.rb:1394) and bot creates already set it to open-api.
  • Options: (a) new creator_flag string column on crm_deals + tickets; (b) overload data_source; (c) a dedicated Crm::Source row.
  • Decision: (a) new column, pending author confirm (OQ-3). Rationale: (b) collides with the existing open-api transport value; (c) crm_source is team-scoped sales attribution, not actor. A nullable creator_flag is additive and back-compat (absent ⇒ existing creator behavior, PRD CDTC-S06 AC-3).
  • Consequences: migration on two large tables (add_column, nullable, no default — no table rewrite on Postgres 11+). Exposed via webhook entity + mobile read entity.
  • Reversibility: High (drop column; absent ⇒ default behavior).
  • Context: PRD §7 + 15a want a timeline entry with a truncated chat history preview + room link, not a full transcript embed. Crm::Note STI HubChannel subtypes already back the timeline, and ChatHistory::GetChatHistory already pulls history by room via the hub message API.
  • Options: (a) reuse Crm::Note HubChannel type, populated async by a worker at creation; (b) new timeline table; (c) full transcript embed.
  • Decision: (a). Rationale: zero new timeline schema; FE already renders these notes (DealDetailTimeline.vue). PRD rejected (c) (15b).
  • Consequences: new thin worker enqueued from CreateService when channel_integration_room_id present; on hub message-API failure, note is still created with deeplink + creator label, preview omitted (PRD CDTC-S04 ERR-2). Retention: existing 1-year CRM log purge (PRD 5.1) — verify a purge job covers HubChannel notes (OQ).
  • Reversibility: High.

ADR-6 — Latest-wins ordered by CRM creation timestamp

  • Context: One room = one deal + one ticket preview (Non-Goal #1; E1).
  • Options: (a) order by CRM created_at; (b) by hub-core receipt time.
  • Decision: (a). Rationale: PRD 15a; receipt time is non-deterministic under retry. hub-core enforces one active object per [room_id, organization_id, object_type] (unique index, create_qontak_crm_object.rb); the create-receiver currently rejects a second create (deal_receiver.rb:50) — for latest-wins it must replace instead.
  • Consequences: hub-core create branch changes from "fail if exists" to "soft-delete prior + insert" (or upsert) keyed on newer CRM created_at.
  • Reversibility: Medium.

ADR-7 — Chatbot BE extends its existing CRM node executor (not a new node)

  • Context: chatbot already creates CRM records via its generic flow node executor Repositories::NodeExecutions::Nodes::MekariQontakCrm::Execute (execute.rb:18-25 maps qontak_crm_deal_createPOST /api/v3.1/deals, qontak_crm_ticket_createPOST /api/v3.1/tickets). The body is assembled from configured/AI args plus a crm_lead_ids enrichment (:261-275); it does not currently set channel_integration_room_id or creator_flag. To reach parity, those fields must be added to the create body.
  • Options: (a) extend the existing executor's body building (process_body / enrich_body_for_deal_create) + the node_registry property set; (b) build a new dedicated CRM-deal/ticket node; (c) inject fields server-side in qontak.com from auth/session context.
  • Decision: (a), owned by Chatbot Squad. Rationale: the executor, company-token auth, 401-refresh and retry already exist (crm_http_client.rb); adding 3 additive body fields reuses all of it. (b) duplicates the executor; (c) is brittle — room_id and creator_flag are only known to the producer, not derivable from the company token. @room_id is already in scope in the executor (execute.rb:75), so wiring channel_integration_room_id is local. The Agentic AI producer path must mirror the same contract (repo TBD — OQ-11).
  • Consequences: chatbot node_registry properties for the CRM-create actions must expose channel_integration_room_id, creator_flag (and align contact_id vs crm_lead_ids — OQ-10) with destination: 'body'; this is a cross-service change tracked in the chatbot repo, not executed against qontak.com. Blocking dependency (PRD §14 / Risk #7).
  • Reversibility: High (additive body fields; absent ⇒ existing behavior).
Minimum coverage checklist
  • Storage: creator_flag on crm_deals/tickets; timeline via Crm::Note (ADR-4, ADR-5).
  • Sync vs async: webhook emission + timeline-note build are async Sidekiq (existing SendWebhookWorker; new note worker) — external hub calls > 100ms.
  • Caching: reuse ShouldSendWebhookService Redis cache (webhook_<class>_<team>); no new cache.
  • Third-party: hub-core via HTTPS webhook + message API (existing Webhook::Sender, Hub::ChatService); chatbot→CRM via REST /api/v3.1/* (existing CrmHttpClient).
  • Consistency: eventual (async webhook + retry); preview may be briefly stale (PRD E2 accepted).
  • Multi-tenancy: company token → external_company_id/company_id org scoping (verified both directions).
  • Reuse vs new: see §2.4 endpoint tags.

2.3 Data Model

erDiagram
crm_deals ||--o{ crm_people_deals : has
crm_people_deals }o--|| crm_people : links
crm_deals ||--o{ crm_notes : timeline
tickets ||--o{ people_tickets : has
tickets ||--o{ crm_notes : timeline
crm_deals {
bigint id
int creator_id "User FK (existing)"
int crm_source_id "existing"
string channel_integration_room_id "existing"
string data_source "existing default web"
string creator_flag "NEW nullable bot|agentic_ai"
}
tickets {
bigint id
int creator_id "User FK (existing)"
string channel_integration_room_id "existing"
string data_source "existing"
string creator_flag "NEW nullable bot|agentic_ai"
}
crm_notes {
bigint id
string type "STI Crm::HubChannel*"
bigint crm_deal_id
bigint ticket_id
string channel_room_id
string channel_organization_id
}

DDL (matches Rails 5.2 / Postgres migrator):

class AddCreatorFlagToCrmDealsAndTickets < ActiveRecord::Migration[5.2]
def change
add_column :crm_deals, :creator_flag, :string # nullable, no default (back-compat)
add_column :tickets, :creator_flag, :string
end
end

Per-Status Lifecycle (State Surface Contract)

EntityStateVisibilityRetentionRestoreTransitions
QontakCrmObject (deal preview, hub-core)activeinfobar previewuntil replaced (latest-wins) or deletednone (soft deleted_at)create→update→delete; create(newer)→replace
RoomTicket (ticket preview)activeinfobar previewuntil deletenonecreate→update→delete
Crm::Note (HubChannel timeline)createdCRM timeline1 year (system purge, PRD 5.1)nonecreated→purged
creator_flagset-once at createCreator fieldrecord lifetimen/aimmutable post-create

2.4 APIs

Inbound (Chatbot BE / Agentic AI → qontak.com) — existing CRM create REST API, body extended

#EndpointMethodTagCaller must addNotes
IB1/api/v3.1/dealsPOSTextendedchannel_integration_room_id, contact_id/crm_lead_ids (OQ-10), creator_flagchatbot already calls this (execute.rb:19); body assembled in process_body/enrich_body_for_deal_create — add 3 fields
IB2/api/v3.1/ticketsPOSTextendedchannel_integration_room_id, creator_flag, ticket fieldschatbot already calls this (execute.rb:21)
IB3/api/v3.1/contacts?phone=GETreusedchatbot phone→lead lookup that yields crm_lead_ids (execute.rb:277-291)
IB4/api/v3.1/teams/company_tokenPOSTreusedsso_id + X-Crm-Api-Keychatbot token refresh (crm_http_client.rb:143-151) — company-token auth

Outbound (qontak.com → hub-core) — extends existing webhook contract

#EventMethod/transportTagPayload key fieldsTimeout / retry / failure
O1qontak.crm.deal.createHTTPS POST (Webhook::Sender)reusedid,slug,channel_integration_room_id,creator_flag(new),stage,pipeline (entity/deal/base.rb)retry MAX_RETRIES=3; on fail → no preview, logged (Crm::LogApi)
O2qontak.crm.deal.updateHTTPS POSTextended (entity exists; receiver branch new)latest name/stage/pipelineretry 3; on fail → stale preview, logged
O3qontak.crm.deal.deleteHTTPS POSTreusedidretry 3; on fail → stale preview
O4ticket createHTTPS POSTnew-with-justificationroom_id,crm_ticket_name,crm_ticket_id,crm_unique_ticket_id,creator_flagretry; justification: omnichannel sender hardcodes action:'delete' (webhook_sender_service.rb:62-63) — no create path exists
O5ticket updateHTTPS POSTnew-with-justificationlatest name/statussame
O6ticket deleteHTTPS POSTreusedunique_ticket_idticket_receiver.rb:30
O7GET chat historyGET /api/open/v1/messages/rooms/:room_id?offset=1&limit=100reusedon fail → timeline note w/o preview (ERR-2); no retry this phase

Inbound (hub-core receiver branches — implemented in hub-core, READ-ONLY here)

#ReceiverCurrentChange needed
I1DealReceiver#call (:20)create, deleteadd update; change create from reject-if-exists to replace (ADR-6)
I2TicketReceiver#call (:14)deleteadd create + update
I3NotificationReceiver#ticket_webhook_receiver (:42)rejects non-deleteallow create/update routing

Role × Endpoint Authorization

EndpointSystem (bot/AI)Agent (Omni only)Agent (CRM perm)Manager
Create deal/ticket (company token)allown/an/an/a
View infobar previewn/aallowallowallow
Click preview → CRM detailn/a403 (CRM-managed)allowallow
View timeline note + creatorn/an/aallowallow

2.5 Sequence Diagrams

Happy path — bot/AI creates deal → preview + timeline:

sequenceDiagram
participant BOT as chatbot BE (execute.rb)
participant LB
participant WEB as CRM web
participant DBP as PG primary
participant SK as Sidekiq
participant REDIS as Redis
participant HUB as hub-core
participant HDB as hub PG
BOT->>BOT: build body (room_id, contact_id/crm_lead_ids, creator_flag)
BOT->>LB: POST /api/v3.1/deals (company token)
LB->>WEB: route
WEB->>DBP: INSERT deal (channel_integration_room_id, creator_flag)
WEB->>DBP: INSERT crm_people_deals (contact assoc)
WEB-->>BOT: 201 Created
WEB->>SK: enqueue SendWebhookWorker + TimelineNoteWorker
SK->>REDIS: webhook config (Crm::Deal.create?)
SK->>HUB: POST eventType=qontak.crm.deal.create (~80-200ms)
HUB->>HDB: upsert QontakCrmObject (latest-wins)
SK->>HUB: GET /messages/rooms/:room_id (chat history)
HUB-->>SK: messages
SK->>DBP: INSERT Crm::Note (HubChannel: preview + deeplink)

Failure path — hub message API down at timeline build:

sequenceDiagram
participant SK as Sidekiq
participant HUB as hub-core
participant DBP as PG primary
SK->>HUB: GET /messages/rooms/:room_id
HUB-->>SK: timeout / 5xx
Note over SK: rescue (per get_chat_history.rb:64 `rescue nil`)
SK->>DBP: INSERT Crm::Note (deeplink + creator label only, NO preview)
Note over SK: log timeline_log_creation_failed; no agent-facing error (CDTC-S04 ERR-2)

Failure path — webhook delivery fails (deal create):

sequenceDiagram
participant SK as Sidekiq
participant HUB as hub-core
SK->>HUB: POST qontak.crm.deal.create
HUB-->>SK: 5xx / timeout
Note over SK: Webhook::Sender retries (MAX_RETRIES=3, +30s)
SK->>HUB: retry x3
Note over SK: exhausted → Crm::LogApi failure; infobar keeps CTA (no preview)

Failure path — chatbot CRM create token expired (producer side):

sequenceDiagram
participant BOT as chatbot CrmHttpClient
participant WEB as CRM web
BOT->>WEB: POST /api/v3.1/deals (Bearer access_token)
WEB-->>BOT: 401 Unauthorized
Note over BOT: request_with_auth retries once (crm_http_client.rb:51)
BOT->>WEB: POST /api/v3.1/teams/company_token (sso_id + X-Crm-Api-Key)
WEB-->>BOT: new token
BOT->>WEB: POST /api/v3.1/deals (new token)
Note over BOT: on refresh fail → { code: nil, data: 'Failed refresh token' }; Rollbar; no deal created

2.6 State Diagram — infobar preview (re-derived from PRD §7)

stateDiagram-v2
[*] --> CTA: no record
CTA --> Preview: create event received
Preview --> Preview: update event (latest fields) / newer record (latest-wins)
Preview --> CTA: delete event
Preview --> Error: hub cannot reach CRM (CDTC-S01 ERR-2)
Error --> Preview: retry/reload

2.7 Branch & Skip Catalog

BranchConditionBehaviorOwner
Skip preview/timelinechannel_integration_room_id absentrecord created; no webhook, no note (PRD E3, ERR-1)qontak.com CreateService
Skip contact assoccontact_id/crm_lead_ids invalid/absentrecord created, contact empty, log internal (CDTC-S03 ERR-1/2)chatbot execute.rb (lookup) · qontak.com associate_hub_contact
Default creatorcreator_flag absent/unrecognisedexisting creator behavior (CDTC-S06 AC-3)qontak.com
Preserve sourcepayload already has sourcedo not override w/ channel source (NEG-3)qontak.com
Manual flow untouchedmanual createno change (NEG-2)n/a
Latest-wins2nd record for roomreplace prior preview (NEG-1)hub-core receiver

3. High-Availability & Security

  • AuthN: company token → external_company_id (qontak.com) maps to company_id/Organization (hub-core) — verified existing. Chatbot producer authenticates with the same company token (__qontak_crm_company_token) and refreshes via /api/v3.1/teams/company_token (crm_http_client.rb) — verified.
  • AuthZ: preview viewable with Omnichannel inbox access; CRM detail gated by CRM permission (CRM-managed 403 on click — OQ-1 confirms UX). Creator field & timeline read-only, not editable (PRD §5 read/write).
  • Multi-tenancy: all records team/org-scoped; webhook config is per-team (webhook_<class>_<team_id>); chatbot scopes every CRM call by organization_id → company token (crm_http_client.rb:15,90).
  • Idempotency: deal create uses idempotency key (deals.rb:1374); timeline-note worker must guard against duplicate notes per [deal_id, room_id] (E1 concurrency — see OQ).
  • Failure/integrity: every external call has retry+log (O1–O6 retry 3; O7 no retry, graceful degrade; chatbot create has single 401-refresh retry). Webhook delivery failure → stale preview accepted (PRD E2).
  • InfoSec approval: [REQUIRED] before §7 yes.

Responsibility Boundary Matrix (PRD §14)

CapabilityOwnerStatus
Bot payload (room_id,contact_id,creator_flag)Chatbot Squad — chatbot node_executions/.../mekari_qontak_crm/execute.rb (extend body)dependency (blocking) — ADR-7
AI payload (auto-identified)Agentic AI Squad — producer repo TBD (mirror ADR-7 contract)dependency (blocking)
Persist room/contact/creator_flag; emit events; build timeline note + deeplinkCRM (qontak.com)this RFC
Receiver branches (deal update, ticket create/update, latest-wins)Omnichannel (hub-core)this RFC (cross-service)
Infobar preview + new-tab navOmnichannel (hub-chat)this RFC (FE)
Timeline + creator label renderCRM (crm-fe-v3)this RFC (FE, pre-baked)

4. Backwards Compatibility & Rollout Plan

Backwards compat: all changes additive — creator_flag nullable (absent ⇒ unchanged behavior); new event actions ignored by old receivers; new chatbot body fields are additive on the existing /api/v3.1/deals|tickets calls; manual/auto flows untouched (NEG-2).

Feature flag: [REQUIRED: name] (PRD OQ#4) — default OFF, per-account (reuse Feature.enabled_globally/per-team gate pattern seen in webhook_sender_service.rb:114).

4.C Agent Execution Plan (qontak.com only — cross-service work tracked separately)

C1 — Verify existing reuse (no code). Read create_service.rb:97,378 + ticket/create.rb:107; cross-check producer side chatbot execute.rb:261-275. Accept: confirm room-id + contact assoc fire for the bot create path and reconcile the contact_id vs crm_lead_ids contract (OQ-10); write findings into §5 if not.

C2 — Migration: creator_flag. Create db/migrate/*_add_creator_flag_to_crm_deals_and_tickets.rb (ADR-4 DDL). Cmds: bundle exec rails db:migrate; Accept: Crm::Deal.column_names.include?('creator_flag') true; schema.rb diff shows both tables.

C3 — Persist creator_flag in services. Modify crm/deals/create_service.rb + ticket/create.rb to set creator_flag from params (preserve absent ⇒ nil). Accept: bundle exec rspec spec/services/crm/deals/create_service_spec.rb — new examples for bot/agentic_ai/absent pass.

C4 — Expose + emit. Add creator_flag to webhook/entity/deal/base.rb + ticket entity; ensure update + ticket create/update events emit via ShouldSendWebhookService/omnichannel sender (resolve OQ-5 gating). Update api_spec.yaml for any changed mobile read entity; Cmds: bundle exec rspec spec/contract/. Accept: contract test green; payload includes creator_flag.

C5 — Timeline note worker. New thin worker (pattern: resolve_room_worker.rb) → service that builds a Crm::Note HubChannel entry using ChatHistory::GetChatHistory; enqueue from CreateService when room present; graceful degrade on hub failure (ERR-2); idempotency guard. Accept: bundle exec rspec spec/services/... covering happy + hub-failure + no-room; bundle exec rspec spec/workers/....

C6 — Static + security. bundle exec rubocop <files>; bundle exec brakeman --no-pager; bundle exec bundler-audit check --update. Accept: 0 offenses / 0 new warnings.

Cross-service chunks are owned by other squads and tracked in their repos — not executed against qontak.com:

  • chatbot: extend mekari_qontak_crm/execute.rb body building + node_registry properties to send channel_integration_room_id/creator_flag and align contact field (ADR-7, OQ-10).
  • hub-core: receiver branches (deal update, ticket create/update, latest-wins replace).
  • hub-chat / crm-fe-v3: preview render + timeline/creator-label render.

4.B / Verification & Rollback Recipe

Pre-merge (in order): bundle exec rubocop <changed>bundle exec brakeman --no-pagerbundle exec rspec spec/services spec/workersbundle exec rspec spec/contract/. Post-deploy signals (PRD §11): bot_deal_created/ai_deal_created non-zero; deal_preview_displayed ratio ≥95% (alert if 0 while creates >0 for 15min); timeline_log_creation_failed < 5%/1h; P95 room load ≤ baseline+200ms (rollback trigger). Rollback: (1) toggle feature flag [name] OFF (per-account) → bot/AI events stop emitting; (2) creator_flag column is inert when unread — no down-migration required for rollback; (3) confirm deal_preview_displayed returns to pre-rollout and P95 latency recovers; (4) revert PR if needed (column drop only as cleanup, last).


5. Concerns / Open Questions

#SeverityQuestionOwnerBlocks
OQ-1[critical]How does hub-chat DealDetails.vue source Name/Stage/Pipeline — live CRM fetch via external_id, or stored? QontakCrmObject stores none. Determines whether deal update-sync (AC-4) needs new hub-core columns or is just a cache-bust.Omnichannel EngADR-3, O2, I1
OQ-2[critical]Which sender carries bot/AI ticket create+update? Omnichannel sender hardcodes action:'delete' (webhook_sender_service.rb:62-63); generic webhook supports Ticket (webhook.rb:10) but needs a create/update receiver in hub-core.CRM + Omnichannel EngO4,O5,I2,I3
OQ-3[important]Confirm new creator_flag column vs overloading data_source (ADR-4).CRM Eng + PMC2,C3,ADR-4
OQ-4[critical]Exact hub-chat room deeplink URL pattern (host + route) for the timeline link (CDTC-S05). No web deeplink helper found in repo.Omnichannel EngC5, CDTC-S05
OQ-5[important]For bot/AI parity, deal-create webhook must fire automatically — but emission is gated by per-team Crm::Webhook config (should_send_webhook_service.rb). Is there an always-on path for bot/AI, or must each account configure a Crm::Deal.create webhook?CRM EngO1 reliability
OQ-6[important]E1 concurrency: two creates for one room within ms — confirm CRM created_at tie-break + timeline-note dedup key.CRM EngC5, ADR-6
OQ-7[important]Confirm a purge job actually deletes HubChannel Crm::Note after 1y (PRD 5.1) — not verified in repo.CRM Engretention claim
OQ-8[important]Full list of deal/ticket fields that trigger an update event (PRD OQ#8).CRM EngO2,O5
OQ-9[nice-to-have]Latency threshold for "no noticeable degradation" (PRD OQ#3).Omnichannel Engrollout gate
OQ-10[critical]Contact-association contract mismatch (verified across repos): chatbot sends crm_lead_ids (array, derived via /api/v3.1/contacts?phone= lookup — execute.rb:269-291), but PRD/CDTC-S03 + CRM associate_hub_contact (create_service.rb:378) describe a singular contact_id. Confirm the canonical field name and which side performs association — otherwise contact is associated twice or not at all.Chatbot + CRM EngC1, CDTC-S03, ADR-7, IB1
OQ-11[important]Does the Agentic AI producer reuse the chatbot CRM node executor (mekari_qontak_crm/execute.rb) or a separate service? Determines whether ADR-7's body-field work is implemented once or mirrored in a second repo (currently TBD).Agentic AI SquadADR-7, AI payload dep

Fragile assumptions: PRD assumptions #5 (company token) and #6 (chat-history API) are verified in-repo (see Source Verification) — and #5 is now also verified from the producer side (chatbot crm_http_client.rb uses __qontak_crm_company_token + /api/v3.1/teams/company_token). Verified (producer side): chatbot's CRM node executor (execute.rb) already calls /api/v3.1/deals|tickets but does not populate channel_integration_room_id/creator_flag today — confirming the Chatbot Squad payload work (Risk #7) is real and scoped to execute.rb body building. Risk #7 (5 of 9 deps owned by Chatbot/Agentic AI squads) remains the top program risk; mitigation = align payload delivery in sprint planning.


6. Comment logs

DateAuthorNote
2026-06-30rfc-starterInitial draft from PRD v1.1. Key finding: CRM→hub-core webhook pipeline + QontakCrmObject/RoomTicket receivers already exist; PRD "dedicated Omnichannel API" reframed as additive event actions (ADR-2). 4 critical OQs gate readiness.
2026-06-30rfc-starter (refinement r2)Added Chatbot BE (chatbot) as the upstream producer repo. Grounded against mekari_qontak_crm/execute.rb (`POST /api/v3.1/deals

7. Ready for agent execution

Ready for agent execution: no

Blocking gates (must resolve before yes):

  • OQ-1 [critical] — deal preview field source unknown → cannot finalize update-sync design (ADR-3 / hub-core QontakCrmObject shape).
  • OQ-2 [critical] — bot/AI ticket create+update has no sender/receiver path today.
  • OQ-4 [critical] — room deeplink URL pattern unverified (left as [REQUIRED], not invented).
  • OQ-10 [critical] — chatbot↔CRM contact contract mismatch (crm_lead_ids vs contact_id) must be reconciled before C1/CDTC-S03 is safe.
  • InfoSec approver + feature flag name + discussion link are [REQUIRED] metadata.

The qontak.com-only chunks (C1–C6) are executable now for the storage + emission + timeline half; the cross-service contract (chatbot producer body per ADR-7, hub-core I1–I3, FE renders) and OQ-1/2/4/10 must close before the end-to-end flow is agent-runnable without a clarification meeting.