Add Contact Management Functionality

- Introduced a new contact management feature, including the ability to create, retrieve, search, update, and delete contact messages.
- Implemented the Contact entity, repository, service, and handler layers following Clean Architecture principles.
- Added API endpoints for contact operations in Swagger documentation, ensuring comprehensive API specifications.
- Enhanced error handling and validation for contact data, improving robustness and user experience.
- Updated the main application to initialize the contact repository and service, integrating them into the existing system.
This commit is contained in:
n.nakhostin
2025-10-25 18:15:39 +03:30
parent 889a56a9f9
commit 875447ac58
11 changed files with 2151 additions and 10 deletions
+75
View File
@@ -0,0 +1,75 @@
package contact
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// ContactStatus represents the status of a contact message
type ContactStatus string
const (
ContactStatusPending ContactStatus = "pending"
ContactStatusInProgress ContactStatus = "in_progress"
ContactStatusResolved ContactStatus = "resolved"
ContactStatusClosed ContactStatus = "closed"
)
// Contact represents a contact us message in the system
type Contact struct {
mongo.Model `bson:",inline"`
FullName string `bson:"full_name" json:"full_name" validate:"required"`
Email string `bson:"email" json:"email" validate:"required,email"`
Phone string `bson:"phone" json:"phone" validate:"required"`
Message string `bson:"message" json:"message" validate:"required"`
Status ContactStatus `bson:"status" json:"status" validate:"required,oneof=pending in_progress resolved closed"`
AdminNotes *string `bson:"admin_notes,omitempty" json:"admin_notes,omitempty"`
ResolvedAt *int64 `bson:"resolved_at,omitempty" json:"resolved_at,omitempty"`
ResolvedBy *string `bson:"resolved_by,omitempty" json:"resolved_by,omitempty"`
}
// SetID sets the contact ID (implements IDSetter interface)
func (c *Contact) SetID(id string) {
c.ID, _ = bson.ObjectIDFromHex(id)
}
// GetID returns the contact ID (implements IDGetter interface)
func (c *Contact) GetID() string {
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (c *Contact) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (c *Contact) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *Contact) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *Contact) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// IsPending returns true if the contact is pending
func (c *Contact) IsPending() bool {
return c.Status == ContactStatusPending
}
// IsResolved returns true if the contact is resolved
func (c *Contact) IsResolved() bool {
return c.Status == ContactStatusResolved || c.Status == ContactStatusClosed
}
// CanBeUpdated returns true if the contact can be updated
func (c *Contact) CanBeUpdated() bool {
return c.Status != ContactStatusClosed
}
+18
View File
@@ -0,0 +1,18 @@
package contact
import "errors"
// Custom errors for contact domain
var (
ErrContactNotFound = errors.New("contact not found")
ErrContactAlreadyResolved = errors.New("contact is already resolved")
ErrContactClosed = errors.New("contact is closed and cannot be updated")
ErrInvalidStatus = errors.New("invalid contact status")
ErrInvalidDateRange = errors.New("invalid date range: date_from must be before date_to")
ErrUnauthorizedAccess = errors.New("unauthorized access to contact")
ErrInvalidContactData = errors.New("invalid contact data")
ErrEmailRequired = errors.New("email is required")
ErrPhoneRequired = errors.New("phone is required")
ErrMessageRequired = errors.New("message is required")
ErrFullNameRequired = errors.New("full name is required")
)
+79
View File
@@ -0,0 +1,79 @@
package contact
import "tm/pkg/response"
// CreateContactForm represents the form for creating a new contact message
type CreateContactForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"`
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
Phone string `json:"phone" valid:"required,length(10|20)" example:"+1234567890"`
Message string `json:"message" valid:"required,length(10|1000)" example:"I have a question about your services"`
}
// UpdateContactStatusForm represents the form for updating contact status (admin only)
type UpdateContactStatusForm struct {
Status string `json:"status" valid:"required,in(pending|in_progress|resolved|closed)" example:"in_progress"`
AdminNotes *string `json:"admin_notes,omitempty" valid:"optional,length(0|500)" example:"Customer inquiry handled"`
}
// SearchContactsForm represents the form for searching contacts with filters
type SearchContactsForm struct {
Search *string `query:"q" valid:"optional"`
Status *string `query:"status" valid:"optional,in(pending|in_progress|resolved|closed)"`
DateFrom *int64 `query:"date_from" valid:"optional"`
DateTo *int64 `query:"date_to" valid:"optional"`
}
// ContactResponse represents the contact data sent in API responses
type ContactResponse struct {
ID string `json:"id"`
FullName string `json:"full_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Message string `json:"message"`
Status string `json:"status"`
AdminNotes *string `json:"admin_notes,omitempty"`
ResolvedAt *int64 `json:"resolved_at,omitempty"`
ResolvedBy *string `json:"resolved_by,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// PublicContactResponse represents the contact data sent in public API responses (limited fields)
type PublicContactResponse struct {
ID string `json:"id"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
}
// ToResponse converts Contact to ContactResponse
func (c *Contact) ToResponse() *ContactResponse {
return &ContactResponse{
ID: c.ID.Hex(),
FullName: c.FullName,
Email: c.Email,
Phone: c.Phone,
Message: c.Message,
Status: string(c.Status),
AdminNotes: c.AdminNotes,
ResolvedAt: c.ResolvedAt,
ResolvedBy: c.ResolvedBy,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
// ToPublicResponse converts Contact to PublicContactResponse (limited fields for public API)
func (c *Contact) ToPublicResponse() *PublicContactResponse {
return &PublicContactResponse{
ID: c.ID.Hex(),
Status: string(c.Status),
CreatedAt: c.CreatedAt,
}
}
// ContactListResponse represents the response for listing contacts
type ContactListResponse struct {
Contacts []*ContactResponse `json:"contacts"`
Meta *response.Meta `json:"-"`
}
+196
View File
@@ -0,0 +1,196 @@
package contact
import (
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for contact operations
type Handler struct {
service Service
}
// NewHandler creates a new contact handler
func NewHandler(service Service) *Handler {
return &Handler{
service: service,
}
}
// Create handles creating a new contact message
// @Summary Create a new contact message
// @Description Create a new contact message from a user
// @Tags contacts
// @Accept json
// @Produce json
// @Param contact body CreateContactForm true "Contact information"
// @Success 201 {object} response.APIResponse{data=ContactResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/contacts [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CreateContactForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Create contact
contact, err := h.service.Create(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to create contact")
}
// Return public response (limited fields)
return response.Created(c, &PublicContactResponse{
ID: contact.ID,
Status: contact.Status,
CreatedAt: contact.CreatedAt,
}, "Contact message created successfully")
}
// GetByID handles retrieving a contact by ID (admin only)
// @Summary Get contact by ID
// @Description Retrieve a contact message by its ID (admin only)
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
// @Success 200 {object} response.APIResponse{data=ContactResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/contacts/{id} [get]
func (h *Handler) GetByID(c echo.Context) error {
id := c.Param("id")
contact, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "contact not found" {
return response.NotFound(c, "Contact not found")
}
return response.InternalServerError(c, "Failed to get contact")
}
return response.Success(c, contact, "Contact retrieved successfully")
}
// Search handles searching contacts with filters (admin only)
// @Summary Search contacts
// @Description Search contacts with filters and pagination (admin only)
// @Tags contacts
// @Accept json
// @Produce json
// @Param q query string false "Search query"
// @Param status query string false "Contact status" Enums(pending, in_progress, resolved, closed)
// @Param date_from query int false "Date from (Unix timestamp)"
// @Param date_to query int false "Date to (Unix timestamp)"
// @Param limit query integer false "Number of contacts per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of contacts to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(full_name, email, created_at, updated_at, status) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=ContactListResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/contacts [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchContactsForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
return response.InternalServerError(c, "Failed to search contacts")
}
return response.SuccessWithMeta(c, result, result.Meta, "Contacts retrieved successfully")
}
// UpdateStatus handles updating contact status (admin only)
// @Summary Update contact status
// @Description Update the status of a contact message (admin only)
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
// @Param status body UpdateContactStatusForm true "Status update information"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/contacts/{id}/status [put]
func (h *Handler) UpdateStatus(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[UpdateContactStatusForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get admin ID from context (assuming it's set by auth middleware)
adminID := c.Get("user_id")
if adminID == nil {
return response.Unauthorized(c, "Admin authentication required")
}
// Update contact status
err = h.service.UpdateStatus(c.Request().Context(), id, form, adminID.(string))
if err != nil {
if err.Error() == "contact not found" {
return response.NotFound(c, "Contact not found")
}
return response.InternalServerError(c, "Failed to update contact status")
}
return response.Success(c, map[string]interface{}{
"message": "Contact status updated successfully",
}, "Contact status updated successfully")
}
// GetStats handles retrieving contact statistics (admin only)
// @Summary Get contact statistics
// @Description Get contact statistics by status (admin only)
// @Tags contacts
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=ContactStats}
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/contacts/stats [get]
func (h *Handler) GetStats(c echo.Context) error {
stats, err := h.service.GetStats(c.Request().Context())
if err != nil {
return response.InternalServerError(c, "Failed to get contact statistics")
}
return response.Success(c, stats, "Contact statistics retrieved successfully")
}
// Delete handles deleting a contact (admin only)
// @Summary Delete contact
// @Description Delete a contact message by ID (admin only)
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/contacts/{id} [delete]
func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "contact not found" {
return response.NotFound(c, "Contact not found")
}
return response.InternalServerError(c, "Failed to delete contact")
}
return response.Success(c, map[string]interface{}{
"message": "Contact deleted successfully",
}, "Contact deleted successfully")
}
+220
View File
@@ -0,0 +1,220 @@
package contact
import (
"context"
"fmt"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// ContactRepository defines the interface for contact data operations
type ContactRepository interface {
// Create creates a new contact message
Create(ctx context.Context, contact *Contact) error
// GetByID retrieves a contact by ID
GetByID(ctx context.Context, id string) (*Contact, error)
// Search searches contacts with filters and pagination
Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error)
// UpdateStatus updates the status of a contact
UpdateStatus(ctx context.Context, id string, status ContactStatus, adminNotes *string, resolvedBy *string) error
// GetStats returns contact statistics
GetStats(ctx context.Context) (*ContactStats, error)
// Delete deletes a contact by ID
Delete(ctx context.Context, id string) error
}
// ContactStats represents contact statistics
type ContactStats struct {
Total int64 `json:"total"`
Pending int64 `json:"pending"`
InProgress int64 `json:"in_progress"`
Resolved int64 `json:"resolved"`
Closed int64 `json:"closed"`
}
// contactRepository implements ContactRepository interface
type contactRepository struct {
repo orm.Repository[Contact]
logger logger.Logger
}
// NewContactRepository creates a new contact repository
func NewContactRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) ContactRepository {
// Create indexes for contact collection
contactIndexes := []orm.Index{
*orm.NewIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
*orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*orm.CreateTextIndex("contact_text_idx", "full_name", "email", "message"),
}
// Create indexes
if err := mongoManager.CreateIndexes("contacts", contactIndexes); err != nil {
logger.Error("Failed to create contact indexes", map[string]interface{}{
"error": err.Error(),
})
}
collection := mongoManager.GetCollection("contacts")
return &contactRepository{
repo: orm.NewRepository[Contact](collection, logger),
logger: logger,
}
}
// Create creates a new contact message
func (r *contactRepository) Create(ctx context.Context, contact *Contact) error {
// Set default status if not provided
if contact.Status == "" {
contact.Status = ContactStatusPending
}
return r.repo.Create(ctx, contact)
}
// GetByID retrieves a contact by ID
func (r *contactRepository) GetByID(ctx context.Context, id string) (*Contact, error) {
return r.repo.FindByID(ctx, id)
}
// Search searches contacts with filters and pagination
func (r *contactRepository) Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error) {
filter := bson.M{}
// Apply text search if provided
if filters.Search != nil && *filters.Search != "" {
filter["$text"] = bson.M{"$search": *filters.Search}
}
// Apply status filter
if filters.Status != nil && *filters.Status != "" {
filter["status"] = *filters.Status
}
// Apply date range filters
if filters.DateFrom != nil || filters.DateTo != nil {
dateFilter := bson.M{}
if filters.DateFrom != nil {
dateFilter["$gte"] = *filters.DateFrom
}
if filters.DateTo != nil {
dateFilter["$lte"] = *filters.DateTo
}
filter["created_at"] = dateFilter
}
return r.repo.FindAll(ctx, filter, pagination)
}
// UpdateStatus updates the status of a contact
func (r *contactRepository) UpdateStatus(ctx context.Context, id string, status ContactStatus, adminNotes *string, resolvedBy *string) error {
// Get the existing contact
contact, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update the contact fields
contact.Status = status
contact.UpdatedAt = time.Now().Unix()
if adminNotes != nil {
contact.AdminNotes = adminNotes
}
// Set resolved fields if status is resolved or closed
if status == ContactStatusResolved || status == ContactStatusClosed {
now := time.Now().Unix()
contact.ResolvedAt = &now
if resolvedBy != nil {
contact.ResolvedBy = resolvedBy
}
} else {
// Unset resolved fields if status is changed back to pending or in_progress
contact.ResolvedAt = nil
contact.ResolvedBy = nil
}
err = r.repo.Update(ctx, contact)
if err != nil {
r.logger.Error("Failed to update contact status", map[string]interface{}{
"id": id,
"status": status,
"error": err.Error(),
})
return fmt.Errorf("failed to update contact status: %w", err)
}
r.logger.Info("Contact status updated successfully", map[string]interface{}{
"id": id,
"status": status,
})
return nil
}
// GetStats returns contact statistics
func (r *contactRepository) GetStats(ctx context.Context) (*ContactStats, error) {
pipeline := mongo.Pipeline{
{
{Key: "$group", Value: bson.D{
{Key: "_id", Value: "$status"},
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
}},
},
}
results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil {
r.logger.Error("Failed to get contact stats", map[string]interface{}{
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get contact stats: %w", err)
}
stats := &ContactStats{}
for _, result := range results {
status, ok := result["_id"].(string)
if !ok {
continue
}
count, ok := result["count"].(int32)
if !ok {
if count64, ok := result["count"].(int64); ok {
count = int32(count64)
} else {
continue
}
}
switch ContactStatus(status) {
case ContactStatusPending:
stats.Pending = int64(count)
case ContactStatusInProgress:
stats.InProgress = int64(count)
case ContactStatusResolved:
stats.Resolved = int64(count)
case ContactStatusClosed:
stats.Closed = int64(count)
}
stats.Total += int64(count)
}
return stats, nil
}
// Delete deletes a contact by ID
func (r *contactRepository) Delete(ctx context.Context, id string) error {
return r.repo.Delete(ctx, id)
}
+249
View File
@@ -0,0 +1,249 @@
package contact
import (
"context"
"fmt"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
)
// Service defines the interface for contact business operations
type Service interface {
// Create creates a new contact message
Create(ctx context.Context, form *CreateContactForm) (*ContactResponse, error)
// GetByID retrieves a contact by ID
GetByID(ctx context.Context, id string) (*ContactResponse, error)
// Search searches contacts with filters and pagination
Search(ctx context.Context, form *SearchContactsForm, pagination *response.Pagination) (*ContactListResponse, error)
// UpdateStatus updates the status of a contact (admin only)
UpdateStatus(ctx context.Context, id string, form *UpdateContactStatusForm, adminID string) error
// GetStats returns contact statistics (admin only)
GetStats(ctx context.Context) (*ContactStats, error)
// Delete deletes a contact by ID (admin only)
Delete(ctx context.Context, id string) error
}
// contactService implements Service interface
type contactService struct {
repo ContactRepository
logger logger.Logger
}
// NewService creates a new contact service
func NewService(repo ContactRepository, logger logger.Logger) Service {
return &contactService{
repo: repo,
logger: logger,
}
}
// Create creates a new contact message
func (s *contactService) Create(ctx context.Context, form *CreateContactForm) (*ContactResponse, error) {
s.logger.Info("Creating new contact message", map[string]interface{}{
"email": form.Email,
"full_name": form.FullName,
})
// Create contact entity
contact := &Contact{
FullName: form.FullName,
Email: form.Email,
Phone: form.Phone,
Message: form.Message,
Status: ContactStatusPending,
}
// Save to database
if err := s.repo.Create(ctx, contact); err != nil {
s.logger.Error("Failed to create contact", map[string]interface{}{
"email": form.Email,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to create contact: %w", err)
}
s.logger.Info("Contact message created successfully", map[string]interface{}{
"id": contact.GetID(),
"email": form.Email,
})
return contact.ToResponse(), nil
}
// GetByID retrieves a contact by ID
func (s *contactService) GetByID(ctx context.Context, id string) (*ContactResponse, error) {
s.logger.Debug("Retrieving contact by ID", map[string]interface{}{
"id": id,
})
contact, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get contact by ID", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get contact: %w", err)
}
return contact.ToResponse(), nil
}
// Search searches contacts with filters and pagination
func (s *contactService) Search(ctx context.Context, form *SearchContactsForm, pagination *response.Pagination) (*ContactListResponse, error) {
s.logger.Debug("Searching contacts", map[string]interface{}{
"filters": form,
"pagination": pagination,
})
// Validate date range if provided
if form.DateFrom != nil && form.DateTo != nil && *form.DateFrom > *form.DateTo {
return nil, fmt.Errorf("invalid date range: date_from must be before date_to")
}
// Convert response.Pagination to orm.Pagination
ormPagination := orm.Pagination{
Limit: pagination.Limit,
Skip: pagination.Offset,
SortField: pagination.SortBy,
SortOrder: 1, // Default ascending
}
if pagination.SortOrder == "desc" {
ormPagination.SortOrder = -1
}
result, err := s.repo.Search(ctx, *form, ormPagination)
if err != nil {
s.logger.Error("Failed to search contacts", map[string]interface{}{
"filters": form,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to search contacts: %w", err)
}
// Convert to response format
contactResponses := make([]*ContactResponse, len(result.Items))
for i, contact := range result.Items {
contactResponses[i] = contact.ToResponse()
}
s.logger.Info("Contact search completed", map[string]interface{}{
"total_count": result.TotalCount,
"items_count": len(result.Items),
})
return &ContactListResponse{
Contacts: contactResponses,
Meta: pagination.Response(result.TotalCount),
}, nil
}
// UpdateStatus updates the status of a contact (admin only)
func (s *contactService) UpdateStatus(ctx context.Context, id string, form *UpdateContactStatusForm, adminID string) error {
s.logger.Info("Updating contact status", map[string]interface{}{
"id": id,
"status": form.Status,
"admin_id": adminID,
})
// Validate status transition
contact, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get contact for status update", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to get contact: %w", err)
}
// Check if contact can be updated
if !contact.CanBeUpdated() {
s.logger.Warn("Attempted to update closed contact", map[string]interface{}{
"id": id,
"current_status": contact.Status,
"new_status": form.Status,
})
return fmt.Errorf("contact is closed and cannot be updated")
}
// Update status
status := ContactStatus(form.Status)
err = s.repo.UpdateStatus(ctx, id, status, form.AdminNotes, &adminID)
if err != nil {
s.logger.Error("Failed to update contact status", map[string]interface{}{
"id": id,
"status": form.Status,
"error": err.Error(),
})
return fmt.Errorf("failed to update contact status: %w", err)
}
s.logger.Info("Contact status updated successfully", map[string]interface{}{
"id": id,
"status": form.Status,
"admin_id": adminID,
})
return nil
}
// GetStats returns contact statistics (admin only)
func (s *contactService) GetStats(ctx context.Context) (*ContactStats, error) {
s.logger.Debug("Getting contact statistics", nil)
stats, err := s.repo.GetStats(ctx)
if err != nil {
s.logger.Error("Failed to get contact stats", map[string]interface{}{
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get contact stats: %w", err)
}
s.logger.Info("Contact statistics retrieved", map[string]interface{}{
"total": stats.Total,
"pending": stats.Pending,
"in_progress": stats.InProgress,
"resolved": stats.Resolved,
"closed": stats.Closed,
})
return stats, nil
}
// Delete deletes a contact by ID (admin only)
func (s *contactService) Delete(ctx context.Context, id string) error {
s.logger.Info("Deleting contact", map[string]interface{}{
"id": id,
})
// Check if contact exists
contact, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get contact for deletion", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to get contact: %w", err)
}
// Delete the contact
if err := s.repo.Delete(ctx, id); err != nil {
s.logger.Error("Failed to delete contact", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to delete contact: %w", err)
}
s.logger.Info("Contact deleted successfully", map[string]interface{}{
"id": id,
"email": contact.Email,
})
return nil
}
+1 -4
View File
@@ -8,21 +8,18 @@ import (
"tm/internal/customer"
"tm/internal/user"
"tm/pkg/logger"
"tm/pkg/response"
)
// NotificationHandler handles HTTP requests for notification operations
type NotificationHandler struct {
service Service
logger logger.Logger
}
// NewNotificationHandler creates a new notification handler
func NewHandler(service Service, logger logger.Logger) *NotificationHandler {
func NewHandler(service Service) *NotificationHandler {
return &NotificationHandler{
service: service,
logger: logger,
}
}