Add Company Feedback Statistics Endpoint and Update API Documentation

- Introduced a new endpoint to retrieve comprehensive feedback statistics for the authenticated user's company, including total likes, dislikes, and percentage calculations.
- Implemented the GetCompanyFeedbackStats method in the feedback handler to handle requests, ensuring proper authentication and error handling.
- Updated Swagger JSON, YAML, and Go documentation to accurately reflect the new endpoint, including detailed descriptions, parameters, and response formats for the CompanyFeedbackStatsResponse.
- Enhanced the feedback repository and service layers to support the new statistics functionality, improving the overall data handling capabilities of the tender management system.
- These changes enhance the usability and functionality of the API, providing valuable insights into company feedback statistics.
This commit is contained in:
n.nakhostin
2025-08-25 13:39:35 +03:30
parent 488d43380b
commit 0e14a11daf
8 changed files with 477 additions and 76 deletions
+133 -28
View File
@@ -5418,6 +5418,109 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/feedback/stats/company": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Feedback"
],
"summary": "Get company feedback statistics",
"responses": {
"200": {
"description": "Company feedback statistics retrieved successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/feedback.CompanyFeedbackStatsResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Missing company association",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
},
"401": {
"description": "Unauthorized - User not authenticated",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal server error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/feedback/tender/{tender_id}": { "/api/v1/feedback/tender/{tender_id}": {
"get": { "get": {
"security": [ "security": [
@@ -7592,6 +7695,27 @@ 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": {
@@ -7759,6 +7883,9 @@ const docTemplate = `{
"tender.TenderResponse": { "tender.TenderResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"application_deadline": {
"type": "integer"
},
"buyer_organization": { "buyer_organization": {
"$ref": "#/definitions/tender.OrganizationResponse" "$ref": "#/definitions/tender.OrganizationResponse"
}, },
@@ -7801,9 +7928,15 @@ const docTemplate = `{
"submission_deadline": { "submission_deadline": {
"type": "integer" "type": "integer"
}, },
"submission_url": {
"type": "string"
},
"tender_deadline": { "tender_deadline": {
"type": "integer" "type": "integer"
}, },
"tender_id": {
"type": "string"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -7875,20 +8008,6 @@ const docTemplate = `{
"tender_approval.CompanyTenderApprovalStatsResponse": { "tender_approval.CompanyTenderApprovalStatsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"approvals_by_status": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approvals_by_submission": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approved_tenders": { "approved_tenders": {
"type": "integer" "type": "integer"
}, },
@@ -8002,20 +8121,6 @@ const docTemplate = `{
"tender_approval.TenderApprovalStatsResponse": { "tender_approval.TenderApprovalStatsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"approvals_by_status": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approvals_by_submission": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approved_tenders": { "approved_tenders": {
"type": "integer" "type": "integer"
}, },
+133 -28
View File
@@ -5412,6 +5412,109 @@
} }
} }
}, },
"/api/v1/feedback/stats/company": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Feedback"
],
"summary": "Get company feedback statistics",
"responses": {
"200": {
"description": "Company feedback statistics retrieved successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/feedback.CompanyFeedbackStatsResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Missing company association",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
},
"401": {
"description": "Unauthorized - User not authenticated",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal server error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/feedback/tender/{tender_id}": { "/api/v1/feedback/tender/{tender_id}": {
"get": { "get": {
"security": [ "security": [
@@ -7586,6 +7689,27 @@
} }
} }
}, },
"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": {
@@ -7753,6 +7877,9 @@
"tender.TenderResponse": { "tender.TenderResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"application_deadline": {
"type": "integer"
},
"buyer_organization": { "buyer_organization": {
"$ref": "#/definitions/tender.OrganizationResponse" "$ref": "#/definitions/tender.OrganizationResponse"
}, },
@@ -7795,9 +7922,15 @@
"submission_deadline": { "submission_deadline": {
"type": "integer" "type": "integer"
}, },
"submission_url": {
"type": "string"
},
"tender_deadline": { "tender_deadline": {
"type": "integer" "type": "integer"
}, },
"tender_id": {
"type": "string"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -7869,20 +8002,6 @@
"tender_approval.CompanyTenderApprovalStatsResponse": { "tender_approval.CompanyTenderApprovalStatsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"approvals_by_status": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approvals_by_submission": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approved_tenders": { "approved_tenders": {
"type": "integer" "type": "integer"
}, },
@@ -7996,20 +8115,6 @@
"tender_approval.TenderApprovalStatsResponse": { "tender_approval.TenderApprovalStatsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"approvals_by_status": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approvals_by_submission": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"approved_tenders": { "approved_tenders": {
"type": "integer" "type": "integer"
}, },
+76 -20
View File
@@ -748,6 +748,20 @@ definitions:
is_verified: is_verified:
type: boolean type: boolean
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:
@@ -860,6 +874,8 @@ definitions:
type: object type: object
tender.TenderResponse: tender.TenderResponse:
properties: properties:
application_deadline:
type: integer
buyer_organization: buyer_organization:
$ref: '#/definitions/tender.OrganizationResponse' $ref: '#/definitions/tender.OrganizationResponse'
country_code: country_code:
@@ -888,8 +904,12 @@ definitions:
$ref: '#/definitions/tender.TenderStatus' $ref: '#/definitions/tender.TenderStatus'
submission_deadline: submission_deadline:
type: integer type: integer
submission_url:
type: string
tender_deadline: tender_deadline:
type: integer type: integer
tender_id:
type: string
title: title:
type: string type: string
type: object type: object
@@ -941,16 +961,6 @@ definitions:
- ApprovalStatusRejected - ApprovalStatusRejected
tender_approval.CompanyTenderApprovalStatsResponse: tender_approval.CompanyTenderApprovalStatsResponse:
properties: properties:
approvals_by_status:
additionalProperties:
format: int64
type: integer
type: object
approvals_by_submission:
additionalProperties:
format: int64
type: integer
type: object
approved_tenders: approved_tenders:
type: integer type: integer
company_id: company_id:
@@ -1026,16 +1036,6 @@ definitions:
type: object type: object
tender_approval.TenderApprovalStatsResponse: tender_approval.TenderApprovalStatsResponse:
properties: properties:
approvals_by_status:
additionalProperties:
format: int64
type: integer
type: object
approvals_by_submission:
additionalProperties:
format: int64
type: integer
type: object
approved_tenders: approved_tenders:
type: integer type: integer
last_updated: last_updated:
@@ -4786,6 +4786,62 @@ paths:
summary: Get feedback by ID summary: Get feedback by ID
tags: tags:
- Feedback - Feedback
/api/v1/feedback/stats/company:
get:
consumes:
- application/json
description: Retrieve comprehensive feedback statistics for the authenticated
user's company including total likes, dislikes, and percentage calculations.
produces:
- application/json
responses:
"200":
description: Company feedback statistics retrieved successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/feedback.CompanyFeedbackStatsResponse'
type: object
"400":
description: Bad request - Missing company association
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
error:
type: string
message:
type: string
type: object
"401":
description: Unauthorized - User not authenticated
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
error:
type: string
message:
type: string
type: object
"500":
description: Internal server error
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
error:
type: string
message:
type: string
type: object
security:
- BearerAuth: []
summary: Get company feedback statistics
tags:
- Feedback
/api/v1/feedback/tender/{tender_id}: /api/v1/feedback/tender/{tender_id}:
get: get:
consumes: consumes:
+1
View File
@@ -152,6 +152,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
feedbackGP.GET("", feedbackHandler.PublicListFeedback) feedbackGP.GET("", feedbackHandler.PublicListFeedback)
feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetFeedbackByTenderID) feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetFeedbackByTenderID)
feedbackGP.GET("/:id", feedbackHandler.PublicGetFeedback) feedbackGP.GET("/:id", feedbackHandler.PublicGetFeedback)
feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyFeedbackStats)
} }
// Public Tender Approvals Routes // Public Tender Approvals Routes
+9
View File
@@ -30,6 +30,15 @@ type FeedbackResponse struct {
CompanyId *string `json:"company_id"` CompanyId *string `json:"company_id"`
} }
// CompanyFeedbackStatsResponse represents the response for company feedback statistics
type CompanyFeedbackStatsResponse struct {
CompanyID string `json:"company_id"`
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 // GetDateFromTime returns the date_from as time.Time
func (f *ListFeedbackForm) GetDateFromTime() *time.Time { func (f *ListFeedbackForm) GetDateFromTime() *time.Time {
if f.DateFrom == nil { if f.DateFrom == nil {
+26
View File
@@ -378,3 +378,29 @@ func (h *Handler) PublicGetFeedbackByTenderID(c echo.Context) error {
return response.Success(c, feedback, "Feedback retrieved successfully") 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.
// @Tags Feedback
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CompanyFeedbackStatsResponse} "Company feedback statistics retrieved successfully"
// @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 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
// @Security BearerAuth
// @Router /api/v1/feedback/stats/company [get]
func (h *Handler) GetCompanyFeedbackStats(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.CompanyFeedbackStats(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve company feedback statistics")
}
return response.Success(c, stats, "Company feedback statistics retrieved successfully")
}
+74
View File
@@ -23,6 +23,7 @@ type Repository interface {
GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error) GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error)
GetFeedbackCountByType(ctx context.Context) (map[FeedbackType]int64, 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)
} }
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM // feedbackRepository implements FeedbackRepository interface using MongoDB ORM
@@ -412,6 +413,79 @@ func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, compan
return result, nil 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{
"company_id": companyID,
}},
},
{
{Key: "$group", Value: bson.M{
"_id": nil,
"total_feedback": bson.M{
"$sum": 1,
},
"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,
},
},
},
"last_updated": bson.M{"$max": "$updated_at"},
}},
},
}
results, err := r.repo.Aggregate(ctx, pipeline)
if err != nil {
r.logger.Error("Failed to get company feedback stats", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
return nil, err
}
stats := &CompanyFeedbackStatsResponse{
CompanyID: companyID,
TotalFeedback: 0,
TotalLikes: 0,
TotalDislikes: 0,
LastUpdated: time.Now().Unix(),
}
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
}
}
return stats, nil
}
// 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 FeedbackSearchCriteria) bson.M {
filter := bson.M{} filter := bson.M{}
+25
View File
@@ -13,6 +13,7 @@ type FeedbackService interface {
ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListResponse, error) ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListResponse, error)
DeleteFeedback(ctx context.Context, id string) error DeleteFeedback(ctx context.Context, id string) error
GetFeedbackByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) GetFeedbackByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error)
CompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error)
} }
// feedbackService implements FeedbackService interface // feedbackService implements FeedbackService interface
@@ -152,3 +153,27 @@ func (s *feedbackService) GetFeedbackByTenderID(ctx context.Context, tenderID, c
} }
return feedback.ToResponse(), nil return feedback.ToResponse(), nil
} }
func (s *feedbackService) CompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) {
s.logger.Info("Getting company feedback statistics", map[string]interface{}{
"company_id": companyID,
})
stats, err := s.feedbackRepo.GetCompanyFeedbackStats(ctx, companyID)
if err != nil {
s.logger.Error("Failed to get company feedback statistics", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
return nil, err
}
s.logger.Info("Company feedback statistics retrieved successfully", map[string]interface{}{
"company_id": companyID,
"total_feedback": stats.TotalFeedback,
"total_likes": stats.TotalLikes,
"total_dislikes": stats.TotalDislikes,
})
return stats, nil
}