feat(routes): expand chatwoot parity coverage

This commit is contained in:
2026-06-04 21:07:48 +08:00
parent 4c7ed4f5c3
commit de6a05e0e6
7 changed files with 512 additions and 28 deletions
+119 -1
View File
@@ -177,6 +177,124 @@ var criticalRoutes = []route{
{Method: "GET", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/", Controller: "api/v1/accounts/agent_capacity_policies#index", Source: "routes.rb:123"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/csat_survey_responses/", Controller: "api/v1/accounts/csat_survey_responses#index", Source: "routes.rb:219"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/csat_survey_responses/metrics", Controller: "api/v1/accounts/csat_survey_responses#metrics", Source: "routes.rb:221"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/preferences/", Controller: "api/v1/accounts/captain/preferences#show", Source: "routes.rb:63"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/preferences/", Controller: "api/v1/accounts/captain/preferences#update", Source: "routes.rb:63"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/", Controller: "api/v1/accounts/captain/assistants#index", Source: "routes.rb:64"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/assistants/", Controller: "api/v1/accounts/captain/assistants#create", Source: "routes.rb:64"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id", Controller: "api/v1/accounts/captain/assistants#show", Source: "routes.rb:64"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id", Controller: "api/v1/accounts/captain/assistants#update", Source: "routes.rb:64"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id", Controller: "api/v1/accounts/captain/assistants#destroy", Source: "routes.rb:64"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/playground", Controller: "api/v1/accounts/captain/assistants#playground", Source: "routes.rb:66"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/tools", Controller: "api/v1/accounts/captain/assistants#tools", Source: "routes.rb:69"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes", Controller: "api/v1/accounts/captain/assistants/inboxes#index", Source: "routes.rb:71"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes", Controller: "api/v1/accounts/captain/assistants/inboxes#create", Source: "routes.rb:71"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes/:inbox_id", Controller: "api/v1/accounts/captain/assistants/inboxes#destroy", Source: "routes.rb:71"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/", Controller: "api/v1/accounts/captain/assistants/scenarios#index", Source: "routes.rb:72"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/", Controller: "api/v1/accounts/captain/assistants/scenarios#create", Source: "routes.rb:72"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id", Controller: "api/v1/accounts/captain/assistants/scenarios#show", Source: "routes.rb:72"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id", Controller: "api/v1/accounts/captain/assistants/scenarios#update", Source: "routes.rb:72"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id", Controller: "api/v1/accounts/captain/assistants/scenarios#destroy", Source: "routes.rb:72"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistant_responses/", Controller: "api/v1/accounts/captain/assistant_responses#index", Source: "routes.rb:74"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/assistant_responses/", Controller: "api/v1/accounts/captain/assistant_responses#create", Source: "routes.rb:74"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/assistant_responses/:response_id", Controller: "api/v1/accounts/captain/assistant_responses#show", Source: "routes.rb:74"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/assistant_responses/:response_id", Controller: "api/v1/accounts/captain/assistant_responses#update", Source: "routes.rb:74"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/assistant_responses/:response_id", Controller: "api/v1/accounts/captain/assistant_responses#destroy", Source: "routes.rb:74"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/bulk_actions/", Controller: "api/v1/accounts/captain/bulk_actions#create", Source: "routes.rb:75"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/copilot_threads/", Controller: "api/v1/accounts/captain/copilot_threads#index", Source: "routes.rb:76"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/copilot_threads/", Controller: "api/v1/accounts/captain/copilot_threads#create", Source: "routes.rb:76"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/", Controller: "api/v1/accounts/captain/copilot_messages#index", Source: "routes.rb:77"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/", Controller: "api/v1/accounts/captain/copilot_messages#create", Source: "routes.rb:77"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/custom_tools/", Controller: "api/v1/accounts/captain/custom_tools#index", Source: "routes.rb:79"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/custom_tools/", Controller: "api/v1/accounts/captain/custom_tools#create", Source: "routes.rb:79"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/custom_tools/:tool_id", Controller: "api/v1/accounts/captain/custom_tools#show", Source: "routes.rb:79"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/custom_tools/:tool_id", Controller: "api/v1/accounts/captain/custom_tools#update", Source: "routes.rb:79"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/custom_tools/:tool_id", Controller: "api/v1/accounts/captain/custom_tools#destroy", Source: "routes.rb:79"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/custom_tools/test", Controller: "api/v1/accounts/captain/custom_tools#test", Source: "routes.rb:80"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/documents/", Controller: "api/v1/accounts/captain/documents#index", Source: "routes.rb:82"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/documents/", Controller: "api/v1/accounts/captain/documents#create", Source: "routes.rb:82"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/documents/:document_id", Controller: "api/v1/accounts/captain/documents#show", Source: "routes.rb:82"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/captain/documents/:document_id", Controller: "api/v1/accounts/captain/documents#destroy", Source: "routes.rb:82"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/documents/:document_id/sync", Controller: "api/v1/accounts/captain/documents#sync", Source: "routes.rb:83"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/tasks/rewrite", Controller: "api/v1/accounts/captain/tasks#rewrite", Source: "routes.rb:86"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/tasks/summarize", Controller: "api/v1/accounts/captain/tasks#summarize", Source: "routes.rb:87"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/tasks/reply_suggestion", Controller: "api/v1/accounts/captain/tasks#reply_suggestion", Source: "routes.rb:88"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/tasks/label_suggestion", Controller: "api/v1/accounts/captain/tasks#label_suggestion", Source: "routes.rb:89"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/captain/tasks/follow_up", Controller: "api/v1/accounts/captain/tasks#follow_up", Source: "routes.rb:90"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/assignment_policies/", Controller: "api/v1/accounts/assignment_policies#index", Source: "routes.rb:306"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/assignment_policies/", Controller: "api/v1/accounts/assignment_policies#create", Source: "routes.rb:306"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id", Controller: "api/v1/accounts/assignment_policies#show", Source: "routes.rb:306"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id", Controller: "api/v1/accounts/assignment_policies#update", Source: "routes.rb:306"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id", Controller: "api/v1/accounts/assignment_policies#destroy", Source: "routes.rb:306"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes", Controller: "api/v1/accounts/assignment_policies/inboxes#index", Source: "routes.rb:307"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes", Controller: "api/v1/accounts/assignment_policies/inboxes#create", Source: "routes.rb:307"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id", Controller: "api/v1/accounts/assignment_policies/inboxes#destroy", Source: "routes.rb:307"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/", Controller: "api/v1/accounts/inboxes/assignment_policy#show", Source: "routes.rb:311"},
{Method: "POST", Path: "/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/", Controller: "api/v1/accounts/inboxes/assignment_policy#create", Source: "routes.rb:311"},
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/", Controller: "api/v1/accounts/inboxes/assignment_policy#destroy", Source: "routes.rb:311"},
{Method: "POST", Path: "/api/v1/widget/direct_uploads", Controller: "api/v1/widget/direct_uploads#create", Source: "routes.rb:443"},
{Method: "POST", Path: "/api/v1/widget/config", Controller: "api/v1/widget/config#create", Source: "routes.rb:444"},
{Method: "GET", Path: "/api/v1/widget/campaigns", Controller: "api/v1/widget/campaigns#index", Source: "routes.rb:445"},
{Method: "POST", Path: "/api/v1/widget/events", Controller: "api/v1/widget/events#create", Source: "routes.rb:446"},
{Method: "GET", Path: "/api/v1/widget/messages", Controller: "api/v1/widget/messages#index", Source: "routes.rb:447"},
{Method: "POST", Path: "/api/v1/widget/messages", Controller: "api/v1/widget/messages#create", Source: "routes.rb:447"},
{Method: "PUT", Path: "/api/v1/widget/messages/:message_id", Controller: "api/v1/widget/messages#update", Source: "routes.rb:447"},
{Method: "GET", Path: "/api/v1/widget/conversations", Controller: "api/v1/widget/conversations#index", Source: "routes.rb:448"},
{Method: "POST", Path: "/api/v1/widget/conversations", Controller: "api/v1/widget/conversations#create", Source: "routes.rb:448"},
{Method: "POST", Path: "/api/v1/widget/conversations/destroy_custom_attributes", Controller: "api/v1/widget/conversations#destroy_custom_attributes", Source: "routes.rb:450"},
{Method: "POST", Path: "/api/v1/widget/conversations/set_custom_attributes", Controller: "api/v1/widget/conversations#set_custom_attributes", Source: "routes.rb:451"},
{Method: "POST", Path: "/api/v1/widget/conversations/update_last_seen", Controller: "api/v1/widget/conversations#update_last_seen", Source: "routes.rb:452"},
{Method: "POST", Path: "/api/v1/widget/conversations/toggle_typing", Controller: "api/v1/widget/conversations#toggle_typing", Source: "routes.rb:453"},
{Method: "POST", Path: "/api/v1/widget/conversations/transcript", Controller: "api/v1/widget/conversations#transcript", Source: "routes.rb:454"},
{Method: "GET", Path: "/api/v1/widget/conversations/toggle_status", Controller: "api/v1/widget/conversations#toggle_status", Source: "routes.rb:455"},
{Method: "GET", Path: "/api/v1/widget/contact", Controller: "api/v1/widget/contact#show", Source: "routes.rb:458"},
{Method: "PUT", Path: "/api/v1/widget/contact", Controller: "api/v1/widget/contact#update", Source: "routes.rb:458"},
{Method: "POST", Path: "/api/v1/widget/contact/destroy_custom_attributes", Controller: "api/v1/widget/contact#destroy_custom_attributes", Source: "routes.rb:460"},
{Method: "PATCH", Path: "/api/v1/widget/contact/set_user", Controller: "api/v1/widget/contact#set_user", Source: "routes.rb:461"},
{Method: "GET", Path: "/api/v1/widget/inbox_members", Controller: "api/v1/widget/inbox_members#index", Source: "routes.rb:463"},
{Method: "POST", Path: "/api/v1/widget/labels", Controller: "api/v1/widget/labels#create", Source: "routes.rb:464"},
{Method: "DELETE", Path: "/api/v1/widget/labels/:label_id", Controller: "api/v1/widget/labels#destroy", Source: "routes.rb:464"},
{Method: "POST", Path: "/api/v1/widget/integrations/dyte/add_participant_to_meeting", Controller: "api/v1/widget/integrations/dyte#add_participant_to_meeting", Source: "routes.rb:469"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts", Controller: "public/api/v1/inboxes/contacts#create", Source: "routes.rb:572"},
{Method: "GET", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id", Controller: "public/api/v1/inboxes/contacts#show", Source: "routes.rb:572"},
{Method: "PUT", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id", Controller: "public/api/v1/inboxes/contacts#update", Source: "routes.rb:572"},
{Method: "GET", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations", Controller: "public/api/v1/inboxes/conversations#index", Source: "routes.rb:573"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations", Controller: "public/api/v1/inboxes/conversations#create", Source: "routes.rb:573"},
{Method: "GET", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id", Controller: "public/api/v1/inboxes/conversations#show", Source: "routes.rb:573"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_status", Controller: "public/api/v1/inboxes/conversations#toggle_status", Source: "routes.rb:575"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing", Controller: "public/api/v1/inboxes/conversations#toggle_typing", Source: "routes.rb:576"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen", Controller: "public/api/v1/inboxes/conversations#update_last_seen", Source: "routes.rb:577"},
{Method: "GET", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages", Controller: "public/api/v1/inboxes/messages#index", Source: "routes.rb:580"},
{Method: "POST", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages", Controller: "public/api/v1/inboxes/messages#create", Source: "routes.rb:580"},
{Method: "PUT", Path: "/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id", Controller: "public/api/v1/inboxes/messages#update", Source: "routes.rb:580"},
{Method: "GET", Path: "/public/api/v1/csat_survey/:id", Controller: "public/api/v1/csat_survey#show", Source: "routes.rb:585"},
{Method: "PUT", Path: "/public/api/v1/csat_survey/:id", Controller: "public/api/v1/csat_survey#update", Source: "routes.rb:585"},
{Method: "POST", Path: "/api/v2/accounts/", Controller: "api/v2/accounts#create", Source: "routes.rb:478"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/summary_reports/agent", Controller: "api/v2/accounts/summary_reports#agent", Source: "routes.rb:481"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/summary_reports/team", Controller: "api/v2/accounts/summary_reports#team", Source: "routes.rb:482"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/summary_reports/inbox", Controller: "api/v2/accounts/summary_reports#inbox", Source: "routes.rb:483"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/summary_reports/label", Controller: "api/v2/accounts/summary_reports#label", Source: "routes.rb:484"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/summary_reports/channel", Controller: "api/v2/accounts/summary_reports#channel", Source: "routes.rb:485"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports", Controller: "api/v2/accounts/reports#index", Source: "routes.rb:488"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/summary", Controller: "api/v2/accounts/reports#summary", Source: "routes.rb:491"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/bot_summary", Controller: "api/v2/accounts/reports#bot_summary", Source: "routes.rb:492"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/agents", Controller: "api/v2/accounts/reports#agents", Source: "routes.rb:493"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/inboxes", Controller: "api/v2/accounts/reports#inboxes", Source: "routes.rb:494"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/labels", Controller: "api/v2/accounts/reports#labels", Source: "routes.rb:495"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/teams", Controller: "api/v2/accounts/reports#teams", Source: "routes.rb:496"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/conversations", Controller: "api/v2/accounts/reports#conversations", Source: "routes.rb:497"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/conversations_summary", Controller: "api/v2/accounts/reports#conversations_summary", Source: "routes.rb:498"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/conversation_traffic", Controller: "api/v2/accounts/reports#conversation_traffic", Source: "routes.rb:499"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/bot_metrics", Controller: "api/v2/accounts/reports#bot_metrics", Source: "routes.rb:500"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/inbox_label_matrix", Controller: "api/v2/accounts/reports#inbox_label_matrix", Source: "routes.rb:501"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/first_response_time_distribution", Controller: "api/v2/accounts/reports#first_response_time_distribution", Source: "routes.rb:502"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/reports/outgoing_messages_count", Controller: "api/v2/accounts/reports#outgoing_messages_count", Source: "routes.rb:503"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/live_reports/conversation_metrics", Controller: "api/v2/accounts/live_reports#conversation_metrics", Source: "routes.rb:508"},
{Method: "GET", Path: "/api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics", Controller: "api/v2/accounts/live_reports#grouped_conversation_metrics", Source: "routes.rb:509"},
}
func main() {
@@ -284,7 +402,7 @@ func writeParity(path string, gochatPath string, chatwootPath string, gochatRout
b.WriteString("Generated from:\n\n")
b.WriteString("- GoChat route dump: `" + gochatPath + "`\n")
b.WriteString("- Chatwoot route source: `" + chatwootPath + "`\n\n")
b.WriteString("This report covers tracked frontend-critical API v1 account routes from `reference/chatwoot/config/routes.rb`. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.\n\n")
b.WriteString("This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.\n\n")
b.WriteString(fmt.Sprintf("Summary: %d exact, %d method-compatible, %d parameter-compatible, %d missing out of %d tracked critical routes.\n\n", len(exact), len(methodCompatible), len(compatible), len(missing), len(criticalRoutes)))
b.WriteString("## Missing Critical Routes\n\n")
+28 -22
View File
@@ -17,9 +17,9 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc
## Current Baseline
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 723`.
- Route dump succeeds with `TOTAL: 791`.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
- First tracked frontend-critical API v1 route audit covers 138 Chatwoot routes: 138 exact, 0 method-compatible, 0 parameter-compatible, 0 missing.
- Tracked frontend-critical route audit covers 251 Chatwoot routes: 251 exact, 0 method-compatible, 0 parameter-compatible, 0 missing.
- Handler test stability fixes are committed into the baseline before feature parity work continues.
- `.codegraph/` is generated indexing output and is not part of tracked product code.
@@ -54,10 +54,10 @@ This is the ordered queue for the next implementation slices. Do not skip the ro
| Order | Work item | Primary phase | Done when | Status |
| --- | --- | --- | --- | --- |
| Q1 | Expand tracked route parity to Captain/Copilot, assignment policies, widget, public inbox/contact/conversation, public CSAT, v2 reports, summary reports, and live reports. | Phase 2 | `cmd/route_parity` tracks these groups and `docs/parity/route_parity.md` lists every missing/mismatched route. | Next |
| Q2 | Patch route aliases discovered by Q1, especially Chatwoot widget/public paths such as `/api/v1/widget/...` versus existing `/widget/...`. | Phase 2 | Missing tracked routes return to zero or are explicitly documented with implementation tasks. | Todo |
| Q3 | Add route boot regression coverage for wildcard conflict groups before expanding more Rails-style resources. | Phase 2 | Router tests cover nested dynamic resources that previously risked Gin conflicts. | Todo |
| Q4 | Start serializer parity fixtures for auth/session, conversations/messages, contacts/companies, inboxes, notifications, and search. | Phase 3 | Each area has at least one reference fixture and Go response test. | Todo |
| Q1 | Expand tracked route parity to Captain/Copilot, assignment policies, widget, public inbox/contact/conversation, public CSAT, v2 reports, summary reports, and live reports. | Phase 2 | `cmd/route_parity` tracks these groups and `docs/parity/route_parity.md` lists every missing/mismatched route. | Done |
| Q2 | Patch route aliases discovered by Q1, especially Chatwoot widget/public paths such as `/api/v1/widget/...` versus existing `/widget/...`. | Phase 2 | Missing tracked routes return to zero or are explicitly documented with implementation tasks. | Done |
| Q3 | Add route boot regression coverage for wildcard conflict groups before expanding more Rails-style resources. | Phase 2 | Router tests cover nested dynamic resources that previously risked Gin conflicts. | Done |
| Q4 | Start serializer parity fixtures for auth/session, conversations/messages, contacts/companies, inboxes, notifications, and search. | Phase 3 | Each area has at least one reference fixture and Go response test. | Next |
| Q5 | Review Meilisearch document shape and endpoint payloads against Chatwoot frontend consumers. | Phase 1 and Phase 3 | Search remains Meilisearch-first and payload mismatches are fixed or tracked. | Todo |
| Q6 | Burn down enterprise gaps in this order: SLA, assignment policy and capacity, CSAT, automation/macros, Audit, CustomRole, InboxLimit, Captain/Copilot. | Phase 4 and Phase 5 | Each feature passes route, persistence, auth, side-effect, response, and test checks. | Todo |
| Q7 | Add frontend smoke harness using the reused Chatwoot frontend once core API flows boot end-to-end. | Phase 7 | Login, inbox list, conversation list/detail, message send, contact view, and widget init run without frontend adapters. | Todo |
@@ -156,7 +156,7 @@ Checklist:
- [ ] Extend parity table with auth scope, request params, response serializer, and handler implementation status.
- [x] Patch first-batch route names and wildcard params where Gin constraints require different internal names, while preserving external URLs.
- [x] Add regression tests for the contact conversations relation route added in this slice.
- [ ] Expand route boot regression coverage for all Gin wildcard/param conflict groups.
- [x] Expand route boot regression coverage for tracked Gin wildcard/param conflict groups.
Acceptance:
@@ -173,20 +173,20 @@ Tracking table:
| P2.3 | Produce route parity table: method, path, controller/handler, auth, request params, serializer, status. | `docs/parity/route_parity.md` | Review |
| P2.4 | Mark frontend-critical gaps from Chatwoot web app route usage. | `docs/parity/route_parity.md` | Done |
| P2.5 | Convert existing placeholders/stubs into tracked feature tasks instead of hidden debt. | this doc and gap report | Doing |
| P2.6 | Add route boot regression tests for Gin wildcard/param conflicts. | router and handler tests | Doing |
| P2.7 | Track Captain/Copilot route group from `routes.rb:62-89`. | `cmd/route_parity`, `docs/parity/route_parity.md` | Next |
| P2.8 | Track assignment policies and inbox assignment policy routes from `routes.rb:306-313`. | `cmd/route_parity`, router | Next |
| P2.9 | Track widget API routes from `routes.rb:442-472`; keep `/api/v1/widget` as the Chatwoot-compatible surface. | `cmd/route_parity`, router widget mount | Next |
| P2.10 | Track public API routes from `routes.rb:569-585`, including public inbox contacts, conversations, messages, and CSAT survey. | `cmd/route_parity`, public handlers | Next |
| P2.11 | Track v2 reports, summary reports, and live reports from `routes.rb:479-513`. | `cmd/route_parity`, reports handlers | Next |
| P2.6 | Add route boot regression tests for Gin wildcard/param conflicts. | `internal/router/router_test.go` | Done |
| P2.7 | Track Captain/Copilot route group from `routes.rb:62-89`. | `cmd/route_parity`, `docs/parity/route_parity.md` | Done |
| P2.8 | Track assignment policies and inbox assignment policy routes from `routes.rb:306-313`. | `cmd/route_parity`, router | Done |
| P2.9 | Track widget API routes from `routes.rb:442-472`; keep `/api/v1/widget` as the Chatwoot-compatible surface. | `cmd/route_parity`, router widget mount | Done |
| P2.10 | Track public API routes from `routes.rb:569-585`, including public inbox contacts, conversations, messages, and CSAT survey. | `cmd/route_parity`, public handlers | Done |
| P2.11 | Track v2 reports, summary reports, and live reports from `routes.rb:479-513`. | `cmd/route_parity`, reports handlers | Done |
Current Phase 2 route findings:
| Type | Count | Required action |
| --- | --- | --- |
| Exact tracked critical routes | 138 | Keep covered while expanding audit scope. |
| Exact tracked critical routes | 251 | Keep covered while expanding audit scope. |
| Method-compatible update routes | 0 | First tracked batch now has exact Rails-compatible method coverage. |
| Missing tracked critical routes | 0 | First tracked frontend-critical batch has no route-level gaps. |
| Missing tracked critical routes | 0 | Current tracked frontend-critical route set has no route-level gaps. |
Expanded tracked groups now covered by route parity:
@@ -199,16 +199,21 @@ Expanded tracked groups now covered by route parity:
| Notifications | account-scoped notifications, notification_settings, unread_count, read_all, destroy_all, snooze, unread. |
| Inbox members | Chatwoot account-scoped `inbox_members` create/show/update/destroy routes. |
| Inbox member actions | agent_bot, set_agent_bot, sync_templates, health, register_webhook, reset_secret, avatar. |
| Captain/Copilot | Assistants, assistant inboxes/scenarios, assistant responses, bulk actions, copilot threads/messages, custom tools, documents, preferences, and tasks. |
| Assignment policies | Account assignment policies, nested inbox bindings, and inbox assignment policy routes. |
| Widget API | Chatwoot `/api/v1/widget/*` route surface plus legacy `/widget/*` compatibility. |
| Public API | Public inbox contact/conversation/message routes and public CSAT survey route. |
| Reports v2 | `/api/v2/accounts/:account_id` summary reports, reports, and live reports. |
Next route groups to track:
New route groups tracked in this slice:
| Area | Chatwoot source | Current Go surface observed | Expected action |
| --- | --- | --- | --- |
| Captain/Copilot | `reference/chatwoot/config/routes.rb:62-89` | Many `/api/v1/accounts/:account_id/captain/...` routes already exist. | Add to tracked critical set, then fix param/method drift. |
| Assignment policies | `reference/chatwoot/config/routes.rb:306-313` | `/assignment_policies` and local `/assignment_policies_v2` routes exist. | Track Chatwoot V2 route shape and inbox nested routes. |
| Widget API | `reference/chatwoot/config/routes.rb:442-472` | GoChat currently exposes many routes under `/widget`. | Add `/api/v1/widget` aliases where missing and keep legacy `/widget` only as compatibility. |
| Public API | `reference/chatwoot/config/routes.rb:569-585` | GoChat currently has public CSAT conversation routes. | Add/track public inbox contact, conversation, message, typing, status, and last-seen routes. |
| Reports v2 | `reference/chatwoot/config/routes.rb:479-513` | GoChat has report-style routes under `/api/v1/accounts`. | Track exact v2 `/api/v2/accounts/:account_id/...` report paths and payloads. |
| Captain/Copilot | `reference/chatwoot/config/routes.rb:62-89` | Many `/api/v1/accounts/:account_id/captain/...` routes already existed. | Added tracked critical set; route parity is exact. |
| Assignment policies | `reference/chatwoot/config/routes.rb:306-313` | `/assignment_policies` and local `/assignment_policies_v2` routes exist. | Added tracked route set and aligned singular inbox assignment delete route. |
| Widget API | `reference/chatwoot/config/routes.rb:442-472` | GoChat exposed many routes only under `/widget`. | Added `/api/v1/widget` aliases; behavior parity remains tracked under Phase 3/6. |
| Public API | `reference/chatwoot/config/routes.rb:569-585` | GoChat had public CSAT conversation routes. | Added public inbox/contact/conversation/message route surface and Chatwoot public CSAT path. |
| Reports v2 | `reference/chatwoot/config/routes.rb:479-513` | GoChat had report-style routes under `/api/v1/accounts`. | Added exact v2 `/api/v2/accounts/:account_id/...` report paths. |
Closed tracked critical route gaps in this slice:
@@ -396,7 +401,7 @@ Tracking table:
| P6.3 | Conversation APIs | `docs/ROUTE_GAP_ANALYSIS.md`, conversation handlers/services | Implement frontend-critical filters, assignment, status, snooze, merge, bulk actions. | Todo |
| P6.4 | Message APIs | `docs/ROUTE_GAP_ANALYSIS.md`, message handlers/services | Implement create/list/delete, private notes, attachments, source attribution, events. | Todo |
| P6.5 | Inbox APIs | `docs/ROUTE_GAP_ANALYSIS.md`, inbox handlers/services | Implement CRUD, assignable agents, avatar, campaigns, channel settings, reset secret. | Todo |
| P6.6 | Widget APIs | `docs/ROUTE_GAP_ANALYSIS.md`, widget/channel provider code | Implement init, contact, conversations, messages, cable token with Chatwoot widget contracts. | Todo |
| P6.6 | Widget/public APIs | `docs/ROUTE_GAP_ANALYSIS.md`, widget/channel provider code, `chatwootParityStub` routes | Implement init/config, campaigns, events, contact, conversations, messages, cable token, direct uploads, public inbox flows, and public CSAT with Chatwoot contracts. | Todo |
| P6.7 | Webhook ingress | `internal/router/router.go`, `internal/handler/webhook/*`, channel providers | Replace generic placeholder with provider-specific verified ingestion and dispatch. | Todo |
## Phase 7: Verification Harness
@@ -456,3 +461,4 @@ Verification milestone gates:
- 2026-06-04: Phase 2 route parity tracking added. Ruby/Bundler are unavailable in this workspace, so `cmd/route_parity` records static Chatwoot route DSL declarations from `reference/chatwoot/config/routes.rb`, consumes `cmd/dump_routes` output, and writes `docs/parity/route_parity.md`. First tracked critical route summary: 76 exact, 2 method-compatible, 0 parameter-compatible, 3 missing out of 81; GoChat route dump remains `TOTAL: 704`.
- 2026-06-04: First tracked Phase 2 route gaps closed. Added Chatwoot-compatible contact conversations, contact export POST, conversation `toggle_priority`, and PUT aliases for conversation/message updates; fixed contact handlers to accept `:account_id` as well as legacy `:id`. Regenerated parity report: 81 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 81; route dump now reports `TOTAL: 709`.
- 2026-06-04: Expanded Phase 2 route parity from 81 to 138 tracked frontend-critical account routes. Added account-scoped notification routes, `notification_settings` PUT alias, and Chatwoot account-level `inbox_members` create/show/update/destroy handlers. Regenerated parity report: 138 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 138; route dump now reports `TOTAL: 723`.
- 2026-06-04: Expanded Phase 2 route parity from 138 to 251 tracked frontend-critical routes. Added tracking and route-level coverage for Captain/Copilot, assignment policies, `/api/v1/widget`, public inbox/contact/conversation/message APIs, public CSAT survey, and `/api/v2` reports. Regenerated parity report: 251 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 251; route dump now reports `TOTAL: 791`. Added router boot regression coverage for Captain static/dynamic routes, widget collection routes, public nested message routes, and v2 reports.
+69 -1
View File
@@ -50,6 +50,7 @@ DELETE /api/v1/accounts/:account_id/google_channels/:google_id
DELETE /api/v1/accounts/:account_id/hooks/:id
DELETE /api/v1/accounts/:account_id/inbox_members/
DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id
DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/
DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/:policy_id
DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy_v2/
DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id/avatar
@@ -99,6 +100,7 @@ DELETE /api/v1/notifications/destroy_all
DELETE /api/v1/profile/avatar
DELETE /api/v1/profile/mfa/
DELETE /api/v1/push_subscriptions/:id
DELETE /api/v1/widget/labels/:label_id
DELETE /platform/api/v1/accounts/:account_id
DELETE /platform/api/v1/accounts/:account_id/account_users/:user_id
DELETE /platform/api/v1/agent_bots/:id
@@ -362,6 +364,33 @@ GET /api/v1/saml/slo
GET /api/v1/sso_sessions/:session_id
GET /api/v1/sso_sessions/user/:user_id
GET /api/v1/sso_sessions/user/:user_id/count
GET /api/v1/widget/campaigns
GET /api/v1/widget/contact
GET /api/v1/widget/conversations
GET /api/v1/widget/conversations/toggle_status
GET /api/v1/widget/inbox_members
GET /api/v1/widget/messages
GET /api/v2/accounts/:account_id/live_reports/conversation_metrics
GET /api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics
GET /api/v2/accounts/:account_id/reports
GET /api/v2/accounts/:account_id/reports/agents
GET /api/v2/accounts/:account_id/reports/bot_metrics
GET /api/v2/accounts/:account_id/reports/bot_summary
GET /api/v2/accounts/:account_id/reports/conversation_traffic
GET /api/v2/accounts/:account_id/reports/conversations
GET /api/v2/accounts/:account_id/reports/conversations_summary
GET /api/v2/accounts/:account_id/reports/first_response_time_distribution
GET /api/v2/accounts/:account_id/reports/inbox_label_matrix
GET /api/v2/accounts/:account_id/reports/inboxes
GET /api/v2/accounts/:account_id/reports/labels
GET /api/v2/accounts/:account_id/reports/outgoing_messages_count
GET /api/v2/accounts/:account_id/reports/summary
GET /api/v2/accounts/:account_id/reports/teams
GET /api/v2/accounts/:account_id/summary_reports/agent
GET /api/v2/accounts/:account_id/summary_reports/channel
GET /api/v2/accounts/:account_id/summary_reports/inbox
GET /api/v2/accounts/:account_id/summary_reports/label
GET /api/v2/accounts/:account_id/summary_reports/team
GET /cable
GET /health
GET /platform/api/v1/accounts
@@ -385,6 +414,12 @@ GET /platform/api/v1/users/:id/token
GET /platform/api/v1/widget_tests/
GET /platform/api/v1/widget_tests/:type
GET /public/api/v1/conversations/:conversation_uuid/csats
GET /public/api/v1/csat_survey/:id
GET /public/api/v1/inboxes/:inbox_id
GET /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
GET /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations
GET /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id
GET /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages
GET /webhooks/:channel_type/:identifier
GET /widget/cable_token
GET /widget/conversations
@@ -422,8 +457,14 @@ PATCH /api/v1/accounts/:account_id/integrations/slack/
PATCH /api/v1/accounts/:account_id/notification_settings/
PATCH /api/v1/accounts/:account_id/notifications/:notification_id
PATCH /api/v1/accounts/:account_id/teams/:team_id/team_members/
PATCH /api/v1/widget/contact
PATCH /api/v1/widget/contact/set_user
PATCH /api/v1/widget/messages/:message_id
PATCH /platform/api/v1/accounts/:account_id
PATCH /platform/api/v1/users/:id
PATCH /public/api/v1/csat_survey/:id
PATCH /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
PATCH /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id
PATCH /webhooks/:channel_type/:identifier
PATCH /widget/contact
POST /api/v1/accounts/
@@ -477,6 +518,8 @@ POST /api/v1/accounts/:account_id/captain/documents/
POST /api/v1/accounts/:account_id/captain/documents/:document_id/sync
POST /api/v1/accounts/:account_id/captain/preferences/
POST /api/v1/accounts/:account_id/captain/scenarios/
POST /api/v1/accounts/:account_id/captain/tasks/follow_up
POST /api/v1/accounts/:account_id/captain/tasks/label_suggestion
POST /api/v1/accounts/:account_id/captain/tasks/reply_suggestion
POST /api/v1/accounts/:account_id/captain/tasks/reply_suggestion/stream
POST /api/v1/accounts/:account_id/captain/tasks/rewrite
@@ -631,6 +674,20 @@ POST /api/v1/saml/acs
POST /api/v1/saml/slo
POST /api/v1/sso_sessions/:session_id/terminate
POST /api/v1/sso_sessions/user/:user_id/terminate_all
POST /api/v1/widget/config
POST /api/v1/widget/contact/destroy_custom_attributes
POST /api/v1/widget/conversations
POST /api/v1/widget/conversations/destroy_custom_attributes
POST /api/v1/widget/conversations/set_custom_attributes
POST /api/v1/widget/conversations/toggle_typing
POST /api/v1/widget/conversations/transcript
POST /api/v1/widget/conversations/update_last_seen
POST /api/v1/widget/direct_uploads
POST /api/v1/widget/events
POST /api/v1/widget/integrations/dyte/add_participant_to_meeting
POST /api/v1/widget/labels
POST /api/v1/widget/messages
POST /api/v2/accounts/
POST /platform/api/v1/accounts
POST /platform/api/v1/accounts/:account_id/account_users
POST /platform/api/v1/agent_bots
@@ -647,6 +704,12 @@ POST /platform/api/v1/users
POST /platform/api/v1/users/:id/login
POST /platform/api/v1/users/:id/token
POST /public/api/v1/conversations/:conversation_uuid/csats
POST /public/api/v1/inboxes/:inbox_id/contacts
POST /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations
POST /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages
POST /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_status
POST /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing
POST /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen
POST /webhooks/:channel_type/:identifier
POST /widget/conversations/:id/toggle_typing
POST /widget/direct_uploads
@@ -714,11 +777,16 @@ PUT /api/v1/oidc/config
PUT /api/v1/profile
PUT /api/v1/profile/avatar
PUT /api/v1/profile/set_active_account
PUT /api/v1/widget/contact
PUT /api/v1/widget/messages/:message_id
PUT /platform/api/v1/agent_bots/:id
PUT /platform/api/v1/agent_bots/:id/avatar
PUT /platform/api/v1/apps/:id
PUT /platform/api/v1/banners/:id
PUT /platform/api/v1/installation_configs/:id
PUT /public/api/v1/csat_survey/:id
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id
PUT /webhooks/:channel_type/:identifier
TRACE /webhooks/:channel_type/:identifier
TOTAL: 723
TOTAL: 791
+115 -2
View File
@@ -5,9 +5,9 @@ Generated from:
- GoChat route dump: `docs/parity/gochat_routes.txt`
- Chatwoot route source: `reference/chatwoot/config/routes.rb`
This report covers tracked frontend-critical API v1 account routes from `reference/chatwoot/config/routes.rb`. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.
This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.
Summary: 138 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 138 tracked critical routes.
Summary: 251 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 251 tracked critical routes.
## Missing Critical Routes
@@ -36,8 +36,16 @@ These routes exist with equivalent method and path shape but different parameter
| Method | Chatwoot Path | GoChat Match | Controller | Source | Status |
| --- | --- | --- | --- | --- | --- |
| DELETE | `/api/v1/accounts/:account_id/agents/:agent_id` | `/api/v1/accounts/:account_id/agents/:agent_id` | `api/v1/accounts/agents#destroy` | `routes.rb:59` | exact |
| DELETE | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `api/v1/accounts/assignment_policies#destroy` | `routes.rb:306` | exact |
| DELETE | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id` | `api/v1/accounts/assignment_policies/inboxes#destroy` | `routes.rb:307` | exact |
| DELETE | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `api/v1/accounts/automation_rules#destroy` | `routes.rb:115` | exact |
| DELETE | `/api/v1/accounts/:account_id/canned_responses/:id` | `/api/v1/accounts/:account_id/canned_responses/:id` | `api/v1/accounts/canned_responses#destroy` | `routes.rb:114` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `api/v1/accounts/captain/assistant_responses#destroy` | `routes.rb:74` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `api/v1/accounts/captain/assistants#destroy` | `routes.rb:64` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes/:inbox_id` | `api/v1/accounts/captain/assistants/inboxes#destroy` | `routes.rb:71` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `api/v1/accounts/captain/assistants/scenarios#destroy` | `routes.rb:72` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `api/v1/accounts/captain/custom_tools#destroy` | `routes.rb:79` | exact |
| DELETE | `/api/v1/accounts/:account_id/captain/documents/:document_id` | `/api/v1/accounts/:account_id/captain/documents/:document_id` | `api/v1/accounts/captain/documents#destroy` | `routes.rb:82` | exact |
| DELETE | `/api/v1/accounts/:account_id/companies/:company_id` | `/api/v1/accounts/:account_id/companies/:company_id` | `api/v1/accounts/companies#destroy` | `routes.rb:179` | exact |
| DELETE | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `api/v1/accounts/companies/contacts#destroy` | `routes.rb:188` | exact |
| DELETE | `/api/v1/accounts/:account_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/contacts/:contact_id` | `api/v1/accounts/contacts#destroy` | `routes.rb:197` | exact |
@@ -47,6 +55,7 @@ These routes exist with equivalent method and path shape but different parameter
| DELETE | `/api/v1/accounts/:account_id/custom_filters/:id` | `/api/v1/accounts/:account_id/custom_filters/:id` | `api/v1/accounts/custom_filters#destroy` | `routes.rb:251` | exact |
| DELETE | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#destroy` | `routes.rb:277` | exact |
| DELETE | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#destroy` | `routes.rb:252` | exact |
| DELETE | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `api/v1/accounts/inboxes/assignment_policy#destroy` | `routes.rb:311` | exact |
| DELETE | `/api/v1/accounts/:account_id/inboxes/:inbox_id/avatar` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/avatar` | `api/v1/accounts/inboxes#avatar` | `routes.rb:257` | exact |
| DELETE | `/api/v1/accounts/:account_id/labels/:tag_id` | `/api/v1/accounts/:account_id/labels/:tag_id` | `api/v1/accounts/labels#destroy` | `routes.rb:281` | exact |
| DELETE | `/api/v1/accounts/:account_id/macros/:macro_id` | `/api/v1/accounts/:account_id/macros/:macro_id` | `api/v1/accounts/macros#destroy` | `routes.rb:118` | exact |
@@ -54,15 +63,34 @@ These routes exist with equivalent method and path shape but different parameter
| DELETE | `/api/v1/accounts/:account_id/sla_policies/:id` | `/api/v1/accounts/:account_id/sla_policies/:id` | `api/v1/accounts/sla_policies#destroy` | `routes.rb:121` | exact |
| DELETE | `/api/v1/accounts/:account_id/teams/:team_id` | `/api/v1/accounts/:account_id/teams/:team_id` | `api/v1/accounts/teams#destroy` | `routes.rb:296` | exact |
| DELETE | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#destroy` | `routes.rb:299` | exact |
| DELETE | `/api/v1/widget/labels/:label_id` | `/api/v1/widget/labels/:label_id` | `api/v1/widget/labels#destroy` | `routes.rb:464` | exact |
| GET | `/api/v1/accounts/:account_id` | `/api/v1/accounts/:account_id` | `api/v1/accounts#show` | `routes.rb:47` | exact |
| GET | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `api/v1/accounts/agent_capacity_policies#index` | `routes.rb:123` | exact |
| GET | `/api/v1/accounts/:account_id/agents` | `/api/v1/accounts/:account_id/agents` | `api/v1/accounts/agents#index` | `routes.rb:59` | exact |
| GET | `/api/v1/accounts/:account_id/assignable_agents` | `/api/v1/accounts/:account_id/assignable_agents` | `api/v1/accounts/assignable_agents#index` | `routes.rb:104` | exact |
| GET | `/api/v1/accounts/:account_id/assignment_policies/` | `/api/v1/accounts/:account_id/assignment_policies/` | `api/v1/accounts/assignment_policies#index` | `routes.rb:306` | exact |
| GET | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `api/v1/accounts/assignment_policies#show` | `routes.rb:306` | exact |
| GET | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes` | `api/v1/accounts/assignment_policies/inboxes#index` | `routes.rb:307` | exact |
| GET | `/api/v1/accounts/:account_id/audit_logs/` | `/api/v1/accounts/:account_id/audit_logs/` | `api/v1/accounts/audit_logs#show` | `routes.rb:105` | exact |
| GET | `/api/v1/accounts/:account_id/automation_rules/` | `/api/v1/accounts/:account_id/automation_rules/` | `api/v1/accounts/automation_rules#index` | `routes.rb:115` | exact |
| GET | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `api/v1/accounts/automation_rules#show` | `routes.rb:115` | exact |
| GET | `/api/v1/accounts/:account_id/cache_keys` | `/api/v1/accounts/:account_id/cache_keys` | `api/v1/accounts#cache_keys` | `routes.rb:50` | exact |
| GET | `/api/v1/accounts/:account_id/canned_responses/` | `/api/v1/accounts/:account_id/canned_responses/` | `api/v1/accounts/canned_responses#index` | `routes.rb:114` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistant_responses/` | `/api/v1/accounts/:account_id/captain/assistant_responses/` | `api/v1/accounts/captain/assistant_responses#index` | `routes.rb:74` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `api/v1/accounts/captain/assistant_responses#show` | `routes.rb:74` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/` | `/api/v1/accounts/:account_id/captain/assistants/` | `api/v1/accounts/captain/assistants#index` | `routes.rb:64` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `api/v1/accounts/captain/assistants#show` | `routes.rb:64` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes` | `api/v1/accounts/captain/assistants/inboxes#index` | `routes.rb:71` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/` | `api/v1/accounts/captain/assistants/scenarios#index` | `routes.rb:72` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `api/v1/accounts/captain/assistants/scenarios#show` | `routes.rb:72` | exact |
| GET | `/api/v1/accounts/:account_id/captain/assistants/tools` | `/api/v1/accounts/:account_id/captain/assistants/tools` | `api/v1/accounts/captain/assistants#tools` | `routes.rb:69` | exact |
| GET | `/api/v1/accounts/:account_id/captain/copilot_threads/` | `/api/v1/accounts/:account_id/captain/copilot_threads/` | `api/v1/accounts/captain/copilot_threads#index` | `routes.rb:76` | exact |
| GET | `/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/` | `/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/` | `api/v1/accounts/captain/copilot_messages#index` | `routes.rb:77` | exact |
| GET | `/api/v1/accounts/:account_id/captain/custom_tools/` | `/api/v1/accounts/:account_id/captain/custom_tools/` | `api/v1/accounts/captain/custom_tools#index` | `routes.rb:79` | exact |
| GET | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `api/v1/accounts/captain/custom_tools#show` | `routes.rb:79` | exact |
| GET | `/api/v1/accounts/:account_id/captain/documents/` | `/api/v1/accounts/:account_id/captain/documents/` | `api/v1/accounts/captain/documents#index` | `routes.rb:82` | exact |
| GET | `/api/v1/accounts/:account_id/captain/documents/:document_id` | `/api/v1/accounts/:account_id/captain/documents/:document_id` | `api/v1/accounts/captain/documents#show` | `routes.rb:82` | exact |
| GET | `/api/v1/accounts/:account_id/captain/preferences/` | `/api/v1/accounts/:account_id/captain/preferences/` | `api/v1/accounts/captain/preferences#show` | `routes.rb:63` | exact |
| GET | `/api/v1/accounts/:account_id/companies/` | `/api/v1/accounts/:account_id/companies/` | `api/v1/accounts/companies#index` | `routes.rb:179` | exact |
| GET | `/api/v1/accounts/:account_id/companies/:company_id` | `/api/v1/accounts/:account_id/companies/:company_id` | `api/v1/accounts/companies#show` | `routes.rb:179` | exact |
| GET | `/api/v1/accounts/:account_id/companies/:company_id/contacts` | `/api/v1/accounts/:account_id/companies/:company_id/contacts` | `api/v1/accounts/companies/contacts#index` | `routes.rb:188` | exact |
@@ -93,6 +121,7 @@ These routes exist with equivalent method and path shape but different parameter
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#show` | `routes.rb:252` | exact |
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id/agent_bot` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/agent_bot` | `api/v1/accounts/inboxes#agent_bot` | `routes.rb:255` | exact |
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents` | `api/v1/accounts/inboxes#assignable_agents` | `routes.rb:253` | exact |
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `api/v1/accounts/inboxes/assignment_policy#show` | `routes.rb:311` | exact |
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id/campaigns` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/campaigns` | `api/v1/accounts/inboxes#campaigns` | `routes.rb:254` | exact |
| GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id/health` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/health` | `api/v1/accounts/inboxes#health` | `routes.rb:259` | exact |
| GET | `/api/v1/accounts/:account_id/labels/` | `/api/v1/accounts/:account_id/labels/` | `api/v1/accounts/labels#index` | `routes.rb:281` | exact |
@@ -112,14 +141,66 @@ These routes exist with equivalent method and path shape but different parameter
| GET | `/api/v1/accounts/:account_id/teams/` | `/api/v1/accounts/:account_id/teams/` | `api/v1/accounts/teams#index` | `routes.rb:296` | exact |
| GET | `/api/v1/accounts/:account_id/teams/:team_id` | `/api/v1/accounts/:account_id/teams/:team_id` | `api/v1/accounts/teams#show` | `routes.rb:296` | exact |
| GET | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#index` | `routes.rb:297` | exact |
| GET | `/api/v1/widget/campaigns` | `/api/v1/widget/campaigns` | `api/v1/widget/campaigns#index` | `routes.rb:445` | exact |
| GET | `/api/v1/widget/contact` | `/api/v1/widget/contact` | `api/v1/widget/contact#show` | `routes.rb:458` | exact |
| GET | `/api/v1/widget/conversations` | `/api/v1/widget/conversations` | `api/v1/widget/conversations#index` | `routes.rb:448` | exact |
| GET | `/api/v1/widget/conversations/toggle_status` | `/api/v1/widget/conversations/toggle_status` | `api/v1/widget/conversations#toggle_status` | `routes.rb:455` | exact |
| GET | `/api/v1/widget/inbox_members` | `/api/v1/widget/inbox_members` | `api/v1/widget/inbox_members#index` | `routes.rb:463` | exact |
| GET | `/api/v1/widget/messages` | `/api/v1/widget/messages` | `api/v1/widget/messages#index` | `routes.rb:447` | exact |
| GET | `/api/v2/accounts/:account_id/live_reports/conversation_metrics` | `/api/v2/accounts/:account_id/live_reports/conversation_metrics` | `api/v2/accounts/live_reports#conversation_metrics` | `routes.rb:508` | exact |
| GET | `/api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics` | `/api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics` | `api/v2/accounts/live_reports#grouped_conversation_metrics` | `routes.rb:509` | exact |
| GET | `/api/v2/accounts/:account_id/reports` | `/api/v2/accounts/:account_id/reports` | `api/v2/accounts/reports#index` | `routes.rb:488` | exact |
| GET | `/api/v2/accounts/:account_id/reports/agents` | `/api/v2/accounts/:account_id/reports/agents` | `api/v2/accounts/reports#agents` | `routes.rb:493` | exact |
| GET | `/api/v2/accounts/:account_id/reports/bot_metrics` | `/api/v2/accounts/:account_id/reports/bot_metrics` | `api/v2/accounts/reports#bot_metrics` | `routes.rb:500` | exact |
| GET | `/api/v2/accounts/:account_id/reports/bot_summary` | `/api/v2/accounts/:account_id/reports/bot_summary` | `api/v2/accounts/reports#bot_summary` | `routes.rb:492` | exact |
| GET | `/api/v2/accounts/:account_id/reports/conversation_traffic` | `/api/v2/accounts/:account_id/reports/conversation_traffic` | `api/v2/accounts/reports#conversation_traffic` | `routes.rb:499` | exact |
| GET | `/api/v2/accounts/:account_id/reports/conversations` | `/api/v2/accounts/:account_id/reports/conversations` | `api/v2/accounts/reports#conversations` | `routes.rb:497` | exact |
| GET | `/api/v2/accounts/:account_id/reports/conversations_summary` | `/api/v2/accounts/:account_id/reports/conversations_summary` | `api/v2/accounts/reports#conversations_summary` | `routes.rb:498` | exact |
| GET | `/api/v2/accounts/:account_id/reports/first_response_time_distribution` | `/api/v2/accounts/:account_id/reports/first_response_time_distribution` | `api/v2/accounts/reports#first_response_time_distribution` | `routes.rb:502` | exact |
| GET | `/api/v2/accounts/:account_id/reports/inbox_label_matrix` | `/api/v2/accounts/:account_id/reports/inbox_label_matrix` | `api/v2/accounts/reports#inbox_label_matrix` | `routes.rb:501` | exact |
| GET | `/api/v2/accounts/:account_id/reports/inboxes` | `/api/v2/accounts/:account_id/reports/inboxes` | `api/v2/accounts/reports#inboxes` | `routes.rb:494` | exact |
| GET | `/api/v2/accounts/:account_id/reports/labels` | `/api/v2/accounts/:account_id/reports/labels` | `api/v2/accounts/reports#labels` | `routes.rb:495` | exact |
| GET | `/api/v2/accounts/:account_id/reports/outgoing_messages_count` | `/api/v2/accounts/:account_id/reports/outgoing_messages_count` | `api/v2/accounts/reports#outgoing_messages_count` | `routes.rb:503` | exact |
| GET | `/api/v2/accounts/:account_id/reports/summary` | `/api/v2/accounts/:account_id/reports/summary` | `api/v2/accounts/reports#summary` | `routes.rb:491` | exact |
| GET | `/api/v2/accounts/:account_id/reports/teams` | `/api/v2/accounts/:account_id/reports/teams` | `api/v2/accounts/reports#teams` | `routes.rb:496` | exact |
| GET | `/api/v2/accounts/:account_id/summary_reports/agent` | `/api/v2/accounts/:account_id/summary_reports/agent` | `api/v2/accounts/summary_reports#agent` | `routes.rb:481` | exact |
| GET | `/api/v2/accounts/:account_id/summary_reports/channel` | `/api/v2/accounts/:account_id/summary_reports/channel` | `api/v2/accounts/summary_reports#channel` | `routes.rb:485` | exact |
| GET | `/api/v2/accounts/:account_id/summary_reports/inbox` | `/api/v2/accounts/:account_id/summary_reports/inbox` | `api/v2/accounts/summary_reports#inbox` | `routes.rb:483` | exact |
| GET | `/api/v2/accounts/:account_id/summary_reports/label` | `/api/v2/accounts/:account_id/summary_reports/label` | `api/v2/accounts/summary_reports#label` | `routes.rb:484` | exact |
| GET | `/api/v2/accounts/:account_id/summary_reports/team` | `/api/v2/accounts/:account_id/summary_reports/team` | `api/v2/accounts/summary_reports#team` | `routes.rb:482` | exact |
| GET | `/public/api/v1/csat_survey/:id` | `/public/api/v1/csat_survey/:id` | `public/api/v1/csat_survey#show` | `routes.rb:585` | exact |
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id` | `public/api/v1/inboxes/contacts#show` | `routes.rb:572` | exact |
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `public/api/v1/inboxes/conversations#index` | `routes.rb:573` | exact |
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id` | `public/api/v1/inboxes/conversations#show` | `routes.rb:573` | exact |
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `public/api/v1/inboxes/messages#index` | `routes.rb:580` | exact |
| PATCH | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#update` | `routes.rb:278` | exact |
| PATCH | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#update` | `routes.rb:300` | exact |
| PATCH | `/api/v1/widget/contact/set_user` | `/api/v1/widget/contact/set_user` | `api/v1/widget/contact#set_user` | `routes.rb:461` | exact |
| POST | `/api/v1/accounts/` | `/api/v1/accounts/` | `api/v1/accounts#create` | `routes.rb:47` | exact |
| POST | `/api/v1/accounts/:account_id/agents` | `/api/v1/accounts/:account_id/agents` | `api/v1/accounts/agents#create` | `routes.rb:59` | exact |
| POST | `/api/v1/accounts/:account_id/agents/bulk_create` | `/api/v1/accounts/:account_id/agents/bulk_create` | `api/v1/accounts/agents#bulk_create` | `routes.rb:60` | exact |
| POST | `/api/v1/accounts/:account_id/assignment_policies/` | `/api/v1/accounts/:account_id/assignment_policies/` | `api/v1/accounts/assignment_policies#create` | `routes.rb:306` | exact |
| POST | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes` | `api/v1/accounts/assignment_policies/inboxes#create` | `routes.rb:307` | exact |
| POST | `/api/v1/accounts/:account_id/automation_rules/` | `/api/v1/accounts/:account_id/automation_rules/` | `api/v1/accounts/automation_rules#create` | `routes.rb:115` | exact |
| POST | `/api/v1/accounts/:account_id/automation_rules/:automation_id/clone` | `/api/v1/accounts/:account_id/automation_rules/:automation_id/clone` | `api/v1/accounts/automation_rules#clone` | `routes.rb:116` | exact |
| POST | `/api/v1/accounts/:account_id/canned_responses/` | `/api/v1/accounts/:account_id/canned_responses/` | `api/v1/accounts/canned_responses#create` | `routes.rb:114` | exact |
| POST | `/api/v1/accounts/:account_id/captain/assistant_responses/` | `/api/v1/accounts/:account_id/captain/assistant_responses/` | `api/v1/accounts/captain/assistant_responses#create` | `routes.rb:74` | exact |
| POST | `/api/v1/accounts/:account_id/captain/assistants/` | `/api/v1/accounts/:account_id/captain/assistants/` | `api/v1/accounts/captain/assistants#create` | `routes.rb:64` | exact |
| POST | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes` | `api/v1/accounts/captain/assistants/inboxes#create` | `routes.rb:71` | exact |
| POST | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/playground` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/playground` | `api/v1/accounts/captain/assistants#playground` | `routes.rb:66` | exact |
| POST | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/` | `api/v1/accounts/captain/assistants/scenarios#create` | `routes.rb:72` | exact |
| POST | `/api/v1/accounts/:account_id/captain/bulk_actions/` | `/api/v1/accounts/:account_id/captain/bulk_actions/` | `api/v1/accounts/captain/bulk_actions#create` | `routes.rb:75` | exact |
| POST | `/api/v1/accounts/:account_id/captain/copilot_threads/` | `/api/v1/accounts/:account_id/captain/copilot_threads/` | `api/v1/accounts/captain/copilot_threads#create` | `routes.rb:76` | exact |
| POST | `/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/` | `/api/v1/accounts/:account_id/captain/copilot_threads/:thread_id/copilot_messages/` | `api/v1/accounts/captain/copilot_messages#create` | `routes.rb:77` | exact |
| POST | `/api/v1/accounts/:account_id/captain/custom_tools/` | `/api/v1/accounts/:account_id/captain/custom_tools/` | `api/v1/accounts/captain/custom_tools#create` | `routes.rb:79` | exact |
| POST | `/api/v1/accounts/:account_id/captain/custom_tools/test` | `/api/v1/accounts/:account_id/captain/custom_tools/test` | `api/v1/accounts/captain/custom_tools#test` | `routes.rb:80` | exact |
| POST | `/api/v1/accounts/:account_id/captain/documents/` | `/api/v1/accounts/:account_id/captain/documents/` | `api/v1/accounts/captain/documents#create` | `routes.rb:82` | exact |
| POST | `/api/v1/accounts/:account_id/captain/documents/:document_id/sync` | `/api/v1/accounts/:account_id/captain/documents/:document_id/sync` | `api/v1/accounts/captain/documents#sync` | `routes.rb:83` | exact |
| POST | `/api/v1/accounts/:account_id/captain/tasks/follow_up` | `/api/v1/accounts/:account_id/captain/tasks/follow_up` | `api/v1/accounts/captain/tasks#follow_up` | `routes.rb:90` | exact |
| POST | `/api/v1/accounts/:account_id/captain/tasks/label_suggestion` | `/api/v1/accounts/:account_id/captain/tasks/label_suggestion` | `api/v1/accounts/captain/tasks#label_suggestion` | `routes.rb:89` | exact |
| POST | `/api/v1/accounts/:account_id/captain/tasks/reply_suggestion` | `/api/v1/accounts/:account_id/captain/tasks/reply_suggestion` | `api/v1/accounts/captain/tasks#reply_suggestion` | `routes.rb:88` | exact |
| POST | `/api/v1/accounts/:account_id/captain/tasks/rewrite` | `/api/v1/accounts/:account_id/captain/tasks/rewrite` | `api/v1/accounts/captain/tasks#rewrite` | `routes.rb:86` | exact |
| POST | `/api/v1/accounts/:account_id/captain/tasks/summarize` | `/api/v1/accounts/:account_id/captain/tasks/summarize` | `api/v1/accounts/captain/tasks#summarize` | `routes.rb:87` | exact |
| POST | `/api/v1/accounts/:account_id/companies/` | `/api/v1/accounts/:account_id/companies/` | `api/v1/accounts/companies#create` | `routes.rb:179` | exact |
| POST | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `api/v1/accounts/companies/contacts#create` | `routes.rb:188` | exact |
| POST | `/api/v1/accounts/:account_id/contacts/` | `/api/v1/accounts/:account_id/contacts/` | `api/v1/accounts/contacts#create` | `routes.rb:197` | exact |
@@ -141,6 +222,7 @@ These routes exist with equivalent method and path shape but different parameter
| POST | `/api/v1/accounts/:account_id/custom_filters/` | `/api/v1/accounts/:account_id/custom_filters/` | `api/v1/accounts/custom_filters#create` | `routes.rb:251` | exact |
| POST | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#create` | `routes.rb:275` | exact |
| POST | `/api/v1/accounts/:account_id/inboxes/` | `/api/v1/accounts/:account_id/inboxes/` | `api/v1/accounts/inboxes#create` | `routes.rb:252` | exact |
| POST | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `api/v1/accounts/inboxes/assignment_policy#create` | `routes.rb:311` | exact |
| POST | `/api/v1/accounts/:account_id/inboxes/:inbox_id/register_webhook` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/register_webhook` | `api/v1/accounts/inboxes#register_webhook` | `routes.rb:260` | exact |
| POST | `/api/v1/accounts/:account_id/inboxes/:inbox_id/reset_secret` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/reset_secret` | `api/v1/accounts/inboxes#reset_secret` | `routes.rb:262` | exact |
| POST | `/api/v1/accounts/:account_id/inboxes/:inbox_id/set_agent_bot` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/set_agent_bot` | `api/v1/accounts/inboxes#set_agent_bot` | `routes.rb:256` | exact |
@@ -156,10 +238,36 @@ These routes exist with equivalent method and path shape but different parameter
| POST | `/api/v1/accounts/:account_id/teams/` | `/api/v1/accounts/:account_id/teams/` | `api/v1/accounts/teams#create` | `routes.rb:296` | exact |
| POST | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#create` | `routes.rb:297` | exact |
| POST | `/api/v1/accounts/:account_id/update_active_at` | `/api/v1/accounts/:account_id/update_active_at` | `api/v1/accounts#update_active_at` | `routes.rb:49` | exact |
| POST | `/api/v1/widget/config` | `/api/v1/widget/config` | `api/v1/widget/config#create` | `routes.rb:444` | exact |
| POST | `/api/v1/widget/contact/destroy_custom_attributes` | `/api/v1/widget/contact/destroy_custom_attributes` | `api/v1/widget/contact#destroy_custom_attributes` | `routes.rb:460` | exact |
| POST | `/api/v1/widget/conversations` | `/api/v1/widget/conversations` | `api/v1/widget/conversations#create` | `routes.rb:448` | exact |
| POST | `/api/v1/widget/conversations/destroy_custom_attributes` | `/api/v1/widget/conversations/destroy_custom_attributes` | `api/v1/widget/conversations#destroy_custom_attributes` | `routes.rb:450` | exact |
| POST | `/api/v1/widget/conversations/set_custom_attributes` | `/api/v1/widget/conversations/set_custom_attributes` | `api/v1/widget/conversations#set_custom_attributes` | `routes.rb:451` | exact |
| POST | `/api/v1/widget/conversations/toggle_typing` | `/api/v1/widget/conversations/toggle_typing` | `api/v1/widget/conversations#toggle_typing` | `routes.rb:453` | exact |
| POST | `/api/v1/widget/conversations/transcript` | `/api/v1/widget/conversations/transcript` | `api/v1/widget/conversations#transcript` | `routes.rb:454` | exact |
| POST | `/api/v1/widget/conversations/update_last_seen` | `/api/v1/widget/conversations/update_last_seen` | `api/v1/widget/conversations#update_last_seen` | `routes.rb:452` | exact |
| POST | `/api/v1/widget/direct_uploads` | `/api/v1/widget/direct_uploads` | `api/v1/widget/direct_uploads#create` | `routes.rb:443` | exact |
| POST | `/api/v1/widget/events` | `/api/v1/widget/events` | `api/v1/widget/events#create` | `routes.rb:446` | exact |
| POST | `/api/v1/widget/integrations/dyte/add_participant_to_meeting` | `/api/v1/widget/integrations/dyte/add_participant_to_meeting` | `api/v1/widget/integrations/dyte#add_participant_to_meeting` | `routes.rb:469` | exact |
| POST | `/api/v1/widget/labels` | `/api/v1/widget/labels` | `api/v1/widget/labels#create` | `routes.rb:464` | exact |
| POST | `/api/v1/widget/messages` | `/api/v1/widget/messages` | `api/v1/widget/messages#create` | `routes.rb:447` | exact |
| POST | `/api/v2/accounts/` | `/api/v2/accounts/` | `api/v2/accounts#create` | `routes.rb:478` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts` | `/public/api/v1/inboxes/:inbox_id/contacts` | `public/api/v1/inboxes/contacts#create` | `routes.rb:572` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `public/api/v1/inboxes/conversations#create` | `routes.rb:573` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `public/api/v1/inboxes/messages#create` | `routes.rb:580` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_status` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_status` | `public/api/v1/inboxes/conversations#toggle_status` | `routes.rb:575` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing` | `public/api/v1/inboxes/conversations#toggle_typing` | `routes.rb:576` | exact |
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen` | `public/api/v1/inboxes/conversations#update_last_seen` | `routes.rb:577` | exact |
| PUT | `/api/v1/accounts/:account_id` | `/api/v1/accounts/:account_id` | `api/v1/accounts#update` | `routes.rb:47` | exact |
| PUT | `/api/v1/accounts/:account_id/agents/:agent_id` | `/api/v1/accounts/:account_id/agents/:agent_id` | `api/v1/accounts/agents#update` | `routes.rb:59` | exact |
| PUT | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `api/v1/accounts/assignment_policies#update` | `routes.rb:306` | exact |
| PUT | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `api/v1/accounts/automation_rules#update` | `routes.rb:115` | exact |
| PUT | `/api/v1/accounts/:account_id/canned_responses/:id` | `/api/v1/accounts/:account_id/canned_responses/:id` | `api/v1/accounts/canned_responses#update` | `routes.rb:114` | exact |
| PUT | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `/api/v1/accounts/:account_id/captain/assistant_responses/:response_id` | `api/v1/accounts/captain/assistant_responses#update` | `routes.rb:74` | exact |
| PUT | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id` | `api/v1/accounts/captain/assistants#update` | `routes.rb:64` | exact |
| PUT | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `/api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id` | `api/v1/accounts/captain/assistants/scenarios#update` | `routes.rb:72` | exact |
| PUT | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `/api/v1/accounts/:account_id/captain/custom_tools/:tool_id` | `api/v1/accounts/captain/custom_tools#update` | `routes.rb:79` | exact |
| PUT | `/api/v1/accounts/:account_id/captain/preferences/` | `/api/v1/accounts/:account_id/captain/preferences/` | `api/v1/accounts/captain/preferences#update` | `routes.rb:63` | exact |
| PUT | `/api/v1/accounts/:account_id/companies/:company_id` | `/api/v1/accounts/:account_id/companies/:company_id` | `api/v1/accounts/companies#update` | `routes.rb:179` | exact |
| PUT | `/api/v1/accounts/:account_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/contacts/:contact_id` | `api/v1/accounts/contacts#update` | `routes.rb:197` | exact |
| PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `api/v1/accounts/conversations#update` | `routes.rb:134` | exact |
@@ -173,3 +281,8 @@ These routes exist with equivalent method and path shape but different parameter
| PUT | `/api/v1/accounts/:account_id/notifications/:notification_id` | `/api/v1/accounts/:account_id/notifications/:notification_id` | `api/v1/accounts/notifications#update` | `routes.rb:283` | exact |
| PUT | `/api/v1/accounts/:account_id/sla_policies/:id` | `/api/v1/accounts/:account_id/sla_policies/:id` | `api/v1/accounts/sla_policies#update` | `routes.rb:121` | exact |
| PUT | `/api/v1/accounts/:account_id/teams/:team_id` | `/api/v1/accounts/:account_id/teams/:team_id` | `api/v1/accounts/teams#update` | `routes.rb:296` | exact |
| PUT | `/api/v1/widget/contact` | `/api/v1/widget/contact` | `api/v1/widget/contact#update` | `routes.rb:458` | exact |
| PUT | `/api/v1/widget/messages/:message_id` | `/api/v1/widget/messages/:message_id` | `api/v1/widget/messages#update` | `routes.rb:447` | exact |
| PUT | `/public/api/v1/csat_survey/:id` | `/public/api/v1/csat_survey/:id` | `public/api/v1/csat_survey#update` | `routes.rb:585` | exact |
| PUT | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id` | `public/api/v1/inboxes/contacts#update` | `routes.rb:572` | exact |
| PUT | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id` | `public/api/v1/inboxes/messages#update` | `routes.rb:580` | exact |
@@ -138,6 +138,9 @@ func (h *CsatSurveyHandler) Update(c *gin.Context) {
// GET /public/api/v1/conversations/:conversation_uuid/csats
func (h *CsatSurveyHandler) PublicGet(c *gin.Context) {
conversationUUID := c.Param("conversation_uuid")
if conversationUUID == "" {
conversationUUID = c.Param("id")
}
if conversationUUID == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_uuid is required")
return
@@ -156,6 +159,9 @@ func (h *CsatSurveyHandler) PublicGet(c *gin.Context) {
// POST /public/api/v1/conversations/:conversation_uuid/csats
func (h *CsatSurveyHandler) PublicUpdate(c *gin.Context) {
conversationUUID := c.Param("conversation_uuid")
if conversationUUID == "" {
conversationUUID = c.Param("id")
}
if conversationUUID == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_uuid is required")
return
+127 -2
View File
@@ -228,6 +228,12 @@ func RegisterRoutes(
// Reference: Chatwoot POST /widget/direct_uploads
widget.POST("/direct_uploads", handlers.Upload.DirectUpload)
// Chatwoot widget API routes — public + CORS for reused Chatwoot frontend/widget.
// Reference: Chatwoot namespace :api/:v1/:widget at /api/v1/widget/*.
widgetV1 := engine.Group("/api/v1/widget")
widgetV1.Use(middleware.CORS(corsCfg))
registerChatwootWidgetRoutes(widgetV1, handlers)
// Public CSAT survey routes — no auth required (customer submits rating)
// Reference: Chatwoot /public/api/v1/conversations/:uuid/csats
publicAPI := engine.Group("/public/api/v1")
@@ -235,8 +241,45 @@ func RegisterRoutes(
{
publicAPI.GET("/conversations/:conversation_uuid/csats", handlers.CsatSurvey.PublicGet)
publicAPI.POST("/conversations/:conversation_uuid/csats", handlers.CsatSurvey.PublicUpdate)
publicAPI.GET("/csat_survey/:id", handlers.CsatSurvey.PublicGet)
publicAPI.PUT("/csat_survey/:id", handlers.CsatSurvey.PublicUpdate)
publicAPI.PATCH("/csat_survey/:id", handlers.CsatSurvey.PublicUpdate)
// Public API compatibility routes used by Chatwoot public inbox/contact flows.
publicInboxes := publicAPI.Group("/inboxes")
{
publicInboxes.GET("/:inbox_id", chatwootParityStub)
contacts := publicInboxes.Group("/:inbox_id/contacts")
{
contacts.POST("", chatwootParityStub)
contacts.GET("/:contact_id", chatwootParityStub)
contacts.PUT("/:contact_id", chatwootParityStub)
contacts.PATCH("/:contact_id", chatwootParityStub)
conversations := contacts.Group("/:contact_id/conversations")
{
conversations.GET("", chatwootParityStub)
conversations.POST("", chatwootParityStub)
conversations.GET("/:conversation_id", chatwootParityStub)
conversations.POST("/:conversation_id/toggle_status", chatwootParityStub)
conversations.POST("/:conversation_id/toggle_typing", chatwootParityStub)
conversations.POST("/:conversation_id/update_last_seen", chatwootParityStub)
messages := conversations.Group("/:conversation_id/messages")
{
messages.GET("", chatwootParityStub)
messages.POST("", chatwootParityStub)
messages.PUT("/:message_id", chatwootParityStub)
messages.PATCH("/:message_id", chatwootParityStub)
}
}
}
}
}
// API v2 routes — authenticated, account-scoped report APIs.
apiV2 := engine.Group("/api/v2")
apiV2.Use(middleware.AuthMiddleware(jwtCfg))
registerV2Routes(apiV2, handlers)
// Webhook callback routes — channel-specific auth
// Reference: Chatwoot webhook routes for FB/WhatsApp/Telegram
webhookGroup := engine.Group("/webhooks")
@@ -1081,6 +1124,8 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
tasks.POST("/reply_suggestion", h.CaptainTask.ReplySuggestion)
tasks.POST("/summarize", h.CaptainTask.Summarize)
tasks.POST("/rewrite", h.CaptainTask.Rewrite)
tasks.POST("/label_suggestion", h.CaptainTaskExtended.LabelSuggestion)
tasks.POST("/follow_up", h.CaptainTaskExtended.FollowUp)
// M12: SSE streaming variants of Captain task endpoints
tasks.POST("/reply_suggestion/stream", h.CaptainTask.StreamReplySuggestion)
tasks.POST("/summarize/stream", h.CaptainTask.StreamSummarize)
@@ -1427,8 +1472,9 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
// Inbox-level assignment policy overrides (nested under inboxes)
inboxAssignment := accountScoped.Group("/inboxes/:inbox_id/assignment_policy")
{
inboxAssignment.GET("/", h.AssignmentPolicy.GetInboxPolicy)
inboxAssignment.POST("/", h.AssignmentPolicy.CreateInboxPolicy)
inboxAssignment.GET("/", h.AssignmentPolicyV2.GetInboxPolicy)
inboxAssignment.POST("/", h.AssignmentPolicyV2.SetInboxPolicy)
inboxAssignment.DELETE("/", h.AssignmentPolicyV2.DeleteInboxPolicy)
inboxAssignment.PUT("/:policy_id", h.AssignmentPolicy.UpdateInboxPolicy)
inboxAssignment.DELETE("/:policy_id", h.AssignmentPolicy.DeleteInboxPolicy)
}
@@ -1523,6 +1569,44 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
}
}
// registerV2Routes maps Chatwoot API v2 account-scoped report routes.
// Reference: Chatwoot config/routes.rb namespace :api/:v2.
func registerV2Routes(g *gin.RouterGroup, h *Handlers) {
accounts := g.Group("/accounts")
accounts.Use(middleware.AccountScope())
{
accounts.POST("/", h.Account.Create)
accountScoped := accounts.Group("/:account_id")
{
v1.RegisterSummaryReportRoutes(accountScoped, h.SummaryReport)
reports := accountScoped.Group("/reports")
{
reports.GET("", h.Analytics.Summary)
reports.GET("/summary", h.Analytics.Summary)
reports.GET("/bot_summary", h.Analytics.BotSummary)
reports.GET("/agents", h.Analytics.AgentMetrics)
reports.GET("/inboxes", h.Analytics.InboxMetrics)
reports.GET("/labels", h.Analytics.LabelMetrics)
reports.GET("/teams", h.Analytics.TeamMetrics)
reports.GET("/conversations", h.Analytics.Conversations)
reports.GET("/conversations_summary", h.Analytics.ConversationsSummary)
reports.GET("/conversation_traffic", h.Analytics.ConversationTraffic)
reports.GET("/bot_metrics", h.Analytics.BotMetrics)
reports.GET("/inbox_label_matrix", h.Analytics.InboxLabelMatrix)
reports.GET("/first_response_time_distribution", h.Analytics.FirstResponseTimeDistribution)
reports.GET("/outgoing_messages_count", h.Analytics.OutgoingMessagesCount)
}
liveReports := accountScoped.Group("/live_reports")
{
liveReports.GET("/conversation_metrics", h.LiveReport.ConversationMetrics)
liveReports.GET("/grouped_conversation_metrics", h.LiveReport.GroupedConversationMetrics)
}
}
}
}
// registerPlatformRoutes maps super-admin platform routes.
// Reference: Chatwoot namespace :platform_app (super_admin only)
func registerPlatformRoutes(g *gin.RouterGroup, h *Handlers) {
@@ -1614,6 +1698,40 @@ func registerPlatformTokenRoutes(g *gin.RouterGroup, h *Handlers) {
g.DELETE("/accounts/:account_id/account_users/:user_id", h.PlatformAccountUser.Destroy)
}
// registerChatwootWidgetRoutes maps Chatwoot's /api/v1/widget namespace.
// Some behavior still delegates to tracked stubs until widget serializer parity is completed.
func registerChatwootWidgetRoutes(g *gin.RouterGroup, h *Handlers) {
g.POST("/direct_uploads", h.Upload.DirectUpload)
g.POST("/config", h.Widget.Init)
g.GET("/campaigns", chatwootParityStub)
g.POST("/events", chatwootParityStub)
g.GET("/messages", chatwootParityStub)
g.POST("/messages", h.Widget.SendMessage)
g.PUT("/messages/:message_id", chatwootParityStub)
g.PATCH("/messages/:message_id", chatwootParityStub)
g.GET("/conversations", h.Widget.GetConversations)
g.POST("/conversations", chatwootParityStub)
g.POST("/conversations/destroy_custom_attributes", chatwootParityStub)
g.POST("/conversations/set_custom_attributes", chatwootParityStub)
g.POST("/conversations/update_last_seen", chatwootParityStub)
g.POST("/conversations/toggle_typing", chatwootParityStub)
g.POST("/conversations/transcript", chatwootParityStub)
g.GET("/conversations/toggle_status", chatwootParityStub)
g.GET("/contact", chatwootParityStub)
g.PUT("/contact", h.Widget.UpdateContact)
g.PATCH("/contact", h.Widget.UpdateContact)
g.POST("/contact/destroy_custom_attributes", chatwootParityStub)
g.PATCH("/contact/set_user", chatwootParityStub)
g.GET("/inbox_members", chatwootParityStub)
g.POST("/labels", chatwootParityStub)
g.DELETE("/labels/:label_id", chatwootParityStub)
g.POST("/integrations/dyte/add_participant_to_meeting", chatwootParityStub)
}
// registerWidgetRoutes maps widget API routes (public, for embed).
// Reference: Chatwoot namespace :widget_api — no authentication required
func registerWidgetRoutes(g *gin.RouterGroup, h *widget.WidgetHandler) {
@@ -1638,6 +1756,13 @@ func registerWidgetRoutes(g *gin.RouterGroup, h *widget.WidgetHandler) {
widget.POST("/:website_token/offline_message", h.SubmitOfflineMessage)
}
func chatwootParityStub(c *gin.Context) {
c.JSON(501, gin.H{
"error": "not implemented",
"message": "Chatwoot parity route is registered; behavior is tracked for implementation",
})
}
// --- Placeholder handlers for webhooks ---
// Full implementation deferred to P7 (channels)
+48
View File
@@ -0,0 +1,48 @@
package router
import (
"testing"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/config"
"github.com/gochat/gochat/internal/middleware"
)
func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
RegisterRoutes(
engine,
nil,
nil,
nil,
&Handlers{},
nil,
nil,
&config.JWTConfig{},
middleware.CORSConfig{},
nil,
)
routes := map[string]bool{}
for _, route := range engine.Routes() {
routes[route.Method+" "+route.Path] = true
}
expected := []string{
"GET /api/v1/accounts/:account_id/captain/assistants/tools",
"GET /api/v1/accounts/:account_id/captain/assistants/:assistant_id",
"GET /api/v1/widget/conversations",
"GET /api/v1/widget/conversations/toggle_status",
"PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id",
"GET /api/v2/accounts/:account_id/reports/summary",
"GET /api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics",
}
for _, key := range expected {
if !routes[key] {
t.Fatalf("expected route %s to be registered", key)
}
}
}