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.
167 lines
6.0 KiB
Go
167 lines
6.0 KiB
Go
package inquiry
|
|
|
|
import (
|
|
"strings"
|
|
"tm/pkg/response"
|
|
"tm/pkg/security"
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
)
|
|
|
|
// Inquiry DTOs
|
|
|
|
// CreateInquiryForm represents the data required to create a new inquiry
|
|
type CreateInquiryForm struct {
|
|
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"` // Full name of the inquirer
|
|
CompanyName string `json:"company_name" valid:"required,length(2|100)" example:"Opplens"` // Company name
|
|
WorkEmail string `json:"work_email" valid:"required,email" example:"info@opplens.com"` // Work email address
|
|
PhoneNumber string `json:"phone_number" valid:"required,length(10|20)" example:"+1234567890"` // Phone number
|
|
HCaptchaToken string `json:"hcaptcha_token" valid:"required" example:"P0_eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."`
|
|
}
|
|
|
|
// UpdateInquiryStatusForm represents the data required to update inquiry status
|
|
type UpdateInquiryStatusForm struct {
|
|
Status string `json:"status" valid:"required,in(pending|reviewed|approved|rejected)" example:"reviewed"` // New status
|
|
Reason string `json:"reason" valid:"optional,length(1|500)" example:"Initial review completed"` // Reason for status change
|
|
Description string `json:"description" valid:"optional,length(0|1000)" example:"Additional notes about the review"` // Optional description
|
|
}
|
|
|
|
// SearchInquiriesForm represents search and filter parameters for inquiries
|
|
type SearchInquiriesForm struct {
|
|
Search *string `query:"q" valid:"optional"` // Search term for full name, company name, or email
|
|
Status *string `query:"status" valid:"optional,in(pending|reviewed|approved|rejected)"` // Filter by status
|
|
Limit *int `query:"limit" valid:"optional,range(1|100)"` // Limit results
|
|
Offset *int `query:"offset" valid:"optional,range(0|1000000)"` // Offset results
|
|
SortBy *string `query:"sort_by" valid:"optional,in(full_name|company_name|work_email|status|created_at)"` // Sort field
|
|
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` // Sort order
|
|
}
|
|
|
|
// Response DTOs
|
|
|
|
// StatusHistoryResponse represents status history in API responses
|
|
type StatusHistoryResponse struct {
|
|
Status string `json:"status"`
|
|
Reason string `json:"reason"`
|
|
ChangedAt int64 `json:"changed_at"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// InquiryResponse represents the inquiry data in API responses
|
|
type InquiryResponse struct {
|
|
ID string `json:"id"`
|
|
FullName string `json:"full_name"`
|
|
CompanyName string `json:"company_name"`
|
|
WorkEmail string `json:"work_email"`
|
|
PhoneNumber string `json:"phone_number"`
|
|
Status string `json:"status"`
|
|
StatusHistory []StatusHistoryResponse `json:"status_history"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
// InquiryListResponse represents a paginated list of inquiries
|
|
type InquiryListResponse struct {
|
|
Inquiries []*InquiryResponse `json:"inquiries"`
|
|
Meta *response.Meta `json:"meta"`
|
|
}
|
|
|
|
// Helper function to convert Inquiry to InquiryResponse
|
|
func (i *Inquiry) ToResponse() *InquiryResponse {
|
|
// Convert status history
|
|
var statusHistoryResponses []StatusHistoryResponse
|
|
for _, history := range i.StatusHistory {
|
|
statusHistoryResponses = append(statusHistoryResponses, StatusHistoryResponse{
|
|
Status: history.Status,
|
|
Reason: history.Reason,
|
|
ChangedAt: history.ChangedAt,
|
|
Description: history.Description,
|
|
})
|
|
}
|
|
|
|
return &InquiryResponse{
|
|
ID: i.ID.Hex(),
|
|
FullName: i.FullName,
|
|
CompanyName: i.CompanyName,
|
|
WorkEmail: i.WorkEmail,
|
|
PhoneNumber: i.PhoneNumber,
|
|
Status: i.Status,
|
|
StatusHistory: statusHistoryResponses,
|
|
CreatedAt: i.CreatedAt,
|
|
UpdatedAt: i.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// ValidateAndSanitize validates and sanitizes the form
|
|
func (f *CreateInquiryForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
|
|
var err error
|
|
|
|
// Sanitize and validate FullName
|
|
f.FullName, err = xssPolicy.ValidateAndSanitizeInput(f.FullName, "text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize and validate CompanyName
|
|
f.CompanyName, err = xssPolicy.ValidateAndSanitizeInput(f.CompanyName, "text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize and validate WorkEmail (email validation is separate)
|
|
f.WorkEmail, err = xssPolicy.ValidateAndSanitizeInput(f.WorkEmail, "email")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize and validate PhoneNumber
|
|
f.PhoneNumber, err = xssPolicy.ValidateAndSanitizeInput(f.PhoneNumber, "phone")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Standard govalidator validation
|
|
_, err = govalidator.ValidateStruct(f)
|
|
return err
|
|
}
|
|
|
|
// ValidateAndSanitize validates and sanitizes the status update form
|
|
func (f *UpdateInquiryStatusForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
|
|
f.Status = strings.ToLower(strings.TrimSpace(f.Status))
|
|
if strings.TrimSpace(f.Reason) == "" {
|
|
f.Reason = "Status updated to " + f.Status
|
|
}
|
|
|
|
var err error
|
|
|
|
// Sanitize Reason
|
|
f.Reason, err = xssPolicy.ValidateAndSanitizeInput(f.Reason, "text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize Description
|
|
f.Description, err = xssPolicy.ValidateAndSanitizeInput(f.Description, "html")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Standard govalidator validation
|
|
_, err = govalidator.ValidateStruct(f)
|
|
return err
|
|
}
|
|
|
|
// ValidateAndSanitize validates and sanitizes the search form
|
|
func (f *SearchInquiriesForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
|
|
if f.Search != nil {
|
|
sanitized, err := xssPolicy.ValidateAndSanitizeInput(*f.Search, "text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*f.Search = sanitized
|
|
}
|
|
|
|
// Standard govalidator validation
|
|
_, err := govalidator.ValidateStruct(f)
|
|
return err
|
|
}
|