Skip to main content

Task Breakdown: Phase 1 — Bot & AI Creation Parity (Backend)

RFC: phase-1-bot-ai-backend-rfc.md Repo: qontak.com Mode: Vertical — one task per RFC chunk (C1–C7) Scope: All 7 chunks included


Effort Summary

TaskEffort
C1 — v4 deal: creator_flag + note + customer_ids2 days
C2 — New LinkTicketByCustomerIdService1 day
C3 — New LinkTicketByCustomerIdWorker0.5 day
C4 — v4 ticket: creator_flag + note + customer_ids2 days
C5 — v3.1 deal: creator_flag + note1 day
C6 — v3.1 ticket: creator_flag + note + customer_ids1.5 days
C7 — Audit model: extend mapping_who1 day
Total~9 days

Confidence: high. Key assumptions: all spec files exist and are extended (not created from scratch); LinkTicketByCustomerIdService and Worker are copy-paste mirrors with 3–4 identifier substitutions; audit.rb patch is a 3-line elsif insertion with confirmed before/after in RFC ADR-1; includes manual testing on staging.


Task C1: [BE] v4 Deal — creator_flag + Timeline Note + customer_ids (CDTC-S01, CDTC-S04, CDTC-S06)

A bot or AI agent that creates a deal via the v4 API will have the deal's data_source set to the channel origin, show "Bot"/"Agentic AI" as the creator in the timeline, and optionally get a chat-history note attached — all without affecting existing manual creates.

Status: ✅ Actionable

Purpose

Modifies app/controllers/api/v4/deals.rb to accept creator_flag on deal creation. When present, it sets data_source to the flag value and wraps the entire save block in Audited.audit_class.as_user(creator_flag) so all resulting audit records carry the bot/AI identity as their username. This enables the CRM timeline to display "Bot" or "Agentic AI" via audit.rb#mapping_who without any entity or schema change.

Scope

  • Accept and validate creator_flag param ('bot' | 'agentic_ai'); 422 on invalid value
  • Replace as_user(@current_user) with as_user(audited_actor) where audited_actor = creator_flag.presence || @current_user
  • Set crm_deal.data_source = creator_flag inside the as_user block
  • Update note-creation gate: when creator_flag present + room_id present → call create_notes regardless of crm_note_type
  • Pre-save customer_ids async/sync branch mirroring v3dot1/deals.rb:1354-1371
  • Post-save: dispatch LinkDealByCustomerIdWorker per customer_id when async path

Files modified:

  • app/controllers/api/v4/deals.rb
  • spec/controllers/api/v4/deals_spec.rb

Expected Outcome

  • creator_flag: 'bot'crm_deal.data_source == 'bot', audits.username == 'bot'
  • creator_flag: 'agentic_ai'data_source == 'agentic_ai', audits.username == 'agentic_ai'
  • creator_flag: 'invalid' → 422, record not created
  • room_id + creator_flag present → Crm::HubChannelTicket created with audits.username == creator_flag
  • customer_ids + async_lead_assoc: trueLinkDealByCustomerIdWorker enqueued per customer_id
  • No creator_flagdata_source unchanged, existing behavior unaffected
  • All existing deals spec examples continue to pass

Test command: bundle exec rspec spec/controllers/api/v4/deals_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: The deal create flow is at deals.rb:920-955. Audited.audit_class.as_user(@current_user) wraps the entire save at line 926 — the creator_flag validation and customer_ids branch must be inserted before this block, and only the actor swap happens inside it. create_notes is called at lines 941-943 inside the existing block and will inherit as_user context automatically.

Task 1 — Write failing specs

In spec/controllers/api/v4/deals_spec.rb, add describe 'POST / with creator_flag' block:

describe 'POST / with creator_flag' do
context 'when creator_flag is bot' do
it 'sets data_source to bot' do ... end
it 'sets audits.username to bot' do ... end
end
context 'when creator_flag is invalid' do
it 'returns 422' do ... end
end
context 'when creator_flag + room_id present' do
it 'creates HubChannelTicket note' do ... end
end
context 'when customer_ids + async_lead_assoc true' do
it 'enqueues LinkDealByCustomerIdWorker' do ... end
end
context 'without creator_flag' do
it 'behaves identically to existing flow' do ... end
end
end

Run — expect FAIL: bundle exec rspec spec/controllers/api/v4/deals_spec.rb

Task 2 — Add creator_flag validation + customer_ids pre-save branch

Insert before crm_deal = dl.create(params, idempotency_key) at line 924:

if params[:creator_flag].present? && !%w[bot agentic_ai].include?(params[:creator_flag])
status 422
return present :meta, { "message": "invalid creator_flag" }, with: V4::Entities::Meta::UnprocessableEntity
end

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

Task 3 — Swap as_user actor + set data_source

At line 926, replace Audited.audit_class.as_user(@current_user) do with:

audited_actor = params[:creator_flag].presence || @current_user
Audited.audit_class.as_user(audited_actor) do
crm_deal.data_source = params[:creator_flag] if params[:creator_flag].present?
if crm_deal.save

Task 4 — Update note-creation gate

Replace the existing note gate at line 941:

# BEFORE:
if params[:channel_integration_room_id].present? && params[:crm_note_type].present?
dl.create_notes(params, crm_deal.id)
end

# AFTER:
if params[:channel_integration_room_id].present?
if params[:creator_flag].present? || params[:crm_note_type].present?
dl.create_notes(params, crm_deal.id)
end
end

Task 5 — Post-save worker dispatch

After existing post-save logic, inside the if crm_deal.save block:

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

Task 6 — Run all (expect PASS) + Linter

bundle exec rspec spec/controllers/api/v4/deals_spec.rb --format documentation
bundle exec rubocop app/controllers/api/v4/deals.rb

Acceptance criteria

  • creator_flag: 'bot'data_source='bot', audits.username='bot'
  • creator_flag: 'agentic_ai'data_source='agentic_ai', audits.username='agentic_ai'
  • creator_flag: 'invalid' → 422 generic message, record not created
  • room_id + creator_flag present → Crm::HubChannelTicket created
  • customer_ids + async_lead_assoc: trueLinkDealByCustomerIdWorker enqueued per customer_id
  • customer_ids sync path → validate_and_convert_customer_ids called
  • No creator_flag → existing behavior unchanged

Effort estimate

2 days — existing spec extended (not created); pre-save branch mirrors v3.1 pattern exactly; as_user swap is mechanical; includes manual testing on staging.

Depends on

  • None — fully independent, can run in parallel with C2 and C7

Task C2: [BE] New Contacts::LinkTicketByCustomerIdService (CDTC-S03)

A ticket created via the API with a customer_ids payload will have its contacts correctly associated, using the same idempotent find_or_create_by pattern as the deal counterpart.

Status: ✅ Actionable

Purpose

Creates Contacts::LinkTicketByCustomerIdService as a direct mirror of Contacts::LinkDealByCustomerIdService. Replaces Crm::Deal/Crm::PeopleDeal/crm_deal_id with Ticket/PeopleTicket/ticket_id. The find_or_create_by call on PeopleTicket ensures idempotency on retry. Audited.audit_class.as_user(@audit_user) wraps the create, consistent with the deal counterpart.

Scope

  • Result struct identical: Struct.new(:matched_ids, :created_count, :status)
  • initialize(team_id:, ticket_id:, customer_id:, audit_user_id: nil)
  • Statuses: :ok, :invalid_args, :ticket_not_found, :person_not_found
  • PeopleTicket.find_or_create_by(crm_person_id: person_id, ticket_id: @ticket_id)
  • Crm::Person.where(team_id:, qontak_customer_id:) for person resolution

Files modified:

  • app/services/contacts/link_ticket_by_customer_id_service.rb (new)
  • spec/services/contacts/link_ticket_by_customer_id_service_spec.rb (new)

Expected Outcome

  • Person found → PeopleTicket created, status :ok
  • Person not found → status :person_not_found, no record created
  • Invalid args → status :invalid_args
  • Ticket not found → status :ticket_not_found
  • Duplicate call → find_or_create_by returns existing, created_count: 0
  • All deal service spec patterns pass equivalently

Test command: bundle exec rspec spec/services/contacts/link_ticket_by_customer_id_service_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: Template is app/services/contacts/link_deal_by_customer_id_service.rb (65 lines). Only 3 substitutions: crm_deal_id → ticket_id, Crm::Deal → Ticket, Crm::PeopleDeal → PeopleTicket. Rename deal_exists?ticket_exists? and create_people_dealcreate_people_ticket. Add :ticket_not_found status for the ticket existence check.

Task 1 — Write failing specs

Create spec/services/contacts/link_ticket_by_customer_id_service_spec.rb mirroring link_deal_by_customer_id_service_spec.rb. Replace all deal identifiers with ticket equivalents.

Run — expect FAIL: bundle exec rspec spec/services/contacts/link_ticket_by_customer_id_service_spec.rb

Task 2 — Create service file

Copy link_deal_by_customer_id_service.rblink_ticket_by_customer_id_service.rb. Apply substitutions:

  • Class name: LinkTicketByCustomerIdService
  • @crm_deal_id@ticket_id
  • Crm::Deal.exists?Ticket.exists?(id: @ticket_id, team_id: @team_id)
  • Crm::PeopleDeal.find_or_create_by(crm_person_id:, crm_deal_id:)PeopleTicket.find_or_create_by(crm_person_id:, ticket_id: @ticket_id)
  • Add :ticket_not_found return when ticket_exists? is false

Task 3 — Run all (expect PASS) + Linter

bundle exec rspec spec/services/contacts/link_ticket_by_customer_id_service_spec.rb --format documentation
bundle exec rubocop app/services/contacts/link_ticket_by_customer_id_service.rb

Acceptance criteria

  • Person found → PeopleTicket created via find_or_create_by
  • Person not found → :person_not_found
  • Invalid args → :invalid_args
  • Ticket not found → :ticket_not_found
  • Audited.as_user(@audit_user) wraps PeopleTicket create
  • Duplicate call → no duplicate record

Effort estimate

1 day — direct copy-edit of 65-line service; 3 identifier substitutions; spec mirrors existing.

Depends on

  • None — fully independent, can run in parallel with C1 and C7

Task C3: [BE] New Contacts::LinkTicketByCustomerIdWorker (CDTC-S03)

The async worker wraps LinkTicketByCustomerIdService, retries on transient failure, and logs exhausted retries to Crm::Log with reference_object_type: 'Ticket'.

Status: ✅ Actionable (C2 must be merged first)

Purpose

Creates Contacts::LinkTicketByCustomerIdWorker as a direct mirror of Contacts::LinkDealByCustomerIdWorker. Delegates to LinkTicketByCustomerIdService. Raises PersonNotFoundError on :person_not_found to trigger Sidekiq retry. On sidekiq_retries_exhausted, logs to Crm::Log with reference_object_type: 'Ticket' and type_of_api: 'Background Job - Contacts::LinkTicketByCustomerIdWorker'.

Scope

  • sidekiq_options queue: :contact, retry: 3
  • perform(team_id, ticket_id, customer_id, audit_user_id = nil)
  • PersonNotFoundError < StandardError
  • Exhausted callback: Crm::Log.create_async(...) with correct ticket references

Files modified:

  • app/workers/contacts/link_ticket_by_customer_id_worker.rb (new)
  • spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb (new)

Expected Outcome

  • Queue: :contact, retry: 3
  • :person_not_found → raises PersonNotFoundError
  • Exhausted retries → Crm::Log entry with reference_object_type: 'Ticket'
  • Happy path → delegates to LinkTicketByCustomerIdService and returns result

Test command: bundle exec rspec spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: Template is app/workers/contacts/link_deal_by_customer_id_worker.rb (48 lines). In sidekiq_retries_exhausted, args[1] is crm_deal_id in the deal worker — rename to ticket_id. Update reference_object_type: 'Ticket' and reference_object_id: ticket_id.

Task 1 — Write failing specs

Create spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb mirroring link_deal_by_customer_id_worker_spec.rb. Cover: happy path; :person_not_foundPersonNotFoundError; exhausted → Crm::Log.

Run — expect FAIL: bundle exec rspec spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb

Task 2 — Create worker file

Copy link_deal_by_customer_id_worker.rblink_ticket_by_customer_id_worker.rb. Apply substitutions:

  • Class name: LinkTicketByCustomerIdWorker
  • crm_deal_id = args[1]ticket_id = args[1]
  • reference_object_type: 'Crm::Deal'reference_object_type: 'Ticket'
  • reference_object_id: crm_deal_idreference_object_id: ticket_id
  • type_of_api string → 'Background Job - Contacts::LinkTicketByCustomerIdWorker'
  • perform: call Contacts::LinkTicketByCustomerIdService.new(team_id:, ticket_id:, customer_id:, audit_user_id:).call

Task 3 — Run all (expect PASS) + Linter

bundle exec rspec spec/workers/contacts/link_ticket_by_customer_id_worker_spec.rb --format documentation
bundle exec rubocop app/workers/contacts/link_ticket_by_customer_id_worker.rb

Acceptance criteria

  • Queue: :contact, retry: 3
  • perform delegates to Contacts::LinkTicketByCustomerIdService
  • :person_not_found → raises PersonNotFoundError
  • Exhausted retries → Crm::Log entry with reference_object_type: 'Ticket'

Effort estimate

0.5 day — 48-line copy-edit; 4 string substitutions; spec mirrors existing.

Depends on

  • C2LinkTicketByCustomerIdService must exist before worker can reference it

Task C4: [BE] v4 Ticket — creator_flag + Timeline Note + customer_ids (CDTC-S01, CDTC-S04, CDTC-S06)

A bot or AI agent that creates a ticket via the v4 API will have data_source set, show "Bot"/"Agentic AI" in the timeline, optionally get a Crm::HubChannelTicketV2 chat note, and have contacts associated via the new worker.

Status: ✅ Actionable (depends on C2 + C3)

Purpose

Modifies app/controllers/api/v4/tickets.rb to accept creator_flag on ticket creation. The ticket create handler at lines 57-74 delegates to Ticket::Create service inside Audited.audit_class.as_user(@current_user) — this block is wrapped with as_user(audited_actor) instead. A new create_ticket_notes private helper creates Crm::HubChannelTicketV2 (distinct from the deal's Crm::HubChannelTicket) with nil guard, dedup, and rescue, so note failure never blocks the ticket save.

Scope

  • Same creator_flag validation + audited_actor swap as C1
  • ticket_par[:data_source] = creator_flag before Ticket::Create call
  • New create_ticket_notes(params, ticket_id) private helper (full spec in RFC ADR-2):
    • Guard: skip if crm_note_type unresolvable
    • Dedup: Crm::HubChannelTicketV2.exists?(ticket_id:, team_id:)
    • Rescue StandardErrorRails.logger.warn (non-blocking)
  • customer_ids async/sync branch + LinkTicketByCustomerIdWorker dispatch

Files modified:

  • app/controllers/api/v4/tickets.rb
  • spec/controllers/api/v4/tickets_spec.rb

Expected Outcome

  • creator_flag: 'bot'ticket.data_source == 'bot', audits.username == 'bot'
  • creator_flag: 'invalid' → 422, record not created
  • room_id + creator_flag + resolvable crm_note_typeCrm::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: trueLinkTicketByCustomerIdWorker enqueued
  • Note creation failure → ticket save succeeds, WARN logged
  • Existing ticket creates unaffected

Test command: bundle exec rspec spec/controllers/api/v4/tickets_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: tickets.rb:65 wraps Ticket::Create.new(...).call in as_user(@current_user). The data_source must be set on ticket_par before the Ticket::Create call, not after. create_ticket_notes is called after the Ticket::Create returns the ticket object — use ticket[:ticket].id for the ticket_id argument.

Task 1 — Write failing specs

In spec/controllers/api/v4/tickets_spec.rb, add describe 'POST / with creator_flag' block mirroring C1. Add additional cases: nil crm_note_type → note skipped; dedup → note skipped; rescue → ticket saves.

Run — expect FAIL: bundle exec rspec spec/controllers/api/v4/tickets_spec.rb

Task 2 — Add validation + pre-save branch

Same as C1 Task 2 — insert creator_flag validation and customer_ids_for_worker branch before the as_user block.

Task 3 — Swap as_user actor + set data_source

At line 65, replace actor. Set ticket_par[:data_source] = params[:creator_flag] if params[:creator_flag].present? before Ticket::Create.new(parameters: ticket_par, ...).

Task 4 — Add create_ticket_notes private helper

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)
return unless note_type.present?
return if Crm::HubChannelTicketV2.exists?(ticket_id: ticket_id, team_id: @current_user.team_id)
Crm::HubChannelTicketV2.create!({
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
})
rescue StandardError => e
Rails.logger.warn("[create_ticket_notes] failed ticket_id=#{ticket_id} error=#{e.message}")
end

Task 5 — Wire note + worker post-save

After Ticket::Create call inside the as_user block:

if params[:channel_integration_room_id].present? && params[:creator_flag].present?
create_ticket_notes(params, ticket[:ticket].id)
end
if customer_ids_for_worker.present?
customer_ids_for_worker.each do |customer_id|
::Contacts::LinkTicketByCustomerIdWorker.perform_async(
@current_user.team_id, ticket[:ticket].id, customer_id, @current_user.id
)
end
end

Task 6 — Run all (expect PASS) + Linter

bundle exec rspec spec/controllers/api/v4/tickets_spec.rb --format documentation
bundle exec rubocop app/controllers/api/v4/tickets.rb

Acceptance criteria

  • creator_flag: 'bot'data_source='bot', audits.username='bot'
  • creator_flag: 'invalid' → 422 generic
  • room_id + creator_flag + resolvable crm_note_typeCrm::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: trueLinkTicketByCustomerIdWorker enqueued
  • Note failure → ticket save succeeds, WARN logged

Effort estimate

2 days — existing spec extended; create_ticket_notes is new code but fully specced in RFC ADR-2; Ticket::Create wrapper makes as_user swap slightly more complex than deals; includes manual testing on staging.

Depends on

  • C2LinkTicketByCustomerIdService must exist
  • C3LinkTicketByCustomerIdWorker must exist

Task C5: [BE] v3.1 Deal — creator_flag + Timeline Note (CDTC-S01, CDTC-S04, CDTC-S06)

A bot or AI agent using the v3.1 deal endpoint gets the same data_source and timeline creator label as v4, with no change to the already-wired customer_ids flow.

Status: ✅ Actionable

Purpose

Ports the creator_flag validation, audited_actor swap, data_source assignment, and note-creation gate from C1 into app/controllers/api/v3dot1/deals.rb. The customer_ids_for_worker branch already exists at line 1354 — no change needed there. Touch points are narrow despite the file being 1,757 lines.

Scope

  • Same creator_flag validation as C1
  • audited_actor swap at line 1375 (as_user(@current_user))
  • crm_deal.data_source = params[:creator_flag] inside as_user block before crm_deal.save
  • Note-creation gate at line 1389 updated to same split condition as C1
  • No customer_ids changes — already present

Files modified:

  • app/controllers/api/v3dot1/deals.rb
  • spec/controllers/api/v3dot1/deals_spec.rb

Expected Outcome

  • creator_flag: 'bot'data_source='bot', audits.username='bot'
  • creator_flag: 'invalid' → 422 generic
  • room_id + creator_flagCrm::HubChannelTicket created
  • customer_ids behavior unchanged (already wired at line 1354)
  • All existing v3.1 deal spec examples pass

Test command: bundle exec rspec spec/controllers/api/v3dot1/deals_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: Open v3dot1/deals.rb:1354-1403. The customer_ids_for_worker branch at line 1354 is already present. The as_user(@current_user) block starts at line 1375 — insert creator_flag validation before it. create_notes is called at line 1389.

Task 1 — Write failing specs

Extend spec/controllers/api/v3dot1/deals_spec.rb with creator_flag cases mirroring C1 (omit customer_ids cases — already covered).

Run — expect FAIL: bundle exec rspec spec/controllers/api/v3dot1/deals_spec.rb

Task 2 — Apply C1 changes at v3.1 touch points

Follow C1 Tasks 2–4 verbatim, substituting v3.1 line numbers (validation before line 1375; actor swap at line 1375; data_source before crm_deal.save; note gate at line 1389).

Task 3 — Run all (expect PASS) + Linter

bundle exec rspec spec/controllers/api/v3dot1/deals_spec.rb --format documentation
bundle exec rubocop app/controllers/api/v3dot1/deals.rb

Acceptance criteria

  • creator_flag: 'bot'data_source='bot', audits.username='bot'
  • creator_flag: 'invalid' → 422 generic
  • room_id + creator_flagCrm::HubChannelTicket created
  • customer_ids behavior unchanged

Effort estimate

1 day — mechanical port of C1 changes into v3.1; customer_ids already present; includes manual testing on staging.

Depends on

  • None — can run after C1 is proven, in parallel with C4

Task C6: [BE] v3.1 Ticket — creator_flag + Timeline Note + customer_ids (CDTC-S01, CDTC-S04, CDTC-S06)

A bot or AI agent using the v3.1 ticket endpoint gets the same creator attribution, timeline note, and customer_ids association as v4.

Status: ✅ Actionable (depends on C2 + C3)

Purpose

Ports all C4 changes into app/controllers/api/v3dot1/tickets.rb. validate_and_convert_customer_ids already exists at line 127 — reuse it for the sync path. The create_ticket_notes helper is added identically to C4.

Scope

  • Same as C4 — creator_flag validation, audited_actor swap, data_source set, create_ticket_notes, worker dispatch
  • validate_and_convert_customer_ids already at v3dot1/tickets.rb:127 — reuse directly

Files modified:

  • app/controllers/api/v3dot1/tickets.rb
  • spec/controllers/api/v3dot1/tickets_spec.rb

Expected Outcome

  • Same as C4 for v3.1 ticket endpoint
  • All existing v3.1 ticket spec examples pass

Test command: bundle exec rspec spec/controllers/api/v3dot1/tickets_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: Open v3dot1/tickets.rb:56-74 (create) and :127-162 (validate_and_convert_customer_ids — already present, use it for sync path). The create_ticket_notes method is copy-pasted identically from C4 — no changes needed.

Task 1 — Write failing specs

Create spec/controllers/api/v3dot1/tickets_spec.rb cases mirroring C4.

Run — expect FAIL: bundle exec rspec spec/controllers/api/v3dot1/tickets_spec.rb

Task 2 — Apply C4 changes at v3.1 touch points

Follow C4 Tasks 2–5 verbatim. Use the existing validate_and_convert_customer_ids at line 127 for the sync customer_ids path — no duplication needed.

Task 3 — Run all (expect PASS) + Linter

bundle exec rspec spec/controllers/api/v3dot1/tickets_spec.rb --format documentation
bundle exec rubocop app/controllers/api/v3dot1/tickets.rb

Acceptance criteria

  • Same ACs as C4 for v3.1 endpoint

Effort estimate

1.5 days — same as C4 but validate_and_convert_customer_ids already exists; spec written from scratch; includes manual testing on staging.

Depends on

  • C2LinkTicketByCustomerIdService must exist
  • C3LinkTicketByCustomerIdWorker must exist

Task C7: [BE] Audit Model — Extend mapping_who (CDTC-S06)

The CRM timeline displays "Bot" or "Agentic AI" as the creator label for records created with creator_flag, without breaking existing system actor mappings.

Status: ✅ Actionable

Purpose

Extends audit.rb#mapping_who at line 1824 by inserting a new elsif branch before the existing ['central', 'hub', 'Qontak system'] check. When audits.username is 'bot' or 'agentic_ai' (set by as_user(creator_flag) in C1/C4/C5/C6), define_who is set to "Bot" or "Agentic AI" respectively. No schema change. No new table.

Scope

  • 3-line patch in audit.rb:1823-1829
  • 4 new spec contexts: 'bot', 'agentic_ai', 'hub' (regression), deleted user (regression)

Files modified:

  • app/models/audit.rb:1824
  • spec/models/audit_spec.rb

Expected Outcome

  • audits.username = 'bot'define_who = "Bot"
  • audits.username = 'agentic_ai'define_who = "Agentic AI"
  • audits.username = 'hub'define_who = "Qontak system" (unchanged)
  • audits.username = 'Qontak system'define_who = "Qontak system" (unchanged)
  • Deleted user with no matching username → "<deleted user>" (unchanged)

Test command: bundle exec rspec spec/models/audit_spec.rb --format documentation


Step-by-step Implementation Plan

Critical: The mapping_who method structure at audit.rb:1815-1833 — the else branch at line 1823 handles the case when user.present? is false. The new elsif must be inserted inside this else block, before the system-actor check. Wrong insertion point (e.g. after rescue) would silently skip the new mapping.

Task 1 — Write failing specs

In spec/models/audit_spec.rb, add to the existing describe Audit block:

describe '#mapping_who' do
let(:audit) { build(:audit, user: nil) }

context 'when username is bot' do
before { audit.username = 'bot' }
it { expect { audit.mapping_who }.to change { audit.define_who }.to('Bot') }
end
context 'when username is agentic_ai' do
before { audit.username = 'agentic_ai' }
it { expect { audit.mapping_who }.to change { audit.define_who }.to('Agentic AI') }
end
context 'when username is hub (regression)' do
before { audit.username = 'hub' }
it { expect { audit.mapping_who }.to change { audit.define_who }.to('Qontak system') }
end
context 'when username is unknown (regression)' do
before { audit.username = 'unknown_actor' }
it { expect { audit.mapping_who }.to change { audit.define_who }.to('<deleted user>') }
end
end

Run — expect FAIL: bundle exec rspec spec/models/audit_spec.rb

Task 2 — Apply patch

Replace audit.rb:1823-1829:

# BEFORE:
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

Task 3 — Run all (expect PASS) + Linter

bundle exec rspec spec/models/audit_spec.rb --format documentation
bundle exec rubocop app/models/audit.rb

Acceptance criteria

  • username='bot'define_who="Bot"
  • username='agentic_ai'define_who="Agentic AI"
  • username='hub'define_who="Qontak system" (regression)
  • username='Qontak system'define_who="Qontak system" (regression)
  • Unknown username → "<deleted user>" (regression)

Effort estimate

1 day — 3-line patch; touch point is exactly 7 lines in a 2,315-line file; spec is 4 small contexts; includes manual testing on staging to verify timeline labels.

Depends on

  • None — fully independent, can run in parallel with C1 and C2

Ordering rationale

  • C1 + C2 + C7 first (parallel) — all independent. C2 is on the critical path so must start day 1.
  • C3 after C2 — worker references service directly.
  • C4 + C5 after C3 — C4 needs C2 + C3; C5 has no customer_ids dependency but follows C1's pattern.
  • C6 last — same as C4 but v3.1; follows C4.
  • Critical path: C2 → C3 → C4 → C6.

Skipped stories

StoryReason
CDTC-S02 — Navigate room → detailN/A — FE scope (hub-chat); no BE work
NEG-01/02/03 — Guard railsCovered by creator_flag absent → unchanged behavior ACs in C1/C4/C5/C6