second commit

This commit is contained in:
Rogee
2026-06-04 15:44:48 +08:00
parent 4db6efb3a7
commit 8ac150bc7b
1275 changed files with 286124 additions and 0 deletions
@@ -0,0 +1,321 @@
# 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)
@@ -0,0 +1,391 @@
# V5 Verification Report: CustomAttributes + CustomFilters
## Executive Summary
GoChat's CustomAttributes and CustomFilters implementation has **significant compatibility gaps** with Chatwoot's API. The core CRUD operations for custom_attribute_definitions and custom_filters are structurally present, but there are critical differences in HTTP methods, response formats, request field names, missing model fields, and fundamentally different custom attribute value endpoint designs. This report identifies all discrepancies organized by severity.
**Compatibility Score: ~55%** — 6 of 14 Chatwoot endpoints are structurally present with compatible semantics. The remaining 8 are either missing, use incompatible methods/routes, or have significant field-level differences.
---
## 1. Route & HTTP Method Comparison
### 1.1 Custom Attribute Definitions (5 endpoints)
| # | Operation | Chatwoot Route | Chatwoot Method | GoChat Route | GoChat Method | Compatible? |
|---|-----------|---------------|-----------------|-------------|---------------|-------------|
| 1 | List | `/api/v1/accounts/:id/custom_attribute_definitions` | GET | `/api/v1/accounts/:id/custom_attribute_definitions` | GET | ✅ Route OK, ⚠️ Response format differs |
| 2 | Show | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | GET | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | GET | ✅ Route OK, ⚠️ Response format differs |
| 3 | Create | `/api/v1/accounts/:id/custom_attribute_definitions` | POST | `/api/v1/accounts/:id/custom_attribute_definitions` | POST | ⚠️ Route OK, status code differs (200 vs 201) |
| 4 | Update | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | **PATCH** | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | **PUT** | ❌ HTTP method mismatch |
| 5 | Delete | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | DELETE | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | DELETE | ⚠️ Route OK, status code differs (200 vs 204) |
### 1.2 Custom Filters (5 endpoints)
| # | Operation | Chatwoot Route | Chatwoot Method | GoChat Route | GoChat Method | Compatible? |
|---|-----------|---------------|-----------------|-------------|---------------|-------------|
| 1 | List | `/api/v1/accounts/:id/custom_filters` | GET | `/api/v1/accounts/:id/custom_filters` | GET | ✅ Route OK |
| 2 | Show | `/api/v1/accounts/:id/custom_filters/:id` | GET | `/api/v1/accounts/:id/custom_filters/:id` | GET | ✅ Route OK |
| 3 | Create | `/api/v1/accounts/:id/custom_filters` | POST | `/api/v1/accounts/:id/custom_filters` | POST | ⚠️ Status differs (200 vs 201) |
| 4 | Update | `/api/v1/accounts/:id/custom_filters/:id` | **PATCH** | `/api/v1/accounts/:id/custom_filters/:id` | **PUT** | ❌ HTTP method mismatch |
| 5 | Delete | `/api/v1/accounts/:id/custom_filters/:id` | DELETE | `/api/v1/accounts/:id/custom_filters/:id` | DELETE | ⚠️ Status differs (200 {} vs 204) |
### 1.3 Custom Attribute Values — Conversations (3 endpoints) ❌ ALL MISSING/DIFFERENT
| # | Operation | Chatwoot Route | Chatwoot Method | GoChat Route | GoChat Method | Compatible? |
|---|-----------|---------------|-----------------|-------------|---------------|-------------|
| 1 | Set (collection) | `/api/v1/accounts/:id/conversations/set_custom_attributes` | POST | `/api/v1/accounts/:id/conversations/:conv_id/custom_attributes` | POST | ❌ Route structure fundamentally different |
| 2 | Set (collection alt) | `/api/v1/accounts/:id/conversations/custom_attributes` | POST | — (same as above) | — | ❌ Duplicate Chatwoot route not present |
| 3 | Destroy | `/api/v1/accounts/:id/conversations/destroy_custom_attributes` | **POST** | `/api/v1/accounts/:id/conversations/:conv_id/custom_attributes/:attr_name` | **DELETE** | ❌ Method + route + payload all differ |
### 1.4 Custom Attribute Values — Contacts (2 endpoints)
| # | Operation | Chatwoot Route | Chatwoot Method | GoChat Route | GoChat Method | Compatible? |
|---|-----------|---------------|-----------------|-------------|---------------|-------------|
| 1 | Destroy | `/api/v1/accounts/:id/contacts/:contact_id/destroy_custom_attributes` | **POST** | `/api/v1/accounts/:id/contacts/:contact_id/custom_attributes/:attr_name` | **DELETE** | ❌ Method + route differ |
| 2 | Set | No direct Chatwoot route (set via contact update) | — | `/api/v1/accounts/:id/contacts/:contact_id/custom_attributes` | POST | ⚠️ GoChat-only route |
### 1.5 Custom Attribute Values — Companies (1 endpoint) ❌ MISSING
| # | Operation | Chatwoot Route | Chatwoot Method | GoChat Route | GoChat Method | Compatible? |
|---|-----------|---------------|-----------------|-------------|---------------|-------------|
| 1 | Destroy | `/api/v1/accounts/:id/companies/:company_id/destroy_custom_attributes` | POST | — | — | ❌ Completely missing |
---
## 2. Request/Response Parameter Differences
### 2.1 CustomAttributeDefinition — Field Name Mapping
| Chatwoot Field | Chatwoot Type | GoChat Field | GoChat Type | Compatible? |
|---------------|--------------|-------------|------------|-------------|
| `attribute_key` | string | `attribute_name` | string | ⚠️ JSON key differs (`attribute_key` vs `attribute_name`) |
| `attribute_display_name` | string | `attribute_display_name` | string | ✅ Same |
| `attribute_display_type` | integer enum (0=text, 1=number, 2=date, 3=checkbox, 4=list, 5=link) | `attribute_type` | string ("text"/"number"/"date"/"checkbox"/"list"/"link") | ⚠️ Name + type representation differ |
| `attribute_model` | integer enum (0=conversation_attribute, 1=contact_attribute) | `attribute_model` | string ("conversation"/"contact") | ⚠️ Type representation differs |
| `attribute_description` | text | `description` | text | ⚠️ JSON key differs |
| `attribute_values` | jsonb array | — | — | ❌ Missing in GoChat |
| `default_value` | string | `default_value` | jsonb | ⚠️ Type differs (string vs jsonb) |
| `regex_pattern` | string | — | — | ❌ Missing in GoChat |
| `regex_cue` | string | — | — | ❌ Missing in GoChat |
| `account_id` | bigint | `account_id` | uint | ✅ Same |
| `created_at` | datetime | `created_at` | time.Time | ✅ Same |
| `updated_at` | datetime | `updated_at` | time.Time | ✅ Same |
**Create Request DTO:**
| Chatwoot permitted_params | GoChat CreateCustomAttributeDefinitionRequest |
|---|---|
| `attribute_display_name` ✅ | `attribute_display_name` ✅ |
| `attribute_description` | `description` ⚠️ renamed |
| `attribute_display_type` | `attribute_type` ⚠️ renamed + type changed |
| `attribute_key` | `attribute_name` ⚠️ renamed |
| `attribute_model` ⚠️ type changed | `attribute_model` ⚠️ type changed |
| `regex_pattern` ❌ missing | — |
| `regex_cue` ❌ missing | — |
| `attribute_values[]` ❌ missing | — |
**Update Request DTO:**
| Chatwoot permitted_params | GoChat UpdateCustomAttributeDefinitionRequest |
|---|---|
| `attribute_display_name` ✅ | `attribute_display_name` ✅ |
| `attribute_description` | `description` ⚠️ renamed |
| `attribute_display_type` | `attribute_type` ⚠️ renamed + type changed |
| `regex_pattern` ❌ missing | — |
| `regex_cue` ❌ missing | — |
| `attribute_values[]` ❌ missing | — |
Both correctly exclude `attribute_key`/`attribute_name` and `attribute_model` from update — this is consistent.
### 2.2 CustomFilter — Field Name Mapping
| Chatwoot Field | Chatwoot Type | GoChat Field | GoChat Type | Compatible? |
|---------------|--------------|-------------|------------|-------------|
| `name` | string | `name` | string | ✅ Same |
| `filter_type` | integer enum (0=conversation, 1=contact, 2=report) | `filter_type` | string ("conversation"/"contact"/"inbox") | ⚠️ Type representation differs; Chatwoot has "report", GoChat has "inbox" |
| `query` | jsonb | `query` | jsonb | ✅ Same |
| `user_id` | bigint | `created_by_id` | uint | ⚠️ JSON key differs |
| `account_id` | bigint | `account_id` | uint | ✅ Same |
**Create/Update Request DTO:** Consistent between both — `{name, filter_type, query}`. Just the filter_type representation differs.
### 2.3 Custom Attribute Values — Request Payload Structure
**Chatwoot Set:**
```json
POST /conversations/set_custom_attributes
{
"id": 42,
"custom_attributes": {
"priority": "high",
"ticket_id": 12345
}
}
```
**GoChat Set:**
```json
POST /conversations/:conversation_id/custom_attributes
{
"attribute_name": "priority",
"value": "high"
}
```
Key differences:
- Chatwoot sends multiple attributes in one request; GoChat sends one attribute per request
- Chatwoot includes the entity `id` in the body; GoChat uses URL path parameter
- Chatwoot uses `custom_attributes` object; GoChat uses flat `{attribute_name, value}`
**Chatwoot Destroy:**
```json
POST /conversations/destroy_custom_attributes
{
"id": 42,
"custom_attributes": ["priority", "ticket_id"]
}
```
**GoChat Remove:**
```json
DELETE /conversations/:conversation_id/custom_attributes/priority
(no body)
```
Key differences:
- Chatwoot uses POST method; GoChat uses DELETE
- Chatwoot removes multiple keys in one request; GoChat removes one key per request
- Chatwoot sends entity id + key list in body; GoChat uses URL path params only
---
## 3. Response Format Differences
### 3.1 Unified Envelope vs Raw JSON
**Chatwoot:** Returns raw model JSON directly:
```json
{
"id": 1,
"attribute_key": "priority",
"attribute_display_name": "Priority",
...
}
```
**GoChat:** Wraps everything in `{success, data, error, meta}` envelope:
```json
{
"success": true,
"data": {
"id": 1,
"attribute_name": "priority",
"attribute_display_name": "Priority",
...
}
}
```
For list endpoints, GoChat adds pagination metadata:
```json
{
"success": true,
"data": [...],
"meta": {
"page": 1,
"per_page": 25,
"total_count": 100
}
}
```
Chatwoot pagination metadata is sent in different headers/fields. This is a fundamental format incompatibility.
### 3.2 Status Code Differences
| Operation | Chatwoot Status | GoChat Status | Match? |
|-----------|----------------|---------------|--------|
| Create (definition & filter) | 200 OK | 201 Created | ❌ |
| Update (definition & filter) | 200 OK | 200 OK | ✅ (method differs though) |
| Delete (definition & filter) | 200 OK with `{}` | 204 No Content | ❌ |
| List/Show | 200 OK | 200 OK | ✅ (but envelope differs) |
| Validation error | 422 Unprocessable Entity | 400 Bad Request | ⚠️ |
| Not found | 404 Not Found | 404 Not Found | ✅ |
| Unauthorized | 401 Unauthorized | Depends on middleware | ⚠️ |
---
## 4. Business Logic Differences
### 4.1 Sort Order
| Module | Chatwoot Default Sort | GoChat Default Sort |
|--------|----------------------|--------------------|
| Custom Attribute Definitions | `created_at DESC` (newest first) | `id ASC` (oldest first) |
| Custom Filters | `created_at DESC` (newest first) | `id ASC` (oldest first) |
**Impact:** Clients expecting newest-first ordering will see reversed results.
### 4.2 Pagination Parameters
Both use `page` and `per_page` query parameters with default 25 per page. This is compatible. ✅
### 4.3 Attribute Model Values
| Chatwoot Enum | Chatwoot Value | GoChat String |
|---------------|---------------|---------------|
| conversation_attribute | 0 | "conversation" |
| contact_attribute | 1 | "contact" |
Chatwoot uses numeric enum values in JSON; GoChat uses human-readable strings. Clients that parse Chatwoot integer enums will break.
### 4.4 Filter Type Values
| Chatwoot Enum | Chatwoot Value | GoChat String |
|---------------|---------------|---------------|
| conversation | 0 | "conversation" |
| contact | 1 | "contact" |
| report/inbox | 2 | "inbox" |
Chatwoot's `filter_type=2` historically maps to "report" in older versions. GoChat uses "inbox" directly. The semantic mapping is unclear — Chatwoot may have both "report" and "inbox" as separate types in newer versions.
### 4.5 Account Scope Validation
Both systems validate that resources belong to the requesting account. ✅ Consistent.
### 4.6 Soft Delete
Both systems use soft delete for custom_attribute_definitions and custom_filters. ✅ Consistent.
### 4.7 Value Type Validation
GoChat validates that attribute values match their `attribute_type` (text, number, etc.) through `validateValueType()`. Chatwoot also validates types. ✅ Consistent in concept, but implementation differs since GoChat uses string type names vs Chatwoot integer enum values.
---
## 5. Missing Features
### 5.1 Missing Model Fields (CustomAttributeDefinition)
| Field | Chatwoot Purpose | GoChat Status |
|-------|-----------------|---------------|
| `attribute_values` | Predefined valid values for list-type attributes | ❌ Not modeled |
| `regex_pattern` | Regex validation pattern for attribute values | ❌ Not modeled |
| `regex_cue` | Human-readable hint about the regex constraint | ❌ Not modeled |
**Impact:** Without `attribute_values`, list-type attributes have no predefined options. Without `regex_pattern`/`regex_cue`, there's no input validation beyond type checking.
### 5.2 Missing Endpoints
| Endpoint | Chatwoot Purpose | GoChat Status |
|----------|-----------------|---------------|
| `POST /conversations/set_custom_attributes` | Batch set attrs on a conversation (collection route) | ❌ Replaced with per-entity nested route |
| `POST /conversations/destroy_custom_attributes` | Batch remove attrs from a conversation | ❌ Replaced with DELETE per-key route |
| `POST /conversations/custom_attributes` | Alternative set route | ❌ Missing |
| `POST /contacts/:id/destroy_custom_attributes` | Batch remove attrs from a contact | ❌ Replaced with DELETE per-key route |
| `POST /companies/:id/destroy_custom_attributes` | Remove attrs from a company | ❌ Completely missing (no companies support) |
### 5.3 Missing Behavior
| Behavior | Chatwoot | GoChat |
|----------|----------|--------|
| Batch set/remove | Multiple attributes in single request | One attribute per request |
| Companies custom attributes | Supported | Not supported at all |
| PATCH for partial updates | Standard PATCH method | Uses PUT (full replacement semantics expected) |
---
## 6. Test Results
All existing GoChat tests pass:
| Test Suite | Tests | Status |
|-----------|-------|--------|
| CustomFilter Handler | 9 | ✅ PASS |
| CustomAttributeDefinition Handler | ~20 | ✅ PASS |
| CustomFilter Repo | 6 | ✅ PASS |
| CustomAttributeDefinition Repo | ~9 | ✅ PASS |
| CustomFilter Service | 9 | ✅ PASS |
| CustomAttributeDefinition Service | ~8 | ✅ PASS |
| CustomAttributeValue Service | ~6 | ✅ PASS |
These tests verify GoChat's own behavior is internally consistent, but they do not test Chatwoot API compatibility.
---
## 7. Summary of All Discrepancies by Severity
### CRITICAL (API-breaking for Chatwoot clients)
1. **PUT vs PATCH** — Both custom_attribute_definitions and custom_filters use PUT instead of PATCH for updates. Chatwoot clients sending PATCH requests will get 404/method-not-allowed.
2. **Custom attribute value routes fundamentally different** — Chatwoot uses collection routes (`/conversations/set_custom_attributes`) with batch payload; GoChat uses nested per-entity routes (`/conversations/:id/custom_attributes`) with single-key payload. A Chatwoot client cannot use GoChat's value endpoints without rewriting the request logic entirely.
3. **POST vs DELETE for attribute removal** — Chatwoot uses `POST /destroy_custom_attributes` with a list of keys; GoChat uses `DELETE /conversations/:id/custom_attributes/:key` with no body.
4. **Response envelope** — Chatwoot returns raw JSON; GoChat wraps in `{success, data, error, meta}`. A Chatwoot client parsing response data directly will fail on GoChat.
5. **Missing companies custom attributes** — No support for setting/removing custom attributes on companies.
### HIGH (Semantically significant but potentially tolerable with client adaptation)
6. **attribute_key vs attribute_name** — Different JSON field name for the primary identifier.
7. **attribute_display_type vs attribute_type** — Different name AND type representation (integer enum vs string).
8. **attribute_description vs description** — Different JSON field name.
9. **user_id vs created_by_id** — Different JSON field name in custom_filter responses.
10. **filter_type type mismatch** — Integer enum vs string representation across the entire custom_filters API.
11. **Missing attribute_values, regex_pattern, regex_cue** — Three Chatwoot model fields not present in GoChat, limiting validation capabilities.
12. **Sort order** — `id ASC` vs `created_at DESC` gives reversed listing results.
13. **Status codes** — Create returns 201 vs Chatwoot's 200; Delete returns 204 vs Chatwoot's 200 with `{}`.
### MEDIUM (Minor differences)
14. **attribute_model type mismatch** — Integer enum vs string for the model type (conversation/contact).
15. **default_value type** — String in Chatwoot vs jsonb in GoChat.
16. **Batch vs single-key operations** — Chatwoot supports setting/removing multiple attributes per request; GoChat requires one attribute per request.
17. **Validation error status** — 422 in Chatwoot vs 400 in GoChat.
---
## 8. Recommended Remediation Actions
1. **Add PATCH support** alongside PUT for update endpoints (or switch to PATCH). This is the most common Chatwoot compatibility issue.
2. **Add Chatwoot-compatible custom attribute value routes** as aliases:
- `POST /accounts/:id/conversations/set_custom_attributes` (batch set)
- `POST /accounts/:id/conversations/destroy_custom_attributes` (batch remove)
- `POST /accounts/:id/contacts/:id/destroy_custom_attributes` (batch remove)
- `POST /accounts/:id/companies/:id/destroy_custom_attributes` (if companies are supported)
3. **Add missing model fields**: `attribute_values`, `regex_pattern`, `regex_cue` to CustomAttributeDefinition.
4. **Rename JSON fields for Chatwoot compatibility** or add dual serialization:
- `attribute_key` (instead of `attribute_name`)
- `attribute_display_type` (instead of `attribute_type`)
- `attribute_description` (instead of `description`)
- `user_id` (instead of `created_by_id`)
5. **Change sort order** from `id ASC` to `created_at DESC` for both list endpoints.
6. **Align status codes**:
- Create: return 200 instead of 201
- Delete: return 200 with empty object `{}` instead of 204
7. **Consider optional raw JSON response mode** (without envelope) for Chatwoot client compatibility, or at minimum document that GoChat uses an envelope format.
---
*Report generated: 2026-05-26*
*GoChat project path: /home/yanghao05/Workspace/gochat*
*Chatwoot reference: routes.rb + custom_attribute_definition.rb + custom_filter.rb models*
@@ -0,0 +1,323 @@
# V5 Verification Report: CustomAttributes + CustomFilters
**Task**: t_d8a04b98 — 1:1 API compatibility check with Chatwoot
**Date**: 2026-05-26
**Status**: NOT COMPATIBLE — Multiple critical and moderate gaps found
---
## Executive Summary
GoChat's CustomAttributeDefinition, CustomAttributeValue, and CustomFilter modules implement functional CRUD operations but have **15 discrepancies** against the Chatwoot API. These range from critical (missing endpoints, incompatible field names) to moderate (HTTP method/status code differences) to minor (response envelope difference, which may be acceptable for GoChat's design).
---
## 1. Route & HTTP Method Comparison
### Custom Attribute Definitions
| # | Operation | Chatwoot Method | Chatwoot Path | GoChat Method | GoChat Path | Compatible? |
|---|-----------|-----------------|---------------|---------------|-------------|-------------|
| 1 | List | GET | `/api/v1/accounts/:id/custom_attribute_definitions` | GET | `/api/v1/accounts/:id/custom_attribute_definitions` | YES |
| 2 | Create | POST | `/api/v1/accounts/:id/custom_attribute_definitions` | POST | `/api/v1/accounts/:id/custom_attribute_definitions` | YES |
| 3 | Show | GET | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | GET | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | YES |
| 4 | Update | PATCH | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | PUT | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | NO — method mismatch |
| 5 | Delete | DELETE | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | DELETE | `/api/v1/accounts/:id/custom_attribute_definitions/:id` | YES |
### Custom Filters
| # | Operation | Chatwoot Method | Chatwoot Path | GoChat Method | GoChat Path | Compatible? |
|---|-----------|-----------------|---------------|---------------|-------------|-------------|
| 6 | List | GET | `/api/v1/accounts/:id/custom_filters` | GET | `/api/v1/accounts/:id/custom_filters` | YES |
| 7 | Create | POST | `/api/v1/accounts/:id/custom_filters` | POST | `/api/v1/accounts/:id/custom_filters` | YES |
| 8 | Show | GET | `/api/v1/accounts/:id/custom_filters/:id` | GET | `/api/v1/accounts/:id/custom_filters/:id` | YES |
| 9 | Update | PATCH | `/api/v1/accounts/:id/custom_filters/:id` | PUT | `/api/v1/accounts/:id/custom_filters/:id` | NO — method mismatch |
| 10 | Delete | DELETE | `/api/v1/accounts/:id/custom_filters/:id` | DELETE | `/api/v1/accounts/:id/custom_filters/:id` | YES |
### Custom Attribute Values — Conversations
| # | Operation | Chatwoot Method | Chatwoot Path | GoChat Method | GoChat Path | Compatible? |
|---|-----------|-----------------|---------------|---------------|-------------|-------------|
| 11 | Set (collection) | POST | `/api/v1/accounts/:id/conversations/set_custom_attributes` | POST | `/api/v1/accounts/:id/conversations/:conv_id/custom_attributes` | NO — route structure differs |
| 12 | Set (collection alt) | POST | `/api/v1/accounts/:id/conversations/custom_attributes` | — | MISSING | NO — missing endpoint |
| 13 | Destroy (collection) | POST | `/api/v1/accounts/:id/conversations/destroy_custom_attributes` | DELETE | `/api/v1/accounts/:id/conversations/:conv_id/custom_attributes/:attr_name` | NO — route + method differ |
### Custom Attribute Values — Contacts
| # | Operation | Chatwoot Method | Chatwoot Path | GoChat Method | GoChat Path | Compatible? |
|---|-----------|-----------------|---------------|---------------|-------------|-------------|
| 14 | Set | (not a separate route in Chatwoot) | — | POST | `/api/v1/accounts/:id/contacts/:contact_id/custom_attributes` | GoChat-only route |
| 15 | Destroy | POST | `/api/v1/accounts/:id/contacts/:contact_id/destroy_custom_attributes` | DELETE | `/api/v1/accounts/:id/contacts/:contact_id/custom_attributes/:attr_name` | NO — route + method differ |
### Custom Attribute Values — Companies
| # | Operation | Chatwoot Method | Chatwoot Path | GoChat Method | GoChat Path | Compatible? |
|---|-----------|-----------------|---------------|---------------|-------------|-------------|
| 16 | Destroy | POST | `/api/v1/accounts/:id/companies/:id/destroy_custom_attributes` | — | MISSING | NO — missing endpoint |
---
## 2. Request Parameter Comparison
### Custom Attribute Definitions — Create
| Chatwoot permitted_param | GoChat request field | JSON key match? | Notes |
|---|---|---|---|
| `attribute_display_name` | `AttributeDisplayName` | YES | `attribute_display_name` in both |
| `attribute_description` | `Description` | NO | Chatwoot: `attribute_description`; GoChat: `description` |
| `attribute_display_type` | `AttributeType` | NO | Chatwoot: `attribute_display_type` (enum integer); GoChat: `attribute_type` (string) |
| `attribute_key` | `AttributeName` | NO | Chatwoot: `attribute_key`; GoChat: `attribute_name` |
| `attribute_model` | `AttributeModel` | SEMI | Same JSON key but different value format (see §3) |
| `regex_pattern` | — | MISSING | Not in GoChat |
| `regex_cue` | — | MISSING | Not in GoChat |
| `attribute_values` | — | MISSING | Not in GoChat |
### Custom Attribute Definitions — Update
Chatwoot permitted_params: `attribute_display_name, attribute_description, attribute_display_type, attribute_key, attribute_model, regex_pattern, regex_cue, attribute_values`
GoChat UpdateCustomAttributeDefinitionRequest: `attribute_display_name, attribute_type, description`
- **Missing in GoChat update**: `attribute_key` (renamed to `attribute_name`), `attribute_model`, `regex_pattern`, `regex_cue`, `attribute_values`
- **Note**: GoChat update does not allow changing `attribute_name` or `attribute_model`; Chatwoot does allow changing `attribute_key` and `attribute_model`
### Custom Filters — Create
| Chatwoot permitted_param | GoChat request field | JSON key match? | Notes |
|---|---|---|---|
| `name` | `Name` | YES | `name` in both |
| `filter_type` | `FilterType` | SEMI | Same key, different value format (see §3) |
| `query` | `Query` | YES | JSON object in both |
### Custom Attribute Values — Set on Conversation
| Chatwoot request | GoChat request | Match? | Notes |
|---|---|---|---|
| `{ custom_attribute: { attribute_name: value } }` | `{ attribute_name: "...", value: ... }` | NO | Different request structure |
Chatwoot sends a nested hash under `custom_attribute` key in the body. GoChat sends a flat `{ attribute_name, value }`.
---
## 3. Field Value Format Differences
### attribute_display_type / attribute_type
- **Chatwoot**: Integer enum — `0=text, 1=number, 2=date, 3=checkbox, 4=list, 5=link`
- **GoChat**: String — `"text"`, `"number"`, `"date"`, `"checkbox"`, `"list"`, `"link"`
- **Impact**: API consumers sending integer values (as Chatwoot docs specify) will get validation errors from GoChat. This is a **compatibility-breaking** difference.
### attribute_model
- **Chatwoot**: Integer enum — `0=conversation_attribute, 1=contact_attribute`
- **GoChat**: String — `"conversation"`, `"contact"`
- **Impact**: Same as above — Chatwoot clients sending integer `0`/`1` will fail against GoChat.
### filter_type
- **Chatwoot**: Integer enum — `0=conversation, 1=contact, 2=report` (older: `inbox`)
- **GoChat**: String — `"conversation"`, `"contact"`, `"inbox"`
- **Impact**: Same pattern. Additionally, Chatwoot has `report` (value 2) where GoChat has `inbox`. The value set differs.
---
## 4. Response Format Comparison
### Response Envelope
- **Chatwoot**: Returns raw model JSON directly. Example:
```json
{
"id": 1,
"attribute_display_name": "Priority",
"attribute_key": "priority",
...
}
```
- **GoChat**: Wraps everything in `{success, data, error, meta}` envelope:
```json
{
"success": true,
"data": {
"id": 1,
"attribute_display_name": "Priority",
"attribute_name": "priority",
...
}
}
```
- **Impact**: Chatwoot clients expecting raw JSON at the top level will need to parse `data` field instead. This may be acceptable if GoChat intentionally uses a unified envelope, but it breaks strict Chatwoot API compatibility.
### List Response Pagination
- **Chatwoot**: Pagination via `meta` in the raw response body, following standard Chatwoot pagination format
- **GoChat**: Uses `OKWithMeta()` which adds `meta: { page, per_page, total_count }` inside the envelope
- **Impact**: Pagination metadata is nested one level deeper in GoChat
---
## 5. Status Code Comparison
| Operation | Chatwoot Status | GoChat Status | Match? |
|-----------|-----------------|---------------|--------|
| Create (definition) | 200 (returns created object) | 201 Created | NO |
| Create (filter) | 200 (returns created object) | 201 Created | NO |
| Update (definition) | 200 | 200 | YES (but method differs: PUT vs PATCH) |
| Update (filter) | 200 | 200 | YES (but method differs: PUT vs PATCH) |
| Delete (definition) | 200 `{}` | 204 No Content | NO |
| Delete (filter) | 200 `{}` | 204 No Content | NO |
| Set attribute value | 200 | 200 | YES |
| Remove attribute value | 200 | 200 | YES |
---
## 6. Missing Model Fields
### CustomAttributeDefinition
| Chatwoot column | GoChat field | Status |
|---|---|---|
| `attribute_key` | `AttributeName` (different name) | RENAMED — breaks JSON key compatibility |
| `attribute_display_type` | `AttributeType` (different name + type) | RENAMED + type changed |
| `attribute_description` | `Description` (different name) | RENAMED — different JSON key |
| `attribute_values` | — | MISSING |
| `regex_pattern` | — | MISSING |
| `regex_cue` | — | MISSING |
### CustomFilter
| Chatwoot column | GoChat field | Status |
|---|---|---|
| `user_id` | `CreatedByID` (different name) | RENAMED — `user_id` vs `created_by_id` |
---
## 7. Business Logic Differences
### Validation
- **Chatwoot**: Validates `attribute_key` uniqueness scoped to `(account_id, attribute_model)` in the model
- **GoChat**: Validates `attribute_name` uniqueness scoped to `(account_id, attribute_model)` via GORM uniqueIndex — functionally equivalent but uses different field name
### Value Type Validation
- **GoChat**: Has `validateValueType()` that checks the value matches the definition's `attribute_type` (e.g., number attributes must receive numeric values)
- **Chatwoot**: No equivalent server-side validation in the controller — values are stored as-is in jsonb
- **Impact**: GoChat is **more restrictive** than Chatwoot here
### Pagination Defaults
- **Chatwoot**: Default `per_page=25` in most list endpoints
- **GoChat**: Default `per_page=25` from `pagination.Parse()` — compatible
### Sorting
- **Chatwoot**: List endpoints return records in default DB order (created_at ASC implicitly)
- **GoChat**: Same — no explicit sort, relies on DB default order
- **Impact**: Compatible
---
## 8. Test Coverage Assessment
| Module | Handler Tests | Repo Tests | Service Tests | All Passing? |
|--------|---------------|------------|---------------|--------------|
| CustomAttributeDefinition | 10 tests | ~8 tests | — | YES |
| CustomAttributeValue | — | — | — | YES (handler/service combined) |
| CustomFilter | 9 tests | ~6 tests | 9 tests | YES |
All existing tests pass (verified with `go test -v`), but they test GoChat's own implementation, not Chatwoot compatibility.
---
## 9. Gap Severity Classification
### Critical (Breaks Chatwoot API client compatibility)
1. **Missing endpoints**: `/conversations/custom_attributes` (set, collection-level), `/conversations/set_custom_attributes`, `/conversations/destroy_custom_attributes` (POST), `/contacts/:id/destroy_custom_attributes` (POST), `/companies/:id/destroy_custom_attributes` (POST)
2. **Field name mismatches**: `attribute_key` → `attribute_name`, `attribute_display_type` → `attribute_type`, `attribute_description` → `description`, `user_id` → `created_by_id`
3. **Enum vs string values**: `attribute_display_type` (integer enum → string), `attribute_model` (integer enum → string), `filter_type` (integer enum → string with different value set)
### Moderate (Differences that may or may not matter depending on client expectations)
4. **PUT vs PATCH**: Both definition and filter updates use PUT instead of PATCH
5. **Status codes**: Create returns 201 vs 200; Delete returns 204 vs 200
6. **Response envelope**: `{success, data, error, meta}` wrapper vs raw JSON
7. **Missing model fields**: `attribute_values`, `regex_pattern`, `regex_cue`
8. **Request structure difference**: Value set uses flat `{attribute_name, value}` vs Chatwoot's nested `{custom_attribute: {attribute_name: value}}`
### Minor (Acceptable differences or GoChat enhancements)
9. **Value type validation**: GoChat validates value types against definitions (more restrictive but arguably better)
10. **GoChat-only route**: `POST /contacts/:id/custom_attributes` (GoChat adds a set route Chatwoot doesn't have as a separate endpoint)
11. **filter_type value set**: GoChat has `inbox`; Chatwoot older versions also had `inbox`, newer has `report` — may be version-dependent
---
## 10. Remediation Recommendations
To achieve 100% Chatwoot API compatibility, the following changes are needed:
### Must-Fix (Critical)
1. **Add missing custom attribute value endpoints**:
- `POST /api/v1/accounts/:id/conversations/set_custom_attributes` (collection-level set)
- `POST /api/v1/accounts/:id/conversations/custom_attributes` (collection-level set, alternate)
- `POST /api/v1/accounts/:id/conversations/destroy_custom_attributes` (collection-level destroy)
- `POST /api/v1/accounts/:id/contacts/:contact_id/destroy_custom_attributes` (member-level destroy)
- `POST /api/v1/accounts/:id/companies/:company_id/destroy_custom_attributes` (member-level destroy)
2. **Rename JSON keys to match Chatwoot**:
- `attribute_name` → `attribute_key` in model, service DTOs, handler, tests
- `attribute_type` → `attribute_display_type` in model, service DTOs, handler, tests
- `description` → `attribute_description` in model, service DTOs, handler, tests
- `created_by_id` → `user_id` in CustomFilter model
3. **Change enum values to match Chatwoot format** (or add dual support):
- `attribute_display_type`: accept integer `0-5` OR string names
- `attribute_model`: accept integer `0-1` OR string names
- `filter_type`: accept integer `0-2` OR string names, and add `report` value
4. **Fix request body structure for attribute value set**:
- Accept `{ custom_attribute: { attribute_name: value } }` format
### Should-Fix (Moderate)
5. **Change PUT to PATCH** for update endpoints (both definitions and filters)
6. **Align status codes**: Create → 200; Delete → 200 with empty body
7. **Add missing model fields**: `attribute_values`, `regex_pattern`, `regex_cue`
8. **Consider removing response envelope** or making it optional for Chatwoot-compat mode
### Optional (Minor)
9. Value type validation is a GoChat enhancement — keep it but make it optional
10. `POST /contacts/:id/custom_attributes` is a GoChat-only addition — acceptable
---
## 11. Files Reviewed
- `/home/yanghao05/Workspace/gochat/internal/model/custom_attribute_definition.go`
- `/home/yanghao05/Workspace/gochat/internal/model/custom_filter.go`
- `/home/yanghao05/Workspace/gochat/internal/handler/api/v1/custom_attribute_definition_handler.go`
- `/home/yanghao05/Workspace/gochat/internal/handler/api/v1/custom_attribute_value_handler.go`
- `/home/yanghao05/Workspace/gochat/internal/handler/api/v1/custom_filter_handler.go`
- `/home/yanghao05/Workspace/gochat/internal/service/custom_attribute_definition_service.go`
- `/home/yanghao05/Workspace/gochat/internal/service/custom_attribute_value_service.go`
- `/home/yanghao05/Workspace/gochat/internal/service/custom_filter_service.go`
- `/home/yanghao05/Workspace/gochat/internal/repository/custom_attribute_definition_repo.go`
- `/home/yanghao05/Workspace/gochat/internal/repository/custom_filter_repo.go`
- `/home/yanghao05/Workspace/gochat/internal/router/router.go` (lines 1078-1135)
- `/home/yanghao05/Workspace/gochat/internal/app/bootstrap.go` (handler/service/repo init)
- `/home/yanghao05/Workspace/gochat/pkg/response/response.go`
- `/home/yanghao05/Workspace/gochat/pkg/pagination/pagination.go`
Chatwoot reference: `routes.rb` (custom attribute definition + custom filter routes), `custom_attribute_definition.rb` model columns, `custom_filter.rb` model columns, controllers permitted_params.
---
## Conclusion
**Overall compatibility score: ~55%**
GoChat implements functional CRUD for the core definition and filter endpoints, but the custom attribute value endpoints have fundamentally different route structures, and multiple field name/type mismatches break Chatwoot client compatibility. The 8 critical gaps require code changes across models, services, handlers, repos, router, and tests before the implementation can be considered Chatwoot API compatible.
@@ -0,0 +1,159 @@
# 验收报告 — G1 Conversation扩展API
**任务**: t_a90059a6
**验收日期**: 2026-05-27
**验收标准**: 5项 — 接口路径、请求参数、响应格式、错误处理与状态码、业务逻辑
---
## 总体结论: ⚠️ 部分通过,存在6个需修差项
所有6个API组 (meta, unread_counts, unread, transcript, custom_attributes, participants, draft_messages) 的 handler/service/repo/router 已实现。所有测试通过 (handler 0.117s, service 0.852s, repo 1.278s)。但与Chatwoot Ruby API存在以下偏差,需在合并前修正。
---
## 逐组5项标准验收
### 1. Meta — `GET /api/v1/accounts/:account_id/conversations/meta`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `GET /conversations/meta` (collection) | `GET /conversations/meta` | ✅ 一致 |
| **请求参数** | `status` (enum: all/open/resolved/pending/snoozed), `q` (search), `inbox_id`, `assignee_type`, `team_id`, `labels`, `sort` | 仅 `account_id` (路径参数) | ❌ 缺少 `status`, `q`, `inbox_id`, `assignee_type`, `team_id`, `labels`, `sort` 过滤参数 |
| **响应格式** | `{ meta: { mine_count, assigned_count, unassigned_count, all_count } }` | `{ success: true, data: { status_counts: {...}, label_counts: {...}, total_count: N } }` | ❌ 字段名不一致; Chatwoot用 mine/assigned/unassigned/all 四字段, Gochat用 status_counts/label_counts/total_count |
| **错误处理** | 标准Rails错误 | 400/500 统一错误体 | ⚠️ 无401/403权限检查(mine_count依赖当前用户) |
| **业务逻辑** | ConversationFinder.perform_meta_only — 基于当前用户的可见范围计算mine_count | DB聚合统计 — 全account维度, 不区分用户 | ❌ 缺少用户上下文过滤 |
**差项**:
- META-1: 响应字段结构不一致 (mine/assigned/unassigned/all vs status_counts/label_counts/total_count)
- META-2: 缺少查询过滤参数 (status, q, inbox_id, assignee_type, team_id, labels, sort)
- META-3: mine_count依赖当前用户上下文, Gochat当前实现无用户过滤
### 2. UnreadCounts — `GET /api/v1/accounts/:account_id/conversations/unread_counts`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `GET /conversations/unread_counts` (collection) | `GET /conversations/unread_counts` | ✅ 一致 |
| **请求参数** | `account_id` (路径), 需feature_flag `conversation_unread_counts` | 仅 `account_id` | ⚠️ 无feature flag检查 |
| **响应格式** | `{ payload: { inboxes: {...}, labels: {...}, teams: {...} } }` | `{ success: true, data: { inboxes: {...}, labels: {...}, teams: {...} } }` | ⚠️ Chatwoot用 `payload` 键, Gochat用 `data` 键 (统一wrapper) |
| **错误处理** | 403 Forbidden (feature not enabled) | 400/500 统一错误体 | ⚠️ 缺少403 feature flag检查 |
| **业务逻辑** | Counter.perform — 基于当前用户权限模式(manage_all/unassigned/participating)计算 | 直接DB聚合 — 无权限模式区分 | ❌ 缺少用户权限过滤 |
**差项**:
- UNREAD_COUNTS-1: 缺少feature flag门控 (Chatwoot需要 `conversation_unread_counts` feature)
- UNREAD_COUNTS-2: 响应envelope键名差异 (payload vs data)
- UNREAD_COUNTS-3: 缺少用户权限模式过滤 (manage_all/unassigned/participating)
### 3. Unread — `POST /api/v1/accounts/:account_id/conversations/:id/unread`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/unread` (member) | `POST /conversations/:id/unread` | ✅ 一致 |
| **请求参数** | 无请求体, 依赖当前用户 | `account_id`, `id` (路径参数) | ✅ 一致 |
| **响应格式** | 返回完整conversation JSON (partial渲染) | `{ success: true, data: conversation_object }` | ⚠️ Chatwoot返回带嵌套的partial格式, Gochat返回扁平model |
| **错误处理** | 404 (conversation not found) | 404 (record not found) | ✅ 一致 |
| **业务逻辑** | agent_last_seen_at = last_incoming_message.created_at - 1秒 | agent_last_seen_at = null (清零) | ❌ 不一致 — Chatwoot设为倒数1秒, Gochat设为null |
**差项**:
- UNREAD-1: 业务逻辑不一致 — agent_last_seen_at应设为 last_incoming_message.created_at - 1秒, 而不是null
### 4. Transcript — `POST /api/v1/accounts/:account_id/conversations/:id/transcript`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/transcript` (member) | `POST /conversations/:id/transcript` | ✅ 一致 |
| **请求参数** | `{ email: string }` | `{ email: string }` (binding:required,email) | ✅ 一致 |
| **响应格式** | `200 OK` (head :ok, 无body) 或 `422 { error: "email param missing" }` | `{ success: true, data: null }` (200) | ⚠️ Chatwoot返回空body 200, Gochat返回JSON envelope |
| **错误处理** | 422 (email missing), 402 (plan限制), 429 (rate limit) | 400 (validation), 无402/429 | ⚠️ 缺少rate limit和plan限制检查 |
| **业务逻辑** | ConversationReplyMailer.deliver_later + account.increment_email_sent_count | DB标记逻辑 (无邮件发送) | ⚠️ Gochat无实际邮件发送能力, 这是基础设施差异 |
**差项**:
- TRANSCRIPT-1: 成功响应应为空body 200, 不是JSON envelope
- TRANSCRIPT-2: 缺少email rate limit检查 (429)
- TRANSCRIPT-3: 无邮件发送基础设施 (Chatwoot用mailer, Gochat标记为TODO)
### 5. CustomAttributes — `POST /api/v1/accounts/:account_id/conversations/:id/custom_attributes`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/custom_attributes` (member) | `POST /conversations/:id/custom_attributes` | ✅ 一致 |
| **请求参数** | `{ custom_attributes: {...} }` (permit) | `{ custom_attributes: {...} }` (binding:required) | ✅ 一致 |
| **响应格式** | `{ custom_attributes: {...} }` | `{ success: true, data: conversation_object }` | ⚠️ Chatwoot返回仅custom_attributes字段, Gochat返回完整conversation |
| **错误处理** | 422 (save失败) | 400/500 统一错误体 | ✅ 语义一致 |
| **业务逻辑** | 直接更新custom_attributes + save! | repo层UpdateCustomAttributes | ✅ 一致 |
**差项**:
- CUSTOM_ATTR-1: 响应应仅返回 `{ custom_attributes: {...} }`, 不是完整conversation对象
### 6. Participants — nested under `/conversations/:conversation_id/participants`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `resource :participants` (singular resource) → show(GET)/create(POST)/update(PATCH)/destroy(DELETE) 无子路径 | GET/POST/PATCH/PATCH/:user_id/DELETE/:user_id | ❌ 路径不一致 — Chatwoot用singular resource (无:user_id), Gochat用plural+子路径 |
| **请求参数** | show: 无; create: `{ user_ids: [...] }`; update: `{ user_ids: [...] }`; destroy: `{ user_ids: [...] }` | List: 无; Add: `{ user_id, role }`; BatchUpdate: `{ user_ids: [...], remove_user_ids: [...] }`; Update: 单user; Remove: 单user_id | ❌ 参数结构不一致 — Chatwoot统一用user_ids数组 |
| **响应格式** | show/create/update → `[{ agent_object }]` (Agent模型的user详情) | List → `[{ ConversationParticipant }]` (仅conversation_id, user_id, role) | ❌ 完全不一致 — Chatwoot返回Agent(User)详情, Gochat返回Participant关联记录 |
| **错误处理** | 标准Rails错误 | 400/500 统一错误体 | ⚠️ 基本一致 |
| **业务逻辑** | find_or_create_by / find_by&.destroy — 幂等操作 | 基于GORM的CRUD | ⚠️ 幂等性差异 |
**差项**:
- PARTICIPANTS-1: 路径不一致 — 应为singular resource (无:user_id子路径)
- PARTICIPANTS-2: 请求参数不一致 — Chatwoot统一用 `user_ids` 数组, create/update/destroy都是
- PARTICIPANTS-3: 响应格式不一致 — 应返回Agent(User)详情对象, 不是ConversationParticipant关联记录
- PARTICIPANTS-4: update方法应同时支持add+remove (Chatwoot的update = add新ids + remove旧ids)
### 7. DraftMessages — nested under `/conversations/:conversation_id/draft_messages`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `resource :draft_messages` (singular) → show(GET)/update(PATCH)/destroy(DELETE) 无:id子路径 | GET/POST/GET/:id/PATCH/:id/DELETE/:id | ❌ 路径不一致 — Chatwoot用singular resource (每conversation每用户仅一个draft) |
| **请求参数** | show: 无; update: `{ draft_message: { message: "..." } }`; destroy: 无 | List: 无; Create: `{ content }`; Get: :id; Update: `{ content }`; Delete: :id | ❌ Chatwoot无create/list/get/:id, 仅show/update/destroy |
| **响应格式** | show: `{ has_draft: bool, message: "..." }` 或 `{ has_draft: false }`; update/destroy: `200 OK` (head :ok) | List/Get: `{ success: true, data: DraftMessage }`; Create: 同; Update: 同; Delete: 200 | ❌ 完全不一致 — Chatwoot用Redis+单一draft, Gochat用DB+多draft |
| **错误处理** | 无特殊错误处理 | 400/404/500 | ⚠️ Chatwoot较简单 |
| **业务逻辑** | Redis-based — 每conversation仅一个draft, 按conversation_id存储, 非DB持久化 | DB-based — DraftMessage表, 多draft记录 | ❌ 架构差异 — Chatwoot用Redis临时存储, Gochat用DB持久化 |
**差项**:
- DRAFT-1: 路径不一致 — 应为singular resource (无:id子路径), 仅show/update/destroy三个动作
- DRAFT-2: 请求参数不一致 — update请求体应为 `{ draft_message: { message: "..." } }`, 不是 `{ content }`
- DRAFT-3: 响应格式完全不一致 — show应返回 `{ has_draft: bool, message: "..." }`, 不是DraftMessage对象
- DRAFT-4: 架构差异 — Chatwoot用Redis临时存储(每conversation一个draft), Gochat用DB持久化(多draft)
---
## 差项汇总 (6个需修差项, 按优先级排序)
### P0 — 阻塞合并 (响应格式/路径/业务逻辑不一致)
1. **META响应结构不一致** — 需改为 `{ meta: { mine_count, assigned_count, unassigned_count, all_count } }`, 加用户上下文过滤
2. **PARTICIPANTS路径+参数+响应全部不一致** — 需改为singular resource, 参数统一用user_ids, 响应返回Agent详情
3. **DRAFT_MESSAGES路径+参数+响应全部不一致** — 需改为singular resource, show返回 `{ has_draft, message }`, update/destroy返回空200
4. **UNREAD业务逻辑不一致** — agent_last_seen_at应设为 last_incoming_message.created_at - 1秒
### P1 — 建议修 (envelope/权限差异)
5. **响应envelope差异** — Gochat统一用 `{ success, data }`, Chatwoot各端点格式各异; 需评估是否需要为特定端点去掉wrapper
6. **权限过滤缺失** — UnreadCounts缺少用户权限模式过滤和feature flag门控
### P2 — 已知基础设施差异 (可后续迭代)
7. **TRANSCRIPT邮件基础设施** — Gochat无邮件发送能力, 需集成邮件服务
8. **DRAFT_MESSAGES存储架构** — Redis vs DB, 功能等价但架构不同
---
## 测试验证结果
```
handler tests: PASS (0.117s)
service tests: PASS (0.852s)
repo tests: PASS (1.278s)
```
所有现有测试通过, 但测试覆盖基于当前(不一致的)实现。修正差项后需更新对应测试。
---
## 建议
1. **P0差项需在合并前修正** — 否则API与Chatwoot不兼容, 前端无法对接
2. **建议创建CTO dispatch任务** — 拆分为6个子任务分别修正差项
3. **P2差项可在后续迭代中处理** — TRANSCRIPT邮件集成和DRAFT Redis迁移需要基础设施支持