# SLA Policy + Assignment Policy V2 — Chatwoot API Compatibility Verification Report **Date**: 2026-05-26 **Task**: t_0c833d5c — Verify SLA policy + assignment strategy V2 implementation for 1:1 functional compatibility with Chatwoot's original API --- ## 1. Endpoint URL Comparison ### SLA Policy | # | Chatwoot Endpoint | GoChat Endpoint | Match? | Notes | |---|---|---|---|---| | 1 | `GET /api/v1/accounts/:account_id/sla_policies` | `GET /api/v1/accounts/:account_id/sla_policies` | ✅ | Identical | | 2 | `POST /api/v1/accounts/:account_id/sla_policies` | `POST /api/v1/accounts/:account_id/sla_policies` | ✅ | Identical | | 3 | `GET /api/v1/accounts/:account_id/sla_policies/:id` | `GET /api/v1/accounts/:account_id/sla_policies/:id` | ✅ | Identical | | 4 | `PUT /api/v1/accounts/:account_id/sla_policies/:id` | `PUT /api/v1/accounts/:account_id/sla_policies/:id` | ✅ | Identical | | 5 | `DELETE /api/v1/accounts/:account_id/sla_policies/:id` | `DELETE /api/v1/accounts/:account_id/sla_policies/:id` | ✅ | Identical | | 6 | *(enterprise)* SLA inbox association | `GET /sla_policies/:id/inboxes` | ⚠️ | Chatwoot public routes.rb has NO inbox association routes for SLA — this is enterprise-only. GoChat has ListInboxes/AddInbox/RemoveInbox. **Functional intent is correct** (enterprise feature), but no public reference to validate against. | | 7 | *(enterprise)* SLA inbox association | `POST /sla_policies/:id/inboxes` | ⚠️ | Same as above | | 8 | *(enterprise)* SLA inbox association | `DELETE /sla_policies/:id/inboxes/:inbox_id` | ⚠️ | Same as above | | 9 | `GET /api/v1/accounts/:account_id/applied_slas/metrics` | `GET /api/v1/accounts/:account_id/applied_slas/metrics` | ✅ | Chatwoot has `applied_slas/metrics` as collection route | | 10 | `GET /api/v1/accounts/:account_id/applied_slas/download` | `GET /api/v1/accounts/:account_id/applied_slas/download` | ✅ | Chatwoot has `applied_slas/download` as collection route | ### Assignment Policy | # | Chatwoot Endpoint | GoChat Endpoint | Match? | Notes | |---|---|---|---|---| | 1 | `GET /api/v1/accounts/:account_id/assignment_policies` | `GET /api/v1/accounts/:account_id/assignment_policies_v2` | ❌ | **CRITICAL**: GoChat uses `assignment_policies_v2` instead of `assignment_policies`. This breaks API compatibility — Chatwoot clients calling `/assignment_policies` will get 404. | | 2 | `POST /api/v1/accounts/:account_id/assignment_policies` | `POST /api/v1/accounts/:account_id/assignment_policies_v2` | ❌ | Same suffix mismatch | | 3 | `GET /api/v1/accounts/:account_id/assignment_policies/:id` | `GET /api/v1/accounts/:account_id/assignment_policies_v2/:id` | ❌ | Same suffix mismatch | | 4 | `PUT /api/v1/accounts/:account_id/assignment_policies/:id` | `PUT /api/v1/accounts/:account_id/assignment_policies_v2/:id` | ❌ | Same suffix mismatch | | 5 | `DELETE /api/v1/accounts/:account_id/assignment_policies/:id` | `DELETE /api/v1/accounts/:account_id/assignment_policies_v2/:id` | ❌ | Same suffix mismatch | | 6 | `GET /assignment_policies/:id/inboxes` | `GET /assignment_policies_v2/:id/inboxes` | ❌ | Same suffix mismatch | | 7 | `POST /assignment_policies/:id/inboxes` | `POST /assignment_policies_v2/:id/inboxes` | ❌ | Same suffix mismatch | | 8 | `DELETE /assignment_policies/:id/inboxes/:inbox_id` | `DELETE /assignment_policies_v2/:id/inboxes/:inbox_id` | ❌ | Same suffix mismatch | | 9 | `GET /inboxes/:inbox_id/assignment_policy` | `GET /inboxes/:inbox_id/assignment_policy_v2` | ❌ | **CRITICAL**: Chatwoot uses singular `assignment_policy`; GoChat uses `assignment_policy_v2`. Again breaks compatibility. | | 10 | `POST /inboxes/:inbox_id/assignment_policy` | `POST /inboxes/:inbox_id/assignment_policy_v2` | ❌ | Same suffix mismatch | | 11 | `DELETE /inboxes/:inbox_id/assignment_policy` | `DELETE /inboxes/:inbox_id/assignment_policy_v2` | ❌ | Same suffix mismatch | --- ## 2. Request Parameter Comparison ### SLA Policy Create/Update **Chatwoot (from M11 spec)**: - `name` (required, string) - `description` (optional, string) - `first_response_time_threshold` (float, seconds) - `next_response_time_threshold` (float, seconds) - `resolution_time_threshold` (float, seconds) - `only_during_business_hours` (boolean) **GoChat**: - `name` (required, string) ✅ - `description` (optional, string) ✅ - `response_time` (int, **minutes**) ❌ — Different field name AND unit - `update_time` (int, **minutes**) ❌ — Different field name AND unit - `resolution_time` (int, **minutes**) ❌ — Different unit (field name partially matches) - Missing: `only_during_business_hours` ❌ **Issues**: 1. **Field name mismatch**: Chatwoot uses `first_response_time_threshold` / `next_response_time_threshold` / `resolution_time_threshold`; GoChat uses `response_time` / `update_time` / `resolution_time`. The names are semantically different. 2. **Unit mismatch**: Chatwoot uses **seconds** (float); GoChat uses **minutes** (int). Clients sending `first_response_time_threshold: 300` (5 min in seconds) would store 300 **minutes** = 5 hours in GoChat. 3. **Missing field**: `only_during_business_hours` is not in GoChat's Create/Update DTO or model. ### Assignment Policy Create/Update **Chatwoot permitted_params**: - `name` (required, string) - `description` (optional, text) - `assignment_order` (integer, enum: round_robin:0; enterprise: balanced:1) - `conversation_priority` (integer, enum: earliest_created:0, longest_waiting:1) - `fair_distribution_limit` (integer, default 100, >0) - `fair_distribution_window` (integer, default 3600, >0) - `enabled` (boolean, default TRUE) **GoChat CreatePolicyV2Request**: - `name` (required, string) ✅ - `description` (optional, string) ✅ - `type` (required, enum: round_robin / fair / best_skill_match) ❌ — **Different field name and values** **GoChat is MISSING these fields from Chatwoot**: - `assignment_order` ❌ — replaced by `type` with different enum values - `conversation_priority` ❌ — not present in DTO - `fair_distribution_limit` ❌ — not present in DTO or model - `fair_distribution_window` ❌ — not present in DTO or model - `enabled` ❌ — not present in DTO or model **Issues**: 1. **`type` vs `assignment_order`**: Chatwoot uses `assignment_order` as an integer enum (0=round_robin, 1=balanced in enterprise). GoChat replaces this with a `type` string enum (round_robin, fair, best_skill_match). The values "fair" and "best_skill_match" don't exist in Chatwoot's public code — they may be enterprise-only, but the field name itself is incompatible. 2. **Missing 5 fields**: `conversation_priority`, `fair_distribution_limit`, `fair_distribution_window`, `enabled` are all absent from GoChat's model and DTO. These are core assignment policy fields in Chatwoot. ### Inbox Policy Association **Chatwoot inbox-level create**: - `assignment_policy_id` (required, in permitted_params) **GoChat SetInboxPolicy**: - `policy_id` (required, in JSON body) ⚠️ — Different field name: `policy_id` vs `assignment_policy_id` **Chatwoot assignment_policies/inboxes (AddInbox)**: - No explicit params — inbox association uses `:id` from route + `inbox_id` from nested route **GoChat AddInbox**: - `inbox_id` (required, in JSON body) — GoChat uses POST body `inbox_id` instead of route param --- ## 3. Response JSON Structure Comparison ### SLA Policy Response **Chatwoot (from M11 spec)**: ```json { "id": 1, "name": "Standard SLA", "description": "...", "first_response_time_threshold": 300.0, "next_response_time_threshold": 600.0, "resolution_time_threshold": 86400.0, "only_during_business_hours": false, "account_id": 1 } ``` **GoChat model JSON tags**: ```json { "id": 1, "account_id": 1, "name": "Standard SLA", "description": "...", "response_time": 5, // minutes, not seconds "update_time": 10, // minutes, different field name "resolution_time": 1440, // minutes "paused_at": null, // extra field not in Chatwoot "created_at": "...", "updated_at": "...", "deleted_at": null // extra field not in Chatwoot } ``` **Issues**: 1. **Field name mismatches**: `response_time` vs `first_response_time_threshold`, `update_time` vs `next_response_time_threshold` 2. **Unit mismatch**: minutes vs seconds 3. **Missing field**: `only_during_business_hours` 4. **Extra fields**: `paused_at` (not in Chatwoot SlaPolicy schema), `deleted_at` (soft delete exposed) 5. **Response wrapper**: GoChat wraps all responses in `{success: true, data: {...}}` — Chatwoot does NOT use this wrapper pattern for individual resource responses (it renders the object directly via Jbuilder or `render json:`) ### Assignment Policy Response **Chatwoot (from M5 spec)**: ```json { "id": 1, "name": "Default", "description": "...", "assignment_order": "round_robin", "conversation_priority": "earliest_created", "fair_distribution_limit": 5, "fair_distribution_window": 300, "enabled": true, "assigned_inbox_count": 2, "account_id": 1, "created_at": "...", "updated_at": "..." } ``` **GoChat model JSON tags**: ```json { "id": 1, "account_id": 1, "name": "Default", "description": "...", "type": "round_robin", // different field name & values // MISSING: assignment_order, conversation_priority, fair_distribution_limit, fair_distribution_window, enabled, assigned_inbox_count "created_at": "...", "updated_at": "...", "deleted_at": null } ``` **Issues**: 1. **`type` vs `assignment_order`**: incompatible field name 2. **Missing 6 fields**: `conversation_priority`, `fair_distribution_limit`, `fair_distribution_window`, `enabled`, `assigned_inbox_count` 3. **Enum rendering**: Chatwoot renders enums as strings ("round_robin", "earliest_created") via Rails enum serialization; GoChat renders `type` as a plain string which is compatible, but the field name is wrong 4. **Response wrapper**: Same `{success, data}` wrapper issue as SLA --- ## 4. Error Handling & Status Codes ### Chatwoot Patterns (from controller source) | Scenario | Chatwoot Status | GoChat Status | Match? | |---|---|---|---| | Create success | 200 (implicit render) | 201 (`response.Created`) | ⚠️ | Chatwoot returns 200 for create (Rails default); GoChat returns 201. **Deviates from Chatwoot convention but follows REST best practice.** | | Delete success | 200 (`head :ok`) | 204 (`response.NoContent`) for assignment; 200 (`response.OK`) for SLA | ⚠️ | Chatwoot uses `head :ok` (200 empty body) for destroy. GoChat assignment policy uses 204 NoContent. SLA policy uses 200 OK with body. **Inconsistent within GoChat itself, and both differ from Chatwoot.** | | Not found | 404 | 404 (via handleServiceError) | ✅ | Match | | Validation error | 422 (Rails default) | 400 (via AbortWithStatusError) | ❌ | **CRITICAL**: Chatwoot uses 422 Unprocessable Entity for validation errors; GoChat uses 400 Bad Request. | | Unauthorized | 403 (Pundit) | 401 (account not identified) | ❌ | Chatwoot uses Pundit authorization returning 403; GoChat returns 401 when account_id is missing from context. These represent different scenarios (auth vs permission). | | Internal error | 500 (Rails default) | 500 (via handleServiceError fallback) | ✅ | Match | **`handleServiceError` mapping logic**: - Contains "not found" → 404 ✅ - Contains "invalid"/"validation"/"required" → 400 ❌ (should be 422) - Everything else → 500 ✅ --- ## 5. Business Logic Comparison ### SLA Policy | Behavior | Chatwoot | GoChat | Match? | |---|---|---|---| | Delete cascades inbox associations | Yes (has_many dependent: :destroy) | Yes (explicit DeleteBySlaPolicy before Delete) | ✅ | Semantically equivalent | | Soft delete vs hard delete | Enterprise: uses `DeleteObjectJob` (async) | GORM soft delete (`DeletedAt`) | ⚠️ | Different mechanism; GoChat uses soft delete which preserves records, Chatwoot async job does hard delete | | List returns all policies for account | `Current.account.sla_policies` | `FindByAccount(accountID)` | ✅ | Match | | Applied SLA metrics | `applied_slas/metrics` + `applied_slas/download` | Same routes implemented | ✅ | Match | | `only_during_business_hours` | Yes (in spec) | Not implemented | ❌ | Missing feature | ### Assignment Policy | Behavior | Chatwoot | GoChat | Match? | |---|---|---|---| | Name uniqueness scoped to account | `validates :name, uniqueness: {scope: :account_id}` | `uniqueIndex:idx_apv2_account_name` on (account_id, name) | ✅ | Match at DB level | | Name presence validation | `validates :name, presence: true` | `validate:"required"` on DTO | ✅ | Match | | fair_distribution_limit > 0 | `validates :fair_distribution_limit, numericality: {greater_than: 0}` | Not present | ❌ | Missing validation | | fair_distribution_window > 0 | `validates :fair_distribution_window, numericality: {greater_than: 0}` | Not present | ❌ | Missing validation | | Delete cascades inbox associations | `has_many :inbox_assignment_policies, dependent: :destroy` | Explicit DeleteByAssignmentPolicy before Delete | ✅ | Semantically equivalent | | Destroy returns `head :ok` (200 empty) | Yes | GoChat returns `response.NoContent` (204) | ❌ | Different status code | | Inbox uniqueness constraint | `validates :inbox_id, uniqueness: true` (single unique index) | `uniqueIndex:idx_apinbox_inbox` on inbox_id | ✅ | Match at DB level | | Inbox policy create = delete old + add new | Yes (remove_inbox_assignment_policy then create) | Yes (DeleteByInbox then Create in SetInboxPolicy) | ✅ | Match — upsert behavior correctly implemented | | Inbox policy show validates existence | `validate_assignment_policy` checks `@inbox.assignment_policy` present | Returns error if FindByInbox fails | ✅ | Match | --- ## 6. Critical Compatibility Issues Summary ### BLOCKING Issues (break API compatibility) 1. **Assignment Policy route path**: `/assignment_policies_v2` vs `/assignment_policies`. Chatwoot clients will 404 on every endpoint. Must use `/assignment_policies` for the account-level CRUD routes. 2. **Inbox-level assignment policy route path**: `/inboxes/:inbox_id/assignment_policy_v2` vs `/inboxes/:inbox_id/assignment_policy`. Singular resource name mismatch. Must match Chatwoot's `assignment_policy` (singular). 3. **SLA field name mismatches**: `response_time`/`update_time` vs `first_response_time_threshold`/`next_response_time_threshold`. Clients using Chatwoot field names will silently get zero values (fields not recognized by GoChat JSON binding). 4. **SLA unit mismatch**: GoChat stores/returns minutes; Chatwoot uses seconds. A client sending `first_response_time_threshold: 300` (5 min in seconds) will result in 300 minutes stored in GoChat — a 60x error. 5. **Assignment Policy missing fields**: `assignment_order`, `conversation_priority`, `fair_distribution_limit`, `fair_distribution_window`, `enabled` are all absent from GoChat's model and DTO. Chatwoot clients sending these fields will have them silently ignored; responses will lack these fields entirely. 6. **Validation error status code**: GoChat returns 400; Chatwoot returns 422. Any client checking for 422 on validation errors will fail. 7. **Inbox policy request field name**: `policy_id` vs `assignment_policy_id`. ### SIGNIFICANT Issues (functional deviation but not breaking) 8. **Response wrapper**: GoChat wraps all responses in `{success, data}` envelope. Chatwoot renders resources directly without envelope. This is a consistent GoChat pattern but differs from Chatwoot. 9. **SLA missing field**: `only_during_business_hours` not implemented in GoChat. 10. **Create response status**: GoChat returns 201; Chatwoot returns 200 for create. REST-correct but incompatible with Chatwoot convention. 11. **Delete response status**: GoChat uses 204 (assignment) / 200 (SLA with body); Chatwoot uses 200 empty (`head :ok`). Inconsistent within GoChat itself. 12. **SLA `type` field vs `assignment_order`**: GoChat uses a `type` string enum with values (round_robin, fair, best_skill_match) instead of Chatwoot's integer enum `assignment_order` (round_robin:0). Both "fair" and "best_skill_match" are not standard Chatwoot values — they appear to be enterprise extensions or GoChat additions. 13. **Missing `assigned_inbox_count` in response**: Chatwoot's assignment policy response includes `assigned_inbox_count` (computed from inbox associations). GoChat doesn't include this. ### ACCEPTABLE Deviations 14. **SLA inbox association routes**: Not present in Chatwoot public routes.rb (enterprise feature). GoChat implements them — this is correct forward-looking behavior. 15. **SLA soft delete vs hard delete**: Different mechanisms but functionally similar for API consumers (deleted records are no longer returned by List). 16. **Applied SLA routes**: GoChat implements `/applied_slas/metrics` and `/applied_slas/download` matching Chatwoot's `applied_slas` collection routes. --- ## 7. Recommendations ### P0 — Must fix before any integration 1. **Rename assignment policy routes**: Change `/assignment_policies_v2` → `/assignment_policies` and `/assignment_policy_v2` → `/assignment_policy` in router.go. Keep the `_v2` suffix in internal naming (handler, service, model, table) but expose Chatwoot-compatible URLs. 2. **Align SLA field names**: Rename GoChat's `response_time` → `first_response_time_threshold`, `update_time` → `next_response_time_threshold` in both the model and DTO JSON tags. Also add `resolution_time_threshold` instead of just `resolution_time`. 3. **Align SLA time units**: Switch from minutes to seconds (float) to match Chatwoot. Or add a conversion layer that accepts seconds from clients and stores in a compatible format. 4. **Add missing Assignment Policy fields**: Add `assignment_order`, `conversation_priority`, `fair_distribution_limit`, `fair_distribution_window`, `enabled` to the model and DTOs. Map `type` to `assignment_order` for backward compatibility or replace `type` entirely. 5. **Fix validation error status code**: Return 422 instead of 400 for validation errors (both in handler validation and handleServiceError). 6. **Rename `policy_id` → `assignment_policy_id`** in SetInboxPolicy request. ### P1 — Should fix for full compatibility 7. **Add `only_during_business_hours`** to SLA model and DTOs. 8. **Add `assigned_inbox_count`** to Assignment Policy response (computed field). 9. **Unify delete status codes**: Use 200 with empty body (`head :ok` pattern) for all destroy endpoints to match Chatwoot, or consistently use 204 across both SLA and assignment policy. 10. **Consider response envelope**: Evaluate whether the `{success, data}` wrapper should be removed for Chatwoot API compatibility, or whether it's a deliberate GoChat design choice. --- ## 8. Test Coverage Assessment No tests were found or run for SLA Policy or Assignment Policy V2 modules. Test files should be created to verify: - Endpoint routing matches Chatwoot URL patterns - Request parameter binding (field names, types, validation) - Response JSON structure (field names, values, status codes) - Business logic (name uniqueness, inbox uniqueness, cascade deletes, upsert behavior) - Error handling (422 for validation, 404 for not found)