feat(account-users): align inviter id

This commit is contained in:
2026-06-06 11:45:56 +08:00
parent fbc0bb833d
commit 2acd93831f
7 changed files with 43 additions and 18 deletions
+6 -4
View File
@@ -49,10 +49,10 @@ Hermes task landing checklist:
## Current Baseline
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(profile): expose hmac identifier`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(profile): expose hmac identifier`.
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(account-users): align inviter id`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(account-users): align inviter id`.
- Latest documentation/tooling checkpoint: this checkpoint, prepared as `docs: record placeholder smoke audit`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`.
- Worktree status at this implementation checkpoint: P3.1 profile `_user` serializer drift from `app/views/api/v1/models/_user.json.jbuilder`, `UserAttributeHelpers#hmac_identifier`, `user_spec.rb`, and reused frontend `scriptHelpers.js` is implemented. Profile responses now omit `hmac_identifier` when `CHATWOOT_INBOX_HMAC_KEY` is absent and expose the Chatwoot-compatible SHA-256 HMAC of the user email when the installation config is present. Phase 6 placeholder audit remains in Review; B12 live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. Next active implementation slice is B12 live smoke finding, remaining P3.2 invitations drift from fresh reference evidence, or another route/controller/serializer drift opened from current `reference/chatwoot` inspection.
- Worktree status at this implementation checkpoint: P3.2 account-user invitation column drift from Chatwoot `account_users.inviter_id`, `AgentBuilder`, `_user.json.jbuilder`, and enterprise audit metadata is implemented. GoChat now persists AccountUser invitation source through the Chatwoot `inviter_id` column/JSON name, migrates existing `invited_by` values forward, keeps agent serializers from leaking inviter fields, and preserves profile `inviter_id` output through the existing `_user` serializer. Phase 6 placeholder audit remains in Review; B12 live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. Next active implementation slice is B12 live smoke finding, remaining P3.2 invitations drift from fresh reference evidence, or another route/controller/serializer drift opened from current `reference/chatwoot` inspection.
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 905` after removing the non-Chatwoot `GET /platform/api/v1/users/:id/token` route and keeping the Chatwoot `GET /platform/api/v1/users/:id/login` route.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
@@ -209,6 +209,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(account-users): align inviter id` | Advances P3.2 invitation/account-user parity by matching Chatwoot `account_users.inviter_id`, `AgentBuilder#create_account_user`, `app/views/api/v1/models/_user.json.jbuilder`, and enterprise `Audit::AccountUser`. GoChat now maps `model.AccountUser.InvitedBy` to the Chatwoot `inviter_id` column and JSON name, emits account-user lifecycle payloads with `inviter_id`, adds migration `000031` to copy existing `invited_by` values forward, and keeps account agent serializers free of inviter fields. | `go test ./internal/model -run AccountUserDefaultValues -count=1`; `go test ./internal/handler/api/v1 -run 'TestAgentHandlerSuite/TestCreateAgent' -count=1`; `go test ./internal/service ./internal/repository -run 'AccountUser\|Agent' -count=1`; `go test ./cmd/migrate ./internal/app -count=1`; full `go test ./...`; `git diff --check`. No route artifacts changed. | AccountUser inviter persistence moves to Review for current reference evidence; continue remaining P3.2 invitation mail/confirmation drift, B12 live smoke, or the next evidence-backed route/controller/serializer drift. |
| `feat(profile): expose hmac identifier` | Advances P3.1 profile `_user` serializer parity by matching Chatwoot `app/views/api/v1/models/_user.json.jbuilder`, `UserAttributeHelpers#hmac_identifier`, `spec/models/user_spec.rb`, and reused frontend `scriptHelpers.js`. GoChat profile responses now omit `hmac_identifier` when `CHATWOOT_INBOX_HMAC_KEY` is not configured and include the SHA-256 HMAC of the user email keyed by that installation config when present. | `go test ./internal/handler/api/v1 -run 'ProfileHandlerSuite/TestGet' -count=1`; `go test ./internal/service -run Profile -count=1`; full `go test ./...`; `git diff --check`. No route artifacts changed. | Profile `_user` HMAC field moves to Review for current reference evidence; continue remaining P3.2 invitations drift, B12 live smoke, or the next evidence-backed route/controller/serializer drift. |
| `feat(platform): align user payloads` | Advances P3.2 platform user parity by matching Chatwoot `Platform::Api::V1::UsersController`, `app/views/platform/api/v1/models/_user.json.jbuilder`, `users/token.json.jbuilder`, and platform routes. GoChat now returns raw user/token/login payloads instead of local envelopes, creates or reuses users by email with confirmed active status, idempotently grants platform-app permission, generates/reuses personal access tokens, merges `custom_attributes` on update, returns empty `200 OK` deletes, registers `GET /platform/api/v1/users/:id/login`, and removes the non-reference `GET /platform/api/v1/users/:id/token` route from the dump. | `go test ./internal/handler/api/v1 -run PlatformUser -count=1`; `go test ./internal/service -run PlatformUser -count=1`; `go test ./internal/router -run RegisterRoutes -count=1`; `go test ./cmd/route_parity -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 905`; tracked route parity remains `353 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 362`. | Platform users move to Review for current reference evidence; continue remaining P3.2 invitations drift, B12 live smoke, or the next evidence-backed route/controller/serializer drift. |
| `feat(agents): align serializer fields` | Advances P3.2 agents serializer parity by matching Chatwoot `app/views/api/v1/models/_agent.json.jbuilder`. GoChat account agent list/create/show/update payloads now return only `id`, `account_id`, `availability_status`, `auto_offline`, `confirmed`, `email`, `provider`, `available_name`, optional `custom_attributes`, `name`, `role`, `thumbnail`, and enterprise `custom_role_id`; local `invited_by` and `account_user_id` fields are no longer emitted. | `go test ./internal/handler/api/v1 -run 'TestAgentHandlerSuite/TestCreateAgent\|TestAgentHandlerSuite/TestListAfterCreate\|TestAgentHandlerSuite/TestGetAgent' -count=1`; `go test ./internal/handler/api/v1 -run AgentHandler -count=1`; `go test ./internal/service ./internal/repository ./internal/router -run Agent -count=1`; sandboxed full `go test ./...` failed only on local socket/miniredis/httptest restrictions; escalated full `go test ./...` passed; `git diff --check`. | Agent serializer field shape moves to Review for current reference evidence; continue remaining P3.2 account users/invitations drift or B12 live smoke. |
@@ -1676,7 +1677,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. | Review; profile account permission arrays, conditional `hmac_identifier`, and user notification-settings raw payloads now match Chatwoot role/custom-role semantics, widget script helper, and dashboard settings store expectations. |
| 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, platform user raw serializer/create-login-token/update-delete semantics, agents `_agent` serializer fields, `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.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, platform user raw serializer/create-login-token/update-delete semantics, AccountUser `inviter_id` persistence/audit naming, agents `_agent` serializer fields, `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, 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. |
@@ -2198,6 +2199,7 @@ Verification milestone gates:
## Progress Log
- 2026-06-06: P3.2 account-user inviter checkpoint prepared as `feat(account-users): align inviter id`; audited Chatwoot `AccountUser` schema, `AgentBuilder#create_account_user`, `api/v1/models/_user.json.jbuilder`, and enterprise `Audit::AccountUser`. GoChat now persists invitation source through the Chatwoot `inviter_id` column/JSON name instead of the local `invited_by` column, copies existing `invited_by` values forward in migration `000031`, emits account-user lifecycle events with `inviter_id`, and verifies agent creation stores the current user as inviter without leaking inviter fields through the `_agent` serializer. Focused model/agent/service/repository/migration tests, full `go test ./...`, and `git diff --check` passed. Continue remaining P3.2 invitation mail/confirmation drift or B12 live smoke.
- 2026-06-06: P3.1 profile HMAC checkpoint prepared as `feat(profile): expose hmac identifier`; audited Chatwoot `app/views/api/v1/models/_user.json.jbuilder`, `UserAttributeHelpers#hmac_identifier`, `spec/models/user_spec.rb`, and reused frontend `scriptHelpers.js`. Profile responses now omit `hmac_identifier` when `CHATWOOT_INBOX_HMAC_KEY` is absent and expose `OpenSSL::HMAC.hexdigest('sha256', key, email)` compatible values when the installation config exists, letting the reused widget script helper send `identifier_hash` without adapter code. Focused profile handler tests, service compile test, full `go test ./...`, and `git diff --check` passed. Continue remaining P3.2 invitations drift or B12 live smoke.
- 2026-06-06: P3.2 platform user checkpoint prepared as `feat(platform): align user payloads`; audited Chatwoot `Platform::Api::V1::UsersController`, `platform/api/v1/models/_user.json.jbuilder`, `users/token.json.jbuilder`, and platform user routes. Platform user show/create/update now return raw Chatwoot user payloads, create reuses existing users by email and creates the platform-app permissible idempotently, access tokens are generated/reused for user payloads, `GET /users/:id/login` returns a Chatwoot-shaped URL payload, `POST /users/:id/token` returns `{ access_token, expiry, user }`, update merges `custom_attributes`, and destroy returns empty `200 OK`. The stale non-reference `GET /platform/api/v1/users/:id/token` route was removed from the dump, leaving route dump `TOTAL: 905` and tracked route parity at `353 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 362`. Focused platform handler/service/router tests, route parity test/generation, full `go test ./...`, and `git diff --check` passed; continue remaining P3.2 invitations drift or B12 live smoke.
- 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.
@@ -180,7 +180,12 @@ func (s *AgentHandlerTestSuite) TestCreateAgent() {
assert.Equal(s.T(), float64(7), data["custom_role_id"])
assert.Contains(s.T(), data, "confirmed")
assert.NotContains(s.T(), data, "invited_by")
assert.NotContains(s.T(), data, "inviter_id")
assert.NotContains(s.T(), data, "account_user_id")
var membership model.AccountUser
s.Require().NoError(s.db.Where("account_id = ? AND user_id = ?", s.account.ID, uint(data["id"].(float64))).First(&membership).Error)
assert.Equal(s.T(), s.user.ID, membership.InvitedBy)
}
func (s *AgentHandlerTestSuite) TestCreateAgentDefaultsBlankNameFromEmail() {
+1 -1
View File
@@ -24,7 +24,7 @@ type AccountUser struct {
Availability string `gorm:"size:50;default:'offline'" json:"availability"` // online/offline/busy
AutoOffline bool `gorm:"default:true" json:"auto_offline"` // auto-offline when idle
ActiveAt *time.Time `json:"active_at,omitempty"` // last active timestamp in this account
InvitedBy uint `gorm:"default:0" json:"invited_by"` // UserID of the user who invited this member
InvitedBy uint `gorm:"column:inviter_id;default:0" json:"inviter_id"` // Chatwoot: inviter_id of the user who invited this member
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
+1
View File
@@ -126,6 +126,7 @@ func TestAccountUserDefaultValues(t *testing.T) {
assert.Equal(t, "agent", fetched.Role, "Role should default to 'agent'")
assert.Equal(t, "offline", fetched.Availability, "Availability should default to 'offline'")
assert.Equal(t, uint(0), fetched.InvitedBy, "InvitedBy should default to 0")
assert.True(t, db.Migrator().HasColumn(&model.AccountUser{}, "inviter_id"), "AccountUser should use Chatwoot inviter_id column")
}
// TestCustomRoleDefaultValues verifies CustomRole model default field values.
+13 -13
View File
@@ -21,11 +21,11 @@ import (
// after_save (update_presence_in_redis on availability change),
// validates user_id uniqueness scoped to account_id
type AccountUserService struct {
repo *repository.AccountUserRepo
notificationRepo *repository.NotificationSettingRepo
accountRepo *repository.AccountRepo
userRepo *repository.UserRepo
eventBus *pubsub.EventBus
repo *repository.AccountUserRepo
notificationRepo *repository.NotificationSettingRepo
accountRepo *repository.AccountRepo
userRepo *repository.UserRepo
eventBus *pubsub.EventBus
}
// NewAccountUserService creates a new AccountUser service.
@@ -78,10 +78,10 @@ func (s *AccountUserService) AddUserToAccount(ctx context.Context, accountID uin
}
au := &model.AccountUser{
AccountID: accountID,
UserID: req.UserID,
Role: req.Role,
InvitedBy: req.InviterID,
AccountID: accountID,
UserID: req.UserID,
Role: req.Role,
InvitedBy: req.InviterID,
Availability: "offline", // 1:1 Chatwoot: default availability
AutoOffline: true, // 1:1 Chatwoot: default auto_offline
}
@@ -104,7 +104,7 @@ func (s *AccountUserService) AddUserToAccount(ctx context.Context, accountID uin
"account_id": accountID,
"user_id": req.UserID,
"role": req.Role,
"invited_by": req.InviterID,
"inviter_id": req.InviterID,
})
if err := s.eventBus.Publish(pubsub.TopicAccountUserCreated, payload); err != nil {
applogger.L().Errorf("Failed to publish AccountUserCreated event: %v", err)
@@ -211,8 +211,8 @@ func (s *AccountUserService) createDefaultNotificationSetting(ctx context.Contex
// 1:1 Chatwoot: email_conversation_assignment = bit 1 → 2
// push_conversation_assignment = bit 1 → 2
setting := &model.NotificationSetting{
UserID: userID,
AccountID: accountID,
UserID: userID,
AccountID: accountID,
EmailFlags: 2, // bit 1 = email_conversation_assignment
PushFlags: 2, // bit 1 = push_conversation_assignment
}
@@ -234,4 +234,4 @@ func (s *AccountUserService) SetAutoOffline(ctx context.Context, accountID, user
// FindOnlineAgents returns all online agents for an account.
func (s *AccountUserService) FindOnlineAgents(ctx context.Context, accountID uint) ([]model.AccountUser, error) {
return s.repo.FindOnlineAgentsByAccount(ctx, accountID)
}
}
@@ -0,0 +1,8 @@
-- Revert Chatwoot inviter_id alignment for account_users.
UPDATE account_users
SET invited_by = inviter_id
WHERE inviter_id IS NOT NULL
AND inviter_id <> 0;
ALTER TABLE account_users DROP COLUMN IF EXISTS inviter_id;
@@ -0,0 +1,9 @@
-- Align account_users inviter column with Chatwoot's schema.
ALTER TABLE account_users ADD COLUMN IF NOT EXISTS inviter_id INTEGER DEFAULT 0;
UPDATE account_users
SET inviter_id = invited_by
WHERE inviter_id = 0
AND invited_by IS NOT NULL
AND invited_by <> 0;