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.
197 lines
6.5 KiB
Go
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{})
|
|
} |