From 4b2c1a97ae1c587b2e2e718a31eea2ba4cfdf91b Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 6 Jun 2026 02:55:51 +0800 Subject: [PATCH] feat(notification-settings): align chatwoot payloads --- docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md | 14 ++++--- docs/parity/gochat_routes.txt | 5 ++- .../api/v1/notification_setting_handler.go | 22 +++++----- .../v1/notification_setting_handler_test.go | 40 ++++++++++++++++++- internal/router/router.go | 3 ++ 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 7f6837ed..196cd240 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -16,12 +16,12 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc ## Current Baseline -- Current tracking checkpoint: 2026-06-06 after `0ebaa51 feat(agents): align update validation errors`, with this implementation checkpoint prepared as `feat(agents): align index pagination`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(agents): align index pagination`. +- Current tracking checkpoint: 2026-06-06 after `d49aafd feat(agents): align index pagination`, with this implementation checkpoint prepared as `feat(notification-settings): align chatwoot payloads`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(notification-settings): align chatwoot payloads`. - Latest documentation-only checkpoint: `8b378c7 docs: refresh parity tracker baseline`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`. -- Worktree status at this implementation checkpoint: Agent index pagination now matches Chatwoot `AgentsController#index`, which renders `Current.account.users.order_by_full_name` without `.page/.per`: reused frontend `page/per_page` query values are ignored and the endpoint returns the full account-agent array ordered by full name. Agent update blank-name validation already matches Chatwoot `User#update!` plus `RequestExceptionHandler#render_record_invalid`: explicit blank `agent[name]` returns `422 { message: "Name can't be blank", attributes: ["name"] }` and leaves the user name unchanged. Agent delete already matches Chatwoot's `agents.find(params[:id])` account scope: deleting a user who is not a member of the current account returns `404` and does not remove that user or any other account membership. Duplicate account agent creation already matches Chatwoot's `ActiveRecord::RecordInvalid` response shape: `422` with `{ message, attributes }` instead of a local `409` envelope. Agent creation already matches Chatwoot `AgentBuilder` name fallback: blank or omitted `agent[name]` no longer fails local validation and new users receive the email local-part as `name`. Account conversation/contact attachment list pagination already matches Chatwoot's fixed 100-row page size for `ConversationsController#attachments` and `Contacts::AttachmentsController#index`; reused frontend `per_page` query values no longer shrink shared attachment payloads below the reference page size. Profile account permission drift is closed for reused route guards and dashboard permission helpers: profile account entries now return Chatwoot `AccountUser#permissions` for ordinary roles (`["administrator"]` or `["agent"]`) and continue to return enterprise custom-role permissions plus `custom_role` when `custom_role_id` is present. The preceding platform account-user membership, agents list ordering, update-route/`auto_offline`, agents bulk invite, B10.3 CustomRole frontend update, and Phase 6 contacts pagination/active payload drifts are closed. Conversation transcript and custom-attribute response drifts are already closed. 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 the next named Phase 2/3 or Phase 6 drift from fresh reference/smoke evidence. +- Worktree status at this implementation checkpoint: User notification settings now match Chatwoot `NotificationSettingsController#show/#update` and `show.json.jbuilder`: account-scoped `GET/PATCH /notification_settings` accepts the reused frontend no-trailing-slash URL, update accepts nested `notification_settings`, and both actions return the raw notification-setting object without local `{ success, data }` or `notification_setting` wrappers. Agent index pagination already matches Chatwoot `AgentsController#index`, which renders `Current.account.users.order_by_full_name` without `.page/.per`: reused frontend `page/per_page` query values are ignored and the endpoint returns the full account-agent array ordered by full name. Agent update blank-name validation already matches Chatwoot `User#update!` plus `RequestExceptionHandler#render_record_invalid`: explicit blank `agent[name]` returns `422 { message: "Name can't be blank", attributes: ["name"] }` and leaves the user name unchanged. Agent delete/create/bulk-create drifts, account attachment pagination, profile account permissions, platform account-user membership, B10.3 CustomRole frontend update, contacts pagination/active payloads, conversation transcript, and custom-attribute response drifts are already closed. 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 the next named Phase 2/3 or Phase 6 drift from fresh reference/smoke evidence. - `go test ./...` passes. -- Route dump succeeds with `TOTAL: 840` after adding the Chatwoot platform account-user collection destroy route. +- Route dump succeeds with `TOTAL: 843` after adding no-trailing-slash notification-settings route aliases for the reused frontend. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. - Tracked frontend-critical route audit covers 277 Chatwoot routes: 270 exact, 0 method-compatible, 7 parameter-compatible, 0 missing. The 7 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits; the external URL shape is equivalent. - `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers. @@ -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 plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, label CRUD payloads, custom filters, and custom attribute definitions now match the inspected Chatwoot frontend contract. | Continue Phase 6 placeholder audit, then open the next named serializer/controller drift slice from reference/smoke evidence. | 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, user notification-settings raw payloads, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, label CRUD payloads, custom filters, and custom attribute definitions now match the inspected Chatwoot frontend contract. | Continue Phase 6 placeholder audit, then open the next named serializer/controller drift slice from reference/smoke evidence. | 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; fresh `rg` audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript response drift is closed. | Continue targeted account/contact/conversation/message/inbox drift from reference/smoke evidence. | `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. | @@ -143,6 +143,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(notification-settings): align chatwoot payloads` | Advances P3.1 auth/profile settings parity by matching Chatwoot `NotificationSettingsController#show/#update`, `show.json.jbuilder`, and reused dashboard `userNotificationSettings` store/API. Account notification-settings show/update now return the raw object with `id`, `user_id`, `account_id`, `all_email_flags`, `selected_email_flags`, `all_push_flags`, and `selected_push_flags`; the local `{ success, data: { notification_setting } }` wrapper is removed, nested update params are retained, and no-trailing-slash route aliases are registered for the frontend `ApiClient` URL. | `go test ./internal/handler/api/v1 -run NotificationSetting -count=1`; `go test ./internal/service -run NotificationSetting -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/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: 843`; tracked route parity remains `270 exact, 7 parameter-compatible, 0 missing`. | Continue Phase 6 placeholder audit or the next named Phase 2/3 drift from reference/smoke evidence. | | `feat(agents): align index pagination` | Advances P3.2 agents/settings parity by matching Chatwoot `AgentsController#index`, which assigns `@agents = agents` from `Current.account.users.order_by_full_name.includes(...)` without pagination. Account agent lists now ignore reused-frontend `page/per_page` query values, return the full account-agent array, and retain `lower(users.name) ASC` ordering plus account-user role, availability, auto-offline, inviter, account-user ID, and custom-role metadata. | `go test ./internal/handler/api/v1 -run 'TestAgentHandlerSuite/TestList' -count=1`; `go test ./internal/service ./internal/repository -run Agent -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/repository -count=1`; `go test ./...`; `git diff --check`. No route changes; route dump remains `TOTAL: 840`. | Continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. | | `feat(agents): align update validation errors` | Advances P3.2 agents/settings parity by matching Chatwoot `AgentsController#update`, `User#update!`, and `RequestExceptionHandler#render_record_invalid`. Explicit blank `agent[name]` updates now return `422 { message: "Name can't be blank", attributes: ["name"] }` and preserve the existing user name instead of silently ignoring the invalid value with a `200 OK`. | `go test ./internal/handler/api/v1 -run 'TestAgentHandlerSuite/TestUpdateAgent' -count=1`; `go test ./internal/service -run Agent -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/repository -count=1`; `go test ./...`; `git diff --check`. No route changes; route dump remains `TOTAL: 840`. | Continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. | | `feat(agents): align delete scope` | Advances P3.2 agents/settings parity by matching Chatwoot `AgentsController#fetch_agent` before `destroy`: deletes are scoped through current account membership and return `404` when the target user is not an agent in the account. The local delete path now looks up the `AccountUser` before deletion so cross-account/non-member users are not accidentally deleted. | `go test ./internal/handler/api/v1 -run 'TestAgentHandlerSuite/TestDeleteAgent' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/repository -count=1`; `go test ./...`; `git diff --check`. No route changes; route dump remains `TOTAL: 840`. | Continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. | @@ -1568,7 +1569,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; profile account permission arrays now match Chatwoot role/custom-role semantics for dashboard route guards. | +| P3.1 | Auth/session/profile | Login, logout, current user, profile, availability, notification settings, Devise password reset, and confirmation. | Done; profile account permission arrays and user notification-settings raw payloads now match Chatwoot role/custom-role semantics 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, 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 | @@ -2083,6 +2084,7 @@ Verification milestone gates: ## Progress Log +- 2026-06-06: P3.1 notification-settings checkpoint prepared as `feat(notification-settings): align chatwoot payloads`; audited Chatwoot `NotificationSettingsController#show/#update`, `notification_settings/show.json.jbuilder`, and the reused dashboard `userNotificationSettings` API/store. Account-scoped notification settings now return the raw Jbuilder object with flag arrays instead of the local success/data wrapper, keep nested `notification_settings` update params, and register no-trailing-slash GET/PATCH/PUT route aliases for the frontend client URL. Focused handler/service tests, combined handler/service/router tests, route dump/parity, full `go test ./...`, and `git diff --check` passed; continue Phase 6 placeholder audit or the next named Phase 2/3 drift. - 2026-06-06: P3.2 agent index-pagination checkpoint prepared as `feat(agents): align index pagination`; audited Chatwoot `AgentsController#index`, `agents`, and `User.order_by_full_name`. Account agent lists now ignore reused-frontend `page/per_page` query values and return the full account-agent array like Chatwoot's unpaginated `@agents = agents`, while preserving lowercased full-name ordering and account-user metadata. Focused list tests, service/repository agent tests, combined handler/service/repository tests, full `go test ./...`, and `git diff --check` passed; continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. - 2026-06-06: P3.2 agent update-validation checkpoint prepared as `feat(agents): align update validation errors`; audited Chatwoot `AgentsController#update`, `User` name presence validation, and `RequestExceptionHandler#render_record_invalid`. Explicit blank `agent[name]` updates now return Chatwoot `422 { message, attributes }`, preserve the existing user name, and continue to allow omitted names plus explicit `auto_offline: false`. Focused update tests, service agent tests, combined handler/service/repository tests, full `go test ./...`, and `git diff --check` passed; continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. - 2026-06-06: P3.2 agent delete-scope checkpoint prepared as `feat(agents): align delete scope`; audited Chatwoot `AgentsController#destroy`, `fetch_agent`, and account-scoped `agents` relation. Agent delete now first resolves the current account membership and returns `404` for non-members, preserving users and memberships in other accounts instead of treating an unscoped delete as success. Focused delete tests, combined handler/service/repository tests, full `go test ./...`, and `git diff --check` passed; continue P3.2 agents/account-user drift or the next Phase 6 reference/smoke finding. diff --git a/docs/parity/gochat_routes.txt b/docs/parity/gochat_routes.txt index 15edb7f6..7591cf71 100644 --- a/docs/parity/gochat_routes.txt +++ b/docs/parity/gochat_routes.txt @@ -287,6 +287,7 @@ GET /api/v1/accounts/:account_id/microsoft/callback GET /api/v1/accounts/:account_id/microsoft/oauth GET /api/v1/accounts/:account_id/microsoft/webhooks GET /api/v1/accounts/:account_id/microsoft_channels/authorization +GET /api/v1/accounts/:account_id/notification_settings GET /api/v1/accounts/:account_id/notification_settings/ GET /api/v1/accounts/:account_id/notifications/ GET /api/v1/accounts/:account_id/notifications/:notification_id @@ -476,6 +477,7 @@ PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id/members/update_multiple PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id/tiktok_channels/:tt_id PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id/twilio_sms_channels/:tw_id PATCH /api/v1/accounts/:account_id/integrations/slack/ +PATCH /api/v1/accounts/:account_id/notification_settings PATCH /api/v1/accounts/:account_id/notification_settings/ PATCH /api/v1/accounts/:account_id/notifications/:notification_id PATCH /api/v1/accounts/:account_id/teams/:team_id/team_members/ @@ -802,6 +804,7 @@ PUT /api/v1/accounts/:account_id/inboxes/:inbox_id/working_hours PUT /api/v1/accounts/:account_id/integrations/hooks/:id PUT /api/v1/accounts/:account_id/labels/:tag_id PUT /api/v1/accounts/:account_id/macros/:macro_id +PUT /api/v1/accounts/:account_id/notification_settings PUT /api/v1/accounts/:account_id/notification_settings/ PUT /api/v1/accounts/:account_id/notifications/:notification_id PUT /api/v1/accounts/:account_id/platform_apps/:platform_app_id @@ -838,4 +841,4 @@ PUT /public/api/v1/csat_survey/:id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id PUT /widget/direct_uploads/:upload_uuid -TOTAL: 840 +TOTAL: 843 diff --git a/internal/handler/api/v1/notification_setting_handler.go b/internal/handler/api/v1/notification_setting_handler.go index f40df54d..c45ec53e 100644 --- a/internal/handler/api/v1/notification_setting_handler.go +++ b/internal/handler/api/v1/notification_setting_handler.go @@ -55,9 +55,7 @@ func (h *NotificationSettingHandler) Show(c *gin.Context) { return } - response.OK(c, gin.H{ - "notification_setting": serializeNotificationSetting(ns), - }) + c.JSON(http.StatusOK, serializeNotificationSetting(ns)) } // Update modifies notification settings for the current user in the account. @@ -89,21 +87,19 @@ func (h *NotificationSettingHandler) Update(c *gin.Context) { return } - response.OK(c, gin.H{ - "notification_setting": serializeNotificationSetting(ns), - }) + c.JSON(http.StatusOK, serializeNotificationSetting(ns)) } // serializeNotificationSetting builds the response object matching Chatwoot Jbuilder show.json.jbuilder: // id, user_id, account_id, all_email_flags, selected_email_flags, all_push_flags, selected_push_flags func serializeNotificationSetting(ns *model.NotificationSetting) gin.H { return gin.H{ - "id": ns.ID, - "user_id": ns.UserID, - "account_id": ns.AccountID, - "all_email_flags": model.AllEmailFlagNames(), + "id": ns.ID, + "user_id": ns.UserID, + "account_id": ns.AccountID, + "all_email_flags": model.AllEmailFlagNames(), "selected_email_flags": ns.SelectedEmailFlagNames(), - "all_push_flags": model.AllPushFlagNames(), - "selected_push_flags": ns.SelectedPushFlagNames(), + "all_push_flags": model.AllPushFlagNames(), + "selected_push_flags": ns.SelectedPushFlagNames(), } -} \ No newline at end of file +} diff --git a/internal/handler/api/v1/notification_setting_handler_test.go b/internal/handler/api/v1/notification_setting_handler_test.go index c15f6cd2..78e1b4d8 100644 --- a/internal/handler/api/v1/notification_setting_handler_test.go +++ b/internal/handler/api/v1/notification_setting_handler_test.go @@ -2,6 +2,7 @@ package v1 import ( "bytes" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -70,7 +71,7 @@ func (s *NotificationSettingHandlerTestSuite) TestShow_BadRequest_InvalidAccount func (s *NotificationSettingHandlerTestSuite) TestShow_Success() { r := gin.New() r.GET("/api/v1/accounts/:account_id/notification_settings", func(c *gin.Context) { - c.Set("user_id", float64(1)) + c.Set("user_id", float64(11)) s.handler.Show(c) }) @@ -79,6 +80,16 @@ func (s *NotificationSettingHandlerTestSuite) TestShow_Success() { r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) + + var body map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &body)) + assert.NotContains(s.T(), body, "success") + assert.NotContains(s.T(), body, "data") + assert.NotContains(s.T(), body, "notification_setting") + assert.Equal(s.T(), float64(s.account.ID), body["account_id"]) + assert.Equal(s.T(), float64(11), body["user_id"]) + s.Require().Contains(body, "all_email_flags") + s.Require().Contains(body, "selected_email_flags") } func (s *NotificationSettingHandlerTestSuite) TestUpdate_BadRequest_InvalidAccountID() { @@ -94,4 +105,29 @@ func (s *NotificationSettingHandlerTestSuite) TestUpdate_BadRequest_InvalidAccou r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) -} \ No newline at end of file +} + +func (s *NotificationSettingHandlerTestSuite) TestUpdate_ReturnsRawChatwootPayload() { + r := gin.New() + r.PATCH("/api/v1/accounts/:account_id/notification_settings", func(c *gin.Context) { + c.Set("user_id", float64(12)) + s.handler.Update(c) + }) + + body := `{"notification_settings":{"selected_email_flags":["email_conversation_assignment"],"selected_push_flags":["push_conversation_mention"]}}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/notification_settings", s.account.ID), bytes.NewBufferString(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code) + var data map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) + assert.NotContains(s.T(), data, "success") + assert.NotContains(s.T(), data, "data") + assert.NotContains(s.T(), data, "notification_setting") + assert.Equal(s.T(), float64(s.account.ID), data["account_id"]) + assert.Equal(s.T(), float64(12), data["user_id"]) + assert.Equal(s.T(), []interface{}{"email_conversation_assignment"}, data["selected_email_flags"]) + assert.Equal(s.T(), []interface{}{"push_conversation_mention"}, data["selected_push_flags"]) +} diff --git a/internal/router/router.go b/internal/router/router.go index c90470aa..f0275875 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -1515,8 +1515,11 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) { // Reference: Chatwoot resource :notification_settings, only: [:show, :update] notifSettings := accountScoped.Group("/notification_settings") { + notifSettings.GET("", h.NotificationSetting.Show) notifSettings.GET("/", h.NotificationSetting.Show) + notifSettings.PATCH("", h.NotificationSetting.Update) notifSettings.PATCH("/", h.NotificationSetting.Update) + notifSettings.PUT("", h.NotificationSetting.Update) notifSettings.PUT("/", h.NotificationSetting.Update) }