feat(companies): align avatar form payloads

This commit is contained in:
2026-06-06 06:54:48 +08:00
parent d241925b6a
commit 0aa0470ac9
4 changed files with 176 additions and 11 deletions
+7 -5
View File
@@ -37,10 +37,10 @@ Hermes plan landing map:
## Current Baseline
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(integrations): align app hook payloads`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(integrations): align app hook payloads`.
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(companies): align avatar form payloads`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(companies): align avatar form payloads`.
- Latest documentation/tooling checkpoint: `docs: land parity execution tracker`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`.
- Worktree status at this implementation checkpoint: P3.11 account integrations apps/hooks API parity is now implemented for the reused dashboard integrations settings flows. Account `/integrations/apps` and `/integrations/hooks` now track Chatwoot `routes.rb:345-348`, return Chatwoot-style raw app/hook payloads, expose frontend no-trailing-slash create/list routes plus hook `PATCH`, accept raw frontend hook bodies and nested `{ hook: ... }`, persist `app_id` and `reference_id`, seed the reference app catalog additions, scope hook show/update/delete through current account, serialize hook `status` as boolean and `hook_type` as account/inbox, include app `hooks`, `settings_form_schema`, and `visible_properties`, and return empty `200 OK` delete. P3.10 account webhook API, P3.9 account agent-bot API, P6.8 contact outbound voice call, P3.7d enterprise help-center article translation, P6.5 inbox agent-bot member actions, P6 message retry parity, P6 conversation custom-attribute response parity, and P3.7a-P3.7c help-center payload parity remain in Review. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence.
- Worktree status at this implementation checkpoint: P3.5 company avatar form/pagination drift is now implemented for reused dashboard company create/update flows. Account `/companies` create/update now accepts Chatwoot nested multipart `company[...]` bodies generated when the frontend uploads an avatar, maps `company[avatar]` into the serialized `avatar_url` boundary, preserves nested `company[custom_attributes][...]` form fields, and forces company list/search/contact pagination to the reference 25-row page size instead of honoring local `per_page` overrides. P3.11 account integrations apps/hooks API, P3.10 account webhook API, P3.9 account agent-bot API, P6.8 contact outbound voice call, P3.7d enterprise help-center article translation, P6.5 inbox agent-bot member actions, P6 message retry parity, P6 conversation custom-attribute response parity, and P3.7a-P3.7c help-center payload parity remain in Review. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence.
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 869` after adding account integration app/hook no-trailing-slash and PATCH routes.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
@@ -195,6 +195,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(companies): align avatar form payloads` | Advances P3.5 company settings/CRM parity by matching Chatwoot enterprise `CompaniesController#create/#update/#index/#search`, company Jbuilder payloads, and the reused dashboard companies Pinia store/API behavior. GoChat company create/update now accepts nested multipart `company[...]` form bodies used when the frontend sends avatar uploads, maps uploaded `company[avatar]` into the serialized `avatar_url` field, preserves nested multipart `company[custom_attributes][...]` values, and makes company list/search/contact list/search use the reference fixed `RESULTS_PER_PAGE = 25` instead of local `per_page` overrides. | `go test ./internal/handler/api/v1 -run CompanyHandler -count=1`; `go test ./internal/service -run Company -count=1`; `go test ./...`; `git diff --check`. No route changes; route dump remains `TOTAL: 869` and tracked route parity remains `321 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 328`. | P3.5 remains Doing for any future contact/company drift from live smoke or fresh reference inspection; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(integrations): align app hook payloads` | Advances P3.11 account integrations apps/hooks parity by matching Chatwoot `Integrations::AppsController`, `Integrations::HooksController`, app/hook Jbuilder views, `Integrations::App`, `Integrations::Hook`, `config/integration/apps.yml`, and the reused dashboard integrations API/store/settings components. GoChat now tracks `routes.rb:345-348`, registers frontend no-trailing-slash app/hook routes plus hook `PATCH`, returns Chatwoot `{ payload: [...] }` app index and raw app/hook objects, accepts raw frontend hook bodies plus nested `{ hook: ... }`, persists `app_id` and `reference_id`, scopes hook show/update/delete by account, serializes boolean hook `status`, account/inbox `hook_type`, settings, inbox, app hooks, settings schemas, visible properties, and seeds the additional reference app catalog rows. | `go test ./internal/handler/api/v1 -run IntegrationHook -count=1`; `go test ./internal/service -run IntegrationHook -count=1`; `go test ./cmd/migrate -count=1`; `go test ./cmd/route_parity -count=1`; `go test ./internal/router -run Router -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 869`; tracked route parity is `321 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 328`. | P3.11 moves to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(webhooks): align account payloads` | Advances P3.10 account webhook API parity by matching Chatwoot `WebhooksController`, account webhook Jbuilder views, `Webhook` model validation, and the reused dashboard `webhooks` API/store/settings components. GoChat now tracks `routes.rb:342`, registers frontend `PATCH /api/v1/accounts/:account_id/webhooks/:webhook_id`, returns Chatwoot `{ payload: { webhooks } }` and `{ payload: { webhook } }` payloads, accepts nested `{ webhook: ... }` bodies, persists `name`, `inbox_id`, `url`, `subscriptions`, and generated `secret`, scopes show/update/delete by account, validates HTTP/HTTPS URLs plus the reference allowed event set, returns empty `200 OK` delete, and keeps delivery filtering backed by persisted subscription rows. | `go test ./internal/handler/api/v1 -run WebhookSubscription -count=1`; `go test ./internal/service -run WebhookSubscription -count=1`; `go test ./internal/router -run Router -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'WebhookSubscription\|Router\|RouteParity' -count=1`; `go test ./cmd/migrate -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 865`; tracked route parity is `313 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 320`. | P3.10 moves to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(agent-bots): align account payloads` | Advances P3.9 account agent-bot API parity by matching Chatwoot `AgentBotsController`, account agent-bot Jbuilder views, `_agent_bot.json.jbuilder`, `AgentBotPresenter`, and the reused dashboard `AgentBotsAPI`/`agentBots` store path. GoChat now registers frontend no-trailing-slash `GET/POST /api/v1/accounts/:account_id/agent_bots` plus `PATCH /agent_bots/:id`, tracks `routes.rb:94-97`, returns raw bot arrays/objects with `thumbnail`, `bot_config`, `system_bot`, account-scoped `access_token`/`secret`, accepts JSON and multipart form bodies, preserves explicit empty update values, scopes mutation/reset/avatar actions to current-account bots while allowing index/show of system bots, returns empty `200 OK` delete, and returns full bot payloads for reset/avatar actions. | `go test ./internal/handler/api/v1 -run AgentBot -count=1`; `go test ./internal/service -run AgentBot -count=1`; `go test ./internal/router -run Router -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -run 'AgentBot\|Router' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./cmd/route_parity -count=1`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 864`; tracked route parity is `308 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 315`. | P3.9 moves to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
@@ -1645,7 +1646,7 @@ Frontend-critical API groups to audit first:
| 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, assignable-agent settings payloads, profile account permission arrays, platform account-user raw/idempotent membership, agents `order_by_full_name` ordering with Chatwoot no-pagination index behavior, agents bulk invite best-effort behavior, and agent update PATCH/`auto_offline`/validation semantics 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 |
| P3.5 | Contacts/companies | CRUD, merge, labels, notes, custom attributes, import/export, conversations relation, avatars, and shared attachments. | Doing; company multipart avatar create/update and fixed 25-row company pagination now match the reused dashboard companies store/API path. |
| P3.6 | Labels/custom attributes/custom filters | Create/update/list behavior and exact response shapes. | Doing; label CRUD is in Review with Chatwoot `title/description/color/show_on_sidebar` payloads and raw/list response contracts, while custom attributes/custom filters remain evidence-driven follow-ups. |
| P3.7 | Notifications/reports/help center/campaigns | Frontend-visible payloads and pagination/error envelopes. | Doing; notifications, reports, account campaign CRUD/scheduling, and help-center portal/category/article routes/payloads now have focused Chatwoot payload fixtures. Continue only with fresh reference/smoke drift. |
| P3.8 | Widget/public APIs | Widget init, campaigns, config, contact, conversations, messages, direct uploads, public inbox flow, public CSAT. | Doing |
@@ -1658,7 +1659,7 @@ Serializer parity work plan:
| --- | --- | --- | --- | --- |
| S1 | Auth and profile | `reference/chatwoot/app/controllers/api/v1/profile*`, frontend auth API usage | fixture tests for current user/profile payloads | Done |
| S2 | Conversations and messages | `reference/chatwoot/app/controllers/api/v1/accounts/conversations*`, serializers/entities | fixture tests for index/show/message create/update | Review |
| S3 | Contacts and companies | `reference/chatwoot/app/controllers/api/v1/accounts/contacts*`, `companies*` | fixture tests for list/show/search/merge/relation/shared attachment payloads | Doing; contact shared attachments and conversation shared-file payloads now have focused Chatwoot-style coverage. |
| S3 | Contacts and companies | `reference/chatwoot/app/controllers/api/v1/accounts/contacts*`, enterprise `companies*` | fixture tests for list/show/search/merge/relation/shared attachment/avatar payloads | Doing; contact shared attachments, conversation shared-file payloads, company multipart avatar form bodies, and fixed company pagination now have focused Chatwoot-style coverage. |
| S4 | Inboxes and channels | `reference/chatwoot/app/controllers/api/v1/accounts/inboxes*`, channel controllers | fixture tests for inbox CRUD, channel settings, widget config | Done |
| S5 | Notifications and settings | `reference/chatwoot/app/controllers/api/v1/accounts/notifications*` | fixture tests for notification list/actions/settings | Review; list envelope, includes filters, unread counts, read_all, unread/snooze/update raw responses, and destroy_all read/all are covered. |
| S6 | Reports and CSAT | `reference/chatwoot/app/controllers/api/v1/accounts/reports*`, `csat_survey_responses*` | fixture tests for report filters and CSAT metrics/list | Doing |
@@ -2158,6 +2159,7 @@ Verification milestone gates:
## Progress Log
- 2026-06-06: P3.5 company avatar form/pagination checkpoint prepared as `feat(companies): align avatar form payloads`; audited Chatwoot enterprise `CompaniesController`, company Jbuilder views, reused dashboard `api/companies.js`, and Pinia `stores/companies.js`. Company create/update now accepts nested multipart `company[...]` form payloads generated when the frontend includes an avatar, maps `company[avatar]` into the existing serialized `avatar_url` boundary, preserves nested multipart `company[custom_attributes][...]`, and makes company list/search/contact list/search use Chatwoot's fixed `RESULTS_PER_PAGE = 25` instead of local `per_page` overrides. Focused company handler tests, focused company service tests, full `go test ./...`, and `git diff --check` passed; no route artifacts changed.
- 2026-06-06: P3.11 account integrations apps/hooks checkpoint prepared as `feat(integrations): align app hook payloads`; audited Chatwoot `routes.rb:345-348`, `Integrations::AppsController`, `Integrations::HooksController`, app/hook Jbuilder views, `Integrations::App`, `Integrations::Hook`, `config/integration/apps.yml`, and reused dashboard `api/integrations.js`, `store/modules/integrations.js`, and settings integration components. Account integration apps/hooks now return Chatwoot `{ payload: [...] }` app index payloads and raw app/hook objects, accept raw frontend hook bodies plus nested `{ hook: ... }`, register frontend no-trailing-slash app/hook routes and hook `PATCH`, persist `app_id/reference_id`, scope hook show/update/delete by account, serialize boolean `status`, account/inbox `hook_type`, `settings`, `inbox`, app `hooks`, settings schemas, and visible properties, seed additional reference app catalog rows, and return empty `200 OK` delete. Focused integration hook handler/service tests, migration test, route parity test, router focused test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed; route dump is `TOTAL: 869` and tracked route parity is `321 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 328`.
- 2026-06-06: P3.10 account webhook API checkpoint prepared as `feat(webhooks): align account payloads`; audited Chatwoot `routes.rb:342`, `WebhooksController`, account webhook Jbuilder views, `Webhook`, reused dashboard `api/webhooks.js`, `store/modules/webhooks.js`, and settings Webhooks components. Account webhook APIs now return Chatwoot `{ payload: { webhooks } }` list payloads and `{ payload: { webhook } }` mutation/show payloads, accept nested `{ webhook: ... }` frontend bodies plus legacy raw bodies, register frontend `PATCH` update, persist `name/url/subscriptions/inbox_id/secret`, scope show/update/delete by account, validate HTTP/HTTPS URLs and allowed subscriptions, return empty `200 OK` delete, and keep outbound delivery event filtering on the same rows. Focused webhook handler/service/router tests, migration test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed; route dump is `TOTAL: 865` and tracked route parity is `313 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 320`.
- 2026-06-06: P3.9 account agent-bot API checkpoint prepared as `feat(agent-bots): align account payloads`; audited Chatwoot `routes.rb:94-97`, `AgentBotsController`, account agent-bot Jbuilder views, `_agent_bot.json.jbuilder`, `AgentBotPresenter`, `AgentBot`, and reused dashboard `api/agentBots.js`, `store/modules/agentBots.js`, and settings agent-bot screens. Account agent-bot APIs now expose frontend no-trailing-slash GET/POST and PATCH update routes, return raw Chatwoot bot arrays/objects instead of local success/data/meta envelopes, serialize `thumbnail`, `bot_config`, `system_bot`, account-scoped `access_token`/`secret`, accept JSON or multipart form bodies, preserve explicit empty update fields, scope create/update/delete/reset/avatar mutations to current-account bots, allow show/index of system bots through `accessible_to`, return empty `200 OK` delete, and return full bot payloads after reset-access-token/reset-secret/avatar deletion. Focused AgentBot handler/service/router tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed; route dump is `TOTAL: 864` and tracked route parity is `308 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 315`.
+87 -6
View File
@@ -1,8 +1,10 @@
package v1
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -33,7 +35,7 @@ func (h *CompanyHandler) List(c *gin.Context) {
return
}
pg := pagination.Parse(c)
pg := parseCompanyPagination(c)
sort := c.DefaultQuery("sort", "")
companies, total, err := h.svc.List(c.Request.Context(), accountID, pg.Offset, pg.PerPage, sort)
@@ -55,7 +57,7 @@ func (h *CompanyHandler) Search(c *gin.Context) {
return
}
pg := pagination.Parse(c)
pg := parseCompanyPagination(c)
query := c.DefaultQuery("q", "")
if query == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
@@ -108,7 +110,7 @@ func (h *CompanyHandler) Create(c *gin.Context) {
}
var req service.CreateCompanyRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err := bindCompanyRequest(c, &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
@@ -138,7 +140,7 @@ func (h *CompanyHandler) Update(c *gin.Context) {
}
var req service.UpdateCompanyRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err := bindCompanyRequest(c, &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
@@ -239,7 +241,7 @@ func (h *CompanyHandler) ListContacts(c *gin.Context) {
return
}
pg := pagination.Parse(c)
pg := parseCompanyPagination(c)
contacts, total, svcErr := h.svc.ListContacts(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage)
if svcErr != nil {
@@ -292,7 +294,7 @@ func (h *CompanyHandler) SearchContacts(c *gin.Context) {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
return
}
pg := pagination.Parse(c)
pg := parseCompanyPagination(c)
contacts, total, svcErr := h.svc.SearchContacts(c.Request.Context(), uint(companyID), accountID, query, pg.Offset, pg.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -471,3 +473,82 @@ func parseCompanyContactID(c *gin.Context) (uint, error) {
}
return req.ContactID, nil
}
type companyRequest interface {
*service.CreateCompanyRequest | *service.UpdateCompanyRequest
}
func parseCompanyPagination(c *gin.Context) pagination.Params {
pg := pagination.Parse(c)
pg.PerPage = service.CompanyResultsPerPage
pg.Offset = (pg.Page - 1) * pg.PerPage
return pg
}
func bindCompanyRequest[T companyRequest](c *gin.Context, req T) error {
if !strings.HasPrefix(c.GetHeader("Content-Type"), "multipart/form-data") {
return c.ShouldBindJSON(req)
}
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
return err
}
name := companyFormValue(c, "name")
description := companyFormValue(c, "description")
websiteURL := companyFormValue(c, "website_url")
faviconURL := companyFormValue(c, "favicon_url")
domain := companyFormValue(c, "domain")
if file, err := c.FormFile("company[avatar]"); err == nil && file != nil {
faviconURL = file.Filename
}
customAttributes := companyFormJSON(c, "custom_attributes")
switch r := any(req).(type) {
case *service.CreateCompanyRequest:
r.Name = name
r.Description = description
r.WebsiteURL = websiteURL
r.FaviconURL = faviconURL
r.Domain = domain
r.CustomAttributes = customAttributes
case *service.UpdateCompanyRequest:
r.Name = name
r.Description = description
r.WebsiteURL = websiteURL
r.FaviconURL = faviconURL
r.Domain = domain
r.CustomAttributes = customAttributes
}
return nil
}
func companyFormValue(c *gin.Context, key string) string {
if value := c.PostForm("company[" + key + "]"); value != "" {
return value
}
return c.PostForm(key)
}
func companyFormJSON(c *gin.Context, key string) []byte {
if c.Request.MultipartForm == nil {
return nil
}
if value := companyFormValue(c, key); value != "" {
if json.Valid([]byte(value)) {
return []byte(value)
}
}
prefix := "company[" + key + "]["
attrs := map[string]any{}
for formKey, values := range c.Request.MultipartForm.Value {
if !strings.HasPrefix(formKey, prefix) || !strings.HasSuffix(formKey, "]") || len(values) == 0 {
continue
}
attrKey := strings.TrimSuffix(strings.TrimPrefix(formKey, prefix), "]")
attrs[attrKey] = values[0]
}
if len(attrs) == 0 {
return nil
}
data, _ := json.Marshal(attrs)
return data
}
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
@@ -153,6 +154,26 @@ func (s *CompanyHandlerTestSuite) makeRequest(method, path string, body interfac
return w
}
func (s *CompanyHandlerTestSuite) makeMultipartRequest(method, path string, fields map[string]string, files map[string]string) *httptest.ResponseRecorder {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, value := range fields {
s.Require().NoError(writer.WriteField(key, value))
}
for key, filename := range files {
part, err := writer.CreateFormFile(key, filename)
s.Require().NoError(err)
_, err = part.Write([]byte("avatar-bytes"))
s.Require().NoError(err)
}
s.Require().NoError(writer.Close())
req, _ := http.NewRequest(method, path, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
s.router.ServeHTTP(w, req)
return w
}
// ========== List ==========
func (s *CompanyHandlerTestSuite) TestList_Empty() {
@@ -184,6 +205,22 @@ func (s *CompanyHandlerTestSuite) TestList_WithCompanies() {
assert.Equal(s.T(), float64(2), resp["meta"].(map[string]interface{})["total_count"])
}
func (s *CompanyHandlerTestSuite) TestList_IgnoresPerPage() {
companyRepo := repository.NewCompanyRepo(s.db)
for _, name := range []string{"ListCorp1", "ListCorp2", "ListCorp3"} {
s.Require().NoError(companyRepo.Create(context.Background(), &model.Company{AccountID: s.accountID, Name: name}))
}
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/?page=1&per_page=1", s.accountID), nil)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].([]interface{})
assert.Len(s.T(), payload, 3)
assert.Equal(s.T(), float64(3), resp["meta"].(map[string]interface{})["total_count"])
}
// ========== Create ==========
func (s *CompanyHandlerTestSuite) TestCreate_Success() {
@@ -204,6 +241,29 @@ func (s *CompanyHandlerTestSuite) TestCreate_Success() {
assert.Equal(s.T(), "NewCorp", companyData["name"])
}
func (s *CompanyHandlerTestSuite) TestCreate_MultipartAvatar() {
w := s.makeMultipartRequest(
"POST",
fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID),
map[string]string{
"company[name]": "AvatarCorp",
"company[domain]": "avatar.example",
"company[custom_attributes][segment]": "enterprise",
},
map[string]string{"company[avatar]": "avatar.png"},
)
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
companyData := resp["payload"].(map[string]interface{})
assert.Equal(s.T(), "AvatarCorp", companyData["name"])
assert.Equal(s.T(), "avatar.example", companyData["domain"])
assert.Equal(s.T(), "avatar.png", companyData["avatar_url"])
attrs := companyData["custom_attributes"].(map[string]interface{})
assert.Equal(s.T(), "enterprise", attrs["segment"])
}
func (s *CompanyHandlerTestSuite) TestCreate_ValidationError() {
body := map[string]interface{}{
"name": "", // required field
@@ -276,6 +336,26 @@ func (s *CompanyHandlerTestSuite) TestUpdate_Success() {
assert.Equal(s.T(), "UpdateCorpUpdated", companyData["name"])
}
func (s *CompanyHandlerTestSuite) TestUpdate_MultipartAvatar() {
companyRepo := repository.NewCompanyRepo(s.db)
company := &model.Company{AccountID: s.accountID, Name: "AvatarUpdateCorp"}
s.Require().NoError(companyRepo.Create(context.Background(), company))
w := s.makeMultipartRequest(
"PATCH",
fmt.Sprintf("/api/v1/accounts/%d/companies/%d", s.accountID, company.ID),
map[string]string{"company[name]": "AvatarUpdateCorp", "company[domain]": "updated-avatar.example"},
map[string]string{"company[avatar]": "updated-avatar.png"},
)
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
companyData := resp["payload"].(map[string]interface{})
assert.Equal(s.T(), "updated-avatar.example", companyData["domain"])
assert.Equal(s.T(), "updated-avatar.png", companyData["avatar_url"])
}
func (s *CompanyHandlerTestSuite) TestUpdate_NotFound() {
body := map[string]interface{}{
"name": "NonexistentCorp",
+2
View File
@@ -25,6 +25,8 @@ type CompanyService struct {
searchReader CompanySearchReader
}
const CompanyResultsPerPage = 25
// NewCompanyService creates a new Company service.
func NewCompanyService(companyRepo *repository.CompanyRepo, contactRepo *repository.ContactRepo, conversationRepo *repository.ConversationRepo) *CompanyService {
return &CompanyService{