875447ac58
- 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.
221 lines
6.0 KiB
Go
221 lines
6.0 KiB
Go
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)
|
|
}
|