From 2ab7b582e0c5df191ca42120f718c164b7fe3900 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 6 Jun 2026 00:07:17 +0800 Subject: [PATCH] feat(accounts): align account payloads --- docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md | 12 ++-- internal/handler/api/v1/account_handler.go | 58 +++++++++++++++++-- .../handler/api/v1/account_handler_test.go | 50 +++++++++++----- internal/service/account_service.go | 53 ++++++++++------- internal/service/account_service_test.go | 9 +-- 5 files changed, 133 insertions(+), 49 deletions(-) diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index f52b8591..c16ce6af 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -16,10 +16,10 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc ## Current Baseline -- Current tracking checkpoint: 2026-06-05 after `dc09036 feat(settings): align agents teams payloads`, with this implementation checkpoint prepared as `feat(settings): align assignable agents payloads`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(settings): align assignable agents payloads`. +- Current tracking checkpoint: 2026-06-05 after `5efa195 feat(settings): align assignable agents payloads`, with this implementation checkpoint prepared as `feat(accounts): align account payloads`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(accounts): align account payloads`. - Latest documentation-only checkpoint: `2923aae docs: land parity execution tracker`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`. -- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage; B11.3b aligns Captain playground request/response payloads, account scoping, v2 history handling, and no-LLM fallback; B11.3c adds the fakeable Captain document sync backend gate with disabled, failed, and fake-success states; B11.3d aligns Captain task request/response payloads, no-provider disabled states, follow-up context, suggestion persistence, and Copilot message tool-call key validation; B11.3e aligns Captain stream DTOs/disabled SSE fallbacks and Copilot push-event payload shapes; B12.1 adds the reusable GoChat server/seed entrypoint plus a Meilisearch-first reused Chatwoot frontend smoke harness and report; B12.2a adds API smoke assertions for auth/profile, inbox, conversation/messages, contact/company, widget config/message, and public CSAT; B12.2b adds a zero-dependency Chrome DevTools browser smoke that loads the reused Chatwoot login and dashboard entrypoints through Vite and checks browser auth/dashboard API requests; B12.3a adds enterprise API smoke assertions for SLA reports/download, CSAT reports/download, automation/macros, audit/custom roles, capacity, Captain, and Copilot; B12.3b adds reused-frontend enterprise browser route navigation for SLA, CSAT, automation, macros, audit logs, custom roles, capacity, Captain, and Copilot request coverage; P5.1 adds the PostgreSQL-backed durable `background_jobs` model/migration plus WorkerPool enqueue, schedule, retry/backoff, dead-letter, idempotency, stale-lock recovery, and focused tests; P5.2 wires `channel.Dispatcher` and `dispatch.EventDispatcher` async paths into durable event jobs with worker replay tests; P5.3 queues Meilisearch write-side index/delete jobs for conversations, messages, contacts, companies, and articles while keeping search reads Meilisearch-first; P5.4 queues automation webhook and email transcript side effects as durable jobs while preserving fakeable delivery boundaries; P5.5 queues Chatwoot-style macro execute fan-out through durable `automation:macro_execution` jobs; P5.6 queues resolve-triggered CSAT survey sends and WhatsApp/Twilio CSAT template creation through durable jobs; P5.7 queues Chatwoot enterprise SLA account scans and applied-SLA evaluation jobs through the durable worker; P5.8 queues Chatwoot-style contact export artifact generation through durable `contact:export` jobs; P5.9 queues normalized provider inbound message persistence/dispatch through durable `webhook:incoming_message_persist` jobs; P5.10 queues Chatwoot `SendReplyJob`-style outbound message delivery through durable `message:send_reply` jobs and provider delivery-status/read-receipt updates through durable webhook status jobs; P5.11 queues Captain document sync, crawl/parser, schedule-sync, response-builder, embedding-update, Copilot response, and Captain conversation response-builder work through durable jobs; P5.12 queues scheduled item fan-out, one-off campaigns, snoozed conversation reopening, account auto-resolution, widget/public message status updates, and account conversation bulk actions through durable jobs; P5.13a replaces the live report, bot report, conversation summary, inbox-label matrix, first-response distribution, and outgoing-message placeholder responses with persisted conversation/message/reporting-event aggregations; P5.13b routes `GET /reports` to Chatwoot-style metric timeseries, adds lazy rollup freshness/idempotency, registers durable `reporting:rollup_day` jobs, and makes rollup replacement hard-delete soft-deleted rows before recompute; B9.3 verifies the current reference exposes no delayed automation action params, preserves scheduled-item coverage under P5.12, and completes remaining automation action side effects by queueing `send_email_to_team` plus applying enterprise `add_sla`; Phase 2/3 drift audit now aligns account notifications with Chatwoot `NotificationFinder` list envelopes, `includes` filters, unread counts, primary-actor `read_all`, raw action responses, and `destroy_all` read/all semantics; P6.5 now aligns WhatsApp Cloud inbox health/register-webhook endpoints with Chatwoot raw payloads, Cloud-only guards, Graph health fields, default callback URL construction, and no-body register behavior; P6.5 also aligns `sync_templates` with Chatwoot's async low-priority template sync job and `{ message: "Template sync initiated successfully" }` response; Phase 2/3 auth now aligns Chatwoot Devise password reset/confirmation token columns, `POST/PUT /auth/password`, and `POST /auth/confirmation` payload/auth-header behavior; Phase 3/6 CRM attachments now aligns conversation shared-file payloads and adds Chatwoot contact shared-file attachments with meta totals, serialized attachment fields, sender, created_at, and display conversation IDs; Phase 2/3 settings now aligns Chatwoot agents, teams, team members, and assignable-agent payload envelopes, raw frontend request bodies, `allow_auto_assign`, custom-role assignment, and `:team_id` route params. Next active implementation slice is continued Phase 6 placeholder audit, followed by further Phase 2/3 drift from smoke/reference evidence. +- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage; B11.3b aligns Captain playground request/response payloads, account scoping, and no-LLM fallback; B11.3c adds the fakeable Captain document sync backend gate with disabled, failed, and fake-success states; B11.3d aligns Captain task request/response payloads, no-provider disabled states, follow-up context, suggestion persistence, and Copilot message tool-call key validation; B11.3e aligns Captain stream DTOs/disabled SSE fallbacks and Copilot push-event payload shapes; B12.1 adds the reusable GoChat server/seed entrypoint plus a Meilisearch-first reused Chatwoot frontend smoke harness and report; B12.2a adds API smoke assertions for auth/profile, inbox, conversation/messages, contact/company, widget config/message, and public CSAT; B12.2b adds a zero-dependency Chrome DevTools browser smoke that loads the reused Chatwoot login and dashboard entrypoints through Vite and checks browser auth/dashboard API requests; B12.3a adds enterprise API smoke assertions for SLA reports/download, CSAT reports/download, automation/macros, audit/custom roles, capacity, Captain, and Copilot; B12.3b adds reused-frontend enterprise browser route navigation for SLA, CSAT, automation, macros, audit logs, custom roles, capacity, Captain, and Copilot request coverage; P5.1 adds the PostgreSQL-backed durable `background_jobs` model/migration plus WorkerPool enqueue, schedule, retry/backoff, dead-letter, idempotency, stale-lock recovery, and focused tests; P5.2 wires `channel.Dispatcher` and `dispatch.EventDispatcher` async paths into durable event jobs with worker replay tests; P5.3 queues Meilisearch write-side index/delete jobs for conversations, messages, contacts, companies, and articles while keeping search reads Meilisearch-first; P5.4 queues automation webhook and email transcript side effects as durable jobs while preserving fakeable delivery boundaries; P5.5 queues Chatwoot-style macro execute fan-out through durable `automation:macro_execution` jobs; P5.6 queues resolve-triggered CSAT survey sends and WhatsApp/Twilio CSAT template creation through durable jobs; P5.7 queues Chatwoot enterprise SLA account scans and applied-SLA evaluation jobs through the durable worker; P5.8 queues Chatwoot-style contact export artifact generation through durable `contact:export` jobs; P5.9 queues normalized provider inbound message persistence/dispatch through durable `webhook:incoming_message_persist` jobs; P5.10 queues Chatwoot `SendReplyJob`-style outbound message delivery through durable `message:send_reply` jobs and provider delivery-status/read-receipt updates through durable webhook status jobs; P5.11 queues Captain document sync, crawl/parser, schedule-sync, response-builder, embedding-update, Copilot response, and Captain conversation response-builder work through durable jobs; P5.12 queues scheduled item fan-out, one-off campaigns, snoozed conversation reopening, account auto-resolution, widget/public message status updates, and account conversation bulk actions through durable jobs; P5.13a replaces the live report, bot report, conversation summary, inbox-label matrix, first-response distribution, and outgoing-message placeholder responses with persisted conversation/message/reporting-event aggregations; P5.13b routes `GET /reports` to Chatwoot-style metric timeseries, adds lazy rollup freshness/idempotency, registers durable `reporting:rollup_day` jobs, and makes rollup replacement hard-delete soft-deleted rows before recompute; B9.3 verifies the current reference exposes no delayed automation action params, preserves scheduled-item coverage under P5.12, and completes remaining automation action side effects by queueing `send_email_to_team` plus applying enterprise `add_sla`; Phase 2/3 drift audit now aligns account notifications with Chatwoot `NotificationFinder` list envelopes, `includes` filters, unread counts, primary-actor `read_all`, raw action responses, and `destroy_all` read/all semantics; P6.5 now aligns WhatsApp Cloud inbox health/register-webhook endpoints with Chatwoot raw payloads, Cloud-only guards, Graph health fields, default callback URL construction, and no-body register behavior; P6.5 also aligns `sync_templates` with Chatwoot's async low-priority template sync job and `{ message: "Template sync initiated successfully" }` response; Phase 2/3 auth now aligns Chatwoot Devise password reset/confirmation token columns, `POST/PUT /auth/password`, and `POST /auth/confirmation` payload/auth-header behavior; Phase 3/6 CRM attachments now aligns conversation shared-file payloads and adds Chatwoot contact shared-file attachments with meta totals, serialized attachment fields, sender, created_at, and display conversation IDs; Phase 2/3 settings now aligns Chatwoot account show/update/create/cache-key/active-at payloads plus agents, teams, team members, and assignable-agent payload envelopes, raw frontend request bodies, `allow_auto_assign`, custom-role assignment, and `:team_id` route params. Next active implementation slice is continued Phase 6 placeholder audit, followed by further Phase 2/3 drift from smoke/reference evidence. - `go test ./...` passes. - Route dump succeeds with `TOTAL: 837` after adding Chatwoot auth reset/confirmation routes and `GET /api/v1/accounts/:account_id/contacts/:contact_id/attachments`. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. @@ -87,7 +87,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | -| 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current critical set; notification list/action serializers, Devise password reset/confirmation payloads, CRM shared attachment payloads, settings agents/teams payloads, and assignable-agent payloads now match Chatwoot's frontend contract; broader serializer fixtures remain partial. | Expand route/serializer fixtures when smoke or reference inspection exposes drift. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | +| 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current critical set; notification list/action serializers, Devise password reset/confirmation payloads, CRM shared attachment payloads, account show/update/create/cache-key/active-at payloads, settings agents/teams payloads, and assignable-agent payloads now match Chatwoot's frontend contract; broader serializer fixtures remain partial. | Expand route/serializer fixtures when smoke or reference inspection exposes drift. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | | 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; account/contact/conversation/message/inbox audit remains broad. | Continue fresh placeholder audit and assign every frontend-reachable stub to a tracked owner. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. | | 3 | B12 optional live smoke | API/browser/enterprise smoke commands are checked in; live runs need PostgreSQL, Redis, Meilisearch, Vite, and Chrome. | Run full live smoke when environment is available and map failures to the board. | `docs/parity/frontend_smoke_report.md` records pass/fail and linked owners. | | 4 | B9.3 delayed automation actions | Current reference exposes no delayed automation action params; scheduled-item work is already P5.12; `send_email_to_team` is durable and `add_sla` mutates conversation/applied SLA state. | Keep automation drift closed if future reference/smoke exposes delayed params or unsupported action shapes. | Automation worker/action fixtures verify queued team email replay, retry visibility through worker jobs, and SLA action idempotency. | @@ -139,6 +139,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(accounts): align account payloads` | Advances Phase 2/3 account settings parity by aligning core reused-frontend account endpoints with Chatwoot `AccountsController`, `_account.json.jbuilder`, `show/update/create` views, dashboard `account.js`, and the account Vuex store. Account show/update/settings now return raw account objects instead of local `{ success, data }` envelopes, with `settings`, `custom_attributes`, `features`, `cache_keys`, `support_email`, `created_at`, locale/name/domain/status fields. Authenticated account creation accepts Chatwoot `account_name` and returns `{ data: { account_id } }` for the add-account modal. `update_active_at` now returns empty `200 OK`, and `cache_keys` now returns Chatwoot `{ cache_keys: { label, inbox, team } }` with zero-epoch defaults. | `go test ./internal/handler/api/v1 -run AccountHandler -count=1`; `go test ./internal/service -run AccountService -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | P3.2 remains Doing for account users/invitations/permissions drift; continue Phase 6 placeholder audit from fresh reference/smoke evidence. | | `feat(settings): align assignable agents payloads` | Advances P3.2/P3.3 settings and inbox assignment parity by aligning `GET /api/v1/accounts/:account_id/assignable_agents` and `GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents` with Chatwoot `AssignableAgentsController#index`, `inboxes#assignable_agents`, and `_agent.json.jbuilder`. Assignable-agent endpoints now return `{ payload: [...] }` instead of the local `{ success, data }` envelope and serialize agent fields required by the reused conversation assignment UI: `account_id`, `availability_status`, `auto_offline`, `confirmed`, `provider`, `available_name`, `thumbnail`, role, and enterprise `custom_role_id`. AccountUser role, availability, auto-offline, and custom-role data are loaded from the account membership instead of the global user role. | `go test ./internal/handler/api/v1 -run AssignableAgent -count=1`; `go test ./internal/service -run AssignableAgent -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | P3.2/P3.3 remain Doing for deeper account users/invitations/permissions and inbox serializer drift; continue Phase 6 placeholder audit from fresh reference/smoke evidence. | | `feat(settings): align agents teams payloads` | Advances P3.2 account/users/teams parity by aligning settings agents, teams, and team-member endpoints with Chatwoot `agents_controller`, `teams_controller`, `team_members_controller`, and the reused frontend stores/API clients. Agent list/create/update/get now return raw Chatwoot agent objects/arrays with `availability_status`, `available_name`, `confirmed`, `thumbnail`, `auto_offline`, and `custom_role_id`; create/update accept the raw frontend body as well as wrapped params; bulk create and delete return Chatwoot `head :ok`. Team list/show/create/update now return raw team objects/arrays with `allow_auto_assign` and `is_member`, accept raw `allow_auto_assign`, preserve explicit false values despite DB defaults, and team-member list/create/update return agent serializer arrays. The handler also accepts router `:team_id` params used by the real account-scoped routes. | `go test ./internal/handler/api/v1 -run 'AgentHandler\|TeamHandler' -count=1`; `go test ./internal/service -run 'AgentService\|TeamService' -count=1`; `go test ./internal/repository ./internal/handler/api/v1 ./internal/router -count=1`; `go test ./...`; `git diff --check`. | P3.2 moves to Doing; continue accounts/users/invitations/permissions drift, then Phase 6 placeholder audit from fresh reference/smoke evidence. | | `feat(crm): align shared attachment payloads` | Advances Phase 3/6 CRM serializer parity by adding Chatwoot's `GET /api/v1/accounts/:account_id/contacts/:contact_id/attachments` shared-file route and aligning conversation/contact attachment responses with `contacts/attachments#index`, `conversations#attachments`, and `_attachment.json.jbuilder`. Attachment lists now return `{ meta: { total_count }, payload }`, order newest first, serialize `data_url`, `thumb_url`, `file_size`, `file_type`, `extension`, dimensions, `created_at`, sender data, and contact attachment `conversation_id` as the Chatwoot display ID. | `go test ./internal/service -run 'MessageService\|ContactService' -count=1`; `go test ./internal/handler/api/v1 -run 'ContactHandlerCRUD\|ConversationHandler' -count=1`; `go test ./internal/service ./internal/handler/api/v1 ./internal/router -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. | P3.5 remains Doing for broader contact/company drift; continue Phase 6 placeholder audit and CRM serializer fixture expansion from reference/smoke evidence. | @@ -1541,7 +1542,7 @@ Frontend-critical API groups to audit first: | ID | Area | Scope | Status | | --- | --- | --- | --- | | P3.1 | Auth/session/profile | Login, logout, current user, profile, availability, notification settings, Devise password reset, and confirmation. | Done | -| P3.2 | Accounts/users/teams | Account settings, users, agents, teams, team members, invitations, roles, permissions. | Doing; agents/teams/team-member and assignable-agent settings payloads now have focused Chatwoot-style coverage. | +| P3.2 | Accounts/users/teams | Account settings, users, agents, teams, team members, invitations, roles, permissions. | Doing; account show/update/create/cache-key/active-at plus agents/teams/team-member and assignable-agent settings payloads now have focused Chatwoot-style coverage. | | P3.3 | Inboxes/channels | Inbox CRUD, assignable agents, avatars, channel config, business hours, widget config. | Review; assignable-agent payload drift closed, broader inbox serializer drift remains evidence-driven. | | P3.4 | Conversations/messages | List filters, status changes, assignment, labels, private notes, attachments, drafts, typing/read events. | Review | | P3.5 | Contacts/companies | CRUD, merge, labels, notes, custom attributes, import/export, conversations relation, and shared attachments. | Doing | @@ -2055,6 +2056,7 @@ Verification milestone gates: ## Progress Log +- 2026-06-05: Phase 2/3 account settings checkpoint prepared as `feat(accounts): align account payloads`; audited Chatwoot `AccountsController`, `_account.json.jbuilder`, account create/show/update views, dashboard `account.js`, account Vuex store, and account settings components. Account show/update/settings now return raw Chatwoot account objects with settings, features, custom attributes, cache keys, locale/name/domain/status/support fields instead of local success/data envelopes. Account creation now accepts Chatwoot `account_name` and returns `{ data: { account_id } }` for the add-account modal. `update_active_at` returns empty `200 OK`, and `cache_keys` returns `{ cache_keys: { label, inbox, team } }` with zero-epoch defaults. Focused account handler/service tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` passed; continue account users/invitations/permissions drift and Phase 6 placeholder audit next. - 2026-06-05: Phase 2/3 settings/inbox assignment checkpoint prepared as `feat(settings): align assignable agents payloads`; audited Chatwoot `AssignableAgentsController#index`, inbox `assignable_agents` view, `_agent.json.jbuilder`, and reused dashboard assignable-agent API/store consumers. Assignable-agent endpoints now return Chatwoot `{ payload: [...] }` instead of local success/data envelopes, serialize the same agent fields used by conversation assignment UI, and source role, availability, auto-offline, and custom-role data from the account membership. Focused handler/service tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` passed; continue account users/invitations/permissions drift and Phase 6 placeholder audit next. - 2026-06-05: Phase 2/3 settings checkpoint prepared as `feat(settings): align agents teams payloads`; audited Chatwoot `AgentsController`, `TeamsController`, `TeamMembersController`, `_agent.json.jbuilder`, `_team.json.jbuilder`, and reused dashboard agents/teams API/store code. Agents now return raw Chatwoot arrays/objects instead of local `{ success, data, meta }` envelopes, accept raw frontend request bodies, expose `availability_status`, `available_name`, `confirmed`, `thumbnail`, `auto_offline`, and custom-role IDs, and match `head :ok` for bulk create/delete. Teams now return raw `allow_auto_assign`/`is_member` payloads, accept raw `allow_auto_assign`, preserve explicit false values, return team-member agent arrays, and parse account-scoped `:team_id` routes. Focused handler/service/repository/router tests, full `go test ./...`, and `git diff --check` passed; continue accounts/users/invitations drift and Phase 6 placeholder audit next. - 2026-06-05: Phase 3/6 CRM attachment checkpoint prepared as `feat(crm): align shared attachment payloads`; audited Chatwoot `Contacts::AttachmentsController#index`, `conversations#attachments`, `_attachment.json.jbuilder`, and the reused dashboard shared-files store/API. Added `GET /api/v1/accounts/:account_id/contacts/:contact_id/attachments`, aligned conversation attachment lists to `{ meta: { total_count }, payload }`, serialized shared-file fields through the Chatwoot attachment shape with sender and `created_at`, returned contact attachment `conversation_id` as display ID, regenerated `docs/parity/gochat_routes.txt` to `TOTAL: 837`, and reran route parity. Focused service/handler/router tests, full `go test ./...`, and `git diff --check` passed; continue Phase 6 placeholder audit next. diff --git a/internal/handler/api/v1/account_handler.go b/internal/handler/api/v1/account_handler.go index 23c04162..b778b7e1 100644 --- a/internal/handler/api/v1/account_handler.go +++ b/internal/handler/api/v1/account_handler.go @@ -1,9 +1,11 @@ package v1 import ( + "encoding/json" "net/http" "github.com/gin-gonic/gin" + "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/pagination" @@ -78,7 +80,7 @@ func (h *AccountHandler) Get(c *gin.Context) { return } - response.OK(c, account) + c.JSON(http.StatusOK, serializeAccount(account)) } // @Summary Create a new account @@ -113,7 +115,7 @@ func (h *AccountHandler) Create(c *gin.Context) { return } - response.Created(c, account) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": userID, "account_id": account.ID}}) } // @Summary Update an existing account @@ -149,7 +151,7 @@ func (h *AccountHandler) Update(c *gin.Context) { return } - response.OK(c, account) + c.JSON(http.StatusOK, serializeAccount(account)) } // @Summary Delete an account @@ -310,7 +312,7 @@ func (h *AccountHandler) UpdateSettings(c *gin.Context) { return } - response.OK(c, account) + c.JSON(http.StatusOK, serializeAccount(account)) } // --- Account extension handlers (G8) --- @@ -332,7 +334,7 @@ func (h *AccountHandler) UpdateActiveAt(c *gin.Context) { return } - response.OK(c, gin.H{"message": "Active timestamp updated"}) + c.Status(http.StatusOK) } // CacheKeys returns cache key identifiers for frontend cache invalidation. @@ -352,5 +354,49 @@ func (h *AccountHandler) CacheKeys(c *gin.Context) { return } - response.OK(c, keys) + c.JSON(http.StatusOK, gin.H{"cache_keys": keys}) +} + +func serializeAccount(account *model.Account) map[string]any { + if account == nil { + return map[string]any{} + } + return map[string]any{ + "settings": serializeAccountSettings(account), + "created_at": account.CreatedAt, + "domain": account.Domain, + "features": parseAccountFeatures(account.FeatureFlags), + "id": account.ID, + "locale": nonEmpty(account.Locale, "en"), + "name": account.Name, + "support_email": nil, + "status": nonEmpty(account.Status, "active"), + "cache_keys": map[string]string{"label": "0000000000", "inbox": "0000000000", "team": "0000000000"}, + "custom_attributes": map[string]any{ + "timezone": account.Timezone, + }, + } +} + +func serializeAccountSettings(account *model.Account) map[string]any { + settings := map[string]any{ + "auto_resolve_after": account.AutoResolveDuration, + "auto_resolve_duration": account.AutoResolveDuration, + "auto_resolve_message": "", + "auto_resolve_ignore_waiting": false, + "audio_transcriptions": false, + "auto_resolve_label": "", + } + return settings +} + +func parseAccountFeatures(raw string) map[string]any { + features := map[string]any{} + if raw == "" { + return features + } + if err := json.Unmarshal([]byte(raw), &features); err != nil { + return features + } + return features } diff --git a/internal/handler/api/v1/account_handler_test.go b/internal/handler/api/v1/account_handler_test.go index c50ee653..f7442d5f 100644 --- a/internal/handler/api/v1/account_handler_test.go +++ b/internal/handler/api/v1/account_handler_test.go @@ -28,9 +28,9 @@ import ( type AccountHandlerTestSuite struct { suite.Suite - router *gin.Engine - handler *AccountHandler - db *gorm.DB + router *gin.Engine + handler *AccountHandler + db *gorm.DB testUserID uint } @@ -191,8 +191,11 @@ func (s *AccountHandlerTestSuite) TestGet_Success() { assert.Equal(s.T(), http.StatusOK, w.Code) resp := s.unmarshalResponse(w) - data := resp["data"].(map[string]interface{}) - assert.Equal(s.T(), "Get Account", data["name"]) + assert.NotContains(s.T(), resp, "success") + assert.Equal(s.T(), "Get Account", resp["name"]) + assert.Contains(s.T(), resp, "settings") + assert.Contains(s.T(), resp, "features") + assert.Contains(s.T(), resp, "cache_keys") } func (s *AccountHandlerTestSuite) TestGet_NotFound() { @@ -221,10 +224,24 @@ func (s *AccountHandlerTestSuite) TestCreate_Success() { req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) - assert.Equal(s.T(), http.StatusCreated, w.Code) + assert.Equal(s.T(), http.StatusOK, w.Code) resp := s.unmarshalResponse(w) data := resp["data"].(map[string]interface{}) - assert.Equal(s.T(), "New Test Account", data["name"]) + assert.NotZero(s.T(), data["account_id"]) + assert.NotContains(s.T(), resp, "success") +} + +func (s *AccountHandlerTestSuite) TestCreate_ChatwootAccountName() { + body := `{"account_name":"Chatwoot Account"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/accounts", bytes.NewBufferString(body)) + req.Header.Set("Content-Type", "application/json") + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code) + resp := s.unmarshalResponse(w) + data := resp["data"].(map[string]interface{}) + assert.NotZero(s.T(), data["account_id"]) } func (s *AccountHandlerTestSuite) TestCreate_MissingName() { @@ -251,8 +268,8 @@ func (s *AccountHandlerTestSuite) TestUpdate_Success() { assert.Equal(s.T(), http.StatusOK, w.Code) resp := s.unmarshalResponse(w) - data := resp["data"].(map[string]interface{}) - assert.Equal(s.T(), "After Update", data["name"]) + assert.NotContains(s.T(), resp, "success") + assert.Equal(s.T(), "After Update", resp["name"]) } // ====== Delete Account ====== @@ -278,8 +295,10 @@ func (s *AccountHandlerTestSuite) TestUpdateSettings() { req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) - // Should succeed or return acceptable status - assert.True(s.T(), w.Code == http.StatusOK || w.Code == http.StatusNoContent || w.Code < 500) + assert.Equal(s.T(), http.StatusOK, w.Code) + resp := s.unmarshalResponse(w) + assert.NotContains(s.T(), resp, "success") + assert.Contains(s.T(), resp, "settings") } // ====== List Users ====== @@ -429,6 +448,7 @@ func (s *AccountHandlerTestSuite) TestUpdateActiveAt_Success() { s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) + assert.Empty(s.T(), w.Body.String()) // Verify active_at was updated in DB var au model.AccountUser @@ -456,7 +476,11 @@ func (s *AccountHandlerTestSuite) TestCacheKeys_Success() { assert.Equal(s.T(), http.StatusOK, w.Code) data := s.unmarshalResponse(w) - assert.NotNil(s.T(), data) + keys := data["cache_keys"].(map[string]interface{}) + assert.Equal(s.T(), "0000000000", keys["label"]) + assert.Equal(s.T(), "0000000000", keys["inbox"]) + assert.Equal(s.T(), "0000000000", keys["team"]) + assert.NotContains(s.T(), data, "success") } func (s *AccountHandlerTestSuite) TestCacheKeys_InvalidID() { @@ -465,4 +489,4 @@ func (s *AccountHandlerTestSuite) TestCacheKeys_InvalidID() { s.router.ServeHTTP(w, req) assert.True(s.T(), w.Code >= 400) -} \ No newline at end of file +} diff --git a/internal/service/account_service.go b/internal/service/account_service.go index da2219e9..74507960 100644 --- a/internal/service/account_service.go +++ b/internal/service/account_service.go @@ -3,7 +3,6 @@ package service import ( "context" "errors" - "fmt" "time" "github.com/gochat/gochat/internal/model" @@ -35,16 +34,23 @@ func (s *AccountService) GetByID(ctx context.Context, id uint) (*model.Account, // CreateAccountRequest is the DTO for creating an account. type CreateAccountRequest struct { - Name string `json:"name" validate:"required,min=2"` - Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` - Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` + Name string `json:"name,omitempty" validate:"omitempty,min=2"` + AccountName string `json:"account_name,omitempty" validate:"omitempty,min=2"` + Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` + Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` } // Create creates a new account and assigns the creator as administrator. func (s *AccountService) Create(ctx context.Context, userID uint, req CreateAccountRequest) (*model.Account, error) { + if req.Name == "" { + req.Name = req.AccountName + } if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } + if req.Name == "" { + return nil, errors.New("account_name is required") + } account := &model.Account{ Name: req.Name, @@ -69,12 +75,18 @@ func (s *AccountService) Create(ctx context.Context, userID uint, req CreateAcco // UpdateAccountRequest is the DTO for updating an account. type UpdateAccountRequest struct { - Name string `json:"name,omitempty" validate:"omitempty,min=2"` - Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` - Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` - FeatureFlags string `json:"feature_flags,omitempty"` - Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive"` - AutoResolveDuration int `json:"auto_resolve_duration,omitempty" validate:"omitempty,gte=0"` + Name string `json:"name,omitempty" validate:"omitempty,min=2"` + Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` + Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` + SupportEmail string `json:"support_email,omitempty"` + FeatureFlags string `json:"feature_flags,omitempty"` + Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive"` + AutoResolveDuration int `json:"auto_resolve_duration,omitempty" validate:"omitempty,gte=0"` + AutoResolveAfter int `json:"auto_resolve_after,omitempty" validate:"omitempty,gte=0"` + AutoResolveMessage string `json:"auto_resolve_message,omitempty"` + AutoResolveIgnoreWaiting *bool `json:"auto_resolve_ignore_waiting,omitempty"` + AudioTranscriptions *bool `json:"audio_transcriptions,omitempty"` + AutoResolveLabel string `json:"auto_resolve_label,omitempty"` } // Update modifies an existing account. @@ -103,7 +115,9 @@ func (s *AccountService) Update(ctx context.Context, id uint, req UpdateAccountR if req.Status != "" { account.Status = req.Status } - if req.AutoResolveDuration > 0 { + if req.AutoResolveAfter > 0 { + account.AutoResolveDuration = req.AutoResolveAfter + } else if req.AutoResolveDuration > 0 { account.AutoResolveDuration = req.AutoResolveDuration } @@ -151,7 +165,7 @@ func (s *AccountService) RemoveUser(ctx context.Context, accountID, userID uint) // UpdateAccountSettingsRequest is the DTO for updating account settings. type UpdateAccountSettingsRequest struct { AutoResolveDuration int `json:"auto_resolve_duration" validate:"gte=0"` - Locale string `json:"locale" validate:"omitempty,len=2"` + Locale string `json:"locale" validate:"omitempty,len=2"` } // UpdateSettings updates account-level settings. @@ -200,19 +214,16 @@ func (s *AccountService) UpdateActiveAt(ctx context.Context, accountID, userID u // The keys are derived from the account's updatedAt timestamp and user membership. // Reference: Chatwoot accounts_controller.rb#cache_keys func (s *AccountService) CacheKeys(ctx context.Context, accountID, userID uint) (map[string]string, error) { - account, err := s.repo.FindByID(ctx, accountID) - if err != nil { + if _, err := s.repo.FindByID(ctx, accountID); err != nil { return nil, err } - - au, err := s.repo.FindAccountUserByUserAndAccount(ctx, accountID, userID) - if err != nil { + if _, err := s.repo.FindAccountUserByUserAndAccount(ctx, accountID, userID); err != nil { return nil, err } - keys := map[string]string{ - "account": fmt.Sprintf("account_%d_%d", accountID, account.UpdatedAt.Unix()), - "account_user": fmt.Sprintf("account_user_%d_%d_%d", accountID, userID, au.UpdatedAt.Unix()), + "label": "0000000000", + "inbox": "0000000000", + "team": "0000000000", } return keys, nil -} \ No newline at end of file +} diff --git a/internal/service/account_service_test.go b/internal/service/account_service_test.go index be44552f..ea032052 100644 --- a/internal/service/account_service_test.go +++ b/internal/service/account_service_test.go @@ -309,7 +309,7 @@ func TestAccountService_UpdateSettings_成功(t *testing.T) { req := UpdateAccountSettingsRequest{ AutoResolveDuration: 5, - Locale: "jp", + Locale: "jp", } result, err := svc.UpdateSettings(context.Background(), account.ID, req) @@ -465,8 +465,9 @@ func TestAccountService_CacheKeys(t *testing.T) { keys, err := svc.CacheKeys(context.Background(), account.ID, user.ID) require.NoError(t, err) assert.NotNil(t, keys) - assert.Contains(t, keys, "account") - assert.Contains(t, keys, "account_user") + assert.Equal(t, "0000000000", keys["label"]) + assert.Equal(t, "0000000000", keys["inbox"]) + assert.Equal(t, "0000000000", keys["team"]) } func TestAccountService_CacheKeys_InvalidAccount(t *testing.T) { @@ -477,4 +478,4 @@ func TestAccountService_CacheKeys_InvalidAccount(t *testing.T) { keys, err := svc.CacheKeys(context.Background(), uint(9999), user.ID) assert.Error(t, err) assert.Nil(t, keys) -} \ No newline at end of file +}