eeafe2a625
continuous-integration/drone/push Build is passing
- Introduced InvalidStatusTransitionError to handle invalid status transitions for inquiries, providing clearer error messages. - Updated UpdateInquiryStatusForm to make the Reason field optional and added logic to set a default reason if not provided. - Enhanced form validation tests to cover new status transition error scenarios and validation messages. - Refactored handler methods to utilize new error handling functions for improved response management. This update improves the robustness of inquiry status management by ensuring proper error handling and validation, enhancing user experience during status updates.
281 lines
7.8 KiB
Go
281 lines
7.8 KiB
Go
package inquiry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
"tm/pkg/logger"
|
|
orm "tm/pkg/mongo"
|
|
"tm/pkg/response"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
)
|
|
|
|
// Repository defines methods for inquiry data access
|
|
type Repository interface {
|
|
Create(ctx context.Context, inquiry *Inquiry) error
|
|
GetByID(ctx context.Context, id string) (*Inquiry, error)
|
|
UpdateStatus(ctx context.Context, id string, status, reason, changedBy, description string) error
|
|
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*orm.PaginatedResult[Inquiry], error)
|
|
Delete(ctx context.Context, id string) error
|
|
CheckPendingInquiry(ctx context.Context, email string) (*Inquiry, error)
|
|
CheckPendingInquiryByPhone(ctx context.Context, phone string) (*Inquiry, error)
|
|
}
|
|
|
|
// inquiryRepository implements the Repository interface using the MongoDB ORM
|
|
type inquiryRepository struct {
|
|
ormRepo orm.Repository[Inquiry]
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewInquiryRepository creates a new inquiry repository
|
|
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
|
|
// Create indexes using the ORM's index management
|
|
indexes := []orm.Index{
|
|
*orm.CreateTextIndex("search_idx", "full_name", "company_name", "work_email"),
|
|
*orm.NewIndex("company_name_idx", bson.D{bson.E{Key: "company_name", Value: 1}}),
|
|
*orm.NewIndex("work_email_idx", bson.D{bson.E{Key: "work_email", Value: 1}}),
|
|
*orm.NewIndex("phone_number_idx", bson.D{bson.E{Key: "phone_number", Value: 1}}),
|
|
*orm.NewIndex("status_idx", bson.D{bson.E{Key: "status", Value: 1}}),
|
|
*orm.NewIndex("created_at_idx", bson.D{bson.E{Key: "created_at", Value: -1}}),
|
|
// Compound index for duplicate prevention
|
|
*orm.NewIndex("email_status_idx", bson.D{
|
|
bson.E{Key: "work_email", Value: 1},
|
|
bson.E{Key: "status", Value: 1},
|
|
}),
|
|
*orm.NewIndex("phone_status_idx", bson.D{
|
|
bson.E{Key: "phone_number", Value: 1},
|
|
bson.E{Key: "status", Value: 1},
|
|
}),
|
|
}
|
|
|
|
// Create indexes
|
|
err := mongoManager.CreateIndexes("inquiries", indexes)
|
|
if err != nil {
|
|
logger.Warn("Failed to create inquiry indexes", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
// Create ORM repository
|
|
ormRepo := orm.NewRepository[Inquiry](mongoManager.GetCollection("inquiries"), logger)
|
|
|
|
return &inquiryRepository{
|
|
ormRepo: ormRepo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Create creates a new inquiry
|
|
func (r *inquiryRepository) Create(ctx context.Context, inquiry *Inquiry) error {
|
|
// Set created timestamp using Unix timestamp
|
|
now := time.Now().Unix()
|
|
inquiry.SetCreatedAt(now)
|
|
inquiry.SetUpdatedAt(now)
|
|
|
|
// Set initial status to pending
|
|
inquiry.Status = StatusPending
|
|
|
|
// Add initial status change to history
|
|
inquiry.AddStatusChange(StatusPending, "Inquiry submitted", "system", "Initial inquiry submission")
|
|
|
|
// Use ORM to create inquiry
|
|
err := r.ormRepo.Create(ctx, inquiry)
|
|
if err != nil {
|
|
r.logger.Error("Failed to create inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"work_email": inquiry.WorkEmail,
|
|
"company_name": inquiry.CompanyName,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("Inquiry created successfully", map[string]interface{}{
|
|
"inquiry_id": inquiry.ID,
|
|
"work_email": inquiry.WorkEmail,
|
|
"company_name": inquiry.CompanyName,
|
|
"status": inquiry.Status,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves an inquiry by ID
|
|
func (r *inquiryRepository) GetByID(ctx context.Context, id string) (*Inquiry, error) {
|
|
inquiry, err := r.ormRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return nil, ErrInquiryNotFound
|
|
}
|
|
r.logger.Error("Failed to get inquiry by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return inquiry, nil
|
|
}
|
|
|
|
// UpdateStatus updates the status of an inquiry
|
|
func (r *inquiryRepository) UpdateStatus(ctx context.Context, id string, status, reason, changedBy, description string) error {
|
|
// Get inquiry first
|
|
inquiry, err := r.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add status change to history
|
|
inquiry.AddStatusChange(status, reason, changedBy, description)
|
|
|
|
// Update in database - use the correct Update method signature
|
|
err = r.ormRepo.Update(ctx, inquiry)
|
|
if err != nil {
|
|
r.logger.Error("Failed to update inquiry status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
"new_status": status,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("Inquiry status updated successfully", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
"old_status": inquiry.StatusHistory[len(inquiry.StatusHistory)-2].Status, // Previous status
|
|
"new_status": status,
|
|
"changed_by": changedBy,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckPendingInquiry checks if there's a pending inquiry for the given email
|
|
func (r *inquiryRepository) CheckPendingInquiry(ctx context.Context, email string) (*Inquiry, error) {
|
|
filter := bson.M{
|
|
"work_email": email,
|
|
"status": bson.M{"$in": []string{StatusPending, StatusReviewed}},
|
|
}
|
|
|
|
pagination := orm.NewPaginationBuilder().
|
|
Limit(1).
|
|
SortBy("created_at", "desc").
|
|
Build()
|
|
|
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
|
if err != nil {
|
|
r.logger.Error("Failed to check pending inquiry by email", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"work_email": email,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
if len(result.Items) > 0 {
|
|
return &result.Items[0], nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// CheckPendingInquiryByPhone checks if there's a pending inquiry for the given phone number
|
|
func (r *inquiryRepository) CheckPendingInquiryByPhone(ctx context.Context, phone string) (*Inquiry, error) {
|
|
filter := bson.M{
|
|
"phone_number": phone,
|
|
"status": bson.M{"$in": []string{StatusPending, StatusReviewed}},
|
|
}
|
|
|
|
pagination := orm.NewPaginationBuilder().
|
|
Limit(1).
|
|
SortBy("created_at", "desc").
|
|
Build()
|
|
|
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
|
if err != nil {
|
|
r.logger.Error("Failed to check pending inquiry by phone", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"phone_number": phone,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
if len(result.Items) > 0 {
|
|
return &result.Items[0], nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// Search retrieves inquiries with pagination and filters
|
|
func (r *inquiryRepository) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*orm.PaginatedResult[Inquiry], error) {
|
|
filter := bson.M{}
|
|
|
|
// Add search filter
|
|
if form.Search != nil {
|
|
filter["$or"] = bson.A{
|
|
bson.M{"full_name": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
bson.M{"company_name": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
bson.M{"work_email": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
}
|
|
}
|
|
|
|
// Add status filter
|
|
if form.Status != nil {
|
|
filter["status"] = *form.Status
|
|
}
|
|
|
|
mongoPagination, err := orm.BuildListPagination(
|
|
pagination.Limit,
|
|
pagination.Offset,
|
|
pagination.Cursor,
|
|
pagination.SortBy,
|
|
pagination.SortOrder,
|
|
filter,
|
|
orm.ListPaginationOptions{},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
|
|
if err != nil {
|
|
r.logger.Error("Failed to search inquiries", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Delete deletes an inquiry
|
|
func (r *inquiryRepository) Delete(ctx context.Context, id string) error {
|
|
// Get inquiry first to ensure it exists
|
|
_, err := r.GetByID(ctx, id)
|
|
if err != nil {
|
|
r.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return errors.New("inquiry not found")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Delete in database
|
|
err = r.ormRepo.Delete(ctx, id)
|
|
if err != nil {
|
|
r.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("Inquiry deleted successfully", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|