Merge branch 'develop' into TM-563

This commit is contained in:
m.nazemi
2026-05-31 18:29:15 +03:30
13 changed files with 421 additions and 98 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ type CMSForm struct {
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
Language string `json:"language" valid:"optional,length(2|100)" example:"en"`
CompanyName string `json:"company_name" valid:"optional,length(2|100)" example:"Opplens"`
Country string `json:"country" valid:"optional,length(3|3)" example:"SWE"`
Country string `json:"country" valid:"optional,length(2|3)" example:"SWE"`
Type CMSType `json:"type" valid:"optional,in(company|organization)" example:"company"`
Hero heroSectionForm `json:"hero" valid:"optional"`
Chart chartSectionForm `json:"chart" valid:"optional"`
@@ -24,7 +24,7 @@ type heroSectionForm struct {
Description string `json:"description" valid:"optional,length(2|1000)" example:"Hero Description"`
ButtonText string `json:"button_text" valid:"optional,length(2|100)" example:"Button Text"`
ButtonLink string `json:"button_link" valid:"optional,url" example:"https://example.com"`
GifFile string `json:"gif_file" valid:"optional,url" example:"https://example.com/gif.gif"`
GifFile string `json:"gif_file" valid:"optional,mediaRef" example:"674abc123def456789012345"`
}
// chartSectionForm represents the form for creating/updating the chart section
@@ -49,7 +49,7 @@ type cardSectionForm struct {
// cardForm represents the form for creating/updating a card
type cardForm struct {
Icon string `json:"icon" valid:"optional,url" example:"https://example.com/icon.png"`
Icon string `json:"icon" valid:"optional,mediaRef" example:"674abc123def456789012345"`
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
}
@@ -73,7 +73,7 @@ type formFieldForm struct {
// footerSectionForm represents the form for creating/updating the footer section
type footerSectionForm struct {
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
Phone string `json:"phone" valid:"optional,length(10|20)" example:"+1234567890"`
Phone string `json:"phone" valid:"optional,length(0|30)" example:"+1234567890"`
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
+12 -29
View File
@@ -53,21 +53,17 @@ func (h *Handler) GetByKey(c echo.Context) error {
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CMSForm](c)
form, err := ParseForm(c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
return handleParseError(c, err)
}
// Create CMS
cms, err := h.service.Create(c.Request().Context(), form)
if err != nil {
if err == ErrCMSKeyExists {
return response.Conflict(c, "CMS with this key already exists")
}
return response.InternalServerError(c, "Failed to create CMS")
return handleServiceError(c, err, "Failed to create marketing page")
}
return response.Created(c, cms, "CMS entry created successfully")
return response.Created(c, cms, "Marketing page created successfully")
}
// GetByID handles retrieving a CMS entry by ID (admin only)
@@ -86,13 +82,10 @@ func (h *Handler) GetByID(c echo.Context) error {
cms, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to get CMS entry")
return handleServiceError(c, err, "Failed to get marketing page")
}
return response.Success(c, cms, "CMS entry retrieved successfully")
return response.Success(c, cms, "Marketing page retrieved successfully")
}
// Update handles updating a CMS entry (admin only)
@@ -112,24 +105,17 @@ func (h *Handler) GetByID(c echo.Context) error {
func (h *Handler) Update(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[CMSForm](c)
form, err := ParseForm(c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
return handleParseError(c, err)
}
// Update CMS
cms, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err == ErrCMSKeyExists {
return response.Conflict(c, "CMS with this key already exists")
}
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to update CMS entry")
return handleServiceError(c, err, "Failed to update marketing page")
}
return response.Success(c, cms, "CMS entry updated successfully")
return response.Success(c, cms, "Marketing page updated successfully")
}
// Delete handles deleting a CMS entry (admin only)
@@ -148,13 +134,10 @@ func (h *Handler) Delete(c echo.Context) error {
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to delete CMS entry")
return handleServiceError(c, err, "Failed to delete marketing page")
}
return response.Success(c, nil, "CMS entry deleted successfully")
return response.Success(c, nil, "Marketing page deleted successfully")
}
// Search handles searching CMS entries (admin only)
+33
View File
@@ -0,0 +1,33 @@
package cms
import (
"errors"
"strings"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
func handleParseError(c echo.Context, err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "invalid request format") {
return response.BadRequest(c, "Invalid request format", err.Error())
}
return response.ValidationError(c, "Please fix the validation errors", FormatValidationErrors(err))
}
func handleServiceError(c echo.Context, err error, fallback string) error {
if err == nil {
return nil
}
if errors.Is(err, ErrCMSKeyExists) {
return response.Conflict(c, "A marketing page with this key already exists")
}
if errors.Is(err, ErrCMSNotFound) {
return response.NotFound(c, "Marketing page not found")
}
return response.InternalServerError(c, fallback)
}
+13
View File
@@ -2,6 +2,7 @@ package cms
import (
"context"
"errors"
"fmt"
"tm/pkg/logger"
"tm/pkg/response"
@@ -90,6 +91,9 @@ func (s *cmsService) GetByID(ctx context.Context, id string) (*CMSResponse, erro
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, ErrCMSNotFound) {
return nil, ErrCMSNotFound
}
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
"id": id,
"error": err.Error(),
@@ -108,6 +112,9 @@ func (s *cmsService) GetByKey(ctx context.Context, key string) (*CMSResponse, er
cms, err := s.repo.GetByKey(ctx, key)
if err != nil {
if errors.Is(err, ErrCMSNotFound) {
return nil, ErrCMSNotFound
}
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
"key": key,
"error": err.Error(),
@@ -128,6 +135,9 @@ func (s *cmsService) Update(ctx context.Context, id string, form *CMSForm) (*CMS
// Get existing CMS
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, ErrCMSNotFound) {
return nil, ErrCMSNotFound
}
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
"id": id,
"error": err.Error(),
@@ -175,6 +185,9 @@ func (s *cmsService) Delete(ctx context.Context, id string) error {
})
if err := s.repo.Delete(ctx, id); err != nil {
if errors.Is(err, ErrCMSNotFound) {
return ErrCMSNotFound
}
s.logger.Error("Failed to delete CMS", map[string]interface{}{
"id": id,
"error": err.Error(),
+136
View File
@@ -0,0 +1,136 @@
package cms
import (
"fmt"
"regexp"
"strings"
"github.com/asaskevich/govalidator"
"github.com/labstack/echo/v4"
)
var (
objectIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{24}$`)
filePathPattern = regexp.MustCompile(`^/[^/]*/v\d+/files/[a-fA-F0-9]{24}(/download)?$`)
)
// ValidationService registers CMS-specific govalidator rules.
type ValidationService struct{}
// NewValidationService registers custom validators used by CMS forms.
func NewValidationService() ValidationService {
govalidator.CustomTypeTagMap.Set("mediaRef", govalidator.CustomTypeValidator(func(i interface{}, _ interface{}) bool {
value, ok := i.(string)
if !ok {
return false
}
return isMediaReference(value)
}))
return ValidationService{}
}
func isMediaReference(value string) bool {
if value == "" {
return true
}
if govalidator.IsURL(value) {
return true
}
if objectIDPattern.MatchString(value) {
return true
}
return filePathPattern.MatchString(value)
}
// ValidateCMSForm validates a CMS create/update payload.
func ValidateCMSForm(form *CMSForm) error {
_, err := govalidator.ValidateStruct(form)
return err
}
// ParseForm binds and validates a CMS form from the request.
func ParseForm(c echo.Context) (*CMSForm, error) {
var form CMSForm
if err := c.Bind(&form); err != nil {
return nil, fmt.Errorf("invalid request format: %w", err)
}
if err := ValidateCMSForm(&form); err != nil {
return nil, err
}
return &form, nil
}
// FormatValidationErrors converts govalidator errors into user-facing messages.
func FormatValidationErrors(err error) string {
if err == nil {
return ""
}
errs, ok := err.(govalidator.Errors)
if !ok {
return humanizeValidationError(err.Error())
}
messages := make([]string, 0, len(errs))
for _, item := range errs {
messages = append(messages, humanizeValidationError(item.Error()))
}
return strings.Join(messages, "; ")
}
func humanizeValidationError(raw string) string {
field, rule, ok := parseGovalidatorError(raw)
if !ok {
return raw
}
label := fieldLabel(field)
lowerRule := strings.ToLower(rule)
switch {
case strings.Contains(lowerRule, "mediaref"):
return label + " must be a valid URL or uploaded file"
case strings.Contains(lowerRule, "url"):
return label + " must be a valid URL"
case strings.Contains(lowerRule, "email"):
return label + " must be a valid email address"
case strings.Contains(lowerRule, "length"):
return label + " has an invalid length"
case strings.Contains(lowerRule, "range"):
return label + " is out of the allowed range"
case strings.Contains(lowerRule, "in("):
return label + " has an invalid value"
default:
return label + " is invalid"
}
}
func parseGovalidatorError(raw string) (field, rule string, ok bool) {
parts := strings.SplitN(raw, ": ", 2)
if len(parts) != 2 {
return "", "", false
}
return parts[0], parts[1], true
}
func fieldLabel(field string) string {
field = strings.TrimPrefix(field, "CMSForm.")
replacer := strings.NewReplacer(
"Hero.", "Hero section: ",
"Chart.", "Chart section: ",
"Features.", "Features section: ",
"Challenges.", "Challenges section: ",
"Advantages.", "Advantages section: ",
"Contact.", "Contact section: ",
"Footer.", "Footer section: ",
".", " ",
)
label := replacer.Replace(field)
label = strings.ReplaceAll(label, "Cards 0", "card 1")
label = strings.ReplaceAll(label, "Cards 1", "card 2")
label = strings.ReplaceAll(label, "Cards 2", "card 3")
label = strings.ReplaceAll(label, "Cards 3", "card 4")
label = strings.ReplaceAll(label, "Cards 4", "card 5")
return label
}
+2
View File
@@ -54,6 +54,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
customer, err := h.service.Register(c.Request().Context(), form)
if err != nil {
if err.Error() == "customer with this email already exists" ||
err.Error() == "customer with this username already exists" ||
err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
err.Error() == "company with this tax ID already exists" {
@@ -126,6 +127,7 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
return response.NotFound(c, "Customer not found")
}
if err.Error() == "customer with this email already exists" ||
err.Error() == "customer with this username already exists" ||
err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
err.Error() == "company with this tax ID already exists" {
+6
View File
@@ -89,6 +89,12 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
return nil, errors.New("customer with this email already exists")
}
// Check if username already exists
existingCustomer, _ = s.repository.GetByUsername(ctx, strings.ToLower(form.Username))
if existingCustomer != nil {
return nil, errors.New("customer with this username already exists")
}
// Check validator
if !s.validator.IsValidUsername(form.Username) {
return nil, errors.New("invalid username format")
+9
View File
@@ -87,3 +87,12 @@ type StatsResponse struct {
TotalFeedback int64 `json:"total_feedback"`
LastUpdated int64 `json:"last_updated"`
}
// CustomerStatsResponse represents feedback statistics for a single customer.
type CustomerStatsResponse struct {
CustomerID string `json:"customer_id"`
TotalLikes int64 `json:"total_likes"`
TotalDislikes int64 `json:"total_dislikes"`
TotalFeedback int64 `json:"total_feedback"`
LastUpdated int64 `json:"last_updated"`
}
+39 -23
View File
@@ -195,7 +195,7 @@ func (h *Handler) Toggle(c echo.Context) error {
// ListFeedback retrieves feedback with pagination and filtering
// @Summary List feedback with filters
// @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company.
// @Description Retrieve paginated list of feedback for the authenticated customer with optional filters.
// @Tags Feedback
// @Accept json
// @Produce json
@@ -214,16 +214,16 @@ func (h *Handler) Toggle(c echo.Context) error {
// @Security BearerAuth
// @Router /api/v1/feedback [get]
func (h *Handler) PublicList(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c)
customerID, err := customer.GetCustomerIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
}
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
form.Company = &companyID
form.Customer = &customerID
pagination, err := response.NewPagination(c)
if err != nil {
@@ -287,12 +287,12 @@ func (h *Handler) PublicGetByTenderID(c echo.Context) error {
return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing")
}
companyID, err := user.GetCompanyIDFromContext(c)
customerID, err := customer.GetCustomerIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
}
feedback, err := h.service.GetByTenderID(c.Request().Context(), tenderID, companyID)
feedback, err := h.service.GetByTenderIDForCustomer(c.Request().Context(), tenderID, customerID)
if err != nil {
return response.BadRequest(c, "Failed to get feedback", "")
}
@@ -300,28 +300,44 @@ func (h *Handler) PublicGetByTenderID(c echo.Context) error {
return response.Success(c, feedback, "Feedback retrieved successfully")
}
// GetCompanyFeedbackStats retrieves feedback statistics for the authenticated user's company
// @Summary Get company feedback statistics
// @Description Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations.
// GetCustomerStats retrieves feedback statistics for the authenticated customer.
// @Summary Get customer feedback statistics
// @Description Retrieve feedback statistics (likes, dislikes, totals) for the authenticated customer only.
// @Tags Feedback
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=StatsResponse} "Company feedback statistics retrieved successfully"
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing company association"
// @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully"
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association"
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth
// @Router /api/v1/feedback/stats/customer [get]
func (h *Handler) GetCustomerStats(c echo.Context) error {
customerID, err := customer.GetCustomerIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
}
stats, err := h.service.CustomerStats(c.Request().Context(), customerID)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve customer stats")
}
return response.Success(c, stats, "Customer stats retrieved successfully")
}
// GetCompanyStats returns customer-scoped stats for backward compatibility with clients using /stats/company.
// @Summary Get feedback statistics (customer-scoped, legacy path)
// @Description Same as GET /feedback/stats/customer. Prefer /stats/customer for new integrations.
// @Tags Feedback
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully"
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association"
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth
// @Router /api/v1/feedback/stats/company [get]
func (h *Handler) GetCompanyStats(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
}
stats, err := h.service.CompanyStats(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve company stats")
}
return response.Success(c, stats, "Company stats retrieved successfully")
return h.GetCustomerStats(c)
}
+115 -35
View File
@@ -19,9 +19,12 @@ type Repository interface {
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Feedback], error)
GetByID(ctx context.Context, id string) (*Feedback, error)
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
GetByCustomerID(ctx context.Context, customerID string, tenderID string) (*Feedback, error)
GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error)
GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*Feedback, error)
Delete(ctx context.Context, id string) error
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error)
}
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM
@@ -42,6 +45,11 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
{Key: "tender_id", Value: 1},
{Key: "company_id", Value: 1},
}),
*orm.NewIndex("customer_id_idx", bson.D{{Key: "customer_id", Value: 1}}),
*orm.NewIndex("tender_customer_idx", bson.D{
{Key: "tender_id", Value: 1},
{Key: "customer_id", Value: 1},
}),
*orm.NewIndex("tender_type_idx", bson.D{
{Key: "tender_id", Value: 1},
{Key: "feedback_type", Value: 1},
@@ -140,6 +148,23 @@ func (r *feedbackRepository) GetByCompanyID(ctx context.Context, companyID strin
return result, nil
}
// GetByCustomerID retrieves feedback by customer ID and tender ID.
func (r *feedbackRepository) GetByCustomerID(ctx context.Context, customerID string, tenderID string) (*Feedback, error) {
filter := bson.M{"customer_id": customerID, "tender_id": tenderID}
result, err := r.repo.FindOne(ctx, filter)
if err != nil {
r.logger.Error("Failed to get feedback by customer ID and tender ID", map[string]interface{}{
"customer_id": customerID,
"tender_id": tenderID,
"error": err.Error(),
})
return nil, err
}
return result, nil
}
// GetByTenderID retrieves feedback by tender ID and company ID
func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error) {
filter := bson.M{"tender_id": tenderID, "company_id": companyID}
@@ -157,6 +182,23 @@ func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, compan
return result, nil
}
// GetByTenderIDForCustomer retrieves feedback by tender ID and customer ID.
func (r *feedbackRepository) GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*Feedback, error) {
filter := bson.M{"tender_id": tenderID, "customer_id": customerID}
result, err := r.repo.FindOne(ctx, filter)
if err != nil {
r.logger.Error("Failed to get feedback by tender ID and customer ID", map[string]interface{}{
"tender_id": tenderID,
"customer_id": customerID,
"error": err.Error(),
})
return nil, err
}
return result, nil
}
// Delete deletes a feedback by ID
func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
err := r.repo.Delete(ctx, id)
@@ -199,20 +241,59 @@ func (r *feedbackRepository) Search(ctx context.Context, criteria SearchForm, pa
return result, nil
}
// Stats calculates feedback statistics for a company
// CompanyStats calculates feedback statistics for a company.
func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) {
counts, lastUpdated, err := r.aggregateFeedbackCounts(ctx, bson.M{"company_id": companyID})
if err != nil {
r.logger.Error("Failed to get feedback stats", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
return nil, err
}
return &StatsResponse{
CompanyID: companyID,
TotalFeedback: counts.totalFeedback,
TotalLikes: counts.totalLikes,
TotalDislikes: counts.totalDislikes,
LastUpdated: lastUpdated,
}, nil
}
// CustomerStats calculates feedback statistics for a customer.
func (r *feedbackRepository) CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error) {
counts, lastUpdated, err := r.aggregateFeedbackCounts(ctx, bson.M{"customer_id": customerID})
if err != nil {
r.logger.Error("Failed to get customer feedback stats", map[string]interface{}{
"customer_id": customerID,
"error": err.Error(),
})
return nil, err
}
return &CustomerStatsResponse{
CustomerID: customerID,
TotalFeedback: counts.totalFeedback,
TotalLikes: counts.totalLikes,
TotalDislikes: counts.totalDislikes,
LastUpdated: lastUpdated,
}, nil
}
type feedbackCountAggregate struct {
totalFeedback int64
totalLikes int64
totalDislikes int64
}
func (r *feedbackRepository) aggregateFeedbackCounts(ctx context.Context, match bson.M) (feedbackCountAggregate, int64, error) {
pipeline := mongo.Pipeline{
{
{Key: "$match", Value: bson.M{
"company_id": companyID,
}},
},
{{Key: "$match", Value: match}},
{
{Key: "$group", Value: bson.M{
"_id": nil,
"total_feedback": bson.M{
"$sum": 1,
},
"total_feedback": bson.M{"$sum": 1},
"total_likes": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
@@ -238,38 +319,37 @@ func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string)
results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil {
r.logger.Error("Failed to get feedback stats", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
return nil, err
return feedbackCountAggregate{}, time.Now().Unix(), err
}
stats := &StatsResponse{
CompanyID: companyID,
TotalFeedback: 0,
TotalLikes: 0,
TotalDislikes: 0,
LastUpdated: time.Now().Unix(),
counts := feedbackCountAggregate{}
lastUpdated := time.Now().Unix()
if len(results) == 0 {
return counts, lastUpdated, nil
}
if len(results) > 0 {
result := results[0]
if totalFeedback, ok := result["total_feedback"].(int32); ok {
stats.TotalFeedback = int64(totalFeedback)
}
if totalLikes, ok := result["total_likes"].(int32); ok {
stats.TotalLikes = int64(totalLikes)
}
if totalDislikes, ok := result["total_dislikes"].(int32); ok {
stats.TotalDislikes = int64(totalDislikes)
}
if lastUpdated, ok := result["last_updated"].(int64); ok {
stats.LastUpdated = lastUpdated
}
result := results[0]
counts.totalFeedback = aggregateInt64(result["total_feedback"])
counts.totalLikes = aggregateInt64(result["total_likes"])
counts.totalDislikes = aggregateInt64(result["total_dislikes"])
if v, ok := result["last_updated"].(int64); ok && v > 0 {
lastUpdated = v
}
return stats, nil
return counts, lastUpdated, nil
}
func aggregateInt64(value interface{}) int64 {
switch v := value.(type) {
case int32:
return int64(v)
case int64:
return v
case float64:
return int64(v)
default:
return 0
}
}
// buildSearchFilter builds MongoDB filter from search criteria
+50 -7
View File
@@ -14,8 +14,10 @@ type Service interface {
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error)
Get(ctx context.Context, id string) (*FeedbackResponse, error)
GetByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error)
GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*FeedbackResponse, error)
Delete(ctx context.Context, id string) error
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error)
getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse)
}
@@ -38,7 +40,7 @@ func NewService(feedbackRepo Repository, tenderService tender.Service, companySe
}
func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) {
existing, err := s.feedbackRepo.GetByCompanyID(ctx, *companyID, req.Tender)
existing, err := s.feedbackRepo.GetByCustomerID(ctx, *customerID, req.Tender)
if err != nil {
feedback := &Feedback{
@@ -51,9 +53,10 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
err = s.feedbackRepo.Create(ctx, feedback)
if err != nil {
s.logger.Error("Failed to create new feedback", map[string]interface{}{
"tender_id": req.Tender,
"company_id": *companyID,
"error": err.Error(),
"tender_id": req.Tender,
"customer_id": *customerID,
"company_id": *companyID,
"error": err.Error(),
})
return nil, err
}
@@ -66,10 +69,10 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
if existing.FeedbackType == req.Type {
err = s.feedbackRepo.Delete(ctx, existing.GetID())
if err != nil {
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
s.logger.Error("Failed to delete existing feedback", map[string]interface{}{
"feedback_id": existing.GetID(),
"tender_id": req.Tender,
"company_id": *companyID,
"customer_id": *customerID,
"error": err.Error(),
})
return nil, err
@@ -97,7 +100,7 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
"feedback_id": existing.GetID(),
"tender_id": req.Tender,
"company_id": *companyID,
"customer_id": *customerID,
"error": err.Error(),
})
return nil, err
@@ -177,6 +180,22 @@ func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID
return feedback.ToResponse(tender, company), nil
}
func (s *feedbackService) GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*FeedbackResponse, error) {
feedback, err := s.feedbackRepo.GetByTenderIDForCustomer(ctx, tenderID, customerID)
if err != nil {
s.logger.Error("Failed to get feedback by tender ID for customer", map[string]interface{}{
"tender_id": tenderID,
"customer_id": customerID,
"error": err.Error(),
})
return nil, err
}
tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(*feedback))
return feedback.ToResponse(tender, company), nil
}
func feedbackCompanyID(f Feedback) string {
if f.CompanyID == nil {
return ""
@@ -346,3 +365,27 @@ func (s *feedbackService) CompanyStats(ctx context.Context, companyID string) (*
return stats, nil
}
func (s *feedbackService) CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error) {
s.logger.Info("Getting customer feedback statistics", map[string]interface{}{
"customer_id": customerID,
})
stats, err := s.feedbackRepo.CustomerStats(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer stats", map[string]interface{}{
"customer_id": customerID,
"error": err.Error(),
})
return nil, err
}
s.logger.Info("Customer stats retrieved successfully", map[string]interface{}{
"customer_id": customerID,
"total_feedback": stats.TotalFeedback,
"total_likes": stats.TotalLikes,
"total_dislikes": stats.TotalDislikes,
})
return stats, nil
}