19 KiB
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 unitupdate_time(int, minutes) ❌ — Different field name AND unitresolution_time(int, minutes) ❌ — Different unit (field name partially matches)- Missing:
only_during_business_hours❌
Issues:
- Field name mismatch: Chatwoot uses
first_response_time_threshold/next_response_time_threshold/resolution_time_threshold; GoChat usesresponse_time/update_time/resolution_time. The names are semantically different. - 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. - Missing field:
only_during_business_hoursis 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 bytypewith different enum valuesconversation_priority❌ — not present in DTOfair_distribution_limit❌ — not present in DTO or modelfair_distribution_window❌ — not present in DTO or modelenabled❌ — not present in DTO or model
Issues:
typevsassignment_order: Chatwoot usesassignment_orderas an integer enum (0=round_robin, 1=balanced in enterprise). GoChat replaces this with atypestring 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.- Missing 5 fields:
conversation_priority,fair_distribution_limit,fair_distribution_window,enabledare 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_idvsassignment_policy_id
Chatwoot assignment_policies/inboxes (AddInbox):
- No explicit params — inbox association uses
:idfrom route +inbox_idfrom nested route
GoChat AddInbox:
inbox_id(required, in JSON body) — GoChat uses POST bodyinbox_idinstead of route param
3. Response JSON Structure Comparison
SLA Policy Response
Chatwoot (from M11 spec):
{
"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:
{
"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:
- Field name mismatches:
response_timevsfirst_response_time_threshold,update_timevsnext_response_time_threshold - Unit mismatch: minutes vs seconds
- Missing field:
only_during_business_hours - Extra fields:
paused_at(not in Chatwoot SlaPolicy schema),deleted_at(soft delete exposed) - 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 orrender json:)
Assignment Policy Response
Chatwoot (from M5 spec):
{
"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:
{
"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:
typevsassignment_order: incompatible field name- Missing 6 fields:
conversation_priority,fair_distribution_limit,fair_distribution_window,enabled,assigned_inbox_count - Enum rendering: Chatwoot renders enums as strings ("round_robin", "earliest_created") via Rails enum serialization; GoChat renders
typeas a plain string which is compatible, but the field name is wrong - 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) |
⚠️ |
| Delete success | 200 (head :ok) |
204 (response.NoContent) for assignment; 200 (response.OK) for SLA |
⚠️ |
| Not found | 404 | 404 (via handleServiceError) | ✅ |
| Validation error | 422 (Rails default) | 400 (via AbortWithStatusError) | ❌ |
| Unauthorized | 403 (Pundit) | 401 (account not identified) | ❌ |
| Internal error | 500 (Rails default) | 500 (via handleServiceError fallback) | ✅ |
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) | ✅ |
| Soft delete vs hard delete | Enterprise: uses DeleteObjectJob (async) |
GORM soft delete (DeletedAt) |
⚠️ |
| List returns all policies for account | Current.account.sla_policies |
FindByAccount(accountID) |
✅ |
| Applied SLA metrics | applied_slas/metrics + applied_slas/download |
Same routes implemented | ✅ |
only_during_business_hours |
Yes (in spec) | Not implemented | ❌ |
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) |
✅ |
| Name presence validation | validates :name, presence: true |
validate:"required" on DTO |
✅ |
| fair_distribution_limit > 0 | validates :fair_distribution_limit, numericality: {greater_than: 0} |
Not present | ❌ |
| fair_distribution_window > 0 | validates :fair_distribution_window, numericality: {greater_than: 0} |
Not present | ❌ |
| Delete cascades inbox associations | has_many :inbox_assignment_policies, dependent: :destroy |
Explicit DeleteByAssignmentPolicy before Delete | ✅ |
Destroy returns head :ok (200 empty) |
Yes | GoChat returns response.NoContent (204) |
❌ |
| Inbox uniqueness constraint | validates :inbox_id, uniqueness: true (single unique index) |
uniqueIndex:idx_apinbox_inbox on inbox_id |
✅ |
| Inbox policy create = delete old + add new | Yes (remove_inbox_assignment_policy then create) | Yes (DeleteByInbox then Create in SetInboxPolicy) | ✅ |
| Inbox policy show validates existence | validate_assignment_policy checks @inbox.assignment_policy present |
Returns error if FindByInbox fails | ✅ |
6. Critical Compatibility Issues Summary
BLOCKING Issues (break API compatibility)
-
Assignment Policy route path:
/assignment_policies_v2vs/assignment_policies. Chatwoot clients will 404 on every endpoint. Must use/assignment_policiesfor the account-level CRUD routes. -
Inbox-level assignment policy route path:
/inboxes/:inbox_id/assignment_policy_v2vs/inboxes/:inbox_id/assignment_policy. Singular resource name mismatch. Must match Chatwoot'sassignment_policy(singular). -
SLA field name mismatches:
response_time/update_timevsfirst_response_time_threshold/next_response_time_threshold. Clients using Chatwoot field names will silently get zero values (fields not recognized by GoChat JSON binding). -
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. -
Assignment Policy missing fields:
assignment_order,conversation_priority,fair_distribution_limit,fair_distribution_window,enabledare all absent from GoChat's model and DTO. Chatwoot clients sending these fields will have them silently ignored; responses will lack these fields entirely. -
Validation error status code: GoChat returns 400; Chatwoot returns 422. Any client checking for 422 on validation errors will fail.
-
Inbox policy request field name:
policy_idvsassignment_policy_id.
SIGNIFICANT Issues (functional deviation but not breaking)
-
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. -
SLA missing field:
only_during_business_hoursnot implemented in GoChat. -
Create response status: GoChat returns 201; Chatwoot returns 200 for create. REST-correct but incompatible with Chatwoot convention.
-
Delete response status: GoChat uses 204 (assignment) / 200 (SLA with body); Chatwoot uses 200 empty (
head :ok). Inconsistent within GoChat itself. -
SLA
typefield vsassignment_order: GoChat uses atypestring enum with values (round_robin, fair, best_skill_match) instead of Chatwoot's integer enumassignment_order(round_robin:0). Both "fair" and "best_skill_match" are not standard Chatwoot values — they appear to be enterprise extensions or GoChat additions. -
Missing
assigned_inbox_countin response: Chatwoot's assignment policy response includesassigned_inbox_count(computed from inbox associations). GoChat doesn't include this.
ACCEPTABLE Deviations
-
SLA inbox association routes: Not present in Chatwoot public routes.rb (enterprise feature). GoChat implements them — this is correct forward-looking behavior.
-
SLA soft delete vs hard delete: Different mechanisms but functionally similar for API consumers (deleted records are no longer returned by List).
-
Applied SLA routes: GoChat implements
/applied_slas/metricsand/applied_slas/downloadmatching Chatwoot'sapplied_slascollection routes.
7. Recommendations
P0 — Must fix before any integration
-
Rename assignment policy routes: Change
/assignment_policies_v2→/assignment_policiesand/assignment_policy_v2→/assignment_policyin router.go. Keep the_v2suffix in internal naming (handler, service, model, table) but expose Chatwoot-compatible URLs. -
Align SLA field names: Rename GoChat's
response_time→first_response_time_threshold,update_time→next_response_time_thresholdin both the model and DTO JSON tags. Also addresolution_time_thresholdinstead of justresolution_time. -
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.
-
Add missing Assignment Policy fields: Add
assignment_order,conversation_priority,fair_distribution_limit,fair_distribution_window,enabledto the model and DTOs. Maptypetoassignment_orderfor backward compatibility or replacetypeentirely. -
Fix validation error status code: Return 422 instead of 400 for validation errors (both in handler validation and handleServiceError).
-
Rename
policy_id→assignment_policy_idin SetInboxPolicy request.
P1 — Should fix for full compatibility
-
Add
only_during_business_hoursto SLA model and DTOs. -
Add
assigned_inbox_countto Assignment Policy response (computed field). -
Unify delete status codes: Use 200 with empty body (
head :okpattern) for all destroy endpoints to match Chatwoot, or consistently use 204 across both SLA and assignment policy. -
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)