feat(routes): close first chatwoot parity gaps
This commit is contained in:
@@ -17,9 +17,9 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc
|
||||
## Current Baseline
|
||||
|
||||
- `go test ./...` passes.
|
||||
- Route dump succeeds with `TOTAL: 704`.
|
||||
- Route dump succeeds with `TOTAL: 709`.
|
||||
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
|
||||
- First tracked frontend-critical API v1 route audit covers 81 Chatwoot routes: 76 exact, 2 method-compatible, 0 parameter-compatible, 3 missing.
|
||||
- First tracked frontend-critical API v1 route audit covers 81 Chatwoot routes: 81 exact, 0 method-compatible, 0 parameter-compatible, 0 missing.
|
||||
- Handler test stability fixes are committed into the baseline before feature parity work continues.
|
||||
- `.codegraph/` is generated indexing output and is not part of tracked product code.
|
||||
|
||||
@@ -128,8 +128,9 @@ Checklist:
|
||||
- [x] Build a first tracked route parity table covering method, path, controller, source, and route status.
|
||||
- [x] Prioritize first frontend-critical API v1 account routes used by the Chatwoot web app.
|
||||
- [ ] Extend parity table with auth scope, request params, response serializer, and handler implementation status.
|
||||
- [ ] Patch route names and wildcard params where Gin constraints require different internal names, while preserving external URLs.
|
||||
- [ ] Add regression tests for route groups that previously conflicted.
|
||||
- [x] Patch first-batch route names and wildcard params where Gin constraints require different internal names, while preserving external URLs.
|
||||
- [x] Add regression tests for the contact conversations relation route added in this slice.
|
||||
- [ ] Expand route boot regression coverage for all Gin wildcard/param conflict groups.
|
||||
|
||||
Acceptance:
|
||||
|
||||
@@ -144,32 +145,32 @@ Tracking table:
|
||||
| P2.1 | Generate Chatwoot route dump from `reference/chatwoot`. | `docs/parity/chatwoot_routes_static.md` | Review |
|
||||
| P2.2 | Generate GoChat route dump with `cmd/dump_routes`. | `docs/parity/gochat_routes.txt` | Done |
|
||||
| P2.3 | Produce route parity table: method, path, controller/handler, auth, request params, serializer, status. | `docs/parity/route_parity.md` | Review |
|
||||
| P2.4 | Mark frontend-critical gaps from Chatwoot web app route usage. | `docs/parity/route_parity.md` | Review |
|
||||
| P2.4 | Mark frontend-critical gaps from Chatwoot web app route usage. | `docs/parity/route_parity.md` | Done |
|
||||
| P2.5 | Convert existing placeholders/stubs into tracked feature tasks instead of hidden debt. | this doc and gap report | Doing |
|
||||
| P2.6 | Add route boot regression tests for Gin wildcard/param conflicts. | router and handler tests | Todo |
|
||||
| P2.6 | Add route boot regression tests for Gin wildcard/param conflicts. | router and handler tests | Doing |
|
||||
|
||||
Current Phase 2 route findings:
|
||||
|
||||
| Type | Count | Required action |
|
||||
| --- | --- | --- |
|
||||
| Exact tracked critical routes | 76 | Keep covered while expanding audit scope. |
|
||||
| Method-compatible update routes | 2 | Register both Rails-compatible update methods or verify frontend only calls the implemented method. |
|
||||
| Missing tracked critical routes | 3 | Implement exact Chatwoot external paths before frontend reuse. |
|
||||
| Exact tracked critical routes | 81 | Keep covered while expanding audit scope. |
|
||||
| Method-compatible update routes | 0 | First tracked batch now has exact Rails-compatible method coverage. |
|
||||
| Missing tracked critical routes | 0 | First tracked frontend-critical batch has no route-level gaps. |
|
||||
|
||||
Missing tracked critical routes:
|
||||
Closed tracked critical route gaps in this slice:
|
||||
|
||||
| Method | Path | Chatwoot controller | Required next task |
|
||||
| Method | Path | Chatwoot controller | Implementation note |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id/conversations` | `api/v1/accounts/contacts/conversations#index` | Add contact conversation relation endpoint and serializer parity test. |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/export` | `api/v1/accounts/contacts#export` | Add POST export route; current GoChat has `GET /contacts/export`, which is not frontend-compatible. |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority` | `api/v1/accounts/conversations#toggle_priority` | Add Chatwoot action path; current GoChat has `PATCH /conversations/:conversation_id/priority`. |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id/conversations` | `api/v1/accounts/contacts/conversations#index` | Added contact conversation relation endpoint returning Chatwoot `payload` shape, optional `inbox_id`, and latest-20 ordering. |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/export` | `api/v1/accounts/contacts#export` | Added Chatwoot async request path returning `200 OK`; existing CSV download route remains for local compatibility. |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority` | `api/v1/accounts/conversations#toggle_priority` | Added Chatwoot action path returning `200 OK`. |
|
||||
|
||||
Method-compatible routes requiring exact Rails method coverage:
|
||||
Closed Rails update method gaps:
|
||||
|
||||
| Expected | Existing GoChat route | Required next task |
|
||||
| Expected | Existing GoChat route | Implementation note |
|
||||
| --- | --- | --- |
|
||||
| `PUT /api/v1/accounts/:account_id/conversations/:conversation_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id` | Add PUT alias or prove frontend only calls PATCH. |
|
||||
| `PUT /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | Add PUT alias or prove frontend only calls PATCH. |
|
||||
| `PUT /api/v1/accounts/:account_id/conversations/:conversation_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id` | Added PUT alias to the same update handler. |
|
||||
| `PUT /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | Added PUT alias to the same update handler. |
|
||||
|
||||
Phase 2 commands:
|
||||
|
||||
@@ -361,3 +362,4 @@ env GOCACHE=/tmp/gochat-gocache GOMODCACHE=/tmp/gochat-gomodcache go run ./cmd/d
|
||||
- 2026-06-04: Phase 1 search foundation added: Meilisearch config/env defaults, `SearchEngine` contract, Meilisearch HTTP wrapper with bootstrap/settings, DB fallback adapter, document builders, reindex command, and no-live-Meilisearch tests. Verified `go test ./...` in unsandboxed mode because miniredis/httptest need local sockets; route dump still reports `TOTAL: 704`.
|
||||
- 2026-06-04: Phase 1 indexing hooks wired for conversations, messages, contacts, companies, and articles. Create/update/delete paths now call the service-layer `SearchIndexer` boundary, bootstrap injects the Meilisearch-backed search service, and unit tests cover each entity hook path.
|
||||
- 2026-06-04: Phase 2 route parity tracking added. Ruby/Bundler are unavailable in this workspace, so `cmd/route_parity` records static Chatwoot route DSL declarations from `reference/chatwoot/config/routes.rb`, consumes `cmd/dump_routes` output, and writes `docs/parity/route_parity.md`. First tracked critical route summary: 76 exact, 2 method-compatible, 0 parameter-compatible, 3 missing out of 81; GoChat route dump remains `TOTAL: 704`.
|
||||
- 2026-06-04: First tracked Phase 2 route gaps closed. Added Chatwoot-compatible contact conversations, contact export POST, conversation `toggle_priority`, and PUT aliases for conversation/message updates; fixed contact handlers to accept `:account_id` as well as legacy `:id`. Regenerated parity report: 81 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 81; route dump now reports `TOTAL: 709`.
|
||||
|
||||
@@ -178,6 +178,7 @@ GET /api/v1/accounts/:account_id/contacts/
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id/contact_inboxes
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id/contactable_inboxes
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id/conversations
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id/notes
|
||||
GET /api/v1/accounts/:account_id/contacts/:contact_id/notes/:note_id
|
||||
GET /api/v1/accounts/:account_id/contacts/active
|
||||
@@ -486,6 +487,7 @@ POST /api/v1/accounts/:account_id/contacts/:contact_id/contact_inboxes
|
||||
POST /api/v1/accounts/:account_id/contacts/:contact_id/custom_attributes/
|
||||
POST /api/v1/accounts/:account_id/contacts/:contact_id/destroy_custom_attributes
|
||||
POST /api/v1/accounts/:account_id/contacts/:contact_id/notes
|
||||
POST /api/v1/accounts/:account_id/contacts/export
|
||||
POST /api/v1/accounts/:account_id/contacts/filter
|
||||
POST /api/v1/accounts/:account_id/contacts/import
|
||||
POST /api/v1/accounts/:account_id/contacts/merge
|
||||
@@ -501,6 +503,7 @@ POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:messa
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/translate
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/mute
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/participants/
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/toggle_status
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/toggle_typing_status
|
||||
POST /api/v1/accounts/:account_id/conversations/:conversation_id/transcript
|
||||
@@ -659,6 +662,8 @@ PUT /api/v1/accounts/:account_id/captain/preferences/
|
||||
PUT /api/v1/accounts/:account_id/captain/scenarios/:scenario_id
|
||||
PUT /api/v1/accounts/:account_id/companies/:company_id
|
||||
PUT /api/v1/accounts/:account_id/contacts/:contact_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_calls/:call_id
|
||||
PUT /api/v1/accounts/:account_id/custom_attribute_definitions/:id
|
||||
PUT /api/v1/accounts/:account_id/custom_filters/:id
|
||||
@@ -702,4 +707,4 @@ PUT /platform/api/v1/banners/:id
|
||||
PUT /platform/api/v1/installation_configs/:id
|
||||
PUT /webhooks/:channel_type/:identifier
|
||||
TRACE /webhooks/:channel_type/:identifier
|
||||
TOTAL: 704
|
||||
TOTAL: 709
|
||||
|
||||
@@ -7,15 +7,13 @@ Generated from:
|
||||
|
||||
This first report covers frontend-critical API v1 account routes from `reference/chatwoot/config/routes.rb`. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.
|
||||
|
||||
Summary: 76 exact, 2 method-compatible, 0 parameter-compatible, 3 missing out of 81 tracked critical routes.
|
||||
Summary: 81 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 81 tracked critical routes.
|
||||
|
||||
## Missing Critical Routes
|
||||
|
||||
| Method | Chatwoot Path | GoChat Match | Controller | Source | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id/conversations` | `-` | `api/v1/accounts/contacts/conversations#index` | `routes.rb:211` | missing |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/export` | `-` | `api/v1/accounts/contacts#export` | `routes.rb:204` | missing |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority` | `-` | `api/v1/accounts/conversations#toggle_priority` | `routes.rb:159` | missing |
|
||||
| - | - | - | - | - | none |
|
||||
|
||||
## Method-Compatible Routes
|
||||
|
||||
@@ -23,8 +21,7 @@ These routes exist at the same path but with a compatible HTTP method. Rails res
|
||||
|
||||
| Method | Chatwoot Path | GoChat Match | Controller | Source | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id` | `api/v1/accounts/conversations#update` | `routes.rb:134` | method-compatible |
|
||||
| PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `api/v1/accounts/conversations/messages#update` | `routes.rb:142` | method-compatible |
|
||||
| - | - | - | - | - | none |
|
||||
|
||||
## Parameter-Compatible Routes
|
||||
|
||||
@@ -58,6 +55,7 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| GET | `/api/v1/accounts/:account_id/companies/search` | `/api/v1/accounts/:account_id/companies/search` | `api/v1/accounts/companies#search` | `routes.rb:181` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/` | `/api/v1/accounts/:account_id/contacts/` | `api/v1/accounts/contacts#index` | `routes.rb:197` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/contacts/:contact_id` | `api/v1/accounts/contacts#show` | `routes.rb:197` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id/conversations` | `/api/v1/accounts/:account_id/contacts/:contact_id/conversations` | `api/v1/accounts/contacts/conversations#index` | `routes.rb:211` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/:contact_id/notes` | `/api/v1/accounts/:account_id/contacts/:contact_id/notes` | `api/v1/accounts/contacts/notes#index` | `routes.rb:214` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/contacts/search` | `/api/v1/accounts/:account_id/contacts/search` | `api/v1/accounts/contacts#search` | `routes.rb:201` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/conversations/` | `/api/v1/accounts/:account_id/conversations/` | `api/v1/accounts/conversations#index` | `routes.rb:134` | exact |
|
||||
@@ -90,6 +88,7 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| POST | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/companies/:company_id/contacts/:contact_id` | `api/v1/accounts/companies/contacts#create` | `routes.rb:188` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/` | `/api/v1/accounts/:account_id/contacts/` | `api/v1/accounts/contacts#create` | `routes.rb:197` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/:contact_id/notes` | `/api/v1/accounts/:account_id/contacts/:contact_id/notes` | `api/v1/accounts/contacts/notes#create` | `routes.rb:214` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/export` | `/api/v1/accounts/:account_id/contacts/export` | `api/v1/accounts/contacts#export` | `routes.rb:204` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/filter` | `/api/v1/accounts/:account_id/contacts/filter` | `api/v1/accounts/contacts#filter` | `routes.rb:202` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/contacts/import` | `/api/v1/accounts/:account_id/contacts/import` | `api/v1/accounts/contacts#import` | `routes.rb:203` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/` | `/api/v1/accounts/:account_id/conversations/` | `api/v1/accounts/conversations#create` | `routes.rb:134` | exact |
|
||||
@@ -98,6 +97,7 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `api/v1/accounts/conversations/messages#create` | `routes.rb:142` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/retry` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/retry` | `api/v1/accounts/conversations/messages#retry` | `routes.rb:145` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/translate` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/translate` | `api/v1/accounts/conversations/messages#translate` | `routes.rb:144` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority` | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority` | `api/v1/accounts/conversations#toggle_priority` | `routes.rb:159` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_status` | `/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_status` | `api/v1/accounts/conversations#toggle_status` | `routes.rb:158` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/:conversation_id/transcript` | `/api/v1/accounts/:account_id/conversations/:conversation_id/transcript` | `api/v1/accounts/conversations#transcript` | `routes.rb:157` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/conversations/filter` | `/api/v1/accounts/:account_id/conversations/filter` | `api/v1/accounts/conversations#filter` | `routes.rb:139` | exact |
|
||||
@@ -111,6 +111,8 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| PUT | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `api/v1/accounts/automation_rules#update` | `routes.rb:115` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/companies/:company_id` | `/api/v1/accounts/:account_id/companies/:company_id` | `api/v1/accounts/companies#update` | `routes.rb:179` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/contacts/:contact_id` | `/api/v1/accounts/:account_id/contacts/:contact_id` | `api/v1/accounts/contacts#update` | `routes.rb:197` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `api/v1/accounts/conversations#update` | `routes.rb:134` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id` | `api/v1/accounts/conversations/messages#update` | `routes.rb:142` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#update` | `routes.rb:252` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/macros/:macro_id` | `/api/v1/accounts/:account_id/macros/:macro_id` | `api/v1/accounts/macros#update` | `routes.rb:118` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/sla_policies/:id` | `/api/v1/accounts/:account_id/sla_policies/:id` | `api/v1/accounts/sla_policies#update` | `routes.rb:121` | exact |
|
||||
|
||||
@@ -686,7 +686,7 @@ func Bootstrap(env string) (*App, error) {
|
||||
MFA: v1.NewMFAHandler(mfaService),
|
||||
SAML: v1.NewSAMLHandler(samlService, jwtService, refreshStore, ssoSessionStore, &cfg.SAML),
|
||||
Account: v1.NewAccountHandler(accountService),
|
||||
Contact: v1.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService),
|
||||
Contact: v1.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService, conversationService),
|
||||
Conversation: v1.NewConversationHandler(conversationService, messageService),
|
||||
Inbox: v1.NewInboxHandler(inboxService),
|
||||
InboxMember: v1.NewInboxMemberHandler(inboxMemberService),
|
||||
|
||||
@@ -20,11 +20,16 @@ type ContactHandler struct {
|
||||
contactInboxSvc *service.ContactInboxService
|
||||
mergeSvc *service.ContactMergeService
|
||||
contactNoteSvc *service.ContactNoteService
|
||||
conversationSvc *service.ConversationService
|
||||
}
|
||||
|
||||
// NewContactHandler creates a new ContactHandler.
|
||||
func NewContactHandler(svc *service.ContactService, contactInboxSvc *service.ContactInboxService, mergeSvc *service.ContactMergeService, contactNoteSvc *service.ContactNoteService) *ContactHandler {
|
||||
return &ContactHandler{svc: svc, contactInboxSvc: contactInboxSvc, mergeSvc: mergeSvc, contactNoteSvc: contactNoteSvc}
|
||||
func NewContactHandler(svc *service.ContactService, contactInboxSvc *service.ContactInboxService, mergeSvc *service.ContactMergeService, contactNoteSvc *service.ContactNoteService, conversationSvc ...*service.ConversationService) *ContactHandler {
|
||||
h := &ContactHandler{svc: svc, contactInboxSvc: contactInboxSvc, mergeSvc: mergeSvc, contactNoteSvc: contactNoteSvc}
|
||||
if len(conversationSvc) > 0 {
|
||||
h.conversationSvc = conversationSvc[0]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// @Summary List contacts for an account
|
||||
@@ -46,8 +51,8 @@ func NewContactHandler(svc *service.ContactService, contactInboxSvc *service.Con
|
||||
// GET /api/v1/accounts/:id/contacts?sort=name&page=1&page_size=25
|
||||
// Reference: Chatwoot contacts#index (sort param)
|
||||
func (h *ContactHandler) List(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -90,8 +95,8 @@ func (h *ContactHandler) List(c *gin.Context) {
|
||||
// GET /api/v1/accounts/:id/contacts/search?q=...&sort=name
|
||||
// Reference: Chatwoot contacts#search
|
||||
func (h *ContactHandler) Search(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -132,8 +137,8 @@ func (h *ContactHandler) Search(c *gin.Context) {
|
||||
// Get retrieves a single contact.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Get(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -171,8 +176,8 @@ func (h *ContactHandler) Get(c *gin.Context) {
|
||||
// Body: {name, email, phone, inbox_id, source_id, ...}
|
||||
// Reference: Chatwoot contacts#create (auto-creates ContactInbox when inbox_id provided)
|
||||
func (h *ContactHandler) Create(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -210,8 +215,8 @@ func (h *ContactHandler) Create(c *gin.Context) {
|
||||
// Update modifies an existing contact.
|
||||
// PUT /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Update(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -254,8 +259,8 @@ func (h *ContactHandler) Update(c *gin.Context) {
|
||||
// Delete soft-deletes a contact.
|
||||
// DELETE /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Delete(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -296,12 +301,57 @@ func (h *ContactHandler) ListContactInboxes(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ListConversations retrieves recent conversations for a contact.
|
||||
// GET /api/v1/accounts/:account_id/contacts/:contact_id/conversations
|
||||
// Reference: Chatwoot contacts/conversations#index.
|
||||
func (h *ContactHandler) ListConversations(c *gin.Context) {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
if !h.svc.Ready() || h.conversationSvc == nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list contact conversations"})
|
||||
return
|
||||
}
|
||||
if _, svcErr := h.svc.GetByAccountAndID(c.Request.Context(), accountID, contactID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
var inboxID *uint
|
||||
if rawInboxID := c.Query("inbox_id"); rawInboxID != "" {
|
||||
parsed, parseErr := strconv.ParseUint(rawInboxID, 10, 32)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
|
||||
return
|
||||
}
|
||||
id := uint(parsed)
|
||||
inboxID = &id
|
||||
}
|
||||
|
||||
conversations, svcErr := h.conversationSvc.ListRecentByContact(c.Request.Context(), accountID, contactID, inboxID, 20)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list contact conversations"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": conversations})
|
||||
}
|
||||
|
||||
// ListNotes retrieves notes for a contact.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id/notes
|
||||
// Reference: Chatwoot contacts#notes
|
||||
func (h *ContactHandler) ListNotes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -327,8 +377,8 @@ func (h *ContactHandler) ListNotes(c *gin.Context) {
|
||||
// POST /api/v1/accounts/:id/contacts/:contact_id/notes
|
||||
// Reference: Chatwoot contacts#create_note
|
||||
func (h *ContactHandler) CreateNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -363,8 +413,8 @@ func (h *ContactHandler) CreateNote(c *gin.Context) {
|
||||
// ShowNote retrieves a single note for a contact.
|
||||
// GET /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) ShowNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
@@ -387,8 +437,8 @@ func (h *ContactHandler) ShowNote(c *gin.Context) {
|
||||
// UpdateNote updates a note on a contact.
|
||||
// PATCH /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) UpdateNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
@@ -417,8 +467,8 @@ func (h *ContactHandler) UpdateNote(c *gin.Context) {
|
||||
// DestroyNote deletes a note on a contact.
|
||||
// DELETE /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) DestroyNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
@@ -513,8 +563,8 @@ func (h *ContactHandler) DeleteContactInbox(c *gin.Context) {
|
||||
// GET /api/v1/accounts/:id/contacts/active?sort=name&page=1&page_size=25
|
||||
// Reference: Chatwoot contacts#active
|
||||
func (h *ContactHandler) Active(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -544,8 +594,8 @@ func (h *ContactHandler) Active(c *gin.Context) {
|
||||
// GET /api/v1/accounts/:id/contacts/export
|
||||
// Reference: Chatwoot contacts#export
|
||||
func (h *ContactHandler) Export(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -559,12 +609,29 @@ func (h *ContactHandler) Export(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExportRequest accepts Chatwoot's asynchronous contact export request.
|
||||
// POST /api/v1/accounts/:account_id/contacts/export
|
||||
// Reference: Chatwoot contacts#export enqueues Account::ContactsExportJob and returns head :ok.
|
||||
func (h *ContactHandler) ExportRequest(c *gin.Context) {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
if !h.svc.Ready() {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to export contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Import uploads contacts from a CSV file.
|
||||
// POST /api/v1/accounts/:id/contacts/import
|
||||
// Reference: Chatwoot contacts#import
|
||||
func (h *ContactHandler) Import(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -589,8 +656,8 @@ func (h *ContactHandler) Import(c *gin.Context) {
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id/contactable_inboxes
|
||||
// Reference: Chatwoot contacts#contactable_inboxes
|
||||
func (h *ContactHandler) ContactableInboxes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -614,8 +681,8 @@ func (h *ContactHandler) ContactableInboxes(c *gin.Context) {
|
||||
// DELETE /api/v1/accounts/:id/contacts/:contact_id/custom_attributes
|
||||
// Reference: Chatwoot contacts#destroy_custom_attributes
|
||||
func (h *ContactHandler) DeleteCustomAttributes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -638,8 +705,8 @@ func (h *ContactHandler) DeleteCustomAttributes(c *gin.Context) {
|
||||
// POST /api/v1/accounts/:id/contacts/merge
|
||||
// Reference: Chatwoot app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
|
||||
func (h *ContactHandler) Merge(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -663,8 +730,8 @@ func (h *ContactHandler) Merge(c *gin.Context) {
|
||||
// POST /api/v1/accounts/:id/contacts/filter
|
||||
// Reference: Chatwoot contacts#filter — uses ContactFilterService with payload params.
|
||||
func (h *ContactHandler) Filter(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
@@ -690,8 +757,8 @@ func (h *ContactHandler) Filter(c *gin.Context) {
|
||||
// Reference: Chatwoot contacts#destroy_custom_attributes — uses POST method.
|
||||
// This is an alias for DeleteCustomAttributes (which uses DELETE method) to match Chatwoot's API.
|
||||
func (h *ContactHandler) DestroyCustomAttributes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
type ContactHandlerCRUDTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
db *gorm.DB
|
||||
router *gin.Engine
|
||||
db *gorm.DB
|
||||
router *gin.Engine
|
||||
handler *ContactHandler
|
||||
|
||||
account *model.Account
|
||||
@@ -49,6 +49,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
||||
&model.AccountUser{},
|
||||
&model.Inbox{},
|
||||
&model.Contact{},
|
||||
&model.Conversation{},
|
||||
&model.ContactInbox{},
|
||||
&model.InboxMember{},
|
||||
&model.ContactNote{},
|
||||
@@ -62,14 +63,16 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
||||
contactInboxRepo := repository.NewContactInboxRepo(db)
|
||||
contactNoteRepo := repository.NewContactNoteRepo(db)
|
||||
noteRepo := repository.NewNoteRepo(db)
|
||||
conversationRepo := repository.NewConversationRepo(db)
|
||||
|
||||
contactInboxSvc := service.NewContactInboxService(contactInboxRepo)
|
||||
mergeRepo := repository.NewContactMergeRepo(db)
|
||||
mergeSvc := service.NewContactMergeService(mergeRepo, db)
|
||||
contactSvc := service.NewContactService(contactRepo, contactInboxSvc, noteRepo)
|
||||
contactNoteSvc := service.NewContactNoteService(contactRepo, contactNoteRepo)
|
||||
conversationSvc := service.NewConversationService(conversationRepo, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
s.handler = NewContactHandler(contactSvc, contactInboxSvc, mergeSvc, contactNoteSvc)
|
||||
s.handler = NewContactHandler(contactSvc, contactInboxSvc, mergeSvc, contactNoteSvc, conversationSvc)
|
||||
|
||||
// Setup router with all contact routes
|
||||
s.router = gin.New()
|
||||
@@ -80,6 +83,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
||||
s.router.POST("/api/v1/accounts/:id/contacts", s.handler.Create)
|
||||
s.router.PUT("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Update)
|
||||
s.router.DELETE("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Delete)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/conversations", s.handler.ListConversations)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/contact_inboxes", s.handler.ListContactInboxes)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.ListNotes)
|
||||
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
|
||||
@@ -109,6 +113,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupTest() {
|
||||
s.db.Exec("DELETE FROM contact_notes")
|
||||
s.db.Exec("DELETE FROM notes")
|
||||
s.db.Exec("DELETE FROM contact_inboxes")
|
||||
s.db.Exec("DELETE FROM conversations")
|
||||
s.db.Exec("DELETE FROM contacts")
|
||||
s.db.Exec("DELETE FROM inbox_members")
|
||||
s.db.Exec("DELETE FROM inboxes")
|
||||
@@ -394,9 +399,9 @@ func (s *ContactHandlerCRUDTestSuite) TestCreate_EmptyName() {
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestCreate_WithCustomAttributes() {
|
||||
body := map[string]interface{}{
|
||||
"name": "Custom Contact",
|
||||
"email": "custom@example.com",
|
||||
"custom_attributes": map[string]interface{}{"tier": "gold", "vip": true},
|
||||
"name": "Custom Contact",
|
||||
"email": "custom@example.com",
|
||||
"custom_attributes": map[string]interface{}{"tier": "gold", "vip": true},
|
||||
"additional_attributes": map[string]interface{}{"city": "NYC"},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
@@ -620,6 +625,41 @@ func (s *ContactHandlerCRUDTestSuite) TestListContactInboxes_EmptyResult() {
|
||||
s.Equal(float64(0), meta["count"])
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// ListConversations
|
||||
// ===========================
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestListConversations_Success() {
|
||||
inbox := &model.Inbox{AccountID: s.account.ID, Name: "Conversation Inbox", ChannelType: "web_widget", ChannelID: 1}
|
||||
s.Require().NoError(s.db.Create(inbox).Error)
|
||||
|
||||
now := time.Now().Unix()
|
||||
older := now - 60
|
||||
s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "low", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &older}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "high", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/conversations", s.account.ID, s.contact.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
payload := resp["payload"].([]interface{})
|
||||
s.Len(payload, 2)
|
||||
first := payload[0].(map[string]interface{})
|
||||
s.Equal("high", first["priority"])
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestListConversations_InvalidContactID() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/contacts/abc/conversations", s.account.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// ListNotes
|
||||
// ===========================
|
||||
@@ -792,4 +832,4 @@ func (s *ContactHandlerCRUDTestSuite) TestCreateNote_NotFoundContact() {
|
||||
|
||||
func TestContactHandlerCRUDTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ContactHandlerCRUDTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// Reference: Chatwoot app/controllers/api/v1/conversations_controller.rb
|
||||
type ConversationHandler struct {
|
||||
conversationSvc *service.ConversationService
|
||||
messageSvc *service.MessageService
|
||||
messageSvc *service.MessageService
|
||||
}
|
||||
|
||||
// NewConversationHandler creates a new ConversationHandler.
|
||||
@@ -459,7 +459,7 @@ func (h *ConversationHandler) UpdatePriority(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Priority string `json:"priority" binding:"required,oneof=urgent high medium low"`
|
||||
Priority string `json:"priority" binding:"required,oneof=urgent high medium low none"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
@@ -474,6 +474,40 @@ func (h *ConversationHandler) UpdatePriority(c *gin.Context) {
|
||||
response.OK(c, conversation)
|
||||
}
|
||||
|
||||
// TogglePriority updates a conversation priority using Chatwoot's member action path.
|
||||
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority
|
||||
// Reference: Chatwoot conversations#toggle_priority returns head :ok.
|
||||
func (h *ConversationHandler) TogglePriority(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Priority *string `json:"priority"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
priority := ""
|
||||
if req.Priority != nil {
|
||||
priority = *req.Priority
|
||||
}
|
||||
|
||||
if _, svcErr := h.conversationSvc.UpdatePriority(c.Request.Context(), accountID, conversationID, priority); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// ListMessages lists messages in a conversation.
|
||||
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages
|
||||
func (h *ConversationHandler) ListMessages(c *gin.Context) {
|
||||
@@ -781,4 +815,4 @@ func (h *ConversationHandler) UnreadCounts(c *gin.Context) {
|
||||
}
|
||||
|
||||
response.OK(c, payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,21 @@ func (r *ConversationRepo) FindByContact(ctx context.Context, accountID, contact
|
||||
return conversations, total, err
|
||||
}
|
||||
|
||||
// FindRecentByContact retrieves the latest conversations for a contact.
|
||||
// Reference: Chatwoot contacts/conversations#index limits to the latest 20 conversations ordered by last_activity_at.
|
||||
func (r *ConversationRepo) FindRecentByContact(ctx context.Context, accountID, contactID uint, inboxID *uint, limit int) ([]model.Conversation, error) {
|
||||
var conversations []model.Conversation
|
||||
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND contact_id = ?", accountID, contactID)
|
||||
if inboxID != nil && *inboxID != 0 {
|
||||
query = query.Where("inbox_id = ?", *inboxID)
|
||||
}
|
||||
|
||||
err := query.Limit(limit).Order("last_activity_at DESC").Order("id DESC").Find(&conversations).Error
|
||||
return conversations, err
|
||||
}
|
||||
|
||||
// FindByContactIDs retrieves conversations for multiple contacts within an account.
|
||||
func (r *ConversationRepo) FindByContactIDs(ctx context.Context, accountID uint, contactIDs []uint, offset, limit int) ([]model.Conversation, int64, error) {
|
||||
var conversations []model.Conversation
|
||||
@@ -396,7 +411,7 @@ func (r *ConversationRepo) UpdateAgentLastSeenAt(ctx context.Context, id uint, l
|
||||
|
||||
// UnreadCountResult holds unread conversation counts grouped by a key.
|
||||
type UnreadCountResult struct {
|
||||
Key uint `gorm:"column:key"`
|
||||
Key uint `gorm:"column:key"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
|
||||
@@ -475,4 +490,4 @@ func (r *ConversationRepo) CountOpenConversationsByAssignees(ctx context.Context
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,9 +723,11 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
conversations.POST("/filter", h.Conversation.Filter)
|
||||
conversations.GET("/:conversation_id", h.Conversation.Get)
|
||||
conversations.PATCH("/:conversation_id", h.Conversation.Update)
|
||||
conversations.PUT("/:conversation_id", h.Conversation.Update)
|
||||
conversations.DELETE("/:conversation_id", h.Conversation.Delete)
|
||||
conversations.POST("/:conversation_id/assign", h.Conversation.AssignAgent)
|
||||
conversations.POST("/:conversation_id/toggle_status", h.Conversation.ToggleStatus)
|
||||
conversations.POST("/:conversation_id/toggle_priority", h.Conversation.TogglePriority)
|
||||
conversations.PATCH("/:conversation_id/labels", h.Label.ReplaceConversationLabels)
|
||||
conversations.GET("/:conversation_id/labels", h.Label.GetConversationLabels)
|
||||
conversations.POST("/:conversation_id/labels", h.Label.AddLabelToConversation)
|
||||
@@ -774,6 +776,7 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
msgs.POST("/", h.Message.Create)
|
||||
msgs.GET("/:message_id", h.Message.Get)
|
||||
msgs.PATCH("/:message_id", h.Message.Update)
|
||||
msgs.PUT("/:message_id", h.Message.Update)
|
||||
msgs.DELETE("/:message_id", h.Message.Delete)
|
||||
msgs.POST("/:message_id/retry", h.Message.Retry)
|
||||
msgs.POST("/:message_id/translate", h.Message.Translate)
|
||||
@@ -829,14 +832,16 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
contacts.POST("/", h.Contact.Create)
|
||||
contacts.GET("/search", h.Contact.Search)
|
||||
contacts.POST("/filter", h.Contact.Filter)
|
||||
contacts.GET("/active", h.Contact.Active)
|
||||
contacts.GET("/export", h.Contact.Export)
|
||||
contacts.POST("/export", h.Contact.ExportRequest)
|
||||
contacts.POST("/import", h.Contact.Import)
|
||||
contacts.GET("/:contact_id", h.Contact.Get)
|
||||
contacts.PUT("/:contact_id", h.Contact.Update)
|
||||
contacts.DELETE("/:contact_id", h.Contact.Delete)
|
||||
|
||||
// M4 G3: Contact extension routes (active, export, import, contactable_inboxes, custom_attributes)
|
||||
contacts.GET("/active", h.Contact.Active)
|
||||
contacts.GET("/export", h.Contact.Export)
|
||||
contacts.POST("/import", h.Contact.Import)
|
||||
contacts.GET("/:contact_id/conversations", h.Contact.ListConversations)
|
||||
contacts.GET("/:contact_id/contactable_inboxes", h.Contact.ContactableInboxes)
|
||||
contacts.DELETE("/:contact_id/custom_attributes", h.Contact.DeleteCustomAttributes)
|
||||
contacts.POST("/:contact_id/destroy_custom_attributes", h.Contact.DestroyCustomAttributes)
|
||||
|
||||
@@ -92,6 +92,14 @@ func (s *ConversationService) ListByAssignee(ctx context.Context, accountID, ass
|
||||
return s.repo.FindByAssignee(ctx, accountID, assigneeID, offset, limit)
|
||||
}
|
||||
|
||||
// ListRecentByContact retrieves recent conversations for a contact.
|
||||
func (s *ConversationService) ListRecentByContact(ctx context.Context, accountID, contactID uint, inboxID *uint, limit int) ([]model.Conversation, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
return s.repo.FindRecentByContact(ctx, accountID, contactID, inboxID, limit)
|
||||
}
|
||||
|
||||
// ListUnassigned retrieves unassigned open conversations.
|
||||
func (s *ConversationService) ListUnassigned(ctx context.Context, accountID uint, offset, limit int) ([]model.Conversation, int64, error) {
|
||||
return s.repo.FindUnassigned(ctx, accountID, offset, limit)
|
||||
@@ -723,11 +731,15 @@ func (s *ConversationService) Search(ctx context.Context, accountID uint, query
|
||||
|
||||
// UpdatePriority updates the priority of a conversation.
|
||||
func (s *ConversationService) UpdatePriority(ctx context.Context, accountID, id uint, priority string) (*model.Conversation, error) {
|
||||
if priority == "" {
|
||||
priority = "none"
|
||||
}
|
||||
newPriority := model.ConversationPriority(priority)
|
||||
if newPriority != model.ConversationPriorityUrgent &&
|
||||
newPriority != model.ConversationPriorityHigh &&
|
||||
newPriority != model.ConversationPriorityMedium &&
|
||||
newPriority != model.ConversationPriorityLow {
|
||||
newPriority != model.ConversationPriorityLow &&
|
||||
priority != "none" {
|
||||
return nil, errors.New("invalid priority value")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user