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:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user