second commit
This commit is contained in:
@@ -0,0 +1,705 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/search"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/pagination"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
// ContactHandler handles contact-related API endpoints.
|
||||
// Reference: Chatwoot app/controllers/api/v1/contacts_controller.rb
|
||||
type ContactHandler struct {
|
||||
svc *service.ContactService
|
||||
contactInboxSvc *service.ContactInboxService
|
||||
mergeSvc *service.ContactMergeService
|
||||
contactNoteSvc *service.ContactNoteService
|
||||
}
|
||||
|
||||
// NewContactHandler creates a new ContactHandler.
|
||||
func NewContactHandler(svc *service.ContactService, contactInboxSvc *service.ContactInboxService, mergeSvc *service.ContactMergeService, contactNoteSvc *service.ContactNoteService) *ContactHandler {
|
||||
return &ContactHandler{svc: svc, contactInboxSvc: contactInboxSvc, mergeSvc: mergeSvc, contactNoteSvc: contactNoteSvc}
|
||||
}
|
||||
|
||||
// @Summary List contacts for an account
|
||||
// @Description Retrieves all contacts for an account with pagination and optional sort
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param sort query string false "Sort field" default(name)
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param page_size query int false "Items per page" default(25)
|
||||
// @Success 200 {object} []model.Contact
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts [get]
|
||||
// List retrieves all contacts for an account.
|
||||
// GET /api/v1/accounts/:id/contacts?sort=name&page=1&page_size=25
|
||||
// Reference: Chatwoot contacts#index (sort param)
|
||||
func (h *ContactHandler) List(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
page := getPage(c)
|
||||
perPage := getPageSize(c)
|
||||
offset := (page - 1) * perPage
|
||||
sort := c.DefaultQuery("sort", "")
|
||||
|
||||
contacts, total, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, offset, perPage, sort)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"contacts": contacts,
|
||||
"meta": gin.H{"count": total, "page": page, "page_size": perPage},
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Search contacts
|
||||
// @Description Searches contacts by query string with pagination, sort, and search mode support
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param q query string true "Search query"
|
||||
// @Param sort query string false "Sort field"
|
||||
// @Param search_mode query string false "Search mode (prefix/semantic/fulltext)" default(prefix)
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param page_size query int false "Items per page" default(25)
|
||||
// @Success 200 {object} []model.Contact
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts/search [get]
|
||||
// Search searches contacts by query with sort support.
|
||||
// GET /api/v1/accounts/:id/contacts/search?q=...&sort=name
|
||||
// Reference: Chatwoot contacts#search
|
||||
func (h *ContactHandler) Search(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
query := c.Query("q")
|
||||
page := getPage(c)
|
||||
perPage := getPageSize(c)
|
||||
offset := (page - 1) * perPage
|
||||
sort := c.DefaultQuery("sort", "")
|
||||
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
|
||||
|
||||
contacts, total, svcErr := h.svc.Search(c.Request.Context(), accountID, query, offset, perPage, sort, searchMode)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to search contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"contacts": contacts,
|
||||
"meta": gin.H{"count": total, "page": page, "page_size": perPage},
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get a single contact
|
||||
// @Description Retrieves detailed information about a specific contact within an account
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param contact_id path uint true "Contact ID"
|
||||
// @Success 200 {object} model.Contact
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 404 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts/{contact_id} [get]
|
||||
// Get retrieves a single contact.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Get(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
contact, svcErr := h.svc.GetByAccountAndID(c.Request.Context(), accountID, contactID)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "contact not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, contact)
|
||||
}
|
||||
|
||||
// @Summary Create a new contact
|
||||
// @Description Creates a new contact, optionally auto-creating a ContactInbox when inbox_id is provided
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param body body service.CreateContactRequest true "Contact creation payload"
|
||||
// @Success 201 {object} model.Contact
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts [post]
|
||||
// Create creates a new contact, optionally auto-creating a ContactInbox.
|
||||
// POST /api/v1/accounts/:id/contacts
|
||||
// Body: {name, email, phone, inbox_id, source_id, ...}
|
||||
// Reference: Chatwoot contacts#create (auto-creates ContactInbox when inbox_id provided)
|
||||
func (h *ContactHandler) Create(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateContactRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contact, svcErr := h.svc.Create(c.Request.Context(), accountID, req)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create contact"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, contact)
|
||||
}
|
||||
|
||||
// @Summary Update a contact
|
||||
// @Description Modifies an existing contact's details
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param contact_id path uint true "Contact ID"
|
||||
// @Param body body service.UpdateContactRequest true "Contact update payload"
|
||||
// @Success 200 {object} model.Contact
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 404 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts/{contact_id} [put]
|
||||
// Update modifies an existing contact.
|
||||
// PUT /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Update(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.UpdateContactRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contact, svcErr := h.svc.Update(c.Request.Context(), accountID, contactID, req)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update contact"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, contact)
|
||||
}
|
||||
|
||||
// @Summary Delete a contact
|
||||
// @Description Soft-deletes a contact from an account
|
||||
// @Tags Contacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path uint true "Account ID"
|
||||
// @Param contact_id path uint true "Contact ID"
|
||||
// @Success 200 {object} object
|
||||
// @Failure 400 {object} model.ErrorResponse
|
||||
// @Failure 401 {object} model.ErrorResponse
|
||||
// @Failure 404 {object} model.ErrorResponse
|
||||
// @Failure 500 {object} model.ErrorResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/accounts/{id}/contacts/{contact_id} [delete]
|
||||
// Delete soft-deletes a contact.
|
||||
// DELETE /api/v1/accounts/:id/contacts/:contact_id
|
||||
func (h *ContactHandler) Delete(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.Delete(c.Request.Context(), accountID, contactID); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete contact"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "contact deleted"})
|
||||
}
|
||||
|
||||
// ListContactInboxes retrieves all contact_inboxes for a contact.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id/contact_inboxes
|
||||
// Reference: Chatwoot contacts#contact_inboxes (nested resource)
|
||||
func (h *ContactHandler) ListContactInboxes(c *gin.Context) {
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactInboxes, svcErr := h.svc.ListContactInboxes(c.Request.Context(), contactID)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list contact inboxes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"contact_inboxes": contactInboxes,
|
||||
"meta": gin.H{"count": len(contactInboxes)},
|
||||
})
|
||||
}
|
||||
|
||||
// ListNotes retrieves notes for a contact.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id/notes
|
||||
// Reference: Chatwoot contacts#notes
|
||||
func (h *ContactHandler) ListNotes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
notes, svcErr := h.svc.ListNotes(c.Request.Context(), accountID, contactID)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list notes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"notes": notes,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateNote creates a note for a contact.
|
||||
// POST /api/v1/accounts/:id/contacts/:contact_id/notes
|
||||
// Reference: Chatwoot contacts#create_note
|
||||
func (h *ContactHandler) CreateNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := getUserID(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateNoteRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.svc.CreateNote(c.Request.Context(), accountID, contactID, userID, req)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create note"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, note)
|
||||
}
|
||||
|
||||
// ShowNote retrieves a single note for a contact.
|
||||
// GET /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) ShowNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
|
||||
noteID, err := parseUintParam(c, "note_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid note id")
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.contactNoteSvc.GetByID(c.Request.Context(), accountID, noteID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, note)
|
||||
}
|
||||
|
||||
// UpdateNote updates a note on a contact.
|
||||
// PATCH /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) UpdateNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
|
||||
noteID, err := parseUintParam(c, "note_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid note id")
|
||||
return
|
||||
}
|
||||
|
||||
var req service.NoteUpdateRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, bindErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.contactNoteSvc.UpdateNote(c.Request.Context(), accountID, noteID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, note)
|
||||
}
|
||||
|
||||
// DestroyNote deletes a note on a contact.
|
||||
// DELETE /api/v1/accounts/:account_id/contacts/:contact_id/notes/:id
|
||||
func (h *ContactHandler) DestroyNote(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
||||
return
|
||||
}
|
||||
|
||||
noteID, err := parseUintParam(c, "note_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid note id")
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.contactNoteSvc.DeleteNote(c.Request.Context(), accountID, noteID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": strconv.FormatUint(uint64(noteID), 10),
|
||||
"deleted": true,
|
||||
})
|
||||
}
|
||||
|
||||
// parseIntOrDefault parses an integer query parameter with a default value.
|
||||
func parseIntOrDefault(c *gin.Context, key string, defaultVal int) int {
|
||||
val := c.Query(key)
|
||||
if val == "" {
|
||||
return defaultVal
|
||||
}
|
||||
n, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// CreateContactInbox adds a contact-inbox association (linking a contact to an inbox).
|
||||
// POST /api/v1/accounts/:id/contacts/:contact_id/contact_inboxes
|
||||
// Reference: Chatwoot contact_inboxes#create
|
||||
func (h *ContactHandler) CreateContactInbox(c *gin.Context) {
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
InboxID uint `json:"inbox_id" binding:"required"`
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ci, svcErr := h.contactInboxSvc.Create(c.Request.Context(), service.CreateContactInboxRequest{
|
||||
ContactID: contactID,
|
||||
InboxID: req.InboxID,
|
||||
SourceID: req.SourceID,
|
||||
})
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create contact inbox"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ci)
|
||||
}
|
||||
|
||||
// DeleteContactInbox removes a contact-inbox association.
|
||||
// DELETE /api/v1/accounts/:id/contacts/:contact_id/contact_inboxes/:inbox_id
|
||||
// Reference: Chatwoot contact_inboxes#destroy
|
||||
func (h *ContactHandler) DeleteContactInbox(c *gin.Context) {
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
inboxID, err := parseUintParam(c, "inbox_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.contactInboxSvc.DeleteByContactAndInbox(c.Request.Context(), contactID, inboxID); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete contact inbox"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"contact_id": contactID, "inbox_id": inboxID, "deleted": true})
|
||||
}
|
||||
|
||||
// Active retrieves contacts with recent activity.
|
||||
// GET /api/v1/accounts/:id/contacts/active?sort=name&page=1&page_size=25
|
||||
// Reference: Chatwoot contacts#active
|
||||
func (h *ContactHandler) Active(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
page := getPage(c)
|
||||
perPage := getPageSize(c)
|
||||
offset := (page - 1) * perPage
|
||||
sort := c.DefaultQuery("sort", "")
|
||||
|
||||
contacts, total, svcErr := h.svc.ListActive(c.Request.Context(), accountID, offset, perPage, sort)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list active contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"contacts": contacts,
|
||||
"meta": gin.H{"count": total, "page": page, "page_size": perPage},
|
||||
})
|
||||
}
|
||||
|
||||
// Export downloads contacts as CSV.
|
||||
// GET /api/v1/accounts/:id/contacts/export
|
||||
// Reference: Chatwoot contacts#export
|
||||
func (h *ContactHandler) Export(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Content-Disposition", "attachment; filename=contacts.csv")
|
||||
|
||||
if svcErr := h.svc.ExportCSV(c.Request.Context(), accountID, c.Writer); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to export contacts"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Import uploads contacts from a CSV file.
|
||||
// POST /api/v1/accounts/:id/contacts/import
|
||||
// Reference: Chatwoot contacts#import
|
||||
func (h *ContactHandler) Import(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
file, _, fileErr := c.Request.FormFile("file")
|
||||
if fileErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "csv file required"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
result, svcErr := h.svc.ImportCSV(c.Request.Context(), accountID, file)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to import contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ContactableInboxes returns inboxes that a contact can be associated with.
|
||||
// GET /api/v1/accounts/:id/contacts/:contact_id/contactable_inboxes
|
||||
// Reference: Chatwoot contacts#contactable_inboxes
|
||||
func (h *ContactHandler) ContactableInboxes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
inboxes, svcErr := h.svc.GetContactableInboxes(c.Request.Context(), accountID, contactID)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to get contactable inboxes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": inboxes})
|
||||
}
|
||||
|
||||
// DeleteCustomAttributes removes all custom attributes from a contact.
|
||||
// DELETE /api/v1/accounts/:id/contacts/:contact_id/custom_attributes
|
||||
// Reference: Chatwoot contacts#destroy_custom_attributes
|
||||
func (h *ContactHandler) DeleteCustomAttributes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.DeleteCustomAttributes(c.Request.Context(), accountID, contactID); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete custom attributes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "custom attributes deleted"})
|
||||
}
|
||||
// Merge two contacts into one. The base contact survives, mergee is deleted.
|
||||
// POST /api/v1/accounts/:id/contacts/merge
|
||||
// Reference: Chatwoot app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
|
||||
func (h *ContactHandler) Merge(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.MergeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: base_contact_id and mergee_contact_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
result, svcErr := h.mergeSvc.MergeWithRequest(c.Request.Context(), accountID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// Filter retrieves contacts matching advanced filter criteria.
|
||||
// POST /api/v1/accounts/:id/contacts/filter
|
||||
// Reference: Chatwoot contacts#filter — uses ContactFilterService with payload params.
|
||||
func (h *ContactHandler) Filter(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
var params repository.ContactFilterParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
p := pagination.Parse(c)
|
||||
contacts, total, svcErr := h.svc.Filter(c.Request.Context(), accountID, params, p.Offset, p.PerPage)
|
||||
if svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to filter contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
response.OKWithMeta(c, contacts, p.Page, p.PerPage, total)
|
||||
}
|
||||
|
||||
// DestroyCustomAttributes removes all custom attributes from a contact.
|
||||
// POST /api/v1/accounts/:id/contacts/:contact_id/destroy_custom_attributes
|
||||
// Reference: Chatwoot contacts#destroy_custom_attributes — uses POST method.
|
||||
// This is an alias for DeleteCustomAttributes (which uses DELETE method) to match Chatwoot's API.
|
||||
func (h *ContactHandler) DestroyCustomAttributes(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid contact id"})
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.DeleteCustomAttributes(c.Request.Context(), accountID, contactID); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to destroy custom attributes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "custom attributes destroyed"})
|
||||
}
|
||||
Reference in New Issue
Block a user