[DRAFT][RFC] Phase 1 — Bot & AI Creation Parity: Backend (qontak.com)
Document Conventions
This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. Replace placeholder values; mark sections
N/A — reasonwhen truly inapplicable.It is also agent-execution-ready: the §1 PRD-to-Schema Derivation, §2 Repo Reading Guide (Detail 2.0), Infrastructure Topology, ADR-format Technical Decisions, mermaid diagrams, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 Ready for agent execution: yes.
Scope boundary: This RFC covers only qontak.com CRM backend changes to accept
creator_flagon deal/ticket create. Creator attribution usesaudits.username(viaas_user); channel origin stored indata_source. Timeline note: deals reuseCrm::HubChannelTicket; tickets get a newcreate_ticket_noteshelper →Crm::HubChannelTicketV2. Contact association viacustomer_idsfor tickets requires two new files:Contacts::LinkTicketByCustomerIdService+ Worker. No new tables, no feature flag.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | IDEA / RFC / ABANDON / AGREED |
| Owner | crm | CRM squad owns qontak.com backend |
| Author(s) | Ardian Pradipta | — |
| Reviewers | CRM Eng lead · Chatbot Squad · Agentic AI Squad | Cross-squad review required |
| Approver(s) | CRM Tech Lead · InfoSec approver | Required before §7 flips to yes |
| Submitted Date | 2026-07-02 | ISO-8601 |
| Last Updated | 2026-07-02 | — |
| Target Release | 2026-Q3 | Quarter target |
| Related Documents | PRD | PRD v1.2 |
| Discussion | TBD | Slack thread or review doc |
Type: backend Sub-type: enhancement
Sections at a Glance
- Overview (PRD-to-Schema Derivation, Traceability, Per-Story Change Map)
- Technical Design (Infrastructure Topology, ADRs, Repo Reading Guide, Architecture, Sequence, APIs, Branch catalog)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan (Agent Execution Plan, Verification & Rollback)
- Concern, Questions, or Known Limitations
- Comment logs
- Ready for agent execution
Table of Contents
[TOC]
1. Overview
Problem & Scope
Deals/tickets created by Chatbot or Agentic AI show no creator attribution — data_source remains 'open-api' and the timeline shows no "Bot"/"AI" creator label. This RFC adds creator_flag acceptance to the existing POST create endpoints. When present: data_source is set to the creator_flag value (channel-origin tracking); audits.username is set via as_user(creator_flag) so the timeline displays "Bot"/"AI" via audit.rb#mapping_who. When creator_flag + channel_integration_room_id are both present, a timeline note is created — Crm::HubChannelTicket for deals (existing create_notes pattern), Crm::HubChannelTicketV2 for tickets (new create_ticket_notes helper).
Success Criteria
| Metric | Target |
|---|---|
Deals with creator_flag: 'bot' have data_source = 'bot' | 100% |
Tickets with creator_flag: 'agentic_ai' have data_source = 'agentic_ai' | 100% |
Timeline entry exists when channel_integration_room_id + creator_flag present | 100% |
Existing creates without creator_flag behave identically | 100% backward compat |
Out of Scope
- New timeline tables (existing
Crm::HubChannelTicket/Crm::HubChannelTicketV2reused) - Chat history fetch outside existing flow
- Feature flag
- Omnichannel notification APIs
- FE rendering
Related Documents
| Document | Path |
|---|---|
| PRD | crm/consistent-deal-ticket-creation/prds/phase-1-bot-ai-creation-parity.md |
Assumptions
| # | Assumption | Risk | Verification |
|---|---|---|---|
| A1 | Chatbot/AI squads send creator_flag + room_id in create payload | High | Align at sprint planning |
| A2 | data_source column accepts any string (no CHECK constraint) | None | Verified: db/schema.rb:1394 |
Dependencies
| Dependency | Owner | Deliverable | Blocking? |
|---|---|---|---|
| Chatbot enhanced payload | Chatbot Squad | creator_flag + room_id in POST create | YES |
| Agentic AI enhanced payload | Agentic AI Squad | creator_flag + room_id in POST create | YES |
PRD-to-Schema Derivation
| PRD entity / attribute / rule | Persisted as (table.column) | Exposed via (endpoint / event) | Enforced where | PRD section # |
|---|---|---|---|---|
creator_flag ("bot" / "agentic_ai") — creation channel origin | crm_deals.data_source / tickets.data_source (records the channel through which the object was created, same purpose as 'open-api', 'mobile', 'webhook_chat') | GET /api/mobile/v2.8/crm/deals/:id · GET /api/mobile/v2.8/crm/tickets/:id (data_source field — for channel tracking, not creator label) | Controller param permit + validation | §6 CHG-002, §8 #1-2 |
creator_flag — timeline creator display name | audits.username = creator_flag value (set via Audited.audit_class.as_user(creator_flag) wrapping both the deal/ticket save and the timeline note create) | GET /api/mobile/v2.8/crm/deals/:id/timeline · GET /api/mobile/v2.8/crm/tickets/:id/timeline — define_who resolved by audit.rb#mapping_who → "Bot" / "Agentic AI" | audit.rb:1824 extended to map 'bot' → "Bot", 'agentic_ai' → "Agentic AI" | ADR-1, ADR-2, C7 |
channel_integration_room_id (room association) | crm_deals.channel_integration_room_id / tickets.channel_integration_room_id (existing columns) | GET /api/mobile/v2.8/crm/deals/:id · GET /api/mobile/v2.8/crm/tickets/:id | Already permitted; no change | §8 #1-2 |
| Contact association | Existing crm_people_deals / people_tickets join tables | GET /api/mobile/v2.8/crm/deals/:id/contacts · GET /api/mobile/v2.8/crm/tickets/:id/contacts | Existing service logic (CreateService#378); tickets: new LinkTicketByCustomerIdWorker | §8 #1, PRD §9 S03 |
| Timeline entry (chat history + room deeplink) | Deals: Crm::HubChannelTicket (existing STI). Tickets: Crm::HubChannelTicketV2 (existing STI, new helper) | GET /api/mobile/v2.8/crm/deals/:id/timeline · GET /api/mobile/v2.8/crm/tickets/:id/timeline (via audited) | Created when both creator_flag + room_id present | ADR-2, §7, PRD §9 S04 |
Detail 1.A — PRD Traceability
Forward (PRD → RFC):
| PRD requirement | Service / endpoint / job | RFC section |
|---|---|---|
Accept creator_flag in deal creation | POST /api/v4/deals · POST /api/v3.1/deals | §2.4 |
Accept creator_flag in ticket creation | POST /api/v4/tickets · POST /api/v3.1/tickets | §2.4 |
Map creator_flag → data_source | Deal/Ticket create services | §2.4 |
Auto-associate contact via crm_lead_ids | Existing param already wired — no change | §2.4 |
Auto-associate contact via customer_ids (deals) | Follow v3.1 pattern (v3dot1/deals.rb:1354): pre-save async/sync branch, post-save Contacts::LinkDealByCustomerIdWorker.perform_async per customer_id | §2.4, Detail 2.0 |
Auto-associate contact via customer_ids (tickets) | No equivalent worker exists — new Contacts::LinkTicketByCustomerIdService + Contacts::LinkTicketByCustomerIdWorker required, mirroring deal counterparts; uses PeopleTicket join model instead of Crm::PeopleDeal | §2.4, ADR-4, C2-C3 |
| Create timeline entry with chat context (deals) | Existing create_notes → Crm::HubChannelTicket when creator_flag + room_id present | §2.4, ADR-2 |
| Create timeline entry with chat context (tickets) | New create_ticket_notes helper → Crm::HubChannelTicketV2 when creator_flag + room_id present | §2.4, ADR-2, C4 |
Expose data_source on read | GET /api/mobile/v2.8/crm/deals/:id · GET /api/mobile/v2.8/crm/tickets/:id entity | §2.4 |
| Creator label "Bot" / "AI" | audits.username → audit.rb#mapping_who → define_who in timeline response; FE renders define_who | ADR-1, C7 |
Reverse (RFC → PRD):
| New endpoint / table / dependency | PRD need it serves |
|---|---|
creator_flag param on existing POST endpoints | §6 CHG-002 — creator attribution (CDTC-S06) |
as_user(creator_flag) → audits.username + mapping_who extension | §7 — timeline creator label "Bot"/"AI" (CDTC-S06) |
Conditional Crm::HubChannelTicket (deals) / Crm::HubChannelTicketV2 (tickets) creation | §7 — timeline log with chat history (CDTC-S04) |
New Contacts::LinkTicketByCustomerIdService + Worker | §9 S03 — contact auto-association for ticket customer_ids (CDTC-S03) |
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE endpoint) | Required writes (BE endpoint) | Status surface |
|---|---|---|---|---|
| Deal/Ticket detail — Timeline (creator label + chat history note) | FE web + mobile app | GET /api/mobile/v2.8/crm/deals/:id/timeline · GET /api/mobile/v2.8/crm/tickets/:id/timeline | — | define_who = "Bot" / "Agentic AI" from audit.rb#mapping_who; chat note entry from Crm::HubChannelTicket / Crm::HubChannelTicketV2 |
| Deal/Ticket create (bot/AI) | External API clients only (Chatbot, Agentic AI) | — | POST /api/v4/deals · POST /api/v4/tickets · POST /api/v3.1/deals · POST /api/v3.1/tickets | FE never calls these create endpoints |
Role Coverage
| PRD role | Authorization mechanism | Endpoints permitted | Cross-tenant? | Audit trail |
|---|---|---|---|---|
| Chatbot / Agentic AI (system) | Company token / API key | POST /api/v4/deals · POST /api/v4/tickets · POST /api/v3.1/deals · POST /api/v3.1/tickets (create only) | No — tenant-scoped via token | data_source records origin |
| Sales Agent / CS Agent (FE web + mobile) | Devise session + CanCanCan | GET /api/mobile/v2.8/crm/deals/:id/timeline · GET /api/mobile/v2.8/crm/tickets/:id/timeline | No | Read-only; define_who from audit.rb#mapping_who → "Bot"/"AI" rendered by FE |
PRD Section Coverage
| PRD section # | Title | Where covered (RFC section) or n/a — reason |
|---|---|---|
| §1 | One-liner + Problem | §1.0 |
| §2 | If we don't ship | §1.0 (motivation) |
| §3 | Personas | §1 Detail 1.A Role Coverage |
| §4 | Non-Goals | §1.0 Out of Scope |
| §5 | Constraints | §3 |
| §6 | Feature Changes (CHG-001/002) | §2.4 APIs |
| §7 | New Feature (Timeline log) | ADR-2 — Crm::HubChannelTicket (deals) + Crm::HubChannelTicketV2 (tickets) |
| §8 | API & Webhook Behavior (#1-2 only) | §2.4 APIs |
| §9 | System Flow + Stories + ACs | §1.C Per-Story Change Map |
| §10 | Rollout | §4 Rollout |
| §11 | Observability | §3 |
| §12 | Success Metrics | §1.0 Success Criteria |
| §13 | Launch Plan & Stage Gates | §4 Rollout |
| §14 | Dependencies | §1 Dependencies |
| §15 | Key Decisions | §2 ADRs |
| §16 | Open Questions | §5 |
Detail 1.B — Key Decisions Summary
| # | Decision | Chosen option | §2 ADR block |
|---|---|---|---|
| 1 | Creator label display | audits.username via as_user(creator_flag) + mapping_who extension (not data_source) | ADR-1 |
| 2 | Timeline note model | Crm::HubChannelTicket for deals, Crm::HubChannelTicketV2 for tickets — not a single shared model | ADR-2 |
| 3 | Feature flag | None — param is self-gating | ADR-3 |
| 4 | customer_ids ticket association | New Contacts::LinkTicketByCustomerIdService + Worker (not generalise deal counterpart) | ADR-4 |
Detail 1.C — Per-Story Change Map
Layer scope values: BE-only, BE + FE consumes existing, Cross-squad.
| Story # | Title | Layer scope | Changes | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|
| CDTC-S01 | Preview in chat room | Cross-squad | Accept creator_flag + room_id on POST create | data_source set correctly; note created | §2.4, ADR-1 |
| CDTC-S02 | Navigate room → detail | N/A — FE (hub-chat) | — | — | n/a |
| CDTC-S03 | Auto-associate contact | BE (reuse + new) | crm_lead_ids: existing, no change. customer_ids deals: follow v3.1 pattern — Contacts::LinkDealByCustomerIdWorker. customer_ids tickets: new Contacts::LinkTicketByCustomerIdWorker + service | Contact linked | §2.4, ADR-4, Detail 2.0 |
| CDTC-S04 | Timeline log with chat preview | BE (reuse + extend) | Deals: Crm::HubChannelTicket via existing create_notes. Tickets: Crm::HubChannelTicketV2 via new create_ticket_notes helper | Timeline entry appears for both | ADR-2, §2.4 |
| CDTC-S05 | Navigate timeline → room | BE (reuse) | Room deeplink handled by existing Crm::HubChannelTicket / Crm::HubChannelTicketV2 behavior | Deeplink present | ADR-2 |
| CDTC-S06 | Creator label "Bot"/"AI" | BE-only | audit.rb#mapping_who extended: 'bot' → "Bot", 'agentic_ai' → "Agentic AI"; FE reads define_who from timeline audit | define_who = "Bot" / "Agentic AI" in timeline response | ADR-1, C7 |
| NEG-01/02/03 | Guard rails | Config/behavior | No change to manual flow; source value preserved | — | §2.C |
2. Technical Design
Infrastructure Topology
No new tables, queues, or feature flags. Two new Sidekiq worker + service files for ticket customer_ids association (ADR-4). All other changes are additive param acceptance on existing endpoints.
Deployment topology
flowchart TB
internet([Internet]) -->|HTTPS| lb[Load Balancer]
lb -->|HTTP| pods["CRM API pods xN"]
pods -->|read/write| db_primary[(Postgres primary)]
pods -->|read-only| db_replica[(Postgres replica)]
pods -->|get/set| cache[(Redis cache)]
Per-service responsibility
| Service | Use cases (this RFC) | Internal calls (owner) | External calls |
|---|---|---|---|
| qontak.com CRM | Accept creator_flag on POST deal/ticket create; set data_source + audits.username via as_user; create Crm::HubChannelTicket (deals) / Crm::HubChannelTicketV2 (tickets) note when room_id present; dispatch LinkTicketByCustomerIdWorker for ticket customer_ids | Contacts::LinkTicketByCustomerIdService [NEW] | — |
Technical Decisions (ADR-format)
ADR-1: Creator label via audits.username (not data_source)
Context: PRD requires "Bot"/"AI" displayed as creator in the timeline. data_source is an origin-channel tracker (values: 'open-api', 'mobile', 'webhook_chat', 'omnichannel') — it records how an object entered the system, not who created it. The timeline creator label is already resolved by audit.rb#mapping_who from audits.username, set at write time via Audited.audit_class.as_user(actor). creator_id is a User FK and cannot hold a non-user string actor.
Options considered:
- Option A — Set
audits.username = creator_flagviaas_user(creator_flag): Wrap the entire deal/ticket save block (and subsequent note creation) inAudited.audit_class.as_user(params[:creator_flag]).mapping_whofalls through to theusernamebranch ataudit.rb:1824— extend it to map'bot'→"Bot",'agentic_ai'→"Agentic AI".- Pros: Correct semantic. No schema change. Follows the same pattern used for
'hub'/'central'/'Qontak system'. Creator label controlled entirely in one place (mapping_who). - Cons: None significant.
- Pros: Correct semantic. No schema change. Follows the same pattern used for
- Option B — Use
data_sourcefor creator label: Store'bot'/'agentic_ai'indata_sourceand have FE read it as the creator label.- Cons: Wrong semantic —
data_sourceis channel origin, not actor identity. Conflates two unrelated concerns. FE would need custom logic to disambiguate "thisdata_sourcevalue means a creator label, not a channel".
- Cons: Wrong semantic —
Decision: Option A
Rationale: audits.username is the canonical actor field for timeline display. data_source retains its meaning as channel-origin tracker and may independently receive 'bot'/'agentic_ai' values to signal creation channel — but that is orthogonal to the creator label. The display label is owned by audit.rb#mapping_who, consistent with how system actors ('hub', 'Qontak system') are already handled.
Consequences: audit.rb:1824 extended with two new username mappings. data_source receives 'bot'/'agentic_ai' as a legitimate channel-origin value — same semantic as 'open-api', 'webhook_chat'. Creator label is independently owned by audits.username.
Exact patch for audit.rb:1824 — replace the else branch inside the user.present? outer else block:
# BEFORE (audit.rb:1823-1828):
else
if ['central', 'hub', 'Qontak system'].include?(self.username)
self.define_who = "Qontak system"
else
self.define_who = "<deleted user>"
end
end
# AFTER:
else
if ['bot', 'agentic_ai'].include?(self.username)
self.define_who = self.username == 'bot' ? 'Bot' : 'Agentic AI'
elsif ['central', 'hub', 'Qontak system'].include?(self.username)
self.define_who = "Qontak system"
else
self.define_who = "<deleted user>"
end
end
Bot/AI check goes before the system-actor check — both are non-user string actors; order within the else block does not affect the existing 'Qontak system' branch.
creator_id behavior: creator_id remains a User FK and is always set to @current_user.id via return_valid_creator_id (crm_referencable.rb:845) — i.e. the service account user that holds the company token. It is not nulled out or overridden for bot/AI records. This is correct: creator_id means "which user account executed this call"; the timeline creator label is a separate concern owned by audits.username. Do not attempt to clear or fake creator_id for bot/AI creates.
Reversibility: High. Remove the two mapping_who entries → falls back to "<deleted user>". No schema changes.
ADR-2: Timeline entry — Crm::HubChannelTicket for deals, Crm::HubChannelTicketV2 for tickets (not new table or worker)
Context: PRD describes a timeline log entry with chat history + room deeplink. The two models are distinct STI subclasses with different parent associations:
| Model | Parent | audited associated_with | Used for |
|---|---|---|---|
Crm::HubChannelTicket | Crm::DealNote | :crm_deal | Deals |
Crm::HubChannelTicketV2 | TicketNote | :tickets | Tickets |
Deals already have a create_notes helper (deals.rb:120-138) that creates Crm::HubChannelTicket. Tickets have no equivalent helper today — Crm::HubChannelTicketV2 is created in hub/ticket/new_ticket_service.rb:226 and hub/ticket/v2/note_creator.rb:50, but not wired into the v4/v3.1 ticket create endpoint.
Options considered:
- Option A — Reuse existing models, add ticket
create_notesequivalent: Deals call existingcreate_notes→Crm::HubChannelTicket. Tickets get a parallelcreate_ticket_noteshelper →Crm::HubChannelTicketV2. No new infrastructure.- Pros: Correct model per object type. Follows established pattern (
hub/ticket/new_ticket_service.rb:226). Timeline auto-populated byaudited. - Cons: Small new helper method needed for ticket endpoint.
- Pros: Correct model per object type. Follows established pattern (
- Option B — New table + async worker: Scope of original draft.
- Cons: Unnecessary complexity. Existing models handle the same use case.
Decision: Option A
Rationale: Crm::HubChannelTicket and Crm::HubChannelTicketV2 are already the canonical note types for deals and tickets respectively — confirmed by hub/ticket/new_ticket_service.rb, note_creator.rb, and audit.rb:1558. Using the wrong type on a ticket would associate the note to the wrong parent and break timeline rendering.
Consequences: Deals use existing create_notes → Crm::HubChannelTicket (unchanged). Ticket create endpoint gets a new create_ticket_notes helper → Crm::HubChannelTicketV2, mirroring hub/ticket/new_ticket_service.rb:217-233 (ticket_id in parameters, type: 'Crm::HubChannelTicketV2'). Both wrapped in as_user(audited_actor).
create_ticket_notes full spec:
def create_ticket_notes(params, ticket_id)
source = { "email": 3, "wa": 8, "wa_cloud": 8, "telegram": 9, "fb": 11, "ig": 13,
"twitter": 14, "line": 15, "livechat_dot_com": 16, "web_chat": 17,
"qontak": 18, "tokopedia_chat": 19, "app_chat": 20, "unknown": 21,
"ig_comment": 22, "shopee": 23 }
source_id = source[params[:crm_note_type].to_s.to_sym]
note_type = Crm::NoteType.find_by(id: source_id)
# Guard: if crm_note_type absent or unresolvable, skip silently
return unless note_type.present?
# Dedup: one note per ticket per team
return if Crm::HubChannelTicketV2.exists?(ticket_id: ticket_id, team_id: @current_user.team_id)
parameters = {
channel_room_id: params[:channel_integration_room_id],
note: note_type.note_type,
crm_person_id: params[:crm_person_id],
crm_note_type_id: note_type.id,
team_id: @current_user.team_id,
organization_id: @current_user.organization_id,
creator_id: @current_user.id,
type: 'Crm::HubChannelTicketV2',
channel_organization_id: params[:channel_integration_organization_id],
ticket_id: ticket_id
}
Crm::HubChannelTicketV2.create!(parameters)
rescue StandardError => e
# Non-blocking: note failure never blocks ticket creation
Rails.logger.warn("[create_ticket_notes] failed ticket_id=#{ticket_id} error=#{e.message}")
end
Reversibility: High. Stop calling the helper, no data loss.
ADR-3: No feature flag
Context: creator_flag is an optional additive param. Absent → existing behavior. Producers adopt when ready.
Options considered:
- Option A — No flag: Param is self-gating.
- Option B — Server-side flag: Adds rollout complexity with no benefit — the flag would gate nothing that the absence of
creator_flagdoesn't already gate.
Decision: Option A
Rationale: Absent param = existing behavior. Producers control when they start sending creator_flag. No flag needed.
ADR-4: New Contacts::LinkTicketByCustomerIdService + Worker (not reuse deal counterpart)
Context: Tickets need the same customer_ids async association pattern as deals (v3.1:1354). The deal side has Contacts::LinkDealByCustomerIdService → Contacts::LinkDealByCustomerIdWorker. No ticket equivalent exists. The join model is PeopleTicket (crm_person_id + ticket_id) vs Crm::PeopleDeal (crm_person_id + crm_deal_id).
Options considered:
- Option A — New service + worker mirroring deal counterparts:
Contacts::LinkTicketByCustomerIdServicewithPeopleTicket.find_or_create_by,Contacts::LinkTicketByCustomerIdWorkerwith identical queue/retry/exhausted-log shape.- Pros: Exact parity. Self-contained. Easy to test independently.
- Cons: Two new files (minimal — same shape as existing).
- Option B — Generalize existing service to handle both: Add
record_type:param toLinkDealByCustomerIdService.- Cons: Touches tested production code. Polymorphic branching adds complexity. Existing callers unaffected anyway — not worth the risk.
Decision: Option A
Rationale: Smallest safe change. Deal service is tested and in production — no reason to touch it. Two ~50-line files following an established template.
Consequences: Two new files: app/services/contacts/link_ticket_by_customer_id_service.rb, app/workers/contacts/link_ticket_by_customer_id_worker.rb.
Reversibility: High. Stop dispatching the worker; no schema changes.
Detail 2.0 — Repo Reading Guide
Repo Map
flowchart LR
subgraph crm[BE: qontak.com]
subgraph controllers[Controllers]
v4d["api/v4/deals.rb"]
v4t["api/v4/tickets.rb"]
v31d["api/v3dot1/deals.rb"]
v31t["api/v3dot1/tickets.rb"]
end
subgraph notes[Timeline Notes]
hct["models/crm/hub_channel_ticket.rb\n(Crm::DealNote — deals)"]
hctv2["models/crm/hub_channel_ticket_v2.rb\n(TicketNote — tickets)"]
note_tmpl["services/hub/ticket/new_ticket_service.rb\n(create_note_ticket — V2 template)"]
end
subgraph contact_assoc[Contact Association]
svc_d["services/contacts/link_deal_by_customer_id_service.rb"]
wrk_d["workers/contacts/link_deal_by_customer_id_worker.rb"]
svc_t["services/contacts/link_ticket_by_customer_id_service.rb [NEW]"]
wrk_t["workers/contacts/link_ticket_by_customer_id_worker.rb [NEW]"]
pt["models/people_ticket.rb"]
end
subgraph models[Models / Schema]
deal_m["models/crm/deal.rb"]
schema["db/schema.rb"]
audit["models/audit.rb\n(mapping_who — extended)"]
end
end
v4d -->|create_notes| hct
v4t -->|create_ticket_notes NEW| hctv2
note_tmpl -.->|template for| hctv2
v4d -->|customer_ids async| wrk_d
v4t -->|customer_ids async| wrk_t
wrk_d --> svc_d
wrk_t --> svc_t
svc_d --> pt
svc_t --> pt
Existing Code Anchors
| Path | Why the agent reads it | What pattern it teaches |
|---|---|---|
app/controllers/api/v4/deals.rb:72-118 | Deal create handler | data_source hardcoded at line 90; channel_integration_room_id at 92-93 |
app/controllers/api/v4/deals.rb:120-138 | create_notes — creates Crm::HubChannelTicket for deal timeline | Existing pattern to reuse for deals |
app/services/hub/ticket/new_ticket_service.rb:217-233 | create_note_ticket — creates Crm::HubChannelTicketV2 for ticket timeline | Template for new create_ticket_notes helper in ticket create endpoint |
app/models/crm/hub_channel_ticket.rb | Crm::HubChannelTicket < Crm::DealNote, audited associated_with: :crm_deal | Deal-specific note model |
app/models/crm/hub_channel_ticket_v2.rb | Crm::HubChannelTicketV2 < TicketNote, audited associated_with: :tickets | Ticket-specific note model |
app/controllers/api/v4/tickets.rb:57-74 | Ticket create handler | data_source = 'open-api-v4' at 62 |
app/controllers/api/v3dot1/deals.rb:1354-1403 | customer_ids async/sync branch + Contacts::LinkDealByCustomerIdWorker dispatch | Template for customer_ids handling in v4 deal/ticket create |
app/services/contacts/link_deal_by_customer_id_service.rb | Deal-contact association service | Template for new LinkTicketByCustomerIdService — same shape, Crm::PeopleDeal → PeopleTicket, crm_deal_id → ticket_id |
app/workers/contacts/link_deal_by_customer_id_worker.rb | Deal-contact async worker | Template for new LinkTicketByCustomerIdWorker — same queue/retry/exhausted-log shape |
app/models/people_ticket.rb | PeopleTicket join model (crm_person_id + ticket_id) | Target join model for ticket-contact association |
app/services/crm/deals/create_service.rb:381-398 | create_deal_note — mobile v2.8 equivalent | Template for HubChannelTicket creation |
db/schema.rb:1394 | crm_deals.data_source column | character varying default 'web' |
db/schema.rb:4961 | tickets.data_source column | character varying |
app/models/crm/deal.rb:150 | belongs_to :creator, class_name: 'User' | Confirms no string actor field today |
Existing Contracts to Reuse, Extend, or Replace
| Contract | Status | Justification | Owner |
|---|---|---|---|
POST /api/v4/deals | extended | Add :creator_flag, customer_ids branch to permitted params | CRM BE |
POST /api/v4/tickets | extended | Same | CRM BE |
POST /api/v3.1/deals | extended | Same | CRM BE |
POST /api/v3.1/tickets | extended | Same | CRM BE |
create_notes (deals.rb:120-138) | reused | Call when creator_flag + room_id present | CRM BE |
Contacts::LinkDealByCustomerIdService | reused (template only) | Reference implementation for new ticket service | CRM BE |
Contacts::LinkDealByCustomerIdWorker | reused (template only) | Reference implementation for new ticket worker | CRM BE |
Contacts::LinkTicketByCustomerIdService | new | Ticket-contact association by customer_id via PeopleTicket | CRM BE |
Contacts::LinkTicketByCustomerIdWorker | new | Async wrapper dispatched after ticket save | CRM BE |
Patterns to Follow
| Concern | Pattern in repo | Reference file | Deviation in this RFC? |
|---|---|---|---|
| Controller param permitting | permit with explicit symbol list | deals.rb:72-118 | Add :creator_flag, :customer_ids, :async_lead_assoc |
| Note creation for timeline | Crm::HubChannelTicket.create! wrapped in Audited.audit_class.as_user | deals.rb:120-138, create_service.rb:381-398 | Identical — reuse as-is |
data_source assignment | API controller sets data_source on new records | deals.rb:90 (hardcodes 'open-api') | Same, but conditional on creator_flag |
customer_ids async worker dispatch (deals) | Pre-save branch → post-save perform_async per id | v3dot1/deals.rb:1354-1403 | Ported to v4 deals — identical pattern |
customer_ids async worker dispatch (tickets) | Same pattern, new worker | link_deal_by_customer_id_worker.rb (template) | New LinkTicketByCustomerIdWorker + service |
Reading Order for the Agent
app/controllers/api/v4/deals.rb:72-118— deal create handler:data_source,creator_id,channel_integration_room_idpatternsapp/controllers/api/v4/deals.rb:120-138—create_notes→Crm::HubChannelTicket: deal timeline note pattern to reuseapp/services/hub/ticket/new_ticket_service.rb:217-233—create_note_ticket→Crm::HubChannelTicketV2: template for ticket timeline note helperapp/models/crm/hub_channel_ticket.rb+hub_channel_ticket_v2.rb— confirm STI parent andaudited associated_withper object typeapp/controllers/api/v3dot1/deals.rb:1354-1403—customer_idsasync/sync branch +LinkDealByCustomerIdWorkerdispatch (primary template for controller)app/services/contacts/link_deal_by_customer_id_service.rb— template for new ticket serviceapp/workers/contacts/link_deal_by_customer_id_worker.rb— template for new ticket workerapp/models/people_ticket.rb— join model for ticket-contact associationapp/controllers/api/v4/tickets.rb:57-74— ticket create handlerapp/services/crm/deals/create_service.rb:381-398— mobile v2.8'screate_deal_note(additional reference)db/schema.rb:1394,4961— column types fordata_sourceapp/models/crm/deal.rb:150—creatoris User FK
Source Verification
| Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|
data_source is string, default 'web', no CHECK | db/schema.rb:1394 | character varying default 'web'::character varying |
channel_integration_room_id column exists | db/schema.rb:1392 | channel_integration_room_id character varying |
data_source hardcoded in v4 deal create | deals.rb:90 | crm_deal.data_source = 'open-api' |
data_source hardcoded in v4 ticket create | tickets.rb:62 | ticket_par[:data_source] = 'open-api-v4' |
creator_id is User FK | crm/deal.rb:150 | belongs_to :creator, class_name: 'User' |
create_notes creates Crm::HubChannelTicket (deals) | deals.rb:120-137 | type: 'Crm::HubChannelTicket', audited associated_with: :crm_deal |
create_note_ticket creates Crm::HubChannelTicketV2 (tickets) | hub/ticket/new_ticket_service.rb:217-233 | type: 'Crm::HubChannelTicketV2', ticket_id: in params, audited associated_with: :tickets |
Crm::HubChannelTicketV2 is tickets-only | app/models/crm/hub_channel_ticket_v2.rb:1-2 | class Crm::HubChannelTicketV2 < TicketNote — wrong model if used on a deal |
audit.rb:1558 handles both note types | audit.rb:1558 | note_class_name.eql?("Crm::HubChannelTicket") || note_class_name.eql?("Crm::HubChannelTicketV2") |
create_deal_note in mobile v2.8 | create_service.rb:381-398 | Same Crm::HubChannelTicket pattern |
No LinkTicketByCustomerIdService/Worker exists | find /app/workers/contacts | Only link_deal_by_customer_id_worker.rb present — new files required |
PeopleTicket join model shape | app/models/people_ticket.rb:1-5 | crm_person_id + ticket_id FKs, audited associated_with: :ticket |
Detail 2.1 — Architecture
Component diagram
flowchart TB
caller([API caller]) --> api[/v4 · v3.1 deals · tickets API/]
api --> svc[create handler]
svc -->|as_user creator_flag| audit_write[(audits table\nusername=creator_flag)]
svc -->|data_source=creator_flag| db[(Postgres)]
svc -->|room_id present - deals| hct[Crm::HubChannelTicket]
svc -->|room_id present - tickets| hctv2[Crm::HubChannelTicketV2]
hct --> audit_write
hctv2 --> audit_write
hct --> db
hctv2 --> db
svc -->|customer_ids async - tickets| wrk[LinkTicketByCustomerIdWorker]
wrk --> svc2[LinkTicketByCustomerIdService]
svc2 --> db
Detail 2.2 — Sequence
Happy path — bot deal creation with timeline note
sequenceDiagram
actor Bot as Chatbot API
participant LB as Load Balancer
participant API as CRM API pod
participant DB as Postgres primary
Bot->>LB: POST /v4/deals (creator_flag:bot, room_id)
LB->>API: HTTP
API->>API: permit params, validate creator_flag
API->>API: set audited_actor = creator_flag ("bot")
note over API: Audited.as_user("bot") block begins
API->>DB: INSERT deal (data_source=bot, channel_integration_room_id)<br/>audits.username="bot"
DB-->>API: commit
API->>DB: INSERT Crm::HubChannelTicket note<br/>audits.username="bot"
DB-->>API: commit
note over API: Audited.as_user block ends
API-->>Bot: 201 Created
Failure path — invalid creator_flag
sequenceDiagram
actor Bot as Chatbot API
participant API as CRM API pod
Bot->>API: POST /v4/deals (creator_flag:invalid)
API->>API: validate creator_flag
alt value not bot or agentic_ai
API-->>Bot: 422 INVALID_CREATOR_FLAG
else absent
API->>API: skip mapping, default behavior
API-->>Bot: 201 (data_source unchanged)
end
Detail 2.3 — Database Model (DDL)
No DDL changes. The data_source column on crm_deals (schema.rb:1394) and tickets (schema.rb:4961) already exists as a free-form character varying with no CHECK constraint — new values 'bot' and 'agentic_ai' are accepted without migration.
Crm::HubChannelTicket (Crm::DealNote STI) and Crm::HubChannelTicketV2 (TicketNote STI) already exist — no schema changes needed.
PII classification: Crm::HubChannelTicket stores chat message content which may contain PII. Existing retention and access policies apply.
Cardinality: No new tables. Note volume matches existing deal/ticket creation volume.
Detail 2.4 — APIs
Outbound endpoints (consumers call us)
| Endpoint | Method | AuthN/AuthZ | Request schema changes | Response changes | Status codes | Reuse? |
|---|---|---|---|---|---|---|---|
| /api/v4/deals | POST | Company token / Doorkeeper | Add optional creator_flag "bot"|"agentic_ai" (case-sensitive, max 20 chars); customer_ids: string[] (qontak_customer_id values); async_lead_assoc: boolean (default false) | data_source may be 'bot'/'agentic_ai'; 201 body unchanged (data_source NOT exposed in entity) | 201, 422, 401, 403 | extended |
| /api/v4/tickets | POST | Company token / Doorkeeper | Same | Same | Same | extended |
| /api/v3.1/deals | POST | Company token / Doorkeeper | Same | Same | Same | extended |
| /api/v3.1/tickets | POST | Company token / Doorkeeper | Same | Same | Same | extended |
creator_flag validation:
- Valid:
'bot','agentic_ai'(case-sensitive) - Absent/empty → default (
data_sourceunchanged, no note) - Invalid → 422
INVALID_CREATOR_FLAG(generic message — allowed values not disclosed)
201 response schema (no change from existing entity):
data_source is not exposed in V4::Entities::Deal (app/controllers/api/v4/entities/deal.rb) or V4::Entities::Ticket (app/controllers/api/v4/entities/ticket.rb) — verified. The 201 response body is identical to a normal create. creator_flag is not echoed back. No entity change needed.
// POST /api/v4/deals — 201 response (unchanged shape)
{
"meta": 1,
"response": {
"id": 123,
"name": "Deal name",
"creator_id": 456,
"creator_name": "Service Account Name",
"idempotency_key": "uuid",
// ... existing fields unchanged
// data_source NOT exposed — channel origin is internal
}
}
// POST /api/v4/deals — 422 invalid creator_flag
{ "meta": { "message": "invalid creator_flag" } }
// POST /api/v4/deals — 404 customer_ids not found
{ "meta": { "message": "<validation_result error message>" } }
data_source mapping + audited actor override:
creator_flag is present → wrap the entire deal save block (and the subsequent note creation) in as_user(creator_flag) instead of as_user(@current_user). This sets audits.username = creator_flag on every audit record produced during that create — the deal's own creation audit and the Crm::HubChannelTicket note audit — so the timeline shows "Bot" / "Agentic AI" as creator for both entries.
# deals.rb — before create(), mirrors v3dot1/deals.rb:1354
customer_ids_for_worker = nil
if params[:customer_ids].present?
if params[:async_lead_assoc] == true && params[:crm_lead_ids].blank?
customer_ids_for_worker = params[:customer_ids]
else
validation_result = dl.validate_and_convert_customer_ids(params)
unless validation_result[:status]
status 404
return present :meta, { "message": validation_result[:error] }, with: V4::Entities::Meta::NotFound
end
end
end
# deals.rb ~926 — replace existing as_user(@current_user) block when creator_flag present
audited_actor = params[:creator_flag].presence || @current_user
Audited.audit_class.as_user(audited_actor) do
# direct assignment — creator_flag IS the data_source value; allowlist validation already ran
crm_deal.data_source = params[:creator_flag] if params[:creator_flag].present?
if crm_deal.save
# ... existing post-save logic unchanged ...
# note creation — inherits same as_user context
if params[:channel_integration_room_id].present?
if params[:creator_flag].present?
dl.create_notes(params, crm_deal.id)
elsif params[:crm_note_type].present?
dl.create_notes(params, crm_deal.id) # existing behavior unchanged
end
end
# customer_ids async path — mirrors v3dot1/deals.rb:1394-1402
if customer_ids_for_worker.present?
customer_ids_for_worker.each do |customer_id|
::Contacts::LinkDealByCustomerIdWorker.perform_async(
@current_user.team_id,
crm_deal.id,
customer_id,
@current_user.id
)
end
end
end
end
audit.rb:1824-1828 maps username to display string — must be extended:
self.username | Current display | Required change |
|---|---|---|
'hub', 'central', 'Qontak system' | "Qontak system" | Unchanged |
'bot' | "<deleted user>" (wrong) | → "Bot" |
'agentic_ai' | "<deleted user>" (wrong) | → "Agentic AI" |
For tickets: create_ticket_notes full spec in ADR-2 (nil guard, dedup, rescue). Ticket create (v4/tickets.rb:57-74) delegates to Ticket::Create service — no Idempotency-Key header support today and none added by this RFC (out of scope; bot/AI callers handle dedup on their side).
Error catalog
| HTTP | Code | Message | When |
|---|---|---|---|
| 422 | INVALID_CREATOR_FLAG | invalid creator_flag | Value present but not allowed |
| 422 | VALIDATION_ERROR | Standard Rails errors | Missing required fields |
| 404 | NOT_FOUND | Deal/ticket not found | Invalid ID on GET |
| 403 | FORBIDDEN | Access denied | No CanCanCan permission |
Detail 2.A — Data Integrity Matrix
| Write path | Transaction scope | Partial failure | Idempotency | Consistency | Duplicate handling |
|---|---|---|---|---|---|
| Deal create + note create | Two separate transactions (deal save, then note save) | Deal created, note skipped if note save fails (rare) | Deal idempotency key (existing) | Strong per-transaction | Note skip on duplicate is acceptable |
| Ticket create + note create | Same pattern | Same | Same | Same | Same |
Detail 2.B — Concurrency Collision Map
No new concurrent resources. Existing deal/ticket creation patterns unchanged. Crm::HubChannelTicket dedup at deals.rb:136 (find_by(crm_deal_id, team_id) → skip if exists). Crm::HubChannelTicketV2 dedup follows same pattern in create_ticket_notes helper.
Detail 2.C — Branch & Skip Catalog
| Condition | Behavior | Owner |
|---|---|---|
creator_flag absent | audited_actor = @current_user; data_source unchanged; no note | Create handler |
creator_flag valid ('bot' / 'agentic_ai') | audited_actor = creator_flag; data_source = creator_flag; audits.username = creator_flag | Create handler |
creator_flag invalid | 422 generic error, record not created | Create handler |
creator_flag present + room_id absent | data_source set, audits.username set; no note (note gated on room_id) | Create handler |
creator_flag present + room_id present (deal) | All above + Crm::HubChannelTicket created via create_notes | Create handler |
creator_flag present + room_id present (ticket) | All above + Crm::HubChannelTicketV2 created via create_ticket_notes | Create handler |
customer_ids + async_lead_assoc: true (deals) | LinkDealByCustomerIdWorker enqueued per customer_id after save | Create handler |
customer_ids + async_lead_assoc: true (tickets) | LinkTicketByCustomerIdWorker enqueued per customer_id after save | Create handler |
3. High-Availability & Security
- AuthN: All callers use existing company token (Doorkeeper). No new auth mechanism.
creator_flagis an optional param available to any token holder — not restricted to bot/AI integrations (see §5 Risks). - AuthZ:
creator_flagvalidated server-side against allowlist. No privilege escalation. Timeline read gated by existing CanCanCan permissions. - PII:
Crm::HubChannelTicket/Crm::HubChannelTicketV2contain chat message content (potentially PII). Existing encryption and retention policies apply unchanged. Bot/AI-created audit records (audits.username = 'bot'/'agentic_ai') have no user FK — the standard right-to-delete user deletion path does not touch them. These records are retained per the existing audit retention policy; no new deletion path is required. - Injection:
creator_flagvalidated against allowlist — not interpolated into SQL. - Performance: No new queries or writes beyond one conditional INSERT on existing endpoint. No latency impact.
- Logging: Structured log lines per observable event:
WARN [creator_flag.invalid] { creator_flag: "<value>", team_id: <id>, endpoint: "POST /api/v4/deals" }— on 422 rejectionINFO [hub_channel_ticket.created] { type: "Crm::HubChannelTicket", deal_id: <id>, creator_flag: "<value>", team_id: <id> }— on successful deal note creationINFO [hub_channel_ticket_v2.created] { type: "Crm::HubChannelTicketV2", ticket_id: <id>, creator_flag: "<value>", team_id: <id> }— on successful ticket note creationWARN [create_ticket_notes.failed] { ticket_id: <id>, error: "<message>", team_id: <id> }— on note rescue (non-blocking)INFO [link_ticket_by_customer_id.enqueued] { ticket_id: <id>, customer_id: "<value>", team_id: <id> }— on worker dispatch
- Failure mode: Note creation failure does not block deal/ticket creation (non-critical path). Errors rescued and logged as WARN per
create_ticket_notesspec.
4. Backwards Compatibility and Rollout Plan
Compatibility
- API contracts: Additive optional param. Existing callers unchanged.
- DB migration: None. No columns added, no tables created.
- Versioning: No change needed.
Rollout Strategy
| Phase | Scope | Guardrail | Exit Criteria |
|---|---|---|---|---|
| 1 — Producers adopt | Chatbot/AI add creator_flag + room_id | Feature inactive while param absent | creator_flag present in production API calls; zero unexpected creator_flag.invalid WARN logs over 7 days |
| 2 — FE reads timeline audit | CRM FE renders define_who from timeline audit entries — "Bot"/"AI" once mapping_who is extended (C7) | Existing audit entries unchanged | Creator label displays correctly in timeline |
Rollback Strategy
- Revert PR — removes
creator_flagparam acceptance - No DB rollback needed (zero schema changes)
- Confirm POST latency unchanged, no errors
Detail 4.A — Configuration Contract
None. No env vars, no feature flags.
Detail 4.B — Test Plan
| Layer | Command | What it must prove |
|---|---|---|
| Request | bundle exec rspec spec/controllers/api/v4/deals_spec.rb | creator_flag → correct data_source; invalid → 422; note created when room_id present; customer_ids async → worker enqueued; sync → validate_and_convert_customer_ids |
| Request | bundle exec rspec spec/controllers/api/v4/tickets_spec.rb | Same for tickets |
| Request | bundle exec rspec spec/controllers/api/v3dot1/deals_spec.rb | v3.1 backward compat |
| Request | bundle exec rspec spec/controllers/api/v3dot1/tickets_spec.rb | v3.1 backward compat |
| Service | bundle exec rspec spec/services/contacts/link_ticket_by_customer_id_service_spec.rb | person found → PeopleTicket created; person not found → :person_not_found; invalid args → :invalid_args |
| Worker | bundle exec rspec spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb | delegates to service; raises PersonNotFoundError on :person_not_found; exhausted callback logs to Crm::Log |
Detail 4.C — Agent Execution Plan
| Order | Chunk | Files | Commands to run | Acceptance criteria (verifiable) |
|---|---|---|---|---|
| C1 | v4 deal: add creator_flag + note + customer_ids | app/controllers/api/v4/deals.rb | bundle exec rspec spec/controllers/api/v4/deals_spec.rb | 'bot' → data_source='bot'; invalid → 422; room_id present → Crm::HubChannelTicket created; customer_ids + async_lead_assoc:true → LinkDealByCustomerIdWorker enqueued; sync path → validate_and_convert_customer_ids called |
| C2 | New LinkTicketByCustomerIdService | app/services/contacts/link_ticket_by_customer_id_service.rb | bundle exec rspec spec/services/contacts/link_ticket_by_customer_id_service_spec.rb | Mirror LinkDealByCustomerIdService: PeopleTicket.find_or_create_by; :person_not_found, :invalid_args, :ticket_not_found statuses; Audited.as_user wraps create |
| C3 | New LinkTicketByCustomerIdWorker | app/workers/contacts/link_ticket_by_customer_id_worker.rb | bundle exec rspec spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb | Mirror LinkDealByCustomerIdWorker: queue :contact, retry 3, exhausted logs Crm::Log with reference_object_type: 'Ticket'; raises PersonNotFoundError on :person_not_found |
| C4 | v4 ticket: add creator_flag + note + customer_ids (dispatches C2/C3 worker) | app/controllers/api/v4/tickets.rb | bundle exec rspec spec/controllers/api/v4/tickets_spec.rb | creator_flag valid → data_source set + as_user; invalid → 422 generic; room_id + creator_flag + resolvable crm_note_type → Crm::HubChannelTicketV2 created; room_id + creator_flag + nil crm_note_type → note skipped silently; duplicate room_id for same ticket → skipped (dedup); customer_ids + async_lead_assoc:true → LinkTicketByCustomerIdWorker enqueued; note failure does not block ticket save |
| C5 | v3.1 deal: add creator_flag + note | app/controllers/api/v3dot1/deals.rb | bundle exec rspec spec/controllers/api/v3dot1/deals_spec.rb | creator_flag → data_source + as_user; room_id → Crm::HubChannelTicket; customer_ids already wired — no change |
| C6 | v3.1 ticket: add creator_flag + note + customer_ids (dispatches C2/C3 worker) | app/controllers/api/v3dot1/tickets.rb | bundle exec rspec spec/controllers/api/v3dot1/tickets_spec.rb | Same as C4 |
| C7 | Audit model: extend mapping_who | app/models/audit.rb:1824 | bundle exec rspec spec/models/audit_spec.rb | username='bot' → define_who="Bot"; username='agentic_ai' → define_who="Agentic AI"; username='hub' still → "Qontak system" (regression check); username=nil with deleted user → "<deleted user>" (regression check) |
Execution order matters: C2 + C3 must merge before C4 and C6 (ticket controllers depend on the new worker). C1 and C2+C3 are independent — can be parallelised.
Deals use existing create_notes (deals.rb:120-138) → Crm::HubChannelTicket. Tickets require a new create_ticket_notes helper → Crm::HubChannelTicketV2, modelled on hub/ticket/new_ticket_service.rb:217-233 (add ticket_id: to parameters, type: 'Crm::HubChannelTicketV2'). Both wrapped in as_user(audited_actor).
Detail 4.D — Verification & Rollback Recipe
- Pre-merge:
bundle exec rspec spec/controllers/api/v4/deals_spec.rb spec/controllers/api/v4/tickets_spec.rb spec/services/contacts/link_ticket_by_customer_id_service_spec.rb spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb && bundle exec rubocop - Post-deploy signals: create deal with
creator_flag: 'bot',channel_integration_room_id: 'room_x'→ GET showsdata_source: 'bot'; timeline shows "Bot" as creator for both deal create and HubChannel note. Create ticket withcustomer_ids: ['cid_1'],async_lead_assoc: true→LinkTicketByCustomerIdWorkerjob appears in Sidekiq queue. - Rollback: revert PR; no DB changes to undo; workers in queue become no-ops (service guard
ticket_not_foundhandles orphaned jobs)
5. Concern, Questions, or Known Limitations
Risks & Mitigations
| Risk | Impact | Mitigation | Owner |
|---|---|---|---|
| Chatbot/AI squads miss payload delivery | Feature inactive | Align at sprint planning; include in RFC | Chatbot + AI squads |
Existing queries filter by data_source = 'open-api' | Bot/AI records excluded — intentional, they are a different channel | Document in release notes; align with consumers (Chatbot, AI squads) | CRM BE |
Open Questions
| # | Severity | Question | Owner |
|---|---|---|---|
| Q1 | [critical] | Chatbot/AI squad payload delivery timeline | Chatbot + AI squads |
Known Limitations
- No historical backfill: Pre-phase records retain existing
data_source. No retroactive note creation. - No source granularity: "Bot"/"AI" only — no specific flow name. Deferred.
- Note gated on
room_id: Withoutroom_id, no timeline note is created (same as manual flow).
6. Comment logs
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-07-02 | Initial draft | Await CRM Tech Lead + InfoSec review |
7. Ready for agent execution
yes
All technical gates pass. An AI agent can implement all C1-C7 chunks from the RFC content alone. Remaining items are organizational, not spec gaps:
| Item | Type | Impact on agent |
|---|---|---|
| Q1 — payload delivery timeline | Dependency | Agent can implement BE code regardless; feature inactive until producers send creator_flag |
| InfoSec approver | Process gate | Add before merging |
Passing gates:
- Infrastructure Topology diagram: ✅
- ADR-format Technical Decisions for minimum coverage: ✅
- Repo Reading Guide with Source Verification: ✅
- Mermaid sequence diagrams (happy + failure path): ✅
- DDL (no change needed): ✅
- APIs with reuse/extend tags: ✅
- Agent Execution Plan with files + commands + AC: ✅
- Verification & Rollback Recipe: ✅