Task Breakdown: Phase 1 — Bot & AI Creation Parity (Backend)
RFC:
phase-1-bot-ai-backend-rfc.mdRepo:qontak.comMode: Vertical — one task per RFC chunk (C1–C7) Scope: All 7 chunks included
Effort Summary
| Task | Effort |
|---|---|
C1 — v4 deal: creator_flag + note + customer_ids | 2 days |
C2 — New LinkTicketByCustomerIdService | 1 day |
C3 — New LinkTicketByCustomerIdWorker | 0.5 day |
C4 — v4 ticket: creator_flag + note + customer_ids | 2 days |
C5 — v3.1 deal: creator_flag + note | 1 day |
C6 — v3.1 ticket: creator_flag + note + customer_ids | 1.5 days |
C7 — Audit model: extend mapping_who | 1 day |
| Total | ~9 days |
Confidence: high. Key assumptions: all spec files exist and are extended (not created from scratch);
LinkTicketByCustomerIdServiceand Worker are copy-paste mirrors with 3–4 identifier substitutions;audit.rbpatch 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_sourceset 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_flagparam ('bot'|'agentic_ai'); 422 on invalid value - Replace
as_user(@current_user)withas_user(audited_actor)whereaudited_actor = creator_flag.presence || @current_user - Set
crm_deal.data_source = creator_flaginside theas_userblock - Update note-creation gate: when
creator_flagpresent +room_idpresent → callcreate_notesregardless ofcrm_note_type - Pre-save
customer_idsasync/sync branch mirroringv3dot1/deals.rb:1354-1371 - Post-save: dispatch
LinkDealByCustomerIdWorkerpercustomer_idwhen async path
Files modified:
app/controllers/api/v4/deals.rbspec/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 createdroom_id+creator_flagpresent →Crm::HubChannelTicketcreated withaudits.username == creator_flagcustomer_ids+async_lead_assoc: true→LinkDealByCustomerIdWorkerenqueued percustomer_id- No
creator_flag→data_sourceunchanged, 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_flagpresent →Crm::HubChannelTicketcreated -
customer_ids+async_lead_assoc: true→LinkDealByCustomerIdWorkerenqueued percustomer_id -
customer_idssync path →validate_and_convert_customer_idscalled - 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_idspayload will have its contacts correctly associated, using the same idempotentfind_or_create_bypattern 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
Resultstruct 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 →
PeopleTicketcreated, 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_byreturns 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_deal → create_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.rb → link_ticket_by_customer_id_service.rb. Apply substitutions:
- Class name:
LinkTicketByCustomerIdService @crm_deal_id→@ticket_idCrm::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_foundreturn whenticket_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 →
PeopleTicketcreated viafind_or_create_by - Person not found →
:person_not_found - Invalid args →
:invalid_args - Ticket not found →
:ticket_not_found -
Audited.as_user(@audit_user)wrapsPeopleTicketcreate - 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 toCrm::Logwithreference_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: 3perform(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→ raisesPersonNotFoundError- Exhausted retries →
Crm::Logentry withreference_object_type: 'Ticket' - Happy path → delegates to
LinkTicketByCustomerIdServiceand 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_found → PersonNotFoundError; 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.rb → link_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_id→reference_object_id: ticket_idtype_of_apistring →'Background Job - Contacts::LinkTicketByCustomerIdWorker'perform: callContacts::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 -
performdelegates toContacts::LinkTicketByCustomerIdService -
:person_not_found→ raisesPersonNotFoundError - Exhausted retries →
Crm::Logentry withreference_object_type: 'Ticket'
Effort estimate
0.5 day — 48-line copy-edit; 4 string substitutions; spec mirrors existing.
Depends on
- C2 —
LinkTicketByCustomerIdServicemust 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_sourceset, show "Bot"/"Agentic AI" in the timeline, optionally get aCrm::HubChannelTicketV2chat 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_flagvalidation +audited_actorswap as C1 ticket_par[:data_source] = creator_flagbeforeTicket::Createcall- New
create_ticket_notes(params, ticket_id)private helper (full spec in RFC ADR-2):- Guard: skip if
crm_note_typeunresolvable - Dedup:
Crm::HubChannelTicketV2.exists?(ticket_id:, team_id:) - Rescue
StandardError→Rails.logger.warn(non-blocking)
- Guard: skip if
customer_idsasync/sync branch +LinkTicketByCustomerIdWorkerdispatch
Files modified:
app/controllers/api/v4/tickets.rbspec/controllers/api/v4/tickets_spec.rb
Expected Outcome
creator_flag: 'bot'→ticket.data_source == 'bot',audits.username == 'bot'creator_flag: 'invalid'→ 422, record not createdroom_id+creator_flag+ resolvablecrm_note_type→Crm::HubChannelTicketV2createdroom_id+creator_flag+ nilcrm_note_type→ note skipped silently- Duplicate
room_idfor same ticket → skipped (dedup) customer_ids+async_lead_assoc: true→LinkTicketByCustomerIdWorkerenqueued- 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+ resolvablecrm_note_type→Crm::HubChannelTicketV2created -
room_id+creator_flag+ nilcrm_note_type→ note skipped silently - Duplicate
room_idfor same ticket → skipped (dedup) -
customer_ids+async_lead_assoc: true→LinkTicketByCustomerIdWorkerenqueued - 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
- C2 —
LinkTicketByCustomerIdServicemust exist - C3 —
LinkTicketByCustomerIdWorkermust 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_sourceand timeline creator label as v4, with no change to the already-wiredcustomer_idsflow.
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_flagvalidation as C1 audited_actorswap at line 1375 (as_user(@current_user))crm_deal.data_source = params[:creator_flag]insideas_userblock beforecrm_deal.save- Note-creation gate at line 1389 updated to same split condition as C1
- No
customer_idschanges — already present
Files modified:
app/controllers/api/v3dot1/deals.rbspec/controllers/api/v3dot1/deals_spec.rb
Expected Outcome
creator_flag: 'bot'→data_source='bot',audits.username='bot'creator_flag: 'invalid'→ 422 genericroom_id+creator_flag→Crm::HubChannelTicketcreatedcustomer_idsbehavior 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_flag→Crm::HubChannelTicketcreated -
customer_idsbehavior 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_idsassociation 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_flagvalidation,audited_actorswap,data_sourceset,create_ticket_notes, worker dispatch validate_and_convert_customer_idsalready atv3dot1/tickets.rb:127— reuse directly
Files modified:
app/controllers/api/v3dot1/tickets.rbspec/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
- C2 —
LinkTicketByCustomerIdServicemust exist - C3 —
LinkTicketByCustomerIdWorkermust 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:1824spec/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_idsdependency but follows C1's pattern. - C6 last — same as C4 but v3.1; follows C4.
- Critical path: C2 → C3 → C4 → C6.
Skipped stories
| Story | Reason |
|---|---|
| CDTC-S02 — Navigate room → detail | N/A — FE scope (hub-chat); no BE work |
| NEG-01/02/03 — Guard rails | Covered by creator_flag absent → unchanged behavior ACs in C1/C4/C5/C6 |