Files
tm_back/internal/inquiry/form.go
T
Mazyar eeafe2a625
continuous-integration/drone/push Build is passing
Add inquiry status transition error handling and validation improvements
- 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.
2026-07-01 22:50:44 +03:30

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
}