diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 872f2505..2a6433d4 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,17 +49,17 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-07 P3.101 WhatsApp call route parameter parity, prepared as `fix(routes): align whatsapp call ids`. -- Latest implementation checkpoint: this checkpoint, prepared as `fix(routes): align whatsapp call ids`. -- Latest documentation/tooling checkpoint: this tracker update records exact Chatwoot `whatsapp_calls/:id` account-route parameter parity, the Gin wildcard-name limit for remaining AgentCapacity nested parameter-compatible rows, and regenerated route artifacts. +- Current tracking checkpoint: 2026-06-07 P6.1 webhook placeholder fallback burn-down, prepared as `fix(webhooks): replace parity stubs`. +- Latest implementation checkpoint: this checkpoint, prepared as `fix(webhooks): replace parity stubs`. +- Latest documentation/tooling checkpoint: this tracker update records removal of `chatwootParityStub`, explicit webhook nil-handler fallback behavior, and the refreshed Phase 6 placeholder audit. - Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker. -- Worktree status at this implementation checkpoint: account-level WhatsApp call routes now register the same `:id` path parameter used by Chatwoot `routes.rb:237-242`, while the handler still accepts legacy local `:call_id` mounts. AgentCapacity nested handlers also accept Chatwoot `:agent_capacity_policy_id`/`:id` params if mounted in isolation, but the main Gin router keeps `/:id/...` plus `:user_id`/`:limit_id` because Gin rejects sibling wildcard-name changes below the existing policy `/:id` member route. Route parity artifacts are regenerated and the tracked frontend-critical audit improves to 430 exact routes. This retains P3.100 dashboard app route-parameter parity, P3.99 conversation destroy async parity, P3.98 conversation transcript delivery parity, P3.97 conversation typing event parity, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. +- Worktree status at this implementation checkpoint: the remaining `chatwootParityStub` webhook nil-handler fallbacks are replaced with explicit `503 webhook provider unavailable` responses, and the unused placeholder helpers are removed. This keeps real provider handlers untouched when wired, prevents any registered route from returning a placeholder/not-implemented body when a handler is missing, and refreshes the Phase 6 placeholder audit. This retains P3.101 WhatsApp call route-parameter parity, P3.100 dashboard app route-parameter parity, P3.99 conversation destroy async parity, P3.98 conversation transcript delivery parity, P3.97 conversation typing event parity, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. - Next executable implementation checkpoint: continue Phase 2/3 drift audit for the next reused-frontend mismatch, or run B12 live smoke when the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack is available. Re-run Phase 6 placeholder audit after future route/smoke changes. - `go test ./...` passes when run outside the restricted socket sandbox for the latest implementation baseline; the latest docs/tooling checkpoint verified `scripts/parity_frontend_smoke.sh --check` with workspace-local temp/cache dirs after `/tmp` was full. -- Route dump succeeds with `967` registered routes after dashboard app route-parameter tracking. +- Route dump succeeds with `967` registered routes after WhatsApp call route-parameter tracking. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. - Tracked frontend-critical route audit covers 439 Chatwoot routes: 430 exact, 0 method-compatible, 9 parameter-compatible, 0 missing. The 9 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. -- `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers. +- `/api/v1/widget` stubs are burned down, public inbox/contact/conversation/message core flows are backed by real handlers, and `chatwootParityStub` no longer exists in product code. - 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. @@ -74,7 +74,7 @@ This section is the quick entry point for the next implementation turn. Use the | Meilisearch search | In scope and mandatory. The Hermes search plan is landed into Phase 1/B6/P5.3; production DB search is not an acceptable final path. | Reopen only from live Meilisearch gate failure, stale index mutation path, or frontend search payload drift. | Meilisearch-first behavior is verified for global/entity search and indexing hooks. | | Enterprise paid features | In scope except SSO/SAML/LDAP/OIDC. SLA, Audit, CustomRole, AgentCapacity, Captain/Copilot, CSAT, InboxLimit, automation, macros, assignment policies, and limits remain tracked. | Use B7-B12 and Phase 5 rows as the source of truth; split new work only from fresh reference or smoke evidence. | Reused Chatwoot enterprise screens/API smoke without adapters. | | Background jobs/integrations | Phase 5 is in Review with durable workers, dispatcher, search indexing, automation/macro/CSAT, SLA, contact import/export, Captain/Copilot, maintenance, message status, bulk actions, and analytics covered by named checkpoints. | Keep closed unless a reference job path or live smoke exposes a missing durable side effect. | No frontend-visible async action silently succeeds without durable persistence or observable failure. | -| Placeholder burn-down | Phase 6 is in Review; current audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`. | Re-run `rg` placeholder audit after route/smoke changes and burn down any frontend-reachable stub. | No account/contact/conversation/message/inbox/widget/public critical stub is ownerless. | +| Placeholder burn-down | Phase 6 is in Review; current audit shows `chatwootParityStub` is removed and no reused-frontend critical handler returns placeholder/not-implemented bodies. | Re-run `rg` placeholder audit after route/smoke changes and burn down any frontend-reachable stub. | No account/contact/conversation/message/inbox/widget/public critical stub is ownerless. | | Live B12 smoke | Harness exists; live run is environment-dependent. | Run when PostgreSQL, Redis, Meilisearch, GoChat, Vite, and Chrome are available. | `docs/parity/frontend_smoke_report.md` maps every failure to an owner slice or records clean pass. | Execution rule for the next turn: start with Phase 2/3 drift audit unless the full B12 live stack is available. Keep implementation slices small, update this document in the same commit, and do not create parallel trackers. Treat Phase 6 as a repeatable guard after route/smoke changes, not as a blocking implementation slice while the audit remains clean. @@ -147,7 +147,7 @@ Open work after the current checkpoint: | Phase 3 | Data and serializer parity | Doing | JSON fixture coverage is partial and still endpoint-family based | | Phase 4 | Enterprise feature completion | Review | B7, B8, B9, B10, and B11 are in Review; B12 reused frontend smoke harnesses exist and optional live runs can expose follow-up slices | | Phase 5 | Background jobs and integrations | Review | P5.1/P5.2/P5.3/P5.4/P5.5/P5.6/P5.7/P5.8/P5.8b/P5.9/P5.10/P5.11/P5.12 durable worker, event dispatch, search indexing, automation delivery, macro, CSAT survey/template, SLA scan, contact export/import, inbound webhook persistence, outbound/provider delivery status, Captain document sync/crawl/response/embedding/Copilot/conversation responses, conversation maintenance, message status update, account bulk-action cores, P5.13 analytics rollups/timeseries, and B9.3 delayed-action reference closure are in Review; further work should be named drift from smoke/reference evidence | -| Phase 6 | Core placeholder burn-down | Review | refreshed `docs/parity/placeholder_audit.md` shows only public webhook nil-handler fallbacks still call `chatwootParityStub`; no reused-frontend critical account/contact/conversation/message/inbox/widget/public path is ownerless | +| Phase 6 | Core placeholder burn-down | Review | refreshed `docs/parity/placeholder_audit.md` shows `chatwootParityStub` has been removed; webhook nil-handler fallbacks now return explicit provider-unavailable errors, and no reused-frontend critical account/contact/conversation/message/inbox/widget/public path is ownerless | | Phase 7 | Verification harness | Review | B12.1 boot/readiness, B12.2a API assertions, B12.2b browser smoke harness, B12.3a enterprise API assertions, and B12.3b enterprise browser route navigation exist; optional live Meilisearch/full-browser runs remain environment-dependent | ## Active Remaining Work Board @@ -156,6 +156,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | +| 0 | P6.1 webhook placeholder fallback burn-down | Implemented for Phase 6 placeholder cleanup: `chatwootParityStub` and the unused webhook placeholder helper are removed from product code. Public webhook nil-handler guards now return explicit `503 { error: "webhook provider unavailable", message: "webhook handler is not configured" }` responses instead of `501 not implemented` placeholder bodies, while wired provider handlers still own real Telegram, WhatsApp, TikTok, LINE, Twilio, Twitter, Instagram, and Shopify behavior. | Keep in Review; reopen from placeholder audit or webhook smoke if a frontend/provider-reachable route returns placeholder/not-implemented content or if a provider handler is missing from normal bootstrap. | Focused router tests cover route boot and nil-handler fallback body/status; placeholder audit refreshed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.101 WhatsApp call route parameter parity | Implemented for reused WhatsApp call controls: account-level show/accept/reject/terminate/upload-recording routes now register Chatwoot's `:id` parameter name from `routes.rb:237-242`, while the handler still accepts legacy `:call_id` mounts. Route parity improves from `425 exact / 14 parameter-compatible` to `430 exact / 9 parameter-compatible` with no missing critical routes. AgentCapacity nested route names stay parameter-compatible in the main Gin router because Gin cannot register `/:id` policy members and `/:agent_capacity_policy_id` nested siblings under the same prefix without a wildcard conflict; the nested handlers now tolerate both parameter-name families. | Keep in Review; reopen from B12 WhatsApp calling smoke or fresh reference evidence for route/action payload drift beyond the inspected enterprise controller/frontend call API contract. | Focused WhatsAppCallHandler, AgentCapacityHandler, and router tests passed; route dump/parity artifacts regenerated; full `go test ./...` passed outside the restricted socket sandbox after retrying the known `internal/worker` SQLite in-memory flake; `git diff --check` passed. | | 0 | P3.100 dashboard app route parameter parity | Implemented for reused dashboard app settings routes: GoChat now registers standard dashboard app member routes with Chatwoot's `:id` parameter name for `show/update/destroy`, keeps `PATCH` and `PUT`, and preserves legacy handler compatibility for local focused tests and widget extension routes. Route parity improves from `421 exact / 18 parameter-compatible` to `425 exact / 14 parameter-compatible` with no missing critical routes. | Keep in Review; reopen from B12 dashboard app settings smoke or fresh reference evidence for serializer/request-permit drift beyond the inspected controller/Jbuilder/frontend store contract. | Focused DashboardAppHandler and router tests passed; route dump/parity artifacts regenerated; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. | | 0 | P3.99 conversation destroy async parity | Implemented for reused conversation delete actions: `DELETE /conversations/:conversation_id` now returns empty `200 OK` like Chatwoot and queues a low-priority durable `conversation:delete_object` job when workers are configured. The job performs the existing soft delete, dispatches `conversation.deleted`, and removes the search index entry; no-worker construction keeps synchronous focused-test behavior. | Keep in Review; reopen from B12 conversation delete smoke or fresh reference evidence for enterprise `DeleteObjectJob#process_post_deletion_tasks`, exact Pundit destroy permission drift, or heavy-association purge behavior beyond conversation objects. | Focused ConversationService delete/job tests and ConversationHandler delete response tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | @@ -239,7 +240,7 @@ This table is the shortest authoritative handoff view. If an older lower section | 0 | P3.23 nested contact inbox creation API | Implemented for Chatwoot nested contact inbox creation: raw JSON/form/query params are accepted, contact and inbox are account-scoped, missing source IDs are generated through Chatwoot channel rules, duplicate contact+inbox+source rows are returned idempotently, `hmac_verified` is persisted on creation, and the response is raw `{ source_id, inbox }` rather than the local model/envelope. | Keep in Review; reopen only if live CRM/new-conversation smoke exposes inbox access-policy, unsupported channel, or serializer drift beyond the inspected controller/builder/Jbuilder contract. | Focused nested ContactInbox handler/service/repository tests, route parity check, full `go test ./...`, and `git diff --check` passed. | | 1 | P3.2a invitation/confirmation mail parity | Implemented for current non-SSO reference behavior: profile resend is no longer a TODO-only log, new invited agents and unconfirmed invited profile resends generate reset-password invitation links, normal unconfirmed profile resends generate confirmation links, `users.unconfirmed_email` is modeled for email-update routing, and fakeable/environment SMTP mailers keep default tests offline. | Keep in Review; reopen only if reused frontend smoke or fresh reference evidence exposes additional Devise confirmation states outside excluded SSO/SAML/LDAP/OIDC variants. | Focused profile and agent invitation tests, combined handler/service/repository/router/migrate/app tests, full `go test ./...`, and `git diff --check` passed. | | 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 439-route critical set; profile/profile-MFA routes from `routes.rb:421-435`, dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, Twilio callback routes from `routes.rb:639-640`, enterprise Twilio voice routes from `routes.rb:643-646`, root Linear/Shopify/Notion OAuth callback routes from `routes.rb:630/634/654`, root Twitter/Google/Microsoft/Instagram/TikTok callback routes from `routes.rb:626/649-652`, assignment policy routes from `routes.rb:306-313`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account Facebook callback routes from `routes.rb:106-111`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, account integration app/hook routes from `routes.rb:345-348`, account Slack routes from `routes.rb:350-352`, account Dyte routes from `routes.rb:357-358`, account Shopify routes from `routes.rb:361-364`, account Linear routes from `routes.rb:365-373`, account Notion authorization/destroy routes from `routes.rb:335/379`, account Twitter/Google/Microsoft/Instagram/TikTok authorization routes from `routes.rb:315/319/323/327/331`, account WhatsApp authorization route from `routes.rb:339`, account inbox WhatsApp calling toggle routes from `routes.rb:266-267`, account WhatsApp call routes from `routes.rb:237-245`, account onboarding route from `routes.rb:58`, API v2 year-in-review route from `routes.rb:505`, dashboard app routes from `routes.rb:130`, canned response routes from `routes.rb:114`, notification subscription routes from `routes.rb:440`, team/team-member routes from `routes.rb:296-300`, conversation participant routes from `routes.rb:150`, conversation direct upload route from `routes.rb:151`, conversation draft message routes from `routes.rb:152`, conversation inbox assistant route from `routes.rb:165`, conversation reporting events route from `routes.rb:166`, and account reporting events route from `routes.rb:234` are now explicitly tracked. Profile MFA setup/verify/backup/disable raw payload behavior, notification list/action serializers plus current-user/current-account mutation scoping, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, account onboarding custom-attribute payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account Facebook callback page discovery/register/reauthorization behavior, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, account Slack OAuth/channel payload behavior, account Dyte create/join payload behavior, account Shopify customer-order payload behavior, account Linear GraphQL issue payload behavior, account Notion authorization/destroy behavior, account Twitter/Google/Microsoft/Instagram/TikTok authorization behavior, account WhatsApp embedded-signup creation and reauthorization behavior, account inbox WhatsApp calling toggle behavior, account WhatsApp call lifecycle/recording behavior, API v2 year-in-review raw/cache/aggregate behavior, dashboard app raw payload/serializer behavior, canned response raw payload/search/delete behavior, notification subscription payload behavior, team update/frontend route behavior, conversation participant route/payload/final-set update behavior, conversation direct upload ActiveStorage behavior, conversation draft message Redis-key-equivalent behavior, conversation inbox assistant Copilot payload behavior, conversation reporting-event raw array behavior, account reporting-events payload/filter/pagination behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, assignment policy CRUD and inbox binding payloads, Twilio inbound/status callbacks, enterprise Twilio voice callbacks, Linear/Shopify/Notion root integration callbacks, Shopify OAuth auth redirects, root channel OAuth callbacks, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Continue the next evidence-backed route/controller/serializer drift after P3.39 profile MFA or from B12 findings. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | -| 3 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; refreshed `docs/parity/placeholder_audit.md` shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Keep in Review; reopen only if fresh `rg`, route smoke, or B12 finds a frontend-reachable placeholder/stub in account/contact/conversation/message/inbox/widget/public paths. | `rg` placeholder audit and `scripts/parity_frontend_smoke.sh --check` are recorded; no reused-frontend blocker is ownerless. | +| 3 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; refreshed `docs/parity/placeholder_audit.md` shows `chatwootParityStub` has been removed and webhook nil-handler fallbacks return explicit provider-unavailable errors; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Keep in Review; reopen only if fresh `rg`, route smoke, or B12 finds a frontend-reachable placeholder/stub in account/contact/conversation/message/inbox/widget/public paths. | `rg` placeholder audit and `scripts/parity_frontend_smoke.sh --check` are recorded; no reused-frontend blocker is ownerless. | | 4 | P3.9 account agent-bot API | Implemented for the reused dashboard AgentBots settings route with no-trailing-slash routes, PATCH update, raw Jbuilder-style payloads, account mutation scope, system-bot show/list visibility, empty `200 OK` delete, and full reset/avatar action payloads. | Keep in Review; reopen only if live settings smoke exposes avatar upload storage or administrator-secret gating drift. | Focused AgentBot handler tests, service/router focused tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 5 | P3.10 account webhooks API | Implemented for the reused dashboard Webhooks settings route with PATCH update, Chatwoot `{ payload }` list/mutation serializers, nested `{ webhook: ... }` bodies, generated secret, account-scoped mutations, URL/subscription validation, optional inbox serialization, and empty `200 OK` delete. | Keep in Review; reopen only if live settings smoke exposes audit writer or delivery-signature drift beyond the existing delivery service boundary. | Focused webhook handler/service/router tests, migration test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 6 | P3.11 account integrations apps/hooks API | Implemented for reused dashboard integrations settings with `{ payload: [...] }` app index, raw app/show and hook mutation payloads, raw or nested create/update bodies, account-scoped hook show/update/delete, persisted `app_id/reference_id`, reference app catalog seed additions, app hook/settings schema serializers, hook boolean `status`, account/inbox `hook_type`, and empty `200 OK` delete. | Keep in Review; reopen if live settings smoke exposes app feature-flag activation, app-specific credential validation, or provider setup side effects beyond this generic hooks boundary. | Focused integration hook handler/service tests, migration/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | @@ -507,7 +508,7 @@ This ledger records the committed parity checkpoints that future slices should b | `feat(help-center): queue article translations` | Advances P3.7d enterprise help-center parity by matching Chatwoot enterprise `Articles::BulkActionsController#translate`, `Captain::Articles::TranslateJob`, `Captain::Llm::ArticleTranslationService`, and the reused dashboard `bulkTranslate` flow. Bulk translate now enforces `captain_tasks`, portal allowed locales, optional locale-matched categories, and article selection; returns `409 { duplicate_articles }` for existing translations unless `force` is true; enqueues low-priority durable `captain:article_translate` jobs; registers replay in bootstrap; and uses a wired LLM-backed article translator with Chatwoot title/content prompts and locale-to-English target language names before creating or updating draft translated articles. | `go test ./internal/service -run 'ArticleService_.*Translate\|LLMArticleTranslationBackend\|ArticleService_BulkActions\|ArticleService_BulkUpdateStatus\|ArticleService_BulkDelete' -count=1`; `go test ./internal/handler/api/v1 -run 'ArticleHandlerSuite/TestBulkTranslate\|ArticleHandlerSuite/TestBulkActions' -count=1`; `go test ./internal/service ./internal/handler/api/v1 ./internal/repository ./internal/app -run 'Article\|Bootstrap\|App' -count=1`; `go test ./...`; `git diff --check`. No route changes. | P3.7d moves to Review; continue Phase 6 placeholder burn-down or the next evidence-backed Phase 2/3 drift slice. | | `feat(inboxes): align agent bot member actions` | Advances P6.5 inbox API parity by aligning `GET /inboxes/:id/agent_bot` and `POST /inboxes/:id/set_agent_bot` with Chatwoot `InboxesController#agent_bot`/`#set_agent_bot`, `inboxes/agent_bot.json.jbuilder`, `_agent_bot.json.jbuilder`, and the reused dashboard `agentBots` store/API path. The setter accepts Chatwoot `{ agent_bot: id|null }` bodies, keeps legacy `{ agent_bot_id }` compatibility, treats omitted/null params as disconnect, returns empty `200 OK`, destroys disconnected bindings instead of leaving inactive local rows, and updates the inbox's single binding on reassignment. The getter now returns `{ agent_bot: {} }` when disconnected and serializes assigned bots with Chatwoot fields such as `thumbnail`, `bot_type`, `bot_config`, and `outgoing_url`. | `go test ./internal/service -run 'InboxService_SetAgentBot' -count=1`; `go test ./internal/handler/api/v1 -run 'InboxHandler_Set.*AgentBot\|InboxHandler_GetAgentBot\|InboxSetAgentBot' -count=1`; `go test ./internal/handler/api/v1 ./internal/service -run 'Inbox.*AgentBot\|SetAgentBot' -count=1`; `go test ./...`; `git diff --check`. No route changes. | P6.5 remains Doing for additional inbox/channel drift; continue Phase 6 placeholder audit or the next evidence-backed inbox member action gap. | | `feat(messages): align retry status parity` | Advances P6 message API parity by matching Chatwoot `MessagesController#retry`, `messages/retry.json.jbuilder`, and the reused dashboard `MessageApi.retry` path. Message retry now resets failed messages to `sent`, clears `content_attributes` to `{}`, returns the Chatwoot message serializer, and queues the existing durable `message:send_reply` path when a WorkerPool is configured instead of leaving local `retrying` state/provider failure details visible. | `go test ./internal/service -run MessageService_Retry -count=1`; `go test ./internal/handler/api/v1 -run 'MessageHandlerTestSuite/TestRetry' -count=1`; `go test ./internal/handler/api/v1 ./internal/service -run 'Message.*Retry' -count=1`; `go test ./...`; `git diff --check`. | P6.4 message retry drift moves to Review; continue Phase 6 placeholder burn-down or fresh evidence-backed Phase 2/3 drift. | -| `feat(conversations): align custom attribute response` | Advances P6 conversation response parity by matching Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, and the reused dashboard `ConversationApi.updateCustomAttributes` store action. The account conversation custom-attributes route now returns only `{ custom_attributes: ... }`, excludes local conversation/payload/meta wrappers, and converts empty/null JSON to `{}` for frontend merge stability. The placeholder audit was rerun and still shows only webhook handler-not-injected fallbacks calling `chatwootParityStub`. | `go test ./internal/handler/api/v1 -run 'ConversationHandler.*UpdateCustomAttributes\|TestConversationHandler' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -run 'UpdateCustomAttributes\|ConversationHandler\|ConversationService\|Router' -count=1`; `rg -n "chatwootParityStub\|TODO.*placeholder\|placeholder\|stub" internal/handler internal/service internal/router -g'*.go'`; `go test ./...`; `git diff --check`. | P6 conversation custom-attribute response drift moves to Review; continue Phase 6 placeholder burn-down from the remaining webhook nil-handler fallbacks or fresh reference/smoke evidence. | +| `feat(conversations): align custom attribute response` | Advances P6 conversation response parity by matching Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, and the reused dashboard `ConversationApi.updateCustomAttributes` store action. The account conversation custom-attributes route now returns only `{ custom_attributes: ... }`, excludes local conversation/payload/meta wrappers, and converts empty/null JSON to `{}` for frontend merge stability. The placeholder audit at that checkpoint showed only webhook handler-not-injected fallbacks; those fallbacks are now closed by P6.1. | `go test ./internal/handler/api/v1 -run 'ConversationHandler.*UpdateCustomAttributes\|TestConversationHandler' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -run 'UpdateCustomAttributes\|ConversationHandler\|ConversationService\|Router' -count=1`; `rg -n "chatwootParityStub\|TODO.*placeholder\|placeholder\|stub" internal/handler internal/service internal/router -g'*.go'`; `go test ./...`; `git diff --check`. | P6 conversation custom-attribute response drift moves to Review; continue from fresh reference/smoke evidence. | | `docs: expand help center route parity tracking` | Audit/tooling checkpoint that expands `cmd/route_parity` to track the full Chatwoot help-center portal/category/article route family from `reference/chatwoot/config/routes.rb:385-404`, including portal member actions, nested category CRUD/reorder, nested article CRUD/reorder, and article bulk actions. Regenerated `docs/parity/route_parity.md` now reports `298 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 305 tracked critical routes`; `docs/parity/gochat_routes.txt` remains `TOTAL: 860`. | `go run ./cmd/route_parity`; `go test ./cmd/route_parity -count=1`; `git diff --check`. | Continue Phase 6 placeholder burn-down or the next evidence-backed Phase 2/3 drift slice; keep route artifacts regenerated whenever the tracked set changes. | | `feat(help-center): align article payloads` | Advances P3.7c help-center parity by matching Chatwoot `ArticlesController`, `Articles::BulkActionsController`, article Jbuilder views, and reused dashboard `helpCenter/articles.js`. Article index/create now expose no-trailing-slash routes; update exposes frontend `PATCH`; frontend bulk update-status/update-category/delete and translate routes are registered; portal IDs resolve through account-scoped slug lookup with numeric fallback; create/update accept raw frontend article bodies plus wrappers; list/search return `{ payload, meta }`; show/create/update/edit return `{ payload: article }` with category, author, views, Unix `updated_at`, `meta`, and associated article fields; delete/reorder/bulk actions return empty `200 OK`; reorder accepts Chatwoot `positions_hash`; and bulk action errors return `422 { error }`. | `go test ./internal/handler/api/v1 -run ArticleHandler -count=1`; `go test ./internal/service ./internal/repository -run Article -count=1`; `go test ./internal/router -run Router -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; sandboxed full `go test ./...` failed only on local socket restrictions; escalated full `go test ./...` passed; `git diff --check`. Route dump is `TOTAL: 860`; tracked route parity remains `270 exact, 7 parameter-compatible, 0 missing`. | P3.7c moves to Review; continue Phase 2/3 drift audit or Phase 6 placeholder burn-down from fresh reference/smoke evidence. | | `docs: lock parity execution tracker` | Documentation-only checkpoint that makes this file the single active tracker after user confirmation. It locks direct Chatwoot frontend reuse, local `reference/chatwoot` as the only baseline, `go test ./...` first, Meilisearch as mandatory final search, and enterprise scope excluding only SSO/SAML/LDAP/OIDC. It also maps the remaining Hermes search and automation/macro/CSAT notes into the existing Phase 1/B6, B8/B9, Phase 5, B7-B12, and active P3.7 help-center rows. | `git diff --check`. No Go code changed. | Continue P3.7c help-center articles and article bulk actions; future slices update this tracker in the same commit. | @@ -531,7 +532,7 @@ This ledger records the committed parity checkpoints that future slices should b | `feat(custom-roles): align update route semantics` | Advances B10.3 CustomRole frontend parity by registering Chatwoot's Rails resource `PATCH /api/v1/accounts/:account_id/custom_roles/:id` update route used by the reused `ApiClient.update` path, while keeping the existing `PUT` alias. Custom role updates now distinguish omitted `description` from an explicit empty string, so the enterprise settings form can clear descriptions like `CustomRolesController#update` with strong params. | `go test ./internal/handler/api/v1 -run CustomRole -count=1`; `go test ./internal/service -run CustomRole -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. | | `feat(contacts): align active pagination payloads` | Advances Phase 6 dashboard CRM drift by aligning contact list/search/filter/active pagination with Chatwoot `ContactsController::RESULTS_PER_PAGE = 15` and `contacts/active.json.jbuilder`. The reused dashboard active-contacts route now returns `{ meta: { count, current_page }, payload }` using the contact serializer instead of the local `{ contacts, meta: { page, page_size } }` envelope, and selected-key `POST /contacts/:id/destroy_custom_attributes` is covered against Chatwoot's `{ payload }` response with `contact_inboxes`. | `go test ./internal/handler/api/v1 -run 'ContactHandlerCRUD' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. | | `feat(conversations): align custom attribute response` | Advances Phase 6 dashboard conversation drift by aligning `POST /api/v1/accounts/:account_id/conversations/:conversation_id/custom_attributes` with Chatwoot `custom_attributes.json.jbuilder` and the reused conversation store action. The endpoint still persists the submitted custom attributes, but the response is now only `{ custom_attributes: ... }` and no longer leaks the full local conversation serializer, `id`, or local envelope fields. | `go test ./internal/handler/api/v1 -run 'TestConversationHandlerTestSuite/TestUpdateCustomAttributes' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. | -| `feat(conversations): align transcript responses` | Advances Phase 6 dashboard conversation drift by aligning `POST /api/v1/accounts/:account_id/conversations/:conversation_id/transcript` with Chatwoot `ConversationsController#transcript` and the reused dashboard conversation API. Transcript success now returns empty `200 OK` instead of the local `{ success, data }` envelope; missing `email` returns Chatwoot `422 { error: "email param missing" }`; local email-format validation no longer rejects nonblank values that Chatwoot would pass through to the mailer. Fresh placeholder audit found only webhook handler nil-fallback `chatwootParityStub` routes, with no account/contact/conversation/message/inbox dashboard-critical stub owner left by this slice. | `go test ./internal/handler/api/v1 -run 'TestConversationHandlerTestSuite/TestTranscript' -count=1`; `go test ./internal/service -run ConversationService_SendTranscript -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. | +| `feat(conversations): align transcript responses` | Advances Phase 6 dashboard conversation drift by aligning `POST /api/v1/accounts/:account_id/conversations/:conversation_id/transcript` with Chatwoot `ConversationsController#transcript` and the reused dashboard conversation API. Transcript success now returns empty `200 OK` instead of the local `{ success, data }` envelope; missing `email` returns Chatwoot `422 { error: "email param missing" }`; local email-format validation no longer rejects nonblank values that Chatwoot would pass through to the mailer. The placeholder audit at that checkpoint found only webhook handler nil-fallback routes, with no account/contact/conversation/message/inbox dashboard-critical stub owner left by this slice; P6.1 later removed the fallback placeholder helper. | `go test ./internal/handler/api/v1 -run 'TestConversationHandlerTestSuite/TestTranscript' -count=1`; `go test ./internal/service -run ConversationService_SendTranscript -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. | | `feat(custom-fields): align chatwoot saved view payloads` | Advances P3.6 serializer/controller parity for saved views and settings attributes. Account `/custom_filters` now matches Chatwoot `CustomFiltersController`, `_custom_filter.json.jbuilder`, and dashboard `customViews.js`/store behavior: list returns a raw array, defaults to `filter_type=conversation`, scopes by current user, accepts raw frontend bodies plus nested `{ custom_filter }`, returns raw mutation payloads with no `account_id/user_id`, and uses `200 OK` for create/update plus `204` delete. Account `/custom_attribute_definitions` now matches `CustomAttributeDefinitionsController`, `_custom_attribute_definition.json.jbuilder`, dashboard `attributes.js`/store, and settings attribute enum constants: list/show/create/update return raw arrays/objects, create/update accept raw integer enum bodies plus wrappers, stored/serialized `attribute_model` values normalize to Chatwoot `conversation_attribute/contact_attribute/company_attribute`, and custom attribute value validation can find normalized and legacy definition rows. | `go test ./internal/handler/api/v1 -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/service -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/repository -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | P3.6 custom filters/custom attribute definitions move to Review; continue Phase 6 placeholder audit from fresh reference/smoke evidence. | | `feat(labels): align chatwoot label payloads` | Advances P3.6 label CRUD parity by aligning account `/labels` with Chatwoot `LabelsController`, label Jbuilder views, `Label` model/schema, and dashboard labels API/store. List now returns `{ payload }`; create/show/update return raw label payloads with `id`, `title`, `description`, `color`, and `show_on_sidebar`; create/update accept nested `{ label }` bodies and legacy raw `name`; delete returns empty `200 OK`; title normalization, default color, account scope, persisted description/sidebar flags, and local conversation/contact association cleanup are covered. | `go test ./internal/handler/api/v1 -run LabelHandler -count=1`; `go test ./internal/service -run 'TagService|LabelService' -count=1`; `go test ./internal/repository -run TagRepo -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./cmd/migrate -count=1`; `go test ./...`; `git diff --check`. | P3.6 label CRUD moves to Review; continue Phase 6 placeholder audit, then custom attributes/custom filters drift. | | `docs: refresh parity tracker baseline` | Documentation-only checkpoint after `2ab7b58 feat(accounts): align account payloads`. Refreshes the Current Baseline to the committed account payload work, records the clean handoff state, and lands the next executable P3.6 label CRUD parity contract from direct Chatwoot reference/frontend inspection. | `git diff --check`. No Go code changed. | Start `feat(labels): align chatwoot label payloads`, then continue Phase 6 placeholder audit. | @@ -2409,8 +2410,8 @@ Tracking table: | P6.3 | Conversation APIs | `docs/ROUTE_GAP_ANALYSIS.md`, conversation handlers/services | Frontend-critical filters, assignment, status, snooze, merge, bulk actions, transcript responses, and custom-attribute responses are implemented or covered by focused drift slices; continue only from fresh reference/smoke evidence. | Review | | P6.4 | Message APIs | `docs/ROUTE_GAP_ANALYSIS.md`, message handlers/services | Create/list/delete/private notes/attachments/source attribution have focused coverage; retry now matches Chatwoot status reset/content-attribute clearing and durable send-reply enqueue behavior. Continue only from fresh reference/smoke evidence. | Review | | P6.5 | Inbox APIs | `docs/ROUTE_GAP_ANALYSIS.md`, inbox handlers/services | CRUD, assignable agents, avatar, campaigns, channel settings, reset secret, working hours, members, WhatsApp Cloud health/register-webhook/sync-template, and agent-bot get/set member actions have focused Chatwoot-style coverage; continue only from fresh reference/smoke evidence. | Review | -| P6.6 | Widget/public APIs | `docs/ROUTE_GAP_ANALYSIS.md`, widget/channel provider code, `chatwootParityStub` routes | Widget/public frontend-critical route behavior is handler-backed, including public inbox flow, direct uploads/attachments, and public CSAT survey submission. | Done | -| P6.7 | Webhook ingress | `internal/router/router.go`, `internal/handler/webhook/*`, channel providers | Replace generic placeholder with provider-specific verified ingestion and dispatch. | Done | +| P6.6 | Widget/public APIs | `docs/ROUTE_GAP_ANALYSIS.md`, widget/channel provider code, `docs/parity/placeholder_audit.md` | Widget/public frontend-critical route behavior is handler-backed, including public inbox flow, direct uploads/attachments, and public CSAT survey submission. | Done | +| P6.7 | Webhook ingress | `internal/router/router.go`, `internal/handler/webhook/*`, channel providers | Provider-specific verified ingestion and dispatch are wired when handlers are configured; nil-handler guards now return explicit provider-unavailable errors instead of placeholders. | Done | Webhook ingress subtracking: @@ -2730,7 +2731,7 @@ Verification milestone gates: - 2026-06-07: P3.51 inbox-label matrix filter checkpoint prepared as `feat(reports): align inbox label matrix filters`; audited Chatwoot `V2::Reports::InboxLabelMatrixBuilder`. GoChat `/reports/inbox_label_matrix` now parses optional frontend `since/until`, `inbox_ids`, and `label_ids`, filters the returned inbox/label axes, and counts only selected account conversations in the requested date and inbox scope. Focused Analytics handler and service tests passed; escalated full `go test ./...` and `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.52 outgoing messages count builder checkpoint prepared as `feat(reports): align outgoing message counts`; audited Chatwoot v2 reports controller `outgoing_messages_count` and `V2::Reports::OutgoingMessagesCountBuilder`. GoChat invalid or blank `group_by` now returns empty `422`, and label grouping now follows Chatwoot by grouping counts on tag name and resolving the current account label id rather than returning the raw joined tag id. Focused Analytics handler and service tests passed; escalated full `go test ./...` and `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.53 first-response distribution range checkpoint prepared as `feat(reports): align first response distribution range`; audited Chatwoot `V2::Reports::FirstResponseTimeDistributionBuilder` and `DateRangeHelper#range`. GoChat `/reports/first_response_time_distribution` now accepts missing or partial `since/until` as an all-time distribution and only applies the created-at range when both params are present. Focused Analytics handler and service tests passed; escalated full `go test ./...` and `git diff --check` passed. No route artifacts change. -- 2026-06-07: Documentation checkpoint prepared as `docs: refresh placeholder audit`; refreshed Phase 6 placeholder evidence and `docs/parity/frontend_smoke_report.md`. The narrow `chatwootParityStub` scan still finds only public webhook nil-handler fallbacks, and the broader placeholder/TODO scan finds no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder. `scripts/parity_frontend_smoke.sh --check` passed after rerunning with workspace-local `GOCACHE`, `GOTMPDIR`, and `TMPDIR` because `/tmp` was full; `git diff --check` passed. Next default action remains Phase 2/3 drift audit unless the full B12 live stack is available. +- 2026-06-07: Documentation checkpoint prepared as `docs: refresh placeholder audit`; refreshed Phase 6 placeholder evidence and `docs/parity/frontend_smoke_report.md`. At that checkpoint the narrow `chatwootParityStub` scan found only public webhook nil-handler fallbacks, and the broader placeholder/TODO scan found no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder; P6.1 later removed those fallback stubs. `scripts/parity_frontend_smoke.sh --check` passed after rerunning with workspace-local `GOCACHE`, `GOTMPDIR`, and `TMPDIR` because `/tmp` was full; `git diff --check` passed. Next default action remains Phase 2/3 drift audit unless the full B12 live stack is available. - 2026-06-07: P5.8b contact import DataImportJob checkpoint prepared as `feat(crm): queue contact imports`; audited Chatwoot contacts import controller/specs plus `DataImportJob`/contact manager behavior. GoChat contact imports now return the exact missing-file `422 { error: "File is blank" }`, create pending `data_imports` with replayable CSV payloads, enqueue `contact:import` jobs on the low queue when workers are configured, and process imports asynchronously while preserving existing merge, label validation, custom-attribute, status, and counter behavior. Focused contact service and handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.54 conversation unread-count checkpoint prepared as `feat(conversations): align unread count payloads`; audited Chatwoot unread counts controller, `Conversations::UnreadCounts::Counter`, and reused dashboard `conversationUnreadCounts` store. GoChat now returns raw `{ payload }`, enforces the feature flag with exact `403` error, emits sidebar label ids, and scopes unread badge counts by visible inbox/team membership plus custom-role permission modes. Focused repository/service/handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.55 conversation meta checkpoint prepared as `feat(conversations): align meta counts`; audited Chatwoot `ConversationsController#meta`, `ConversationFinder#perform_meta_only`, `meta.json.jbuilder`, and reused dashboard conversation API/store. GoChat conversation meta now returns raw `{ meta }` with `mine_count`, `assigned_count`, `unassigned_count`, and `all_count`, defaults to open conversations, preserves meta-only assignee-type behavior, applies status/inbox/team/labels/conversation-type filters, and scopes non-admin agents by assigned inboxes. Focused repository/service/handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. @@ -2784,3 +2785,4 @@ Verification milestone gates: - 2026-06-07: P3.99 conversation destroy checkpoint prepared as `feat(conversations): align destroy job`; audited Chatwoot `ConversationsController#destroy`, `DeleteObjectJob`, and reused dashboard `ConversationApi.delete` / `deleteConversation` action. GoChat account conversation delete now returns empty `200 OK` instead of local `204`, queues a low-priority durable `conversation:delete_object` job when WorkerPool is configured, and the worker performs the existing soft delete, `conversation.deleted` dispatch, and search-index deletion; no-worker construction keeps synchronous deletion for focused tests/local callers. Focused ConversationService delete/job tests and ConversationHandler delete tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.100 dashboard app route-parameter checkpoint prepared as `fix(routes): align dashboard app ids`; audited Chatwoot `routes.rb:130`, `DashboardAppsController`, dashboard app Jbuilder views, and reused dashboard `dashboardApps.js` API/store calls. GoChat now registers dashboard app member CRUD routes with Chatwoot's `:id` parameter name, keeps `PATCH` and `PUT`, and preserves legacy local `:dashboard_app_id` parsing for focused tests and GoChat-only widget extensions. Focused DashboardAppHandler and router tests passed; route dump/parity regenerated to `TOTAL: 967` and `425 exact, 0 method-compatible, 14 parameter-compatible, 0 missing out of 439`; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. - 2026-06-07: P3.101 WhatsApp call route-parameter checkpoint prepared as `fix(routes): align whatsapp call ids`; audited Chatwoot `routes.rb:237-242` and the reused account WhatsApp call route family. GoChat now registers account-level WhatsApp call show/action routes with Chatwoot's `:id` parameter name, preserves legacy `:call_id` handler compatibility, and makes AgentCapacity nested handlers accept both Chatwoot `:agent_capacity_policy_id`/`:id` and local Gin-compatible `:id`/`:user_id`/`:limit_id` parameter names. The main router keeps AgentCapacity nested route names parameter-compatible because Gin rejects wildcard-name changes below the existing policy `/:id` member route. Focused WhatsAppCallHandler, AgentCapacityHandler, and router tests passed; route dump/parity regenerated to `TOTAL: 967` and `430 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 439`; full `go test ./...` passed outside the restricted socket sandbox after retrying the known `internal/worker` SQLite in-memory flake; `git diff --check` passed. +- 2026-06-07: P6.1 webhook placeholder fallback checkpoint prepared as `fix(webhooks): replace parity stubs`; refreshed the Phase 6 placeholder audit and burned down the remaining `chatwootParityStub` nil-handler fallbacks in `internal/router/router.go`. Public webhook routes now return explicit `503 webhook provider unavailable` JSON when a provider handler is not configured instead of `501 not implemented` placeholder bodies, and the unused placeholder helper is removed. Focused router tests cover boot and nil-handler fallback behavior; placeholder audit shows no `chatwootParityStub` product-code matches; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. diff --git a/docs/parity/placeholder_audit.md b/docs/parity/placeholder_audit.md index 06afd763..ed8293e3 100644 --- a/docs/parity/placeholder_audit.md +++ b/docs/parity/placeholder_audit.md @@ -1,6 +1,6 @@ # Phase 6 Placeholder Audit -Updated: 2026-06-06T17:53:18Z +Updated: 2026-06-07 ## Commands @@ -13,11 +13,11 @@ scripts/parity_frontend_smoke.sh --check ## Result -`chatwootParityStub` appears only in `internal/router/router.go`: 20 call sites plus the helper definition. +`chatwootParityStub` has been removed from product code. The exact scan returns no matches. -The broader placeholder scan still has no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder. Non-stub matches are test doubles, expected validation errors such as unsupported file/search/bulk-action operations, excluded SSO/SAML placeholder notes, or provider-specific nil-handler fallbacks. +The broader placeholder scan still has no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder. Non-stub matches are test doubles, expected validation errors such as unsupported file/search/bulk-action operations, excluded SSO/SAML placeholder notes, or explicit assertions that nil webhook fallbacks do not return placeholder bodies. -All call sites are nil-handler guards for public webhook registration paths: +The previously remaining call sites were nil-handler guards for public webhook registration paths: - Telegram webhook callback. - WhatsApp verify and event callbacks. @@ -28,11 +28,11 @@ All call sites are nil-handler guards for public webhook registration paths: - Instagram verify and event callbacks. - Shopify webhook callback. -No `chatwootParityStub` call site is registered on the reused dashboard account/contact/conversation/message/inbox API path, widget API path, public help-center path, public inbox/contact/conversation/message API path, or enterprise settings API path. +No `chatwootParityStub` call site remains on any reused dashboard account/contact/conversation/message/inbox API path, widget API path, public help-center path, public inbox/contact/conversation/message API path, enterprise settings API path, or webhook fallback path. ## Classification -The remaining stubs are defensive nil-handler fallbacks. In normal app bootstrap the matching provider handlers are wired and these branches do not define the route behavior. Provider-specific webhook behavior remains owned by the existing webhook/channel parity slices and future B12 or provider fixture failures. +The former defensive nil-handler fallbacks now return explicit `503 { error: "webhook provider unavailable", message: "webhook handler is not configured" }` responses instead of placeholder/not-implemented bodies. In normal app bootstrap the matching provider handlers are wired and these branches do not define the route behavior. Provider-specific webhook behavior remains owned by the existing webhook/channel parity slices and future B12 or provider fixture failures. Phase 6 is therefore in Review for the inspected placeholder surface. Reopen it only if a fresh `rg` audit or reused frontend smoke identifies a frontend-reachable `chatwootParityStub`, `not implemented`, or placeholder success response in account/contact/conversation/message/inbox/widget/public critical paths. diff --git a/internal/router/router.go b/internal/router/router.go index a8ec0099..3c5899f1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -358,7 +358,7 @@ func RegisterRoutes( tgGroup := webhookGroup.Group("/telegram") tgGroup.POST("/:bot_token", func(c *gin.Context) { if handlers == nil || handlers.TelegramWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TelegramWebhook.HandleTelegramWebhook(c) @@ -372,14 +372,14 @@ func RegisterRoutes( waGroup := webhookGroup.Group("/whatsapp") waGroup.GET("/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.WhatsAppWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.WhatsAppWebhook.HandleWhatsAppVerification(c) }) waGroup.POST("/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.WhatsAppWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.WhatsAppWebhook.HandleWhatsAppWebhook(c) @@ -392,21 +392,21 @@ func RegisterRoutes( ttGroup := webhookGroup.Group("/tiktok") ttGroup.POST("", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TikTokWebhook.HandleTikTokWebhook(c) }) ttGroup.GET("/:business_id", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TikTokWebhook.HandleTikTokVerification(c) }) ttGroup.POST("/:business_id", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TikTokWebhook.HandleTikTokWebhook(c) @@ -418,7 +418,7 @@ func RegisterRoutes( lineGroup := webhookGroup.Group("/line") lineGroup.POST("/:line_channel_id", func(c *gin.Context) { if handlers == nil || handlers.LineWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.LineWebhook.HandleLineWebhook(c) @@ -430,7 +430,7 @@ func RegisterRoutes( // Reference: Twilio SMS API https://www.twilio.com/docs/sms/api/message-resource webhookGroup.POST("/sms/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioInboundSMS(c) @@ -438,35 +438,35 @@ func RegisterRoutes( twilioGroup := webhookGroup.Group("/twilio") twilioGroup.POST("/sms/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioInboundSMS(c) }) twilioGroup.POST("/status/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c) }) twilioGroup.POST("/delivery_status", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c) }) engine.POST("/twilio/delivery_status", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c) }) engine.POST("/twilio/callback", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwilioWebhook.HandleTwilioCallback(c) @@ -485,14 +485,14 @@ func RegisterRoutes( twWebhookGroup := webhookGroup.Group("/twitter") twWebhookGroup.GET("", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwitterChannel.WebhookCRC(c) }) twWebhookGroup.POST("", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwitterChannel.WebhookEvent(c) @@ -500,14 +500,14 @@ func RegisterRoutes( // Legacy GoChat aliases kept for already configured Twitter webhooks. twWebhookGroup.GET("/webhook", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwitterChannel.WebhookCRC(c) }) twWebhookGroup.POST("/webhook", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.TwitterChannel.WebhookEvent(c) @@ -515,21 +515,21 @@ func RegisterRoutes( webhookGroup.GET("/instagram", func(c *gin.Context) { if handlers == nil || handlers.FacebookWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.FacebookWebhook.HandleInstagramVerification(c) }) webhookGroup.POST("/instagram", func(c *gin.Context) { if handlers == nil || handlers.FacebookWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.FacebookWebhook.HandleInstagramWebhook(c) }) webhookGroup.POST("/shopify", func(c *gin.Context) { if handlers == nil || handlers.ShopifyWebhook == nil { - chatwootParityStub(c) + webhookProviderUnavailable(c) return } handlers.ShopifyWebhook.HandleShopifyWebhook(c) @@ -2056,20 +2056,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", +func webhookProviderUnavailable(c *gin.Context) { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "webhook provider unavailable", + "message": "webhook handler is not configured", }) } -// --- Placeholder handlers for webhooks --- -// Full implementation deferred to P7 (channels) - -func webhookStub(c *gin.Context) { - c.JSON(200, gin.H{"message": "Webhook callback placeholder (P7)"}) -} - func healthCheck(c *gin.Context) { uptime := time.Since(startTime) c.JSON(200, gin.H{ diff --git a/internal/router/router_test.go b/internal/router/router_test.go index fed0b2cb..1b932c06 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -98,6 +98,35 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) { } } +func TestWebhookNilHandlerReturnsProviderUnavailable(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + + RegisterRoutes( + engine, + nil, + nil, + nil, + &Handlers{}, + nil, + nil, + &config.JWTConfig{}, + middleware.CORSConfig{}, + nil, + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/webhooks/telegram/bot-token", nil) + engine.ServeHTTP(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) + } + if strings.Contains(w.Body.String(), "not implemented") || strings.Contains(w.Body.String(), "placeholder") { + t.Fatalf("nil webhook fallback returned placeholder body: %s", w.Body.String()) + } +} + func TestTwilioVoiceRoutesServeConferenceAndPersistCallbacks(t *testing.T) { gin.SetMode(gin.TestMode) db, call := setupRouterTwilioVoiceDB(t)