Refactor Feedback Module for Consistency and Clarity

- Updated feedback repository and service methods to use a consistent naming convention, changing `NewTenderRepository` and similar methods to `NewRepository`.
- Refactored feedback handler methods to improve clarity by renaming methods such as `ListFeedback` to `Search` and `GetFeedback` to `Get`.
- Enhanced feedback response structures to include detailed company and tender information, improving the clarity of feedback data returned to API consumers.
- Updated Swagger documentation to reflect changes in feedback response structures and endpoint paths, ensuring accurate representation of the API for consumers.
This commit is contained in:
n.nakhostin
2025-09-24 12:20:19 +03:30
parent cdf15c9d49
commit a342da193e
29 changed files with 666 additions and 702 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ func InitMongoDB(conf config.MongoConfig, log logger.Logger) *mongo.ConnectionMa
// init TED scraper // init TED scraper
func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLogger logger.Logger) { func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLogger logger.Logger) {
// Initialize tender repository // Initialize tender repository
tenderRepo := tender.NewTenderRepository(mongoManager, appLogger) tenderRepo := tender.NewRepository(mongoManager, appLogger)
// Initialize TED scraper // Initialize TED scraper
tedScraper := ted.NewTEDScraper( tedScraper := ted.NewTEDScraper(
+102 -22
View File
@@ -5026,7 +5026,7 @@ const docTemplate = `{
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"$ref": "#/definitions/feedback.CompanyFeedbackStatsResponse" "$ref": "#/definitions/feedback.StatsResponse"
} }
} }
} }
@@ -7675,27 +7675,6 @@ const docTemplate = `{
} }
} }
}, },
"feedback.CompanyFeedbackStatsResponse": {
"type": "object",
"properties": {
"company_id": {
"type": "string"
},
"last_updated": {
"description": "Unix timestamp",
"type": "integer"
},
"total_dislikes": {
"type": "integer"
},
"total_feedback": {
"type": "integer"
},
"total_likes": {
"type": "integer"
}
}
},
"feedback.FeedbackListResponse": { "feedback.FeedbackListResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -7713,6 +7692,9 @@ const docTemplate = `{
"feedback.FeedbackResponse": { "feedback.FeedbackResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"company": {
"$ref": "#/definitions/feedback.companyResponse"
},
"company_id": { "company_id": {
"type": "string" "type": "string"
}, },
@@ -7729,6 +7711,9 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"tender": { "tender": {
"$ref": "#/definitions/feedback.tenderResponse"
},
"tender_id": {
"type": "string" "type": "string"
}, },
"updated_at": { "updated_at": {
@@ -7747,6 +7732,29 @@ const docTemplate = `{
"FeedbackTypeDislike" "FeedbackTypeDislike"
] ]
}, },
"feedback.StatsResponse": {
"type": "object",
"properties": {
"company": {
"$ref": "#/definitions/feedback.companyResponse"
},
"company_id": {
"type": "string"
},
"last_updated": {
"type": "integer"
},
"total_dislikes": {
"type": "integer"
},
"total_feedback": {
"type": "integer"
},
"total_likes": {
"type": "integer"
}
}
},
"feedback.ToggleFeedbackForm": { "feedback.ToggleFeedbackForm": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -7770,6 +7778,78 @@ const docTemplate = `{
} }
} }
}, },
"feedback.companyResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"feedback.organizationResponse": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"feedback.tenderResponse": {
"type": "object",
"properties": {
"buyer_organization": {
"$ref": "#/definitions/feedback.organizationResponse"
},
"country_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"description": {
"type": "string"
},
"duration": {
"type": "string"
},
"duration_unit": {
"type": "string"
},
"estimated_value": {
"type": "number"
},
"id": {
"type": "string"
},
"notice_publication_id": {
"type": "string"
},
"procedure_code": {
"type": "string"
},
"procurement_type_code": {
"type": "string"
},
"publication_date": {
"type": "integer"
},
"status": {
"type": "string"
},
"submission_deadline": {
"type": "integer"
},
"tender_deadline": {
"type": "integer"
},
"title": {
"type": "string"
}
}
},
"inquiry.CreateInquiryForm": { "inquiry.CreateInquiryForm": {
"type": "object", "type": "object",
"properties": { "properties": {
+102 -22
View File
@@ -5020,7 +5020,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"$ref": "#/definitions/feedback.CompanyFeedbackStatsResponse" "$ref": "#/definitions/feedback.StatsResponse"
} }
} }
} }
@@ -7669,27 +7669,6 @@
} }
} }
}, },
"feedback.CompanyFeedbackStatsResponse": {
"type": "object",
"properties": {
"company_id": {
"type": "string"
},
"last_updated": {
"description": "Unix timestamp",
"type": "integer"
},
"total_dislikes": {
"type": "integer"
},
"total_feedback": {
"type": "integer"
},
"total_likes": {
"type": "integer"
}
}
},
"feedback.FeedbackListResponse": { "feedback.FeedbackListResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -7707,6 +7686,9 @@
"feedback.FeedbackResponse": { "feedback.FeedbackResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"company": {
"$ref": "#/definitions/feedback.companyResponse"
},
"company_id": { "company_id": {
"type": "string" "type": "string"
}, },
@@ -7723,6 +7705,9 @@
"type": "string" "type": "string"
}, },
"tender": { "tender": {
"$ref": "#/definitions/feedback.tenderResponse"
},
"tender_id": {
"type": "string" "type": "string"
}, },
"updated_at": { "updated_at": {
@@ -7741,6 +7726,29 @@
"FeedbackTypeDislike" "FeedbackTypeDislike"
] ]
}, },
"feedback.StatsResponse": {
"type": "object",
"properties": {
"company": {
"$ref": "#/definitions/feedback.companyResponse"
},
"company_id": {
"type": "string"
},
"last_updated": {
"type": "integer"
},
"total_dislikes": {
"type": "integer"
},
"total_feedback": {
"type": "integer"
},
"total_likes": {
"type": "integer"
}
}
},
"feedback.ToggleFeedbackForm": { "feedback.ToggleFeedbackForm": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -7764,6 +7772,78 @@
} }
} }
}, },
"feedback.companyResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"feedback.organizationResponse": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"feedback.tenderResponse": {
"type": "object",
"properties": {
"buyer_organization": {
"$ref": "#/definitions/feedback.organizationResponse"
},
"country_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"description": {
"type": "string"
},
"duration": {
"type": "string"
},
"duration_unit": {
"type": "string"
},
"estimated_value": {
"type": "number"
},
"id": {
"type": "string"
},
"notice_publication_id": {
"type": "string"
},
"procedure_code": {
"type": "string"
},
"procurement_type_code": {
"type": "string"
},
"publication_date": {
"type": "integer"
},
"status": {
"type": "string"
},
"submission_deadline": {
"type": "integer"
},
"tender_deadline": {
"type": "integer"
},
"title": {
"type": "string"
}
}
},
"inquiry.CreateInquiryForm": { "inquiry.CreateInquiryForm": {
"type": "object", "type": "object",
"properties": { "properties": {
+67 -15
View File
@@ -512,20 +512,6 @@ definitions:
example: reset_token_here example: reset_token_here
type: string type: string
type: object type: object
feedback.CompanyFeedbackStatsResponse:
properties:
company_id:
type: string
last_updated:
description: Unix timestamp
type: integer
total_dislikes:
type: integer
total_feedback:
type: integer
total_likes:
type: integer
type: object
feedback.FeedbackListResponse: feedback.FeedbackListResponse:
properties: properties:
feedback: feedback:
@@ -537,6 +523,8 @@ definitions:
type: object type: object
feedback.FeedbackResponse: feedback.FeedbackResponse:
properties: properties:
company:
$ref: '#/definitions/feedback.companyResponse'
company_id: company_id:
type: string type: string
created_at: created_at:
@@ -548,6 +536,8 @@ definitions:
id: id:
type: string type: string
tender: tender:
$ref: '#/definitions/feedback.tenderResponse'
tender_id:
type: string type: string
updated_at: updated_at:
type: integer type: integer
@@ -560,6 +550,21 @@ definitions:
x-enum-varnames: x-enum-varnames:
- FeedbackTypeLike - FeedbackTypeLike
- FeedbackTypeDislike - FeedbackTypeDislike
feedback.StatsResponse:
properties:
company:
$ref: '#/definitions/feedback.companyResponse'
company_id:
type: string
last_updated:
type: integer
total_dislikes:
type: integer
total_feedback:
type: integer
total_likes:
type: integer
type: object
feedback.ToggleFeedbackForm: feedback.ToggleFeedbackForm:
properties: properties:
feedback_type: feedback_type:
@@ -574,6 +579,53 @@ definitions:
- feedback_type - feedback_type
- tender_id - tender_id
type: object type: object
feedback.companyResponse:
properties:
id:
type: string
name:
type: string
type: object
feedback.organizationResponse:
properties:
name:
type: string
type: object
feedback.tenderResponse:
properties:
buyer_organization:
$ref: '#/definitions/feedback.organizationResponse'
country_code:
type: string
currency:
type: string
description:
type: string
duration:
type: string
duration_unit:
type: string
estimated_value:
type: number
id:
type: string
notice_publication_id:
type: string
procedure_code:
type: string
procurement_type_code:
type: string
publication_date:
type: integer
status:
type: string
submission_deadline:
type: integer
tender_deadline:
type: integer
title:
type: string
type: object
inquiry.CreateInquiryForm: inquiry.CreateInquiryForm:
properties: properties:
company_name: company_name:
@@ -4508,7 +4560,7 @@ paths:
- $ref: '#/definitions/response.APIResponse' - $ref: '#/definitions/response.APIResponse'
- properties: - properties:
data: data:
$ref: '#/definitions/feedback.CompanyFeedbackStatsResponse' $ref: '#/definitions/feedback.StatsResponse'
type: object type: object
"400": "400":
description: Bad request - Missing company association description: Bad request - Missing company association
+21 -21
View File
@@ -134,14 +134,14 @@ func main() {
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger) customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
// Initialize repositories with MongoDB connection manager // Initialize repositories with MongoDB connection manager
userRepository := user.NewUserRepository(mongoManager, logger) userRepository := user.NewRepository(mongoManager, logger)
customerRepository := customer.NewCustomerRepository(mongoManager, logger) customerRepository := customer.NewRepository(mongoManager, logger)
companyRepository := company.NewCompanyRepository(mongoManager, logger) companyRepository := company.NewRepository(mongoManager, logger)
categoryRepository := company_category.NewCategoryRepository(mongoManager, logger) categoryRepository := company_category.NewRepository(mongoManager, logger)
tenderRepository := tender.NewTenderRepository(mongoManager, logger) tenderRepository := tender.NewRepository(mongoManager, logger)
feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger) feedbackRepo := feedback.NewRepository(mongoManager, logger)
tenderApprovalRepository := tender_approval.NewTenderApprovalRepository(mongoManager, logger) tenderApprovalRepository := tender_approval.NewRepository(mongoManager, logger)
inquiryRepository := inquiry.NewInquiryRepository(mongoManager, logger) inquiryRepository := inquiry.NewRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{ logger.Info("Repositories initialized successfully", map[string]interface{}{
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry"}, "repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry"},
}) })
@@ -150,27 +150,27 @@ func main() {
userValidator := user.NewValidationService() userValidator := user.NewValidationService()
// Initialize services with repositories // Initialize services with repositories
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator, notificationSDK) userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK)
categoryService := company_category.NewCategoryService(categoryRepository, logger) categoryService := company_category.NewService(categoryRepository, logger)
companyService := company.NewCompanyService(companyRepository, categoryService, logger) companyService := company.NewService(companyRepository, categoryService, logger)
customerService := customer.New(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK) customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK)
tenderService := tender.NewTenderService(tenderRepository, companyService, logger) tenderService := tender.NewService(tenderRepository, companyService, logger)
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger) feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger) tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
inquiryService := inquiry.NewInquiryService(inquiryRepository, notificationSDK, logger) inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
flagService := assets.NewFlagService(logger, conf.Assets.FlagsPath) flagService := assets.NewService(logger, conf.Assets.FlagsPath)
notificationService := notification.NewNotificationService(notificationSDK, userService, customerService, logger) notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
logger.Info("Services initialized successfully", map[string]interface{}{ logger.Info("Services initialized successfully", map[string]interface{}{
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"}, "services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"},
}) })
// Initialize handlers with services // Initialize handlers with services
userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService) userHandler := user.NewHandler(userService, logger, userValidator, userAuthService)
customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger) customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger)
companyHandler := company.NewHandler(companyService, userHandler, logger) companyHandler := company.NewHandler(companyService, userHandler, logger)
categoryHandler := company_category.NewHandler(categoryService, logger) categoryHandler := company_category.NewHandler(categoryService, logger)
tenderHandler := tender.NewTenderHandler(tenderService, logger) tenderHandler := tender.NewHandler(tenderService, logger)
feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger) feedbackHandler := feedback.NewHandler(feedbackService, userHandler, customerHandler, logger)
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger) tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, logger) inquiryHandler := inquiry.NewHandler(inquiryService, logger)
flagHandler := assets.NewHandler(flagService, logger) flagHandler := assets.NewHandler(flagService, logger)
+8 -8
View File
@@ -99,9 +99,9 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
feedbackGP := adminV1.Group("/feedback") feedbackGP := adminV1.Group("/feedback")
{ {
feedbackGP.Use(userHandler.AuthMiddleware()) feedbackGP.Use(userHandler.AuthMiddleware())
feedbackGP.GET("", feedbackHandler.ListFeedback) feedbackGP.GET("", feedbackHandler.Search)
feedbackGP.GET("/:id", feedbackHandler.GetFeedback) feedbackGP.GET("/:id", feedbackHandler.Get)
feedbackGP.DELETE("/:id", feedbackHandler.DeleteFeedback) feedbackGP.DELETE("/:id", feedbackHandler.Delete)
} }
// Admin Tender-Approvals Routes // Admin Tender-Approvals Routes
@@ -175,11 +175,11 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
feedbackGP := v1.Group("/feedback") feedbackGP := v1.Group("/feedback")
{ {
feedbackGP.Use(customerHandler.AuthMiddleware()) feedbackGP.Use(customerHandler.AuthMiddleware())
feedbackGP.POST("", feedbackHandler.ToggleFeedback) feedbackGP.POST("", feedbackHandler.Toggle)
feedbackGP.GET("", feedbackHandler.PublicListFeedback) feedbackGP.GET("", feedbackHandler.PublicList)
feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetFeedbackByTenderID) feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetByTenderID)
feedbackGP.GET("/:id", feedbackHandler.PublicGetFeedback) feedbackGP.GET("/:id", feedbackHandler.PublicGet)
feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyFeedbackStats) feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyStats)
} }
// Public Tender Approvals Routes // Public Tender Approvals Routes
+2 -2
View File
@@ -20,8 +20,8 @@ type flagService struct {
flagsPath string flagsPath string
} }
// NewFlagService creates a new flag service // NewService creates a new flag service
func NewFlagService(logger logger.Logger, flagsPath string) Service { func NewService(logger logger.Logger, flagsPath string) Service {
return &flagService{ return &flagService{
logger: logger, logger: logger,
flagsPath: flagsPath, flagsPath: flagsPath,
+1 -1
View File
@@ -33,7 +33,7 @@ type companyRepository struct {
} }
// NewCompanyRepository creates a new company repository // NewCompanyRepository creates a new company repository
func NewCompanyRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []orm.Index{ indexes := []orm.Index{
*orm.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}), *orm.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
+2 -2
View File
@@ -36,8 +36,8 @@ type companyService struct {
logger logger.Logger logger logger.Logger
} }
// NewCompanyService creates a new company service // NewService creates a new company service
func NewCompanyService(repository Repository, categoryService company_category.Service, logger logger.Logger) Service { func NewService(repository Repository, categoryService company_category.Service, logger logger.Logger) Service {
return &companyService{ return &companyService{
repository: repository, repository: repository,
categoryService: categoryService, categoryService: categoryService,
+1 -1
View File
@@ -30,7 +30,7 @@ type categoryRepository struct {
} }
// NewCategoryRepository creates a new category repository // NewCategoryRepository creates a new category repository
func NewCategoryRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []orm.Index{ indexes := []orm.Index{
*orm.NewIndex("name_idx", bson.D{{Key: "name", Value: 1}}), *orm.NewIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
+2 -2
View File
@@ -30,8 +30,8 @@ type categoryService struct {
logger logger.Logger logger logger.Logger
} }
// NewCategoryService creates a new category service // NewService creates a new category service
func NewCategoryService(repository Repository, logger logger.Logger) Service { func NewService(repository Repository, logger logger.Logger) Service {
return &categoryService{ return &categoryService{
repository: repository, repository: repository,
logger: logger, logger: logger,
+1 -1
View File
@@ -39,7 +39,7 @@ func collection() string {
} }
// NewCustomerRepository creates a new customer repository // NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []orm.Index{ indexes := []orm.Index{
*orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}), *orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
+1 -1
View File
@@ -63,7 +63,7 @@ type customerService struct {
} }
// New creates a new customer service // New creates a new customer service
func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK) Service { func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK) Service {
return &customerService{ return &customerService{
repository: repository, repository: repository,
logger: logger, logger: logger,
+26 -26
View File
@@ -1,8 +1,9 @@
package feedback package feedback
import ( import (
"time"
"tm/pkg/mongo" "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
) )
// FeedbackType represents the type of feedback // FeedbackType represents the type of feedback
@@ -22,31 +23,36 @@ type Feedback struct {
FeedbackType FeedbackType `bson:"feedback_type" json:"feedback_type" validate:"required,oneof=like dislike"` FeedbackType FeedbackType `bson:"feedback_type" json:"feedback_type" validate:"required,oneof=like dislike"`
} }
// FeedbackSummary represents aggregated feedback statistics for a tender // SetID sets the feedback ID (implements IDSetter interface)
type FeedbackSummary struct { func (f *Feedback) SetID(id string) {
TenderID string `bson:"tender_id" json:"tender_id"` f.ID, _ = bson.ObjectIDFromHex(id)
TotalLikes int64 `bson:"total_likes" json:"total_likes"`
TotalDislikes int64 `bson:"total_dislikes" json:"total_dislikes"`
LastUpdated int64 `bson:"last_updated" json:"last_updated"` // Unix timestamp
} }
// FeedbackSearchCriteria represents search criteria for feedback // GetID returns the feedback ID (implements IDGetter interface)
type FeedbackSearchCriteria struct {
TenderID *string `json:"tender_id,omitempty"`
CustomerID *string `json:"customer_id,omitempty"`
CompanyID *string `json:"company_id,omitempty"`
FeedbackType *FeedbackType `json:"feedback_type,omitempty"`
DateFrom *int64 `json:"date_from,omitempty"` // Unix timestamp
DateTo *int64 `json:"date_to,omitempty"` // Unix timestamp
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// GetID returns the feedback ID
func (f *Feedback) GetID() string { func (f *Feedback) GetID() string {
return f.ID.Hex() return f.ID.Hex()
} }
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (f *Feedback) SetCreatedAt(timestamp int64) {
f.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (f *Feedback) SetUpdatedAt(timestamp int64) {
f.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (f *Feedback) GetCreatedAt() int64 {
return f.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (f *Feedback) GetUpdatedAt() int64 {
return f.UpdatedAt
}
// IsLike returns true if the feedback is a like // IsLike returns true if the feedback is a like
func (f *Feedback) IsLike() bool { func (f *Feedback) IsLike() bool {
return f.FeedbackType == FeedbackTypeLike return f.FeedbackType == FeedbackTypeLike
@@ -56,9 +62,3 @@ func (f *Feedback) IsLike() bool {
func (f *Feedback) IsDislike() bool { func (f *Feedback) IsDislike() bool {
return f.FeedbackType == FeedbackTypeDislike return f.FeedbackType == FeedbackTypeDislike
} }
// UpdateFeedbackType updates the feedback type and updated timestamp
func (f *Feedback) UpdateFeedbackType(feedbackType FeedbackType) {
f.FeedbackType = feedbackType
f.UpdatedAt = time.Now().Unix()
}
+43 -85
View File
@@ -1,61 +1,37 @@
package feedback package feedback
import ( import "tm/pkg/response"
"time"
"tm/pkg/response"
)
// ListFeedbackForm represents the form for listing feedback
type ListFeedbackForm struct {
Feedbacks []FeedbackResponse `json:"feedbacks"`
DateFrom *int64 `json:"date_from,omitempty" validate:"omitempty,min=0"`
DateTo *int64 `json:"date_to,omitempty" validate:"omitempty,min=0"`
Limit int `json:"limit" validate:"required,min=1,max=100"`
Offset int `json:"offset" validate:"required,min=0"`
}
// ToggleFeedbackForm represents the form for toggling feedback (like/dislike) // ToggleFeedbackForm represents the form for toggling feedback (like/dislike)
type ToggleFeedbackForm struct { type ToggleFeedbackForm struct {
TenderID string `json:"tender_id" validate:"required"` Tender string `json:"tender_id" validate:"required"`
FeedbackType FeedbackType `json:"feedback_type" validate:"required,oneof=like dislike"` Type FeedbackType `json:"feedback_type" validate:"required,oneof=like dislike"`
} }
// FeedbackResponse represents the response for feedback operations // SearchForm represents search criteria for feedback
type FeedbackResponse struct { type SearchForm struct {
ID string `json:"id"` Tender *string `json:"tender_id,omitempty" valid:"optional"`
FeedbackType FeedbackType `json:"feedback_type"` Customer *string `json:"customer_id,omitempty" valid:"optional"`
UpdatedAt int64 `json:"updated_at"` Company *string `json:"company_id,omitempty" valid:"optional"`
CreatedAt int64 `json:"created_at"` Type *FeedbackType `json:"feedback_type,omitempty" valid:"optional,in(like|dislike)"`
TenderID string `json:"tender"` DateFrom *int64 `json:"date_from,omitempty" valid:"optional"`
CustomerID *string `json:"customer_id"` DateTo *int64 `json:"date_to,omitempty" valid:"optional"`
CompanyId *string `json:"company_id"`
} }
// FeedbackListResponse represents a paginated list of feedback
type FeedbackListResponse struct {
Feedback []*FeedbackResponse `json:"feedback"`
Meta *response.Meta `json:"meta"`
}
// FeedbackListResponse represents a paginated list of feedback
type FeedbackListWithTenderResponse struct {
Feedback []*FeedbackWithTenderResponse `json:"feedback"`
Meta *response.Meta `json:"meta"`
}
type FeedbackWithTenderResponse struct {
ID string `json:"id"`
FeedbackType FeedbackType `json:"feedback_type"`
UpdatedAt int64 `json:"updated_at"`
CreatedAt int64 `json:"created_at"`
TenderID string `json:"tender_id"`
CustomerID *string `json:"customer_id"`
CompanyId *string `json:"company_id"`
Tender *tenderDetails `json:"tender"`
}
// tenderDetails represents essential tender information for the response
type ( type (
tenderDetails struct { FeedbackResponse struct {
ID string `json:"id"`
FeedbackType FeedbackType `json:"feedback_type"`
UpdatedAt int64 `json:"updated_at"`
CreatedAt int64 `json:"created_at"`
TenderID string `json:"tender_id"`
CustomerID *string `json:"customer_id"`
CompanyId *string `json:"company_id"`
Tender *tenderResponse `json:"tender"`
Company *companyResponse `json:"company"`
}
tenderResponse struct {
ID string `json:"id"` ID string `json:"id"`
NoticePublicationID string `json:"notice_publication_id"` NoticePublicationID string `json:"notice_publication_id"`
PublicationDate int64 `json:"publication_date"` PublicationDate int64 `json:"publication_date"`
@@ -73,39 +49,23 @@ type (
BuyerOrganization *organizationResponse `json:"buyer_organization"` BuyerOrganization *organizationResponse `json:"buyer_organization"`
Status string `json:"status"` Status string `json:"status"`
} }
organizationResponse struct { organizationResponse struct {
Name string `json:"name"` Name string `json:"name"`
} }
companyResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
) )
// CompanyFeedbackStatsResponse represents the response for company feedback statistics type FeedbackListResponse struct {
type CompanyFeedbackStatsResponse struct { Feedback []*FeedbackResponse `json:"feedback"`
CompanyID string `json:"company_id"` Meta *response.Meta `json:"meta"`
TotalLikes int64 `json:"total_likes"`
TotalDislikes int64 `json:"total_dislikes"`
TotalFeedback int64 `json:"total_feedback"`
LastUpdated int64 `json:"last_updated"` // Unix timestamp
} }
// GetDateFromTime returns the date_from as time.Time func (f *Feedback) ToResponse(tender *tenderResponse, company *companyResponse) *FeedbackResponse {
func (f *ListFeedbackForm) GetDateFromTime() *time.Time {
if f.DateFrom == nil {
return nil
}
t := time.Unix(*f.DateFrom, 0)
return &t
}
// GetDateToTime returns the date_to as time.Time
func (f *ListFeedbackForm) GetDateToTime() *time.Time {
if f.DateTo == nil {
return nil
}
t := time.Unix(*f.DateTo, 0)
return &t
}
func (f *Feedback) ToResponse() *FeedbackResponse {
return &FeedbackResponse{ return &FeedbackResponse{
ID: f.ID.Hex(), ID: f.ID.Hex(),
FeedbackType: f.FeedbackType, FeedbackType: f.FeedbackType,
@@ -114,18 +74,16 @@ func (f *Feedback) ToResponse() *FeedbackResponse {
CompanyId: f.CompanyID, CompanyId: f.CompanyID,
UpdatedAt: f.UpdatedAt, UpdatedAt: f.UpdatedAt,
CreatedAt: f.CreatedAt, CreatedAt: f.CreatedAt,
Tender: tender,
Company: company,
} }
} }
func (f *Feedback) ToResponseWithTender(tender *tenderDetails) *FeedbackWithTenderResponse { type StatsResponse struct {
return &FeedbackWithTenderResponse{ CompanyID string `json:"company_id"`
ID: f.ID.Hex(), Company *companyResponse `json:"company"`
FeedbackType: f.FeedbackType, TotalLikes int64 `json:"total_likes"`
TenderID: f.TenderID, TotalDislikes int64 `json:"total_dislikes"`
CustomerID: f.CustomerID, TotalFeedback int64 `json:"total_feedback"`
CompanyId: f.CompanyID, LastUpdated int64 `json:"last_updated"`
UpdatedAt: f.UpdatedAt,
CreatedAt: f.CreatedAt,
Tender: tender,
}
} }
+30 -124
View File
@@ -1,7 +1,6 @@
package feedback package feedback
import ( import (
"strconv"
"tm/internal/customer" "tm/internal/customer"
"tm/internal/user" "tm/internal/user"
"tm/pkg/logger" "tm/pkg/logger"
@@ -13,15 +12,15 @@ import (
// Handler handles HTTP requests for feedback operations // Handler handles HTTP requests for feedback operations
type Handler struct { type Handler struct {
service FeedbackService service Service
logger logger.Logger logger logger.Logger
userHandler *user.Handler userHandler *user.Handler
customerHandler *customer.Handler customerHandler *customer.Handler
} }
// NewFeedbackHandler creates a new feedback handler // NewHandler creates a new feedback handler
func NewFeedbackHandler( func NewHandler(
service FeedbackService, service Service,
userHandler *user.Handler, userHandler *user.Handler,
customerHandler *customer.Handler, customerHandler *customer.Handler,
logger logger.Logger, logger logger.Logger,
@@ -55,65 +54,13 @@ func NewFeedbackHandler(
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/feedback [get] // @Router /admin/v1/feedback [get]
func (h *Handler) ListFeedback(c echo.Context) error { func (h *Handler) Search(c echo.Context) error {
// Parse query parameters form, err := response.Parse[SearchForm](c)
limitStr := c.QueryParam("limit") if err != nil {
offsetStr := c.QueryParam("offset") return response.ValidationError(c, "Invalid request data", err.Error())
limit := 20 // default limit
offset := 0 // default offset
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
} }
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { feedbackList, err := h.service.Search(c.Request().Context(), *form, response.NewPagination(c))
offset = o
}
// Build search criteria
criteria := FeedbackSearchCriteria{
Limit: limit,
Offset: offset,
}
// Parse optional filters
if tenderID := c.QueryParam("tender_id"); tenderID != "" {
criteria.TenderID = &tenderID
}
if customerID := c.QueryParam("customer_id"); customerID != "" {
criteria.CustomerID = &customerID
}
if companyID := c.QueryParam("company_id"); companyID != "" {
criteria.CompanyID = &companyID
}
if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" {
ft := FeedbackType(feedbackType)
criteria.FeedbackType = &ft
}
if dateFrom := c.QueryParam("date_from"); dateFrom != "" {
if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil {
criteria.DateFrom = &df
}
}
if dateTo := c.QueryParam("date_to"); dateTo != "" {
if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil {
criteria.DateTo = &dt
}
}
// Validate date range
if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo {
return response.ValidationError(c, "Invalid date range", "date_from must be before date_to")
}
// Get enhanced feedback list with related data
feedbackList, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset)
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve feedback list") return response.InternalServerError(c, "Failed to retrieve feedback list")
} }
@@ -134,13 +81,13 @@ func (h *Handler) ListFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/feedback/{id} [get] // @Router /admin/v1/feedback/{id} [get]
func (h *Handler) GetFeedback(c echo.Context) error { func (h *Handler) Get(c echo.Context) error {
feedbackID := c.Param("id") feedbackID := c.Param("id")
if feedbackID == "" { if feedbackID == "" {
return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing") return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing")
} }
feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID) feedback, err := h.service.Get(c.Request().Context(), feedbackID)
if err != nil { if err != nil {
if err == ErrFeedbackNotFound { if err == ErrFeedbackNotFound {
return response.NotFound(c, "Feedback not found") return response.NotFound(c, "Feedback not found")
@@ -169,14 +116,14 @@ func (h *Handler) GetFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/feedback/{id} [delete] // @Router /admin/v1/feedback/{id} [delete]
func (h *Handler) DeleteFeedback(c echo.Context) error { func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
return response.BadRequest(c, "Feedback ID is required", "") return response.BadRequest(c, "Feedback ID is required", "")
} }
// Call service // Call service
err := h.service.DeleteFeedback(c.Request().Context(), id) err := h.service.Delete(c.Request().Context(), id)
if err != nil { if err != nil {
switch err { switch err {
case ErrFeedbackNotFound: case ErrFeedbackNotFound:
@@ -206,7 +153,7 @@ func (h *Handler) DeleteFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/feedback [post] // @Router /api/v1/feedback [post]
func (h *Handler) ToggleFeedback(c echo.Context) error { func (h *Handler) Toggle(c echo.Context) error {
form, err := response.Parse[ToggleFeedbackForm](c) form, err := response.Parse[ToggleFeedbackForm](c)
if err != nil { if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error()) return response.ValidationError(c, "Invalid request data", err.Error())
@@ -230,7 +177,7 @@ func (h *Handler) ToggleFeedback(c echo.Context) error {
} }
// Call service // Call service
result, err := h.service.ToggleFeedback(c.Request().Context(), *form, &customerID, &companyID) result, err := h.service.Toggle(c.Request().Context(), *form, &customerID, &companyID)
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to toggle feedback") return response.InternalServerError(c, "Failed to toggle feedback")
} }
@@ -258,61 +205,20 @@ func (h *Handler) ToggleFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/feedback [get] // @Router /api/v1/feedback [get]
func (h *Handler) PublicListFeedback(c echo.Context) error { func (h *Handler) PublicList(c echo.Context) error {
// Parse query parameters
limitStr := c.QueryParam("limit")
offsetStr := c.QueryParam("offset")
limit := 20
offset := 0
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
offset = o
}
companyID, err := user.GetCompanyIDFromContext(c) companyID, err := user.GetCompanyIDFromContext(c)
if err != nil { if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company") return response.BadRequest(c, "Company ID required", "User must be associated with a company")
} }
// Build search criteria form, err := response.Parse[SearchForm](c)
criteria := FeedbackSearchCriteria{ if err != nil {
Limit: limit, return response.ValidationError(c, "Invalid request data", err.Error())
Offset: offset,
CompanyID: &companyID,
}
if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" {
ft := FeedbackType(feedbackType)
if ft != FeedbackTypeLike && ft != FeedbackTypeDislike {
return response.BadRequest(c, "Invalid feedback type", "Must be 'like' or 'dislike'")
}
criteria.FeedbackType = &ft
}
if dateFrom := c.QueryParam("date_from"); dateFrom != "" {
if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil && df >= 0 {
criteria.DateFrom = &df
}
}
if dateTo := c.QueryParam("date_to"); dateTo != "" {
if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil && dt >= 0 {
criteria.DateTo = &dt
}
}
// Validate date range
if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo {
return response.ValidationError(c, "Invalid date range", "date_from must be before date_to")
} }
form.Company = &companyID
// Call service // Call service
result, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset) result, err := h.service.Search(c.Request().Context(), *form, response.NewPagination(c))
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to list feedback") return response.InternalServerError(c, "Failed to list feedback")
} }
@@ -333,13 +239,13 @@ func (h *Handler) PublicListFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/feedback/{id} [get] // @Router /api/v1/feedback/{id} [get]
func (h *Handler) PublicGetFeedback(c echo.Context) error { func (h *Handler) PublicGet(c echo.Context) error {
feedbackID := c.Param("id") feedbackID := c.Param("id")
if feedbackID == "" { if feedbackID == "" {
return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing") return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing")
} }
feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID) feedback, err := h.service.Get(c.Request().Context(), feedbackID)
if err != nil { if err != nil {
return response.BadRequest(c, "Failed to get feedback", "") return response.BadRequest(c, "Failed to get feedback", "")
} }
@@ -360,7 +266,7 @@ func (h *Handler) PublicGetFeedback(c echo.Context) error {
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/feedback/tender/{tender_id} [get] // @Router /api/v1/feedback/tender/{tender_id} [get]
func (h *Handler) PublicGetFeedbackByTenderID(c echo.Context) error { func (h *Handler) PublicGetByTenderID(c echo.Context) error {
tenderID := c.Param("tender_id") tenderID := c.Param("tender_id")
if tenderID == "" { if tenderID == "" {
return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing") return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing")
@@ -371,7 +277,7 @@ func (h *Handler) PublicGetFeedbackByTenderID(c echo.Context) error {
return response.BadRequest(c, "Company ID required", "User must be associated with a company") return response.BadRequest(c, "Company ID required", "User must be associated with a company")
} }
feedback, err := h.service.GetFeedbackByTenderID(c.Request().Context(), tenderID, companyID) feedback, err := h.service.GetByTenderID(c.Request().Context(), tenderID, companyID)
if err != nil { if err != nil {
return response.BadRequest(c, "Failed to get feedback", "") return response.BadRequest(c, "Failed to get feedback", "")
} }
@@ -385,22 +291,22 @@ func (h *Handler) PublicGetFeedbackByTenderID(c echo.Context) error {
// @Tags Feedback // @Tags Feedback
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} response.APIResponse{data=CompanyFeedbackStatsResponse} "Company feedback statistics retrieved successfully" // @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" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing company association"
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated" // @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" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/feedback/stats/company [get] // @Router /api/v1/feedback/stats/company [get]
func (h *Handler) GetCompanyFeedbackStats(c echo.Context) error { func (h *Handler) GetCompanyStats(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c) companyID, err := user.GetCompanyIDFromContext(c)
if err != nil { if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company") return response.BadRequest(c, "Company ID required", "User must be associated with a company")
} }
stats, err := h.service.CompanyFeedbackStats(c.Request().Context(), companyID) stats, err := h.service.CompanyStats(c.Request().Context(), companyID)
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve company feedback statistics") return response.InternalServerError(c, "Failed to retrieve company stats")
} }
return response.Success(c, stats, "Company feedback statistics retrieved successfully") return response.Success(c, stats, "Company stats retrieved successfully")
} }
+69 -277
View File
@@ -4,10 +4,11 @@ import (
"context" "context"
"time" "time"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/mongo" orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
driver "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo"
) )
// FeedbackRepository interface defines feedback data access methods // FeedbackRepository interface defines feedback data access methods
@@ -15,37 +16,33 @@ type Repository interface {
// Basic CRUD operations // Basic CRUD operations
Create(ctx context.Context, feedback *Feedback) error Create(ctx context.Context, feedback *Feedback) error
Update(ctx context.Context, feedback *Feedback) error Update(ctx context.Context, feedback *Feedback) error
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) ([]Feedback, int64, error)
GetByID(ctx context.Context, id string) (*Feedback, error) GetByID(ctx context.Context, id string) (*Feedback, error)
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error) GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
Delete(ctx context.Context, id string) error
List(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) ([]*Feedback, int64, error)
Search(ctx context.Context, criteria FeedbackSearchCriteria) ([]*Feedback, error)
GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error)
GetFeedbackCountByType(ctx context.Context) (map[FeedbackType]int64, error)
GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error) GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error)
GetCompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) Delete(ctx context.Context, id string) error
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
} }
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM // feedbackRepository implements FeedbackRepository interface using MongoDB ORM
type feedbackRepository struct { type feedbackRepository struct {
repo mongo.Repository[Feedback] repo orm.Repository[Feedback]
logger logger.Logger logger logger.Logger
} }
// NewFeedbackRepository creates a new feedback repository // NewFeedbackRepository creates a new feedback repository
func NewFeedbackRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes for feedback collection // Create indexes for feedback collection
feedbackIndexes := []mongo.Index{ feedbackIndexes := []orm.Index{
*mongo.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}), *orm.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
*mongo.NewIndex("customer_id_idx", bson.D{{Key: "customer_id", Value: 1}}), *orm.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
*mongo.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}), *orm.NewIndex("feedback_type_idx", bson.D{{Key: "feedback_type", Value: 1}}),
*mongo.NewIndex("feedback_type_idx", bson.D{{Key: "feedback_type", Value: 1}}),
// Compound indexes for common queries // Compound indexes for common queries
*mongo.NewIndex("tender_customer_idx", bson.D{ *orm.NewIndex("tender_company_idx", bson.D{
{Key: "tender_id", Value: 1}, {Key: "tender_id", Value: 1},
{Key: "customer_id", Value: 1}, {Key: "company_id", Value: 1},
}), }),
*mongo.NewIndex("tender_type_idx", bson.D{ *orm.NewIndex("tender_type_idx", bson.D{
{Key: "tender_id", Value: 1}, {Key: "tender_id", Value: 1},
{Key: "feedback_type", Value: 1}, {Key: "feedback_type", Value: 1},
}), }),
@@ -59,7 +56,7 @@ func NewFeedbackRepository(mongoManager *mongo.ConnectionManager, logger logger.
} }
// Create ORM repository // Create ORM repository
repo := mongo.NewRepository[Feedback](mongoManager.GetCollection("feedback"), logger) repo := orm.NewRepository[Feedback](mongoManager.GetCollection("feedback"), logger)
return &feedbackRepository{ return &feedbackRepository{
repo: repo, repo: repo,
@@ -72,7 +69,6 @@ func (r *feedbackRepository) Create(ctx context.Context, feedback *Feedback) err
// Set timestamps // Set timestamps
now := time.Now().Unix() now := time.Now().Unix()
feedback.CreatedAt = now feedback.CreatedAt = now
feedback.UpdatedAt = now
err := r.repo.Create(ctx, feedback) err := r.repo.Create(ctx, feedback)
if err != nil { if err != nil {
@@ -144,7 +140,24 @@ func (r *feedbackRepository) GetByCompanyID(ctx context.Context, companyID strin
return result, nil return result, nil
} }
// Delete permanently deletes feedback by ID // 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}
result, err := r.repo.FindOne(ctx, filter)
if err != nil {
r.logger.Error("Failed to get feedback by tender ID and company ID", map[string]interface{}{
"tender_id": tenderID,
"company_id": companyID,
"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 { func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
err := r.repo.Delete(ctx, id) err := r.repo.Delete(ctx, id)
if err != nil { if err != nil {
@@ -155,267 +168,46 @@ func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
return err return err
} }
r.logger.Info("Feedback deleted successfully", map[string]interface{}{
"feedback_id": id,
})
return nil return nil
} }
// List retrieves feedback with pagination and filtering
func (r *feedbackRepository) List(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) ([]*Feedback, int64, error) {
filter := r.buildSearchFilter(criteria)
pagination := mongo.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.repo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to list feedback", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
}
// Convert []TenderFeedback to []*TenderFeedback
feedback := make([]*Feedback, len(result.Items))
for i := range result.Items {
feedback[i] = &result.Items[i]
}
return feedback, result.TotalCount, nil
}
// GetByFeedbackType retrieves feedback by type
func (r *feedbackRepository) GetByFeedbackType(ctx context.Context, feedbackType FeedbackType, limit, offset int) ([]*Feedback, int64, error) {
filter := bson.M{"feedback_type": feedbackType}
pagination := mongo.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.repo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get feedback by type", map[string]interface{}{
"feedback_type": feedbackType,
"error": err.Error(),
})
return nil, 0, err
}
// Convert []TenderFeedback to []*TenderFeedback
feedback := make([]*Feedback, len(result.Items))
for i := range result.Items {
feedback[i] = &result.Items[i]
}
return feedback, result.TotalCount, nil
}
// Search searches feedback based on criteria // Search searches feedback based on criteria
func (r *feedbackRepository) Search(ctx context.Context, criteria FeedbackSearchCriteria) ([]*Feedback, error) { func (r *feedbackRepository) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) ([]Feedback, int64, error) {
filter := r.buildSearchFilter(criteria) filter := r.buildSearchFilter(criteria)
pagination := mongo.Pagination{ // Build sort
Limit: 100, sort := "created_at"
SortField: "created_at", if pagination.SortBy != "" {
SortOrder: -1, sort = pagination.SortBy
} }
result, err := r.repo.FindAll(ctx, filter, pagination) sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find users
result, err := r.repo.FindAll(ctx, filter, paginationBuilder)
if err != nil { if err != nil {
r.logger.Error("Failed to search feedback", map[string]interface{}{ r.logger.Error("Failed to list users", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
return nil, err
}
// Convert []TenderFeedback to []*TenderFeedback
feedback := make([]*Feedback, len(result.Items))
for i := range result.Items {
feedback[i] = &result.Items[i]
}
return feedback, nil
}
// GetFeedbackSummary calculates feedback summary for a tender
func (r *feedbackRepository) GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error) {
pipeline := driver.Pipeline{
{
{Key: "$match", Value: bson.M{
"tender_id": tenderID,
}},
},
{
{Key: "$group", Value: bson.M{
"_id": nil,
"total_likes": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
bson.M{"$eq": []string{"$feedback_type", string(FeedbackTypeLike)}},
1,
0,
},
},
},
"total_dislikes": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
bson.M{"$eq": []string{"$feedback_type", string(FeedbackTypeDislike)}},
1,
0,
},
},
},
"total_comments": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
bson.M{"$ne": []interface{}{"$comment", nil}},
1,
0,
},
},
},
"total_ratings": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
bson.M{"$gt": []interface{}{"$rating", 0}},
1,
0,
},
},
},
"rating_sum": bson.M{
"$sum": bson.M{
"$cond": []interface{}{
bson.M{"$gt": []interface{}{"$rating", 0}},
"$rating",
0,
},
},
},
"last_updated": bson.M{"$max": "$updated_at"},
}},
},
}
results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil {
r.logger.Error("Failed to get feedback summary", map[string]interface{}{
"tender_id": tenderID,
"error": err.Error(),
})
return nil, err
}
summary := &FeedbackSummary{
TenderID: tenderID,
TotalLikes: 0,
TotalDislikes: 0,
LastUpdated: time.Now().Unix(),
}
if len(results) > 0 {
result := results[0]
if totalLikes, ok := result["total_likes"].(int32); ok {
summary.TotalLikes = int64(totalLikes)
}
if totalDislikes, ok := result["total_dislikes"].(int32); ok {
summary.TotalDislikes = int64(totalDislikes)
}
if lastUpdated, ok := result["last_updated"].(int64); ok {
summary.LastUpdated = lastUpdated
}
}
return summary, nil
}
// GetFeedbackCountByType gets feedback count grouped by type
func (r *feedbackRepository) GetFeedbackCountByType(ctx context.Context) (map[FeedbackType]int64, error) {
pipeline := driver.Pipeline{
{
{Key: "$group", Value: bson.M{
"_id": "$feedback_type",
"count": bson.M{"$sum": 1},
}},
},
}
results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
counts := make(map[FeedbackType]int64)
for _, result := range results {
if feedbackType, ok := result["_id"].(string); ok {
if count, ok := result["count"].(int32); ok {
counts[FeedbackType(feedbackType)] = int64(count)
}
}
}
return counts, nil
}
// GetCustomerFeedbackHistory gets the feedback history for a specific customer
func (r *feedbackRepository) GetCustomerFeedbackHistory(ctx context.Context, customerID string, limit, offset int) ([]*Feedback, int64, error) {
filter := bson.M{
"customer_id": customerID,
}
pagination := mongo.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.repo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get customer feedback history", map[string]interface{}{
"customer_id": customerID,
"error": err.Error(),
})
return nil, 0, err return nil, 0, err
} }
// Convert []TenderFeedback to []*TenderFeedback return result.Items, result.TotalCount, nil
feedback := make([]*Feedback, len(result.Items))
for i := range result.Items {
feedback[i] = &result.Items[i]
}
return feedback, result.TotalCount, nil
} }
func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error) { // Stats calculates feedback statistics for a company
filter := bson.M{"tender_id": tenderID, "company_id": companyID} func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) {
pipeline := mongo.Pipeline{
result, err := r.repo.FindOne(ctx, filter)
if err != nil {
r.logger.Error("Failed to get feedback by tender ID", map[string]interface{}{
"tender_id": tenderID,
"error": err.Error(),
})
return nil, err
}
return result, nil
}
// GetCompanyFeedbackStats calculates feedback statistics for a company
func (r *feedbackRepository) GetCompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) {
pipeline := driver.Pipeline{
{ {
{Key: "$match", Value: bson.M{ {Key: "$match", Value: bson.M{
"company_id": companyID, "company_id": companyID,
@@ -452,14 +244,14 @@ func (r *feedbackRepository) GetCompanyFeedbackStats(ctx context.Context, compan
results, err := r.repo.Aggregate(ctx, pipeline) results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil { if err != nil {
r.logger.Error("Failed to get company feedback stats", map[string]interface{}{ r.logger.Error("Failed to get feedback stats", map[string]interface{}{
"company_id": companyID, "company_id": companyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
} }
stats := &CompanyFeedbackStatsResponse{ stats := &StatsResponse{
CompanyID: companyID, CompanyID: companyID,
TotalFeedback: 0, TotalFeedback: 0,
TotalLikes: 0, TotalLikes: 0,
@@ -487,24 +279,24 @@ func (r *feedbackRepository) GetCompanyFeedbackStats(ctx context.Context, compan
} }
// buildSearchFilter builds MongoDB filter from search criteria // buildSearchFilter builds MongoDB filter from search criteria
func (r *feedbackRepository) buildSearchFilter(criteria FeedbackSearchCriteria) bson.M { func (r *feedbackRepository) buildSearchFilter(criteria SearchForm) bson.M {
filter := bson.M{} filter := bson.M{}
// Always exclude deleted feedback unless specifically requested // Always exclude deleted feedback unless specifically requested
if criteria.TenderID != nil { if criteria.Tender != nil {
filter["tender_id"] = *criteria.TenderID filter["tender_id"] = *criteria.Tender
} }
if criteria.CustomerID != nil { if criteria.Customer != nil {
filter["customer_id"] = *criteria.CustomerID filter["customer_id"] = *criteria.Customer
} }
if criteria.CompanyID != nil { if criteria.Company != nil {
filter["company_id"] = *criteria.CompanyID filter["company_id"] = *criteria.Company
} }
if criteria.FeedbackType != nil { if criteria.Type != nil {
filter["feedback_type"] = *criteria.FeedbackType filter["feedback_type"] = *criteria.Type
} }
if criteria.DateFrom != nil || criteria.DateTo != nil { if criteria.DateFrom != nil || criteria.DateTo != nil {
+163 -67
View File
@@ -2,44 +2,48 @@ package feedback
import ( import (
"context" "context"
"tm/internal/company"
"tm/internal/tender" "tm/internal/tender"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/response" "tm/pkg/response"
) )
// FeedbackService interface defines feedback business logic methods // FeedbackService interface defines feedback business logic methods
type FeedbackService interface { type Service interface {
ToggleFeedback(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error)
GetFeedback(ctx context.Context, id string) (*FeedbackResponse, error) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error)
ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListWithTenderResponse, error) Get(ctx context.Context, id string) (*FeedbackResponse, error)
DeleteFeedback(ctx context.Context, id string) error GetByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error)
GetFeedbackByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) Delete(ctx context.Context, id string) error
CompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse, error)
} }
// feedbackService implements FeedbackService interface // feedbackService implements FeedbackService interface
type feedbackService struct { type feedbackService struct {
feedbackRepo Repository feedbackRepo Repository
tenderService tender.Service tenderService tender.Service
logger logger.Logger companyService company.Service
logger logger.Logger
} }
// NewFeedbackService creates a new feedback service // NewService creates a new feedback service
func NewFeedbackService(feedbackRepo Repository, tenderService tender.Service, logger logger.Logger) FeedbackService { func NewService(feedbackRepo Repository, tenderService tender.Service, companyService company.Service, logger logger.Logger) Service {
return &feedbackService{ return &feedbackService{
feedbackRepo: feedbackRepo, feedbackRepo: feedbackRepo,
tenderService: tenderService, tenderService: tenderService,
logger: logger, companyService: companyService,
logger: logger,
} }
} }
func (s *feedbackService) ToggleFeedback(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) { func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) {
existing, err := s.feedbackRepo.GetByCompanyID(ctx, *companyID, req.TenderID) existing, err := s.feedbackRepo.GetByCompanyID(ctx, *companyID, req.Tender)
if err != nil { if err != nil {
feedback := &Feedback{ feedback := &Feedback{
TenderID: req.TenderID, TenderID: req.Tender,
FeedbackType: req.FeedbackType, FeedbackType: req.Type,
CompanyID: companyID, CompanyID: companyID,
CustomerID: customerID, CustomerID: customerID,
} }
@@ -47,35 +51,53 @@ func (s *feedbackService) ToggleFeedback(ctx context.Context, req ToggleFeedback
err = s.feedbackRepo.Create(ctx, feedback) err = s.feedbackRepo.Create(ctx, feedback)
if err != nil { if err != nil {
s.logger.Error("Failed to create new feedback", map[string]interface{}{ s.logger.Error("Failed to create new feedback", map[string]interface{}{
"tender_id": req.TenderID, "tender_id": req.Tender,
"company_id": *companyID, "company_id": *companyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
} }
return feedback.ToResponse(), nil tender, company, err := s.getDetails(ctx, req.Tender, *companyID)
if err != nil {
s.logger.Error("Failed to get details", map[string]interface{}{
"tender_id": req.Tender,
"company_id": *companyID,
"error": err.Error(),
})
}
return feedback.ToResponse(tender, company), nil
} }
if existing.FeedbackType == req.FeedbackType { if existing.FeedbackType == req.Type {
err = s.feedbackRepo.Delete(ctx, existing.GetID()) err = s.feedbackRepo.Delete(ctx, existing.GetID())
if err != nil { if err != nil {
s.logger.Error("Failed to update existing feedback", map[string]interface{}{ s.logger.Error("Failed to update existing feedback", map[string]interface{}{
"feedback_id": existing.GetID(), "feedback_id": existing.GetID(),
"tender_id": req.TenderID, "tender_id": req.Tender,
"company_id": *companyID, "company_id": *companyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
} }
if req.FeedbackType == FeedbackTypeLike { if req.Type == FeedbackTypeLike {
existing.FeedbackType = "unlike" existing.FeedbackType = "unlike"
} else { } else {
existing.FeedbackType = "undislike" existing.FeedbackType = "undislike"
} }
return existing.ToResponse(), nil tender, company, err := s.getDetails(ctx, req.Tender, *companyID)
if err != nil {
s.logger.Error("Failed to get details", map[string]interface{}{
"tender_id": req.Tender,
"company_id": *companyID,
"error": err.Error(),
})
}
return existing.ToResponse(tender, company), nil
} }
if existing.FeedbackType == FeedbackTypeLike { if existing.FeedbackType == FeedbackTypeLike {
@@ -88,17 +110,26 @@ func (s *feedbackService) ToggleFeedback(ctx context.Context, req ToggleFeedback
if err != nil { if err != nil {
s.logger.Error("Failed to update existing feedback", map[string]interface{}{ s.logger.Error("Failed to update existing feedback", map[string]interface{}{
"feedback_id": existing.GetID(), "feedback_id": existing.GetID(),
"tender_id": req.TenderID, "tender_id": req.Tender,
"company_id": *companyID, "company_id": *companyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
} }
return existing.ToResponse(), nil tender, company, err := s.getDetails(ctx, req.Tender, *companyID)
if err != nil {
s.logger.Error("Failed to get details", map[string]interface{}{
"tender_id": req.Tender,
"company_id": *companyID,
"error": err.Error(),
})
}
return existing.ToResponse(tender, company), nil
} }
func (s *feedbackService) GetFeedback(ctx context.Context, id string) (*FeedbackResponse, error) { func (s *feedbackService) Get(ctx context.Context, id string) (*FeedbackResponse, error) {
feedback, err := s.feedbackRepo.GetByID(ctx, id) feedback, err := s.feedbackRepo.GetByID(ctx, id)
if err != nil { if err != nil {
s.logger.Error("Failed to get feedback", map[string]interface{}{ s.logger.Error("Failed to get feedback", map[string]interface{}{
@@ -108,16 +139,25 @@ func (s *feedbackService) GetFeedback(ctx context.Context, id string) (*Feedback
return nil, err return nil, err
} }
return feedback.ToResponse(), nil tender, company, err := s.getDetails(ctx, feedback.TenderID, *feedback.CompanyID)
if err != nil {
s.logger.Error("Failed to get details", map[string]interface{}{
"tender_id": feedback.TenderID,
"company_id": *feedback.CompanyID,
"error": err.Error(),
})
}
return feedback.ToResponse(tender, company), nil
} }
func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListWithTenderResponse, error) { func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error) {
feedbacks, total, err := s.feedbackRepo.List(ctx, criteria, limit, offset) feedbacks, total, err := s.feedbackRepo.Search(ctx, criteria, pagination)
if err != nil { if err != nil {
s.logger.Error("Failed to list feedback", map[string]interface{}{ s.logger.Error("Failed to list feedback", map[string]interface{}{
"criteria": criteria, "criteria": criteria,
"limit": limit, "limit": pagination.Limit,
"offset": offset, "offset": pagination.Offset,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
@@ -125,23 +165,56 @@ func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSea
meta := &response.Meta{ meta := &response.Meta{
Total: int(total), Total: int(total),
Limit: limit, Limit: pagination.Limit,
Offset: offset, Offset: pagination.Offset,
Page: (offset / limit) + 1, Page: (pagination.Offset / pagination.Limit) + 1,
Pages: int((total + int64(limit) - 1) / int64(limit)), Pages: int((total + int64(pagination.Limit) - 1) / int64(pagination.Limit)),
} }
result := make([]*FeedbackWithTenderResponse, len(feedbacks)) result := make([]*FeedbackResponse, len(feedbacks))
for i, feedback := range feedbacks { for i, feedback := range feedbacks {
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID) tender, company, _ := s.getDetails(ctx, feedback.TenderID, *feedback.CompanyID)
if err != nil { result[i] = feedback.ToResponse(tender, company)
s.logger.Error("Failed to get tender", map[string]interface{}{ }
"tender_id": feedback.TenderID,
"error": err.Error(), return &FeedbackListResponse{
}) Feedback: result,
return nil, err Meta: meta,
} }, nil
resp := feedback.ToResponseWithTender(&tenderDetails{ }
func (s *feedbackService) Delete(ctx context.Context, id string) error {
return s.feedbackRepo.Delete(ctx, id)
}
func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) {
feedback, err := s.feedbackRepo.GetByTenderID(ctx, tenderID, companyID)
if err != nil {
s.logger.Error("Failed to get feedback by tender ID", map[string]interface{}{
"tender_id": tenderID,
"error": err.Error(),
})
return nil, err
}
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID)
if err != nil {
s.logger.Error("Failed to get tender", map[string]interface{}{
"tender_id": feedback.TenderID,
"error": err.Error(),
})
}
company, err := s.companyService.GetByID(ctx, *feedback.CompanyID)
if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{
"company_id": feedback.CompanyID,
"error": err.Error(),
})
}
return feedback.ToResponse(
&tenderResponse{
ID: tender.ID, ID: tender.ID,
NoticePublicationID: tender.NoticePublicationID, NoticePublicationID: tender.NoticePublicationID,
PublicationDate: tender.PublicationDate, PublicationDate: tender.PublicationDate,
@@ -158,46 +231,69 @@ func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSea
CountryCode: tender.CountryCode, CountryCode: tender.CountryCode,
BuyerOrganization: &organizationResponse{Name: tender.BuyerOrganization.Name}, BuyerOrganization: &organizationResponse{Name: tender.BuyerOrganization.Name},
Status: string(tender.Status), Status: string(tender.Status),
}) },
result[i] = resp &companyResponse{
} ID: company.ID,
return &FeedbackListWithTenderResponse{ Name: company.Name,
Feedback: result, },
Meta: meta, ), nil
}, nil
} }
func (s *feedbackService) DeleteFeedback(ctx context.Context, id string) error { func (s *feedbackService) getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse, error) {
return s.feedbackRepo.Delete(ctx, id) tender, err := s.tenderService.GetByID(ctx, tenderID)
}
func (s *feedbackService) GetFeedbackByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) {
feedback, err := s.feedbackRepo.GetByTenderID(ctx, tenderID, companyID)
if err != nil { if err != nil {
s.logger.Error("Failed to get feedback by tender ID", map[string]interface{}{ s.logger.Error("Failed to get tender", map[string]interface{}{
"tender_id": tenderID, "tender_id": tenderID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err
} }
return feedback.ToResponse(), nil
company, err := s.companyService.GetByID(ctx, companyID)
if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
}
return &tenderResponse{
ID: tender.ID,
NoticePublicationID: tender.NoticePublicationID,
PublicationDate: tender.PublicationDate,
Title: tender.Title,
Description: tender.Description,
ProcurementTypeCode: tender.ProcurementTypeCode,
ProcedureCode: tender.ProcedureCode,
EstimatedValue: tender.EstimatedValue,
Currency: tender.Currency,
Duration: tender.Duration,
DurationUnit: tender.DurationUnit,
TenderDeadline: tender.TenderDeadline,
SubmissionDeadline: tender.SubmissionDeadline,
CountryCode: tender.CountryCode,
BuyerOrganization: &organizationResponse{Name: tender.BuyerOrganization.Name},
Status: string(tender.Status),
}, &companyResponse{
ID: company.ID,
Name: company.Name,
}, nil
} }
func (s *feedbackService) CompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) { func (s *feedbackService) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) {
s.logger.Info("Getting company feedback statistics", map[string]interface{}{ s.logger.Info("Getting company feedback statistics", map[string]interface{}{
"company_id": companyID, "company_id": companyID,
}) })
stats, err := s.feedbackRepo.GetCompanyFeedbackStats(ctx, companyID) stats, err := s.feedbackRepo.CompanyStats(ctx, companyID)
if err != nil { if err != nil {
s.logger.Error("Failed to get company feedback statistics", map[string]interface{}{ s.logger.Error("Failed to get company stats", map[string]interface{}{
"company_id": companyID, "company_id": companyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, err return nil, err
} }
s.logger.Info("Company feedback statistics retrieved successfully", map[string]interface{}{ s.logger.Info("Company stats retrieved successfully", map[string]interface{}{
"company_id": companyID, "company_id": companyID,
"total_feedback": stats.TotalFeedback, "total_feedback": stats.TotalFeedback,
"total_likes": stats.TotalLikes, "total_likes": stats.TotalLikes,
+5 -5
View File
@@ -29,7 +29,7 @@ type inquiryRepository struct {
} }
// NewInquiryRepository creates a new inquiry repository // NewInquiryRepository creates a new inquiry repository
func NewInquiryRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []orm.Index{ indexes := []orm.Index{
*orm.CreateTextIndex("search_idx", "full_name", "company_name", "work_email"), *orm.CreateTextIndex("search_idx", "full_name", "company_name", "work_email"),
@@ -140,10 +140,10 @@ func (r *inquiryRepository) UpdateStatus(ctx context.Context, id string, status,
} }
r.logger.Info("Inquiry status updated successfully", map[string]interface{}{ r.logger.Info("Inquiry status updated successfully", map[string]interface{}{
"inquiry_id": id, "inquiry_id": id,
"old_status": inquiry.StatusHistory[len(inquiry.StatusHistory)-2].Status, // Previous status "old_status": inquiry.StatusHistory[len(inquiry.StatusHistory)-2].Status, // Previous status
"new_status": status, "new_status": status,
"changed_by": changedBy, "changed_by": changedBy,
}) })
return nil return nil
+2 -2
View File
@@ -25,8 +25,8 @@ type inquiryService struct {
logger logger.Logger logger logger.Logger
} }
// NewInquiryService creates a new inquiry service // NewService creates a new inquiry service
func NewInquiryService(repository Repository, sdk notification.SDK, logger logger.Logger) Service { func NewService(repository Repository, sdk notification.SDK, logger logger.Logger) Service {
return &inquiryService{ return &inquiryService{
repository: repository, repository: repository,
sdk: sdk, sdk: sdk,
+2 -2
View File
@@ -33,8 +33,8 @@ type notificationService struct {
logger logger.Logger logger logger.Logger
} }
// NewNotificationService creates a new notification service // NewService creates a new notification service
func NewNotificationService( func NewService(
sdk notification.SDK, sdk notification.SDK,
userService user.Service, userService user.Service,
customerService customer.Service, customerService customer.Service,
+2 -2
View File
@@ -14,8 +14,8 @@ type TenderHandler struct {
logger logger.Logger logger logger.Logger
} }
// NewTenderHandler creates a new tender handler // NewHandler creates a new tender handler
func NewTenderHandler(service Service, logger logger.Logger) *TenderHandler { func NewHandler(service Service, logger logger.Logger) *TenderHandler {
return &TenderHandler{ return &TenderHandler{
service: service, service: service,
logger: logger, logger: logger,
+1 -1
View File
@@ -40,7 +40,7 @@ type tenderRepository struct {
} }
// NewTenderRepository creates a new tender repository // NewTenderRepository creates a new tender repository
func NewTenderRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository {
// Create indexes for tenders collection // Create indexes for tenders collection
tenderIndexes := []orm.Index{ tenderIndexes := []orm.Index{
*orm.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}), *orm.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}),
+2 -2
View File
@@ -30,8 +30,8 @@ type tenderService struct {
logger logger.Logger logger logger.Logger
} }
// NewTenderService creates a new tender service // NewService creates a new tender service
func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service { func NewService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service {
return &tenderService{ return &tenderService{
repository: repository, repository: repository,
companyService: companyService, companyService: companyService,
+1 -1
View File
@@ -39,7 +39,7 @@ type tenderApprovalRepository struct {
} }
// NewTenderApprovalRepository creates a new tender approval repository // NewTenderApprovalRepository creates a new tender approval repository
func NewTenderApprovalRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []mongo.Index{ indexes := []mongo.Index{
*mongo.CreateUniqueIndex("tender_company_idx", bson.D{{Key: "tender_id", Value: 1}, {Key: "company_id", Value: 1}}), *mongo.CreateUniqueIndex("tender_company_idx", bson.D{{Key: "tender_id", Value: 1}, {Key: "company_id", Value: 1}}),
+2 -2
View File
@@ -55,8 +55,8 @@ type tenderApprovalService struct {
logger logger.Logger logger logger.Logger
} }
// NewTenderApprovalService creates a new tender approval service // NewService creates a new tender approval service
func NewTenderApprovalService(repository Repository, tenderService tender.Service, logger logger.Logger) Service { func NewService(repository Repository, tenderService tender.Service, logger logger.Logger) Service {
return &tenderApprovalService{ return &tenderApprovalService{
repository: repository, repository: repository,
tenderService: tenderService, tenderService: tenderService,
+2 -2
View File
@@ -17,8 +17,8 @@ type Handler struct {
authService authorization.AuthorizationService authService authorization.AuthorizationService
} }
// NewUserHandler creates a new user handler // NewHandler creates a new user handler
func NewUserHandler(service Service, logger logger.Logger, validator ValidationService, authService authorization.AuthorizationService) *Handler { func NewHandler(service Service, logger logger.Logger, validator ValidationService, authService authorization.AuthorizationService) *Handler {
return &Handler{ return &Handler{
service: service, service: service,
logger: logger, logger: logger,
+1 -1
View File
@@ -31,7 +31,7 @@ type userRepository struct {
} }
// NewUserRepository creates a new user repository // NewUserRepository creates a new user repository
func NewUserRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management // Create indexes using the ORM's index management
indexes := []orm.Index{ indexes := []orm.Index{
*orm.CreateTextIndex("search_idx", "full_name", "email", "username"), *orm.CreateTextIndex("search_idx", "full_name", "email", "username"),
+2 -2
View File
@@ -45,8 +45,8 @@ type userService struct {
notification notification.SDK notification notification.SDK
} }
// NewUserService creates a new user service // NewService creates a new user service
func NewUserService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService, notificationSDK notification.SDK) Service { func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService, notificationSDK notification.SDK) Service {
return &userService{ return &userService{
repository: repository, repository: repository,
logger: logger, logger: logger,