Skip to main content

Task Breakdown — Company Token (Omnichannel side)

RFC: channel-integration-company-token.md (rev 4) Repos (verified locally): hub_core = /Users/mekari/chatpanel/hub_core · hub_service = /Users/mekari/chatpanel/hub_service Slicing: by §4.C execution chunks (backend). No FE tasks — the chat web UI is a separate FE RFC (§1 Out of Scope), so no design references apply. Test command: bundle exec rspec / bundle exec rubocop --no-color (hub_core bin/overcommit_run for the full gate); hub_service bundle exec rspec.

Effort Summary

Phase / AreaFE daysBE daysQA daysTotal
Task 1 — Eligibility service + activity-log helper11
Task 2 — Connect flow (eligibility + checked save + connected_at + timeouts)20.52.5
Task 3 — Interactor emit + builder credential-strip20.52.5
Task 4 — Regression + lint gates0.50.51
Grand total5.51.57

Confidence: high. All anchor files verified on disk; the RFC pins line-level targets and the connect branch already exists (pre-phase). Biggest movable: the ineligible-cohort check (REV-9) could add grandfather/migrate work before Stage-2 enablement, and the exact placement/shape of the log-helper PORO is the dev's call (doesn't move the estimate).


Task 1: [BE] Eligibility service + activity-log emit helper (CTOK-S01, CTOK-S09)

A connecting org is routed to the company-token branch by real eligibility (Qontak One / unified_app), and connect/disconnect outcomes are emitted as structured log events — with no new datastore.

Status: ✅ Actionable

What to build

Two small POROs under hub_core/app/apps/crm/services/: a stateless eligibility predicate wrapping the verified billing GetUnified service, and a thin CustomLogFormat wrapper that emits the §2.3 activity events (connect_succeeded / connect_failed / disconnected). No model, no repository, no migration.

Implementation Plan

ActionFileWhat changes
createhub_core/app/apps/crm/services/company_token_eligibility.rbcall(organization) → bool: `organization.unified_sso_id.present?
createhub_core/app/apps/crm/services/company_token_eligibility_spec.rbsso-org → true; unified_app-org → true; neither → false; billing-nil → false + warning
createhub_core/app/apps/crm/services/integration_activity_log.rb.emit(event_type:, organization_id:, actor_id: nil, reason: nil, billing_cid: nil, company_id: nil, duration_ms: nil) wrapping CustomLogFormat; never raises; no email/token in payload
createhub_core/app/apps/crm/services/integration_activity_log_spec.rbasserts each event_type emits with the §2.3 fields; rejects email/credential keys

Implementation steps

  1. Explore — open hub_core/app/apps/crm/services/moderator/apis.rb:10-17 for the CustomLogFormat.new(error:, message:, class_name:, method_name:, args:) shape, and app/apps/billings/services/v2/redis/subscriptions/get_unified.rb for the nil-on-failure contract the eligibility PORO depends on.
  2. Red — write company_token_eligibility_spec.rb and integration_activity_log_spec.rb covering the ACs; run and confirm they fail.
  3. Scaffold — create both service files with call / emit signatures, no logic.
  4. Implement — eligibility predicate (sso OR unified_app, nil→false+warning); log helper builds the structured args hash and calls CustomLogFormat.
  5. Greenbundle exec rspec spec/apps/crm/services/company_token_eligibility_spec.rb spec/apps/crm/services/integration_activity_log_spec.rb.
  6. Quality gatebundle exec rubocop --no-color.

Acceptance criteria

  • Eligibility: unified_sso_id present → true; unified_app true → true; neither → false; GetUnified nil → false + CustomLogFormat warning.
  • Log helper emits connect_succeeded / connect_failed / disconnected with event_type, organization_id, actor_id, reason, billing_cid, company_id, duration_ms.
  • Helper never raises; payload contains no actor_email, token, or credential.

Test strategy

Stub GetUnified to return {'unified_app'=>true}, {}, and nil; assert routing bool + the warning on nil. For the helper, spy on CustomLogFormat and assert the emitted args keys/values; assert no email/credential key present.

Effort estimate

DisciplineDays
Frontend
Backend1
QA
Total1

Assumptions: reuses verified GetUnified; helper wraps existing CustomLogFormat; internal-only (no user-facing behavior → QA 0).

Run to verify

cd /Users/mekari/chatpanel/hub_core && bundle exec rspec spec/apps/crm/services/company_token_eligibility_spec.rb spec/apps/crm/services/integration_activity_log_spec.rb && bundle exec rubocop --no-color

Depends on

  • None.

Task 2: [BE] Connect flow — eligibility + checked save + connected_at + timeouts (CTOK-S01, CTOK-S05-NEG)

An eligible org's connect goes through the company-token branch and can never silently report success on a failed persist; the connected date is recorded and CRM calls are bounded by a timeout.

Status: ✅ Actionable

What to build

Modify the existing Qontak::Create repository: swap the flag-only branch condition for flags && CompanyTokenEligibility, set settings[:connected_at], replace the silent save rescue nil with a checked save, and add timeout: 10 to the CRM calls.

Implementation Plan

ActionFileWhat changes
extendhub_core/app/core/domains/repositories/channel_integrations/qontak/create.rbL12 branch → flags && Crm::Services::CompanyTokenEligibility.new.call(organization); set settings[:connected_at] = Time.now.utc.iso8601 in the company-token branch; L152 integration.save rescue nilreturn failure error_messages_for(integration) unless integration.save (pattern apis.rb:119)
extendhub_core/app/apps/crm/services/team/apis.rbpass timeout: 10 on the teams/company_token (+ /refresh) POSTs
extendhub_core/app/core/domains/repositories/channel_integrations/qontak/create_spec.rbineligible→legacy; failed save→Failure (no success path); success→connected_at present; timeout kwarg asserted

Implementation steps

  1. Explore — re-read qontak/create.rb L12 (branch), L134-155 (create_or_restore_channel, the save rescue nil at L152), and apis.rb:119 for the error_messages_for checked-save precedent.
  2. Red — extend create_spec.rb: ineligible-org routes to legacy; a save-failure stub returns Failure and never hits the success builder; success sets connected_at.
  3. Implement — wire CompanyTokenEligibility at L12; set connected_at; replace L152 with the checked save; add timeout: 10 in team/apis.rb.
  4. Greenbundle exec rspec spec/core/domains/repositories/channel_integrations.
  5. Quality gatebundle exec rubocop --no-color.

Acceptance criteria

  • Ineligible org (no unified_sso_id, no unified_app) → legacy branch.
  • Failed integration.saveFailure with AR messages, no success path executed (closes the save rescue nil hole).
  • Successful company-token connect sets settings['connected_at'] (ISO-8601 UTC).
  • CRM teams/company_token calls carry timeout: 10.

Test strategy

Stub CompanyTokenEligibility both ways; stub the AR model so save returns false and assert the monad short-circuits to Failure before success Builders::…. Assert connected_at on the happy path and the timeout: 10 kwarg on the CRM call.

Effort estimate

DisciplineDays
Frontend
Backend2
QA0.5
Total2.5

Assumptions: company-token branch already exists (pre-phase); checked-save pattern is in-repo (apis.rb:119); no schema change.

Run to verify

cd /Users/mekari/chatpanel/hub_core && bundle exec rspec spec/core/domains/repositories/channel_integrations && bundle exec rubocop --no-color

Depends on

  • Task 1 (CompanyTokenEligibility).

Task 3: [BE] Interactor emit + builder credential-strip (CTOK-S05, CTOK-S07, CTOK-S09)

Connect/disconnect outcomes are logged as activity events with the acting admin's id, and the encrypted credentials blob stops leaking into qontak integration API responses.

Status: ✅ Actionable

What to build

Wire the Task-1 log helper into the connect/disconnect interactors (emit at the interactor layer, actor kept out of the repo params), scope disconnect logging to qontak via target_channel, and add settings.delete('credentials') in the qontak builder branch. Pass me.id from hub_service.

Implementation Plan

ActionFileWhat changes
extendhub_core/app/core/domains/interactors/channel_integrations/user_connects_qontak.rboptional(:actor_id).maybe(:string); after repo call emit connect_succeeded(actor_id, billing_cid, company_id, duration_ms) / connect_failed(reason); do not pass actor into Qontak::Create
extendhub_core/app/core/domains/interactors/channel_integrations/user_disconnects_channel.rboptional(:actor_id); emit disconnected only when result.success.target_channel == 'qontak'
extendhub_core/app/core/domains/builders/channel_integration.rbqontak branch: settings.delete('credentials') (precedent: settings.delete('access_token') for meta_ad_account)
extendhub_service/app/services/api/core/v1/integrations/resources/integrations.rbconnect/disconnect blocks pass me.id as actor_id
extend*_spec.rb (both interactors + builder + hub_service request spec)emit assertions, credential-absence, qontak-scoping

Implementation steps

  1. Explore — open both interactors, builders/channel_integration.rb (find the settings.delete('access_token') line + settings: settings passthrough), and entities/channel_integration.rb (confirm target_channel).
  2. Red — specs: connect success emits connect_succeeded w/ billing_cid; failure emits connect_failed w/ reason; disconnect(qontak) emits disconnected, disconnect(telegram) emits none; uniq/connect response has no credentials key.
  3. Implement — add contract fields, call IntegrationActivityLog.emit(...), slice actor before repo delegation; add the builder delete; pass me.id in hub_service.
  4. Green — hub_core bundle exec rspec spec/core/domains/interactors/channel_integrations spec/core/domains/builders; hub_service bundle exec rspec spec/services/api/core/v1.
  5. Quality gatebundle exec rubocop --no-color.

Acceptance criteria

  • uniq/connect responses contain the four detail fields and no credentials key.
  • Connect success emits connect_succeeded with billing_cid + actor_id; failure emits connect_failed with reason.
  • Disconnect of a qontak channel emits disconnected with actor_id; a non-qontak disconnect emits nothing.
  • Qontak::Create params are unchanged — actor identity never enters the repo.

Test strategy

Spy on IntegrationActivityLog.emit; assert event_type + fields per path. Build the qontak entity and assert settings['credentials'] absent while a non-qontak channel keeps its settings. hub_service request spec asserts response shape + actor_id passed.

Effort estimate

DisciplineDays
Frontend
Backend2
QA0.5
Total2.5

Assumptions: builder stripping has an in-file precedent; disconnect entity exposes target_channel; logging is interactor-layer (closes REV-3).

Run to verify

cd /Users/mekari/chatpanel/hub_core && bundle exec rspec spec/core/domains/interactors/channel_integrations spec/core/domains/builders && \
cd /Users/mekari/chatpanel/hub_service && bundle exec rspec spec/services/api/core/v1

Depends on

  • Task 1 (log helper), Task 2 (connect flow provides billing_cid/duration_ms context).

Task 4: [BE] Regression + lint gates (CTOK-S06)

The existing CRM auto-create / owner / association / auto-resolve behavior is unchanged, and both repos pass their full suites.

Status: ✅ Actionable

What to build

No new code — run the full gates and fix any regressions surfaced by Tasks 1–3.

Implementation steps

  1. hub_core: bundle exec rubocop --no-color && bundle exec rspec (or bin/overcommit_run for lint+tests+Brakeman).
  2. hub_service: bundle exec rspec.
  3. Fix any failures; re-run until green.

Acceptance criteria

  • hub_core full suite + rubocop exit 0 (bin/overcommit_run green).
  • hub_service full suite exit 0.
  • CRM app suite (spec/apps/crm) shows no behavioral regression.

Test strategy

Whole-suite regression; the CRM app suite is the guard for CTOK-S06 "keep existing functions". QA does an exploratory pass on connect / disconnect / auto-create.

Effort estimate

DisciplineDays
Frontend
Backend0.5
QA0.5
Total1

Assumptions: pure gate/regression; QA is exploratory pass on connect/disconnect + auto-create.

Run to verify

cd /Users/mekari/chatpanel/hub_core && bin/overcommit_run && cd /Users/mekari/chatpanel/hub_service && bundle exec rspec

Depends on

  • Tasks 1–3.

Ordering rationale

  • Task 1 first — the eligibility predicate and log helper are pure, dependency-free, and unblock everything else.
  • Task 2 is the critical path — it carries the write-integrity fix (checked save) and the branch selection; Task 3's emit relies on the billing_cid/duration_ms it surfaces.
  • Task 3 depends on 1 + 2 — it's the observability + security-surface layer on top of a correct connect flow.
  • Task 4 last — gates the whole change; the CRM regression suite is the guard for the "keep existing functions" story.
  • External push: run the REV-9 cohort check (orgs flagged in :company_token_specific that lack unified_sso_id/unified_app) before enabling Stage 2 in production — it's a pre-merge query, not a coding task, but it gates rollout.

Skipped stories

StoryReason
CTOK-S02BE already shipped (username optional + me.email fallback); only the FE email-field removal remains → FE RFC
CTOK-S03Reporter/deal-owner is CRM-side + FE; chat BE endpoint (/qontak/crm/users) reused unchanged
CTOK-S04Auto-connect / system-connect endpoint descoped (rev 3, §1 Out of Scope); CRM half owned by CRM squad
CTOK-S08Menu rename — FE-only