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.
320 lines
9.7 KiB
Go
320 lines
9.7 KiB
Go
package inquiry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/notification"
|
|
"tm/pkg/response"
|
|
"tm/pkg/security"
|
|
)
|
|
|
|
// Service defines business logic for inquiry operations
|
|
type Service interface {
|
|
Create(ctx context.Context, form *CreateInquiryForm) (*InquiryResponse, error)
|
|
GetByID(ctx context.Context, id string) (*InquiryResponse, error)
|
|
UpdateStatus(ctx context.Context, id string, form *UpdateInquiryStatusForm, changedBy string) (*InquiryResponse, error)
|
|
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*InquiryListResponse, error)
|
|
Delete(ctx context.Context, id string) error
|
|
}
|
|
|
|
// inquiryService implements the Service interface
|
|
type inquiryService struct {
|
|
repository Repository
|
|
sdk notification.SDK
|
|
logger logger.Logger
|
|
xssPolicy *security.XSSPolicy
|
|
}
|
|
|
|
// NewService creates a new inquiry service
|
|
func NewService(repository Repository, sdk notification.SDK, logger logger.Logger, xssPolicy *security.XSSPolicy) Service {
|
|
return &inquiryService{
|
|
repository: repository,
|
|
sdk: sdk,
|
|
logger: logger,
|
|
xssPolicy: xssPolicy,
|
|
}
|
|
}
|
|
|
|
// Create creates a new inquiry
|
|
func (s *inquiryService) Create(ctx context.Context, form *CreateInquiryForm) (*InquiryResponse, error) {
|
|
s.logger.Info("Creating new inquiry", map[string]interface{}{
|
|
"work_email": form.WorkEmail,
|
|
"company_name": form.CompanyName,
|
|
"full_name": form.FullName,
|
|
})
|
|
|
|
// Check for duplicate pending inquiries by email
|
|
existingInquiry, err := s.repository.CheckPendingInquiry(ctx, form.WorkEmail)
|
|
if err != nil {
|
|
s.logger.Error("Failed to check for duplicate inquiry by email", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"work_email": form.WorkEmail,
|
|
})
|
|
return nil, errors.New("failed to validate inquiry")
|
|
}
|
|
|
|
if existingInquiry != nil {
|
|
s.logger.Warn("Duplicate inquiry attempt by email", map[string]interface{}{
|
|
"work_email": form.WorkEmail,
|
|
"existing_id": existingInquiry.ID,
|
|
"existing_status": existingInquiry.Status,
|
|
"created_at": existingInquiry.CreatedAt,
|
|
})
|
|
return nil, NewDuplicateInquiryError("email", existingInquiry.ID.Hex(), existingInquiry.Status, existingInquiry.CreatedAt)
|
|
}
|
|
|
|
// Check for duplicate pending inquiries by phone number
|
|
existingInquiryByPhone, err := s.repository.CheckPendingInquiryByPhone(ctx, form.PhoneNumber)
|
|
if err != nil {
|
|
s.logger.Error("Failed to check for duplicate inquiry by phone", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"phone_number": form.PhoneNumber,
|
|
})
|
|
return nil, errors.New("failed to validate inquiry")
|
|
}
|
|
|
|
if existingInquiryByPhone != nil {
|
|
s.logger.Warn("Duplicate inquiry attempt by phone", map[string]interface{}{
|
|
"phone_number": form.PhoneNumber,
|
|
"existing_id": existingInquiryByPhone.ID,
|
|
"existing_status": existingInquiryByPhone.Status,
|
|
"created_at": existingInquiryByPhone.CreatedAt,
|
|
})
|
|
return nil, NewDuplicateInquiryError("phone", existingInquiryByPhone.ID.Hex(), existingInquiryByPhone.Status, existingInquiryByPhone.CreatedAt)
|
|
}
|
|
|
|
// Create inquiry entity
|
|
inquiry := &Inquiry{
|
|
FullName: form.FullName,
|
|
CompanyName: form.CompanyName,
|
|
WorkEmail: form.WorkEmail,
|
|
PhoneNumber: form.PhoneNumber,
|
|
}
|
|
|
|
// Save to database
|
|
err = s.repository.Create(ctx, inquiry)
|
|
if err != nil {
|
|
s.logger.Error("Failed to create inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"work_email": form.WorkEmail,
|
|
"company_name": form.CompanyName,
|
|
})
|
|
return nil, errors.New("failed to create inquiry")
|
|
}
|
|
|
|
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
UserID: inquiry.ID.Hex(),
|
|
Title: fmt.Sprintf("New Inquiry from %s", inquiry.CompanyName),
|
|
Message: GenerateNewInquiryEmailTemplate(&EmailTemplateData{
|
|
FullName: inquiry.FullName,
|
|
CompanyName: inquiry.CompanyName,
|
|
WorkEmail: inquiry.WorkEmail,
|
|
PhoneNumber: inquiry.PhoneNumber,
|
|
InquiryID: inquiry.ID.Hex(),
|
|
CreatedAt: inquiry.CreatedAt,
|
|
CompanyLogo: "",
|
|
CompanyURL: "https://opplens.com",
|
|
SupportEmail: "info@opplens.com",
|
|
}, s.xssPolicy),
|
|
Type: "alert",
|
|
Priority: "important",
|
|
EventType: notification.EventTypeEmail,
|
|
Methods: notification.NotificationMethods{
|
|
Email: "info@opplens.com",
|
|
},
|
|
})
|
|
|
|
// TODO: Should be removed only for testing
|
|
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
UserID: inquiry.ID.Hex(),
|
|
Title: fmt.Sprintf("New Inquiry from %s", inquiry.CompanyName),
|
|
Message: GenerateNewInquiryEmailTemplate(&EmailTemplateData{
|
|
FullName: inquiry.FullName,
|
|
CompanyName: inquiry.CompanyName,
|
|
WorkEmail: inquiry.WorkEmail,
|
|
PhoneNumber: inquiry.PhoneNumber,
|
|
InquiryID: inquiry.ID.Hex(),
|
|
CreatedAt: inquiry.CreatedAt,
|
|
CompanyLogo: "",
|
|
CompanyURL: "https://opplens.com",
|
|
SupportEmail: "info@opplens.com",
|
|
}, s.xssPolicy),
|
|
Type: "alert",
|
|
Priority: "important",
|
|
EventType: notification.EventTypeEmail,
|
|
Methods: notification.NotificationMethods{
|
|
Email: "nakhostin.nima1998@gmail.com",
|
|
},
|
|
})
|
|
|
|
s.logger.Info("Inquiry created successfully", map[string]interface{}{
|
|
"inquiry_id": inquiry.ID,
|
|
"work_email": inquiry.WorkEmail,
|
|
"company_name": inquiry.CompanyName,
|
|
"status": inquiry.Status,
|
|
})
|
|
|
|
return inquiry.ToResponse(), nil
|
|
}
|
|
|
|
// GetByID retrieves an inquiry by ID
|
|
func (s *inquiryService) GetByID(ctx context.Context, id string) (*InquiryResponse, error) {
|
|
s.logger.Info("Getting inquiry by ID", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
})
|
|
|
|
inquiry, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get inquiry by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("Inquiry retrieved successfully", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
"status": inquiry.Status,
|
|
})
|
|
|
|
return inquiry.ToResponse(), nil
|
|
}
|
|
|
|
// UpdateStatus updates the status of an inquiry
|
|
func (s *inquiryService) UpdateStatus(ctx context.Context, id string, form *UpdateInquiryStatusForm, changedBy string) (*InquiryResponse, error) {
|
|
s.logger.Info("Updating inquiry status", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
"new_status": form.Status,
|
|
"changed_by": changedBy,
|
|
})
|
|
|
|
// Validate status transition
|
|
err := s.validateStatusTransition(ctx, id, form.Status)
|
|
if err != nil {
|
|
s.logger.Error("Invalid status transition", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
"new_status": form.Status,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
// Update status in repository
|
|
err = s.repository.UpdateStatus(ctx, id, form.Status, form.Reason, changedBy, form.Description)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update inquiry status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
"new_status": form.Status,
|
|
})
|
|
return nil, errors.New("failed to update inquiry status")
|
|
}
|
|
|
|
// Get updated inquiry
|
|
inquiry, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get updated inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("Inquiry status updated successfully", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
"new_status": form.Status,
|
|
"changed_by": changedBy,
|
|
})
|
|
|
|
return inquiry.ToResponse(), nil
|
|
}
|
|
|
|
// validateStatusTransition validates if the status transition is allowed
|
|
func (s *inquiryService) validateStatusTransition(ctx context.Context, id string, newStatus string) error {
|
|
// Get current inquiry
|
|
inquiry, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
currentStatus := inquiry.GetCurrentStatus()
|
|
|
|
// Define allowed transitions
|
|
allowedTransitions := map[string][]string{
|
|
StatusPending: {StatusReviewed, StatusRejected},
|
|
StatusReviewed: {StatusApproved, StatusRejected},
|
|
StatusApproved: {}, // No transitions from approved
|
|
StatusRejected: {}, // No transitions from rejected
|
|
}
|
|
|
|
allowedNextStatuses, exists := allowedTransitions[currentStatus]
|
|
if !exists {
|
|
return fmt.Errorf("inquiry has unknown status %q and cannot be updated", currentStatus)
|
|
}
|
|
|
|
for _, allowedStatus := range allowedNextStatuses {
|
|
if allowedStatus == newStatus {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return NewInvalidStatusTransitionError(currentStatus, newStatus, allowedNextStatuses)
|
|
}
|
|
|
|
// Search searches inquiries with search and filters
|
|
func (s *inquiryService) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*InquiryListResponse, error) {
|
|
s.logger.Info("Searching inquiries", map[string]interface{}{
|
|
"search": form.Search,
|
|
"status": form.Status,
|
|
"limit": pagination.Limit,
|
|
"offset": pagination.Offset,
|
|
})
|
|
|
|
page, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to search inquiries", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
var inquiryResponses []*InquiryResponse
|
|
for _, inquiry := range page.Items {
|
|
inquiryResponses = append(inquiryResponses, inquiry.ToResponse())
|
|
}
|
|
|
|
s.logger.Info("Inquiries search completed successfully", map[string]interface{}{
|
|
"total_found": page.TotalCount,
|
|
"returned": len(inquiryResponses),
|
|
})
|
|
|
|
return &InquiryListResponse{
|
|
Inquiries: inquiryResponses,
|
|
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
|
|
}, nil
|
|
}
|
|
|
|
// Delete deletes an inquiry
|
|
func (s *inquiryService) Delete(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting inquiry", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
})
|
|
|
|
err := s.repository.Delete(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"inquiry_id": id,
|
|
})
|
|
return err
|
|
}
|
|
|
|
s.logger.Info("Inquiry deleted successfully", map[string]interface{}{
|
|
"inquiry_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|