Files
gochat/internal/handler/api/v1/company_handler.go
T

477 lines
15 KiB
Go

package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// CompanyHandler handles Company CRUD + search + nested contacts/conversations/notes.
// Reference: Chatwoot app/controllers/api/v1/companies_controller.rb
type CompanyHandler struct {
svc *service.CompanyService
}
// NewCompanyHandler creates a new CompanyHandler.
func NewCompanyHandler(svc *service.CompanyService) *CompanyHandler {
return &CompanyHandler{svc: svc}
}
// List retrieves all companies for an account.
// GET /api/v1/accounts/:id/companies?sort=name&page=1&per_page=25
func (h *CompanyHandler) List(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
pg := pagination.Parse(c)
sort := c.DefaultQuery("sort", "")
companies, total, err := h.svc.List(c.Request.Context(), accountID, pg.Offset, pg.PerPage, sort)
if err != nil {
applogger.L().Errorf("List companies for account %d: %v", accountID, err)
handleServiceError(c, err)
return
}
c.JSON(http.StatusOK, companyListResponse(c.Request.Context(), h.svc.DB(), companies, total, pg.Page))
}
// Search searches companies by query.
// GET /api/v1/accounts/:id/companies/search?q=Acme&sort=name&page=1&per_page=25
func (h *CompanyHandler) Search(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
pg := pagination.Parse(c)
query := c.DefaultQuery("q", "")
if query == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
return
}
sort := c.DefaultQuery("sort", "")
searchMode := search.SearchMode(c.DefaultQuery("search_mode", "ilike"))
companies, total, err := h.svc.Search(c.Request.Context(), accountID, query, pg.Offset, pg.PerPage, sort, searchMode)
if err != nil {
applogger.L().Errorf("Search companies for account %d: %v", accountID, err)
handleServiceError(c, err)
return
}
c.JSON(http.StatusOK, companyListResponse(c.Request.Context(), h.svc.DB(), companies, total, pg.Page))
}
// Get retrieves a single company by ID.
// GET /api/v1/accounts/:id/companies/:company_id
func (h *CompanyHandler) Get(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
company, svcErr := h.svc.Get(c.Request.Context(), uint(companyID), accountID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyPayloadResponse(c.Request.Context(), h.svc.DB(), company))
}
// Create creates a new company.
// POST /api/v1/accounts/:id/companies
func (h *CompanyHandler) Create(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
var req service.CreateCompanyRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
company, svcErr := h.svc.Create(c.Request.Context(), accountID, &req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyPayloadResponse(c.Request.Context(), h.svc.DB(), company))
}
// Update updates an existing company.
// PUT /api/v1/accounts/:id/companies/:company_id
func (h *CompanyHandler) Update(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
var req service.UpdateCompanyRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
company, svcErr := h.svc.Update(c.Request.Context(), uint(companyID), accountID, &req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyPayloadResponse(c.Request.Context(), h.svc.DB(), company))
}
// Delete deletes a company.
// DELETE /api/v1/accounts/:id/companies/:company_id
func (h *CompanyHandler) Delete(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
if svcErr := h.svc.Delete(c.Request.Context(), uint(companyID), accountID); svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.Status(http.StatusOK)
}
func (h *CompanyHandler) DestroyCustomAttributes(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
var req struct {
CustomAttributes []string `json:"custom_attributes"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.CustomAttributes == nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "custom_attributes must be an array"})
return
}
company, svcErr := h.svc.DestroyCustomAttributes(c.Request.Context(), uint(companyID), accountID, req.CustomAttributes)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyPayloadResponse(c.Request.Context(), h.svc.DB(), company))
}
func (h *CompanyHandler) DeleteAvatar(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
company, svcErr := h.svc.DeleteAvatar(c.Request.Context(), uint(companyID), accountID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyPayloadResponse(c.Request.Context(), h.svc.DB(), company))
}
// ListContacts retrieves contacts associated with a company.
// GET /api/v1/accounts/:id/companies/:company_id/contacts?page=1&per_page=25
func (h *CompanyHandler) ListContacts(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
pg := pagination.Parse(c)
contacts, total, svcErr := h.svc.ListContacts(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyContactsResponse(c.Request.Context(), h.svc.DB(), uint(companyID), contacts, total, pg.Page))
}
// ListConversations retrieves conversations for contacts of a company.
// GET /api/v1/accounts/:id/companies/:company_id/conversations?page=1&per_page=25
func (h *CompanyHandler) ListConversations(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
pg := pagination.Parse(c)
conversations, total, svcErr := h.svc.ListConversations(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
_ = total
c.JSON(http.StatusOK, gin.H{"payload": conversations})
}
func (h *CompanyHandler) SearchContacts(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
query := c.Query("q")
if query == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
return
}
pg := pagination.Parse(c)
contacts, total, svcErr := h.svc.SearchContacts(c.Request.Context(), uint(companyID), accountID, query, pg.Offset, pg.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, companyContactsResponse(c.Request.Context(), h.svc.DB(), uint(companyID), contacts, total, pg.Page))
}
// ListNotes retrieves notes for a company.
// GET /api/v1/accounts/:id/companies/:company_id/notes?page=1&per_page=25
func (h *CompanyHandler) ListNotes(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
pg := pagination.Parse(c)
notes, total, svcErr := h.svc.ListNotes(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
_ = total
c.JSON(http.StatusOK, companyNotesResponse(c.Request.Context(), h.svc.DB(), notes))
}
// CreateNote creates a note for a company.
// POST /api/v1/accounts/:id/companies/:company_id/notes
func (h *CompanyHandler) CreateNote(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
userID := getUserID(c)
if userID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not identified")
return
}
var req service.CreateCompanyNoteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
note, svcErr := h.svc.CreateNote(c.Request.Context(), uint(companyID), accountID, userID, &req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{"payload": note})
}
// DeleteNote deletes a note from a company.
// DELETE /api/v1/accounts/:id/companies/:company_id/notes/:note_id
func (h *CompanyHandler) DeleteNote(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
noteID, err := strconv.ParseUint(c.Param("note_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid note id")
return
}
if err := h.svc.DeleteNote(c.Request.Context(), uint(noteID), uint(companyID), accountID); err != nil {
handleServiceError(c, err)
return
}
c.Status(http.StatusOK)
}
// AddContact adds a contact to a company.
// POST /api/v1/accounts/:id/companies/:company_id/contacts/:contact_id
func (h *CompanyHandler) AddContact(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
contactID, err := parseCompanyContactID(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid contact id")
return
}
if err := h.svc.AddContact(c.Request.Context(), uint(companyID), accountID, uint(contactID)); err != nil {
handleServiceError(c, err)
return
}
contact, svcErr := h.svc.GetContact(c.Request.Context(), uint(companyID), accountID, contactID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{"payload": serializeCompanyContact(c.Request.Context(), h.svc.DB(), contact, uint(companyID))})
}
// RemoveContact removes a contact from a company.
// DELETE /api/v1/accounts/:id/companies/:company_id/contacts/:contact_id
func (h *CompanyHandler) RemoveContact(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
return
}
companyID, err := strconv.ParseUint(c.Param("company_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid company id")
return
}
contactID, err := strconv.ParseUint(c.Param("contact_id"), 10, 32)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid contact id")
return
}
if err := h.svc.RemoveContact(c.Request.Context(), uint(companyID), accountID, uint(contactID)); err != nil {
handleServiceError(c, err)
return
}
c.Status(http.StatusOK)
}
func parseCompanyContactID(c *gin.Context) (uint, error) {
if c.Param("contact_id") != "" {
return parseUintParam(c, "contact_id")
}
var req struct {
ContactID uint `json:"contact_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
return 0, err
}
if req.ContactID == 0 {
return 0, strconv.ErrSyntax
}
return req.ContactID, nil
}