Files
gochat/backend/internal/handler/api/v1/note_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

197 lines
6.5 KiB
Go

package v1
// NoteHandler provides HTTP handlers for contact notes.
// Reference: Chatwoot app/controllers/api/v1/accounts/contacts/notes_controller.rb
//
// Routes:
// GET /api/v1/accounts/:account_id/contacts/:contact_id/notes -> List
// GET /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id -> Show
// POST /api/v1/accounts/:account_id/contacts/:contact_id/notes -> Create
// PUT /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id -> Update
// DELETE /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id -> Destroy
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/response"
)
// NoteHandler handles HTTP requests for contact notes.
type NoteHandler struct {
noteService *service.NoteService
}
// NewNoteHandler creates a new NoteHandler.
func NewNoteHandler(noteService *service.NoteService) *NoteHandler {
return &NoteHandler{noteService: noteService}
}
// noteRequest represents the JSON request body for creating/updating a note.
// Reference: Chatwoot `params.require(:note).permit(:content)`
type noteRequest struct {
Content string `json:"content" binding:"required"`
}
// List returns all notes for a contact.
// Reference: Chatwoot `def index`
func (h *NoteHandler) List(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid account ID")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid contact ID")
return
}
notes, err := h.noteService.List(uint(accountID), uint(contactID))
if err != nil {
response.AbortWithStatusError(c, 500, response.ErrInternal, "Failed to list notes")
return
}
response.OK(c, notes)
}
// Show returns a single note by ID.
// Reference: Chatwoot `def show; end`
func (h *NoteHandler) Show(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid account ID")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid contact ID")
return
}
noteID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid note ID")
return
}
note, err := h.noteService.Get(uint(accountID), uint(contactID), uint(noteID))
if err != nil {
response.AbortWithStatusError(c, 404, response.ErrNotFound, "Note not found")
return
}
response.OK(c, note)
}
// Create creates a new note for a contact.
// Reference: Chatwoot `def create`
// @note = @contact.notes.create!(note_params)
// note_params merges {contact_id: @contact.id, user_id: Current.user.id}
func (h *NoteHandler) Create(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid account ID")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid contact ID")
return
}
// Chatwoot: params.require(:note) → {"note": {"content": "..."}}
var wrapper struct {
Note noteRequest `json:"note"`
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Content is required")
return
}
req := wrapper.Note
userID, exists := c.Get("user_id")
if !exists {
response.AbortWithStatusError(c, 401, response.ErrUnauthorized, "User not authenticated")
return
}
note, err := h.noteService.Create(uint(accountID), uint(contactID), userID.(uint), req.Content)
if err != nil {
if err == service.ErrNoteContentEmpty {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Content is required")
return
}
response.AbortWithStatusError(c, 500, response.ErrInternal, "Failed to create note")
return
}
response.Created(c, note)
}
// Update updates an existing note's content.
// Reference: Chatwoot `def update; @note.update(note_params); end`
func (h *NoteHandler) Update(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid account ID")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid contact ID")
return
}
noteID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid note ID")
return
}
// Chatwoot: params.require(:note) → {"note": {"content": "..."}}
var wrapper struct {
Note noteRequest `json:"note"`
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Content is required")
return
}
req := wrapper.Note
note, err := h.noteService.Update(uint(accountID), uint(contactID), uint(noteID), req.Content)
if err != nil {
if err == service.ErrNoteNotFound {
response.AbortWithStatusError(c, 404, response.ErrNotFound, "Note not found")
return
}
response.AbortWithStatusError(c, 500, response.ErrInternal, "Failed to update note")
return
}
response.OK(c, note)
}
// Destroy deletes a note.
// Reference: Chatwoot `def destroy; @note.destroy!; head :ok; end`
// Chatwoot returns 200 OK on destroy, NOT 204 NoContent.
func (h *NoteHandler) Destroy(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid account ID")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid contact ID")
return
}
noteID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, 400, response.ErrBadRequest, "Invalid note ID")
return
}
err = h.noteService.Delete(uint(accountID), uint(contactID), uint(noteID))
if err != nil {
response.AbortWithStatusError(c, 404, response.ErrNotFound, "Note not found")
return
}
// Reference: Chatwoot returns `head :ok` → 200 OK with empty body
response.OK(c, gin.H{})
}