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.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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{})
|
||||
}
|
||||
Reference in New Issue
Block a user