feat(crm): align contact note payloads
This commit is contained in:
@@ -765,6 +765,7 @@ PUT /api/v1/accounts/:account_id/captain/preferences/
|
||||
PUT /api/v1/accounts/:account_id/captain/scenarios/:scenario_id
|
||||
PUT /api/v1/accounts/:account_id/companies/:company_id
|
||||
PUT /api/v1/accounts/:account_id/contacts/:contact_id
|
||||
PUT /api/v1/accounts/:account_id/contacts/:contact_id/notes/:note_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id
|
||||
PUT /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_calls/:call_id
|
||||
@@ -817,4 +818,4 @@ PUT /public/api/v1/csat_survey/:id
|
||||
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
|
||||
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id
|
||||
PUT /widget/direct_uploads/:upload_uuid
|
||||
TOTAL: 819
|
||||
TOTAL: 820
|
||||
|
||||
@@ -328,7 +328,7 @@ func (h *CompanyHandler) ListNotes(c *gin.Context) {
|
||||
}
|
||||
|
||||
_ = total
|
||||
c.JSON(http.StatusOK, companyNotesResponse(c.Request.Context(), h.svc.DB(), notes))
|
||||
c.JSON(http.StatusOK, companyNotesResponse(c.Request.Context(), h.svc.DB(), notes, accountID))
|
||||
}
|
||||
|
||||
// CreateNote creates a note for a company.
|
||||
@@ -364,7 +364,7 @@ func (h *CompanyHandler) CreateNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": note})
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeCompanyNote(c.Request.Context(), h.svc.DB(), note, accountID)})
|
||||
}
|
||||
|
||||
// DeleteNote deletes a note from a company.
|
||||
|
||||
@@ -405,6 +405,11 @@ func (s *CompanyHandlerTestSuite) TestListNotes_Success() {
|
||||
|
||||
payload := resp["payload"].([]interface{})
|
||||
assert.Len(s.T(), payload, 2)
|
||||
first := payload[0].(map[string]interface{})
|
||||
assert.Contains(s.T(), first, "company_id")
|
||||
assert.Contains(s.T(), first, "user")
|
||||
user := first["user"].(map[string]interface{})
|
||||
assert.Equal(s.T(), "TestUser", user["name"])
|
||||
}
|
||||
|
||||
// ========== CreateNote ==========
|
||||
@@ -425,6 +430,8 @@ func (s *CompanyHandlerTestSuite) TestCreateNote_Success() {
|
||||
|
||||
noteData := resp["payload"].(map[string]interface{})
|
||||
assert.Equal(s.T(), "This is a new note", noteData["content"])
|
||||
assert.Equal(s.T(), float64(company.ID), noteData["company_id"])
|
||||
assert.Contains(s.T(), noteData, "user")
|
||||
}
|
||||
|
||||
func (s *CompanyHandlerTestSuite) TestCreateNote_ValidationError() {
|
||||
|
||||
@@ -441,7 +441,7 @@ func (h *ContactHandler) ListNotes(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": notes})
|
||||
c.JSON(http.StatusOK, contactNotesResponse(notes))
|
||||
}
|
||||
|
||||
// CreateNote creates a note for a contact.
|
||||
@@ -466,8 +466,8 @@ func (h *ContactHandler) CreateNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateNoteRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
req, bindErr := bindContactNoteRequest(c)
|
||||
if bindErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
|
||||
return
|
||||
}
|
||||
@@ -478,7 +478,7 @@ func (h *ContactHandler) CreateNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, note)
|
||||
c.JSON(http.StatusOK, serializeContactNote(note))
|
||||
}
|
||||
|
||||
// ShowNote retrieves a single note for a contact.
|
||||
@@ -496,13 +496,19 @@ func (h *ContactHandler) ShowNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.contactNoteSvc.GetByID(c.Request.Context(), accountID, noteID)
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid contact id")
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.svc.GetNote(c.Request.Context(), accountID, contactID, noteID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, note)
|
||||
c.JSON(http.StatusOK, serializeContactNote(note))
|
||||
}
|
||||
|
||||
// UpdateNote updates a note on a contact.
|
||||
@@ -520,19 +526,25 @@ func (h *ContactHandler) UpdateNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.NoteUpdateRequest
|
||||
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid contact id")
|
||||
return
|
||||
}
|
||||
|
||||
req, bindErr := bindContactNoteRequest(c)
|
||||
if bindErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, bindErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
note, svcErr := h.contactNoteSvc.UpdateNote(c.Request.Context(), accountID, noteID, req)
|
||||
note, svcErr := h.svc.UpdateNote(c.Request.Context(), accountID, contactID, noteID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, note)
|
||||
c.JSON(http.StatusOK, serializeContactNote(note))
|
||||
}
|
||||
|
||||
// DestroyNote deletes a note on a contact.
|
||||
@@ -550,15 +562,35 @@ func (h *ContactHandler) DestroyNote(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.contactNoteSvc.DeleteNote(c.Request.Context(), accountID, noteID); svcErr != nil {
|
||||
contactID, err := parseUintParam(c, "contact_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid contact id")
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.DeleteNote(c.Request.Context(), accountID, contactID, noteID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": strconv.FormatUint(uint64(noteID), 10),
|
||||
"deleted": true,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func bindContactNoteRequest(c *gin.Context) (service.CreateNoteRequest, error) {
|
||||
var body struct {
|
||||
Content string `json:"content"`
|
||||
Note struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"note"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
return service.CreateNoteRequest{}, err
|
||||
}
|
||||
content := body.Content
|
||||
if content == "" {
|
||||
content = body.Note.Content
|
||||
}
|
||||
return service.CreateNoteRequest{Content: content}, nil
|
||||
}
|
||||
|
||||
func includeContactInboxes(c *gin.Context) bool {
|
||||
|
||||
@@ -100,6 +100,10 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/contact_inboxes", s.handler.ListContactInboxes)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.ListNotes)
|
||||
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes/:note_id", s.handler.ShowNote)
|
||||
s.router.PUT("/api/v1/accounts/:id/contacts/:contact_id/notes/:note_id", s.handler.UpdateNote)
|
||||
s.router.PATCH("/api/v1/accounts/:id/contacts/:contact_id/notes/:note_id", s.handler.UpdateNote)
|
||||
s.router.DELETE("/api/v1/accounts/:id/contacts/:contact_id/notes/:note_id", s.handler.DestroyNote)
|
||||
s.router.POST("/api/v1/accounts/:id/actions/contact_merge", s.handler.Merge)
|
||||
|
||||
// Create test data
|
||||
@@ -894,9 +898,15 @@ func (s *ContactHandlerCRUDTestSuite) TestListNotes_Success() {
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
var resp []map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Contains(resp, "payload")
|
||||
s.Len(resp, 1)
|
||||
s.Equal("Test note content", resp[0]["content"])
|
||||
s.Equal(float64(s.account.ID), resp[0]["account_id"])
|
||||
s.Equal(float64(s.contact.ID), resp[0]["contact_id"])
|
||||
user := resp[0]["user"].(map[string]interface{})
|
||||
s.Equal(s.user.Name, user["name"])
|
||||
s.NotContains(resp[0], "payload")
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestListNotes_InvalidAccountID() {
|
||||
@@ -944,12 +954,68 @@ func (s *ContactHandlerCRUDTestSuite) TestCreateNote_Success() {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusCreated, w.Code)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var note model.Note
|
||||
var note map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), ¬e))
|
||||
s.Equal("A new note for the contact", note.Content)
|
||||
s.NotZero(note.ID)
|
||||
s.Equal("A new note for the contact", note["content"])
|
||||
s.Equal(float64(s.account.ID), note["account_id"])
|
||||
s.Equal(float64(s.contact.ID), note["contact_id"])
|
||||
s.NotZero(note["id"])
|
||||
s.Contains(note, "user")
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_NestedNotePayload() {
|
||||
body := map[string]interface{}{
|
||||
"note": map[string]interface{}{"content": "Nested note content"},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", s.account.ID, s.contact.ID),
|
||||
bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var note map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), ¬e))
|
||||
s.Equal("Nested note content", note["content"])
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestShowUpdateDestroyNote_RawChatwootShape() {
|
||||
note := &model.Note{Content: "Original", AccountID: s.account.ID, ContactID: s.contact.ID, UserID: &s.user.ID}
|
||||
s.Require().NoError(s.db.Create(note).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes/%d", s.account.ID, s.contact.ID, note.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal("Original", resp["content"])
|
||||
s.NotContains(resp, "success")
|
||||
s.NotContains(resp, "data")
|
||||
|
||||
bodyBytes, _ := json.Marshal(map[string]interface{}{"note": map[string]interface{}{"content": "Updated"}})
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PATCH",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes/%d", s.account.ID, s.contact.ID, note.ID),
|
||||
bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal("Updated", resp["content"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes/%d", s.account.ID, s.contact.ID, note.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Empty(w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_InvalidAccountID() {
|
||||
|
||||
@@ -160,21 +160,57 @@ func companyContactsResponse(ctx context.Context, db *gorm.DB, companyID uint, c
|
||||
}
|
||||
}
|
||||
|
||||
func companyNotesResponse(ctx context.Context, db *gorm.DB, notes []model.CompanyNote) map[string]any {
|
||||
func companyNotesResponse(ctx context.Context, db *gorm.DB, notes []model.CompanyNote, accountID uint) map[string]any {
|
||||
payload := make([]any, 0, len(notes))
|
||||
for i := range notes {
|
||||
item := map[string]any{
|
||||
"id": notes[i].ID,
|
||||
"content": notes[i].Content,
|
||||
"user_id": notes[i].UserID,
|
||||
"created_at": notes[i].CreatedAt.Unix(),
|
||||
"updated_at": notes[i].UpdatedAt.Unix(),
|
||||
}
|
||||
payload = append(payload, item)
|
||||
payload = append(payload, serializeCompanyNote(ctx, db, ¬es[i], accountID))
|
||||
}
|
||||
return map[string]any{"payload": payload}
|
||||
}
|
||||
|
||||
func contactNotesResponse(notes []model.Note) []any {
|
||||
payload := make([]any, 0, len(notes))
|
||||
for i := range notes {
|
||||
payload = append(payload, serializeContactNote(¬es[i]))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeContactNote(note *model.Note) map[string]any {
|
||||
item := map[string]any{
|
||||
"id": note.ID,
|
||||
"content": note.Content,
|
||||
"account_id": note.AccountID,
|
||||
"contact_id": note.ContactID,
|
||||
"created_at": note.CreatedAt.Unix(),
|
||||
"updated_at": note.UpdatedAt.Unix(),
|
||||
}
|
||||
if note.UserID != nil {
|
||||
item["user_id"] = *note.UserID
|
||||
}
|
||||
if note.User != nil && note.User.ID != 0 {
|
||||
item["user"] = serializeUser(note.User, note.AccountID)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func serializeCompanyNote(ctx context.Context, db *gorm.DB, note *model.CompanyNote, accountID uint) map[string]any {
|
||||
item := map[string]any{
|
||||
"id": note.ID,
|
||||
"content": note.Content,
|
||||
"company_id": note.CompanyID,
|
||||
"user_id": note.UserID,
|
||||
"created_at": note.CreatedAt.Unix(),
|
||||
"updated_at": note.UpdatedAt.Unix(),
|
||||
}
|
||||
if note.User.ID != 0 {
|
||||
item["user"] = serializeUser(¬e.User, accountID)
|
||||
} else if db != nil && note.UserID != 0 {
|
||||
item["user"] = serializeUserFromDB(ctx, db, note.UserID, accountID)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func companyContactsCount(ctx context.Context, db *gorm.DB, company *model.Company) int64 {
|
||||
if db == nil || company == nil || company.ID == 0 {
|
||||
return 0
|
||||
|
||||
@@ -161,12 +161,19 @@ func (r *CompanyRepo) ListNotes(ctx context.Context, companyID uint, offset, lim
|
||||
|
||||
// CreateNote creates a note for a company.
|
||||
func (r *CompanyRepo) CreateNote(ctx context.Context, note *model.CompanyNote) error {
|
||||
return r.db.WithContext(ctx).Create(note).Error
|
||||
if err := r.db.WithContext(ctx).Create(note).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Preload("User").First(note, note.ID).Error
|
||||
}
|
||||
|
||||
// DeleteNote deletes a note belonging to a company.
|
||||
func (r *CompanyRepo) DeleteNote(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.CompanyNote{}, id).Error
|
||||
func (r *CompanyRepo) DeleteNote(ctx context.Context, companyID, id uint) error {
|
||||
var note model.CompanyNote
|
||||
if err := r.db.WithContext(ctx).Where("company_id = ? AND id = ?", companyID, id).First(¬e).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Delete(¬e).Error
|
||||
}
|
||||
|
||||
func (r *CompanyRepo) AddContact(ctx context.Context, companyID, contactID uint) error {
|
||||
|
||||
@@ -281,7 +281,7 @@ func TestCompanyRepo_DeleteNote(t *testing.T) {
|
||||
|
||||
note := createTestCompanyNote(t, db, company.ID, user.ID, "Note to delete")
|
||||
|
||||
err := repo.DeleteNote(context.Background(), note.ID)
|
||||
err := repo.DeleteNote(context.Background(), company.ID, note.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the note is actually gone
|
||||
@@ -294,9 +294,8 @@ func TestCompanyRepo_DeleteNote_NotFound(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewCompanyRepo(db)
|
||||
|
||||
// Deleting a non-existent note should not error (GORM behavior)
|
||||
err := repo.DeleteNote(context.Background(), 9999)
|
||||
assert.NoError(t, err)
|
||||
err := repo.DeleteNote(context.Background(), 1, 9999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ========== AddContact Tests ==========
|
||||
|
||||
@@ -50,8 +50,12 @@ func (r *NoteRepo) ListByContact(accountID, contactID uint) ([]model.Note, error
|
||||
|
||||
// GetByID returns a single note by ID within the account scope.
|
||||
func (r *NoteRepo) GetByID(accountID, contactID, noteID uint) (*model.Note, error) {
|
||||
return r.GetByIDContext(context.Background(), accountID, contactID, noteID)
|
||||
}
|
||||
|
||||
func (r *NoteRepo) GetByIDContext(ctx context.Context, accountID, contactID, noteID uint) (*model.Note, error) {
|
||||
var note model.Note
|
||||
err := r.db.Where("account_id = ? AND contact_id = ? AND id = ?", accountID, contactID, noteID).
|
||||
err := r.db.WithContext(ctx).Where("account_id = ? AND contact_id = ? AND id = ?", accountID, contactID, noteID).
|
||||
Preload("User").
|
||||
First(¬e).Error
|
||||
if err != nil {
|
||||
@@ -80,9 +84,12 @@ func (r *NoteRepo) Update(note *model.Note) (*model.Note, error) {
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// Delete soft-deletes a note.
|
||||
// Reference: Chatwoot uses hard delete `@note.destroy!`, we use soft delete for audit trail.
|
||||
// Delete hard-deletes a note, matching Chatwoot `@note.destroy!` for notes.
|
||||
func (r *NoteRepo) Delete(accountID, contactID, noteID uint) error {
|
||||
return r.db.Where("account_id = ? AND contact_id = ? AND id = ?", accountID, contactID, noteID).
|
||||
Delete(&model.Note{}).Error
|
||||
}
|
||||
return r.DeleteContext(context.Background(), accountID, contactID, noteID)
|
||||
}
|
||||
|
||||
func (r *NoteRepo) DeleteContext(ctx context.Context, accountID, contactID, noteID uint) error {
|
||||
return r.db.WithContext(ctx).Where("account_id = ? AND contact_id = ? AND id = ?", accountID, contactID, noteID).
|
||||
Unscoped().Delete(&model.Note{}).Error
|
||||
}
|
||||
|
||||
@@ -1003,6 +1003,7 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
contacts.GET("/:contact_id/notes", h.Contact.ListNotes)
|
||||
contacts.POST("/:contact_id/notes", h.Contact.CreateNote)
|
||||
contacts.GET("/:contact_id/notes/:note_id", h.Contact.ShowNote)
|
||||
contacts.PUT("/:contact_id/notes/:note_id", h.Contact.UpdateNote)
|
||||
contacts.PATCH("/:contact_id/notes/:note_id", h.Contact.UpdateNote)
|
||||
contacts.DELETE("/:contact_id/notes/:note_id", h.Contact.DestroyNote)
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ func (s *CompanyService) DeleteNote(ctx context.Context, noteID, companyID, acco
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.companyRepo.DeleteNote(ctx, noteID); err != nil {
|
||||
if err := s.companyRepo.DeleteNote(ctx, companyID, noteID); err != nil {
|
||||
applogger.L().Errorf("Delete note %d for company %d: %v", noteID, companyID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -293,9 +293,44 @@ func (s *ContactService) CreateNote(ctx context.Context, accountID, contactID, u
|
||||
if err := s.noteRepo.Create(ctx, note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.repo != nil && s.repo.DB() != nil {
|
||||
_ = s.repo.DB().WithContext(ctx).Preload("User").First(note, note.ID).Error
|
||||
}
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// GetNote retrieves a single note scoped to account and contact.
|
||||
func (s *ContactService) GetNote(ctx context.Context, accountID, contactID, noteID uint) (*model.Note, error) {
|
||||
if s == nil || s.noteRepo == nil {
|
||||
return nil, errors.New("contact service not ready")
|
||||
}
|
||||
return s.noteRepo.GetByIDContext(ctx, accountID, contactID, noteID)
|
||||
}
|
||||
|
||||
// UpdateNote updates a note scoped to account and contact.
|
||||
func (s *ContactService) UpdateNote(ctx context.Context, accountID, contactID, noteID uint, req CreateNoteRequest) (*model.Note, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
note, err := s.noteRepo.GetByIDContext(ctx, accountID, contactID, noteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
note.Content = req.Content
|
||||
return s.noteRepo.Update(note)
|
||||
}
|
||||
|
||||
// DeleteNote removes a note scoped to account and contact.
|
||||
func (s *ContactService) DeleteNote(ctx context.Context, accountID, contactID, noteID uint) error {
|
||||
if s == nil || s.noteRepo == nil {
|
||||
return errors.New("contact service not ready")
|
||||
}
|
||||
if _, err := s.noteRepo.GetByIDContext(ctx, accountID, contactID, noteID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.noteRepo.DeleteContext(ctx, accountID, contactID, noteID)
|
||||
}
|
||||
|
||||
// ListActive retrieves contacts with recent activity for an account.
|
||||
// GET /api/v1/accounts/:id/contacts/active
|
||||
// Reference: Chatwoot contacts#active
|
||||
|
||||
Reference in New Issue
Block a user