From 0e14a11dafe900b78fd677db2279c12720aa6038 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Mon, 25 Aug 2025 13:39:35 +0330 Subject: [PATCH] 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. --- cmd/web/docs/docs.go | 161 ++++++++++++++++++++++++++------ cmd/web/docs/swagger.json | 161 ++++++++++++++++++++++++++------ cmd/web/docs/swagger.yaml | 96 +++++++++++++++---- cmd/web/router/routes.go | 1 + internal/feedback/form.go | 9 ++ internal/feedback/handler.go | 26 ++++++ internal/feedback/repository.go | 74 +++++++++++++++ internal/feedback/service.go | 25 +++++ 8 files changed, 477 insertions(+), 76 deletions(-) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index dac41e3..4333463 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -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}": { "get": { "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": { "type": "object", "properties": { @@ -7759,6 +7883,9 @@ const docTemplate = `{ "tender.TenderResponse": { "type": "object", "properties": { + "application_deadline": { + "type": "integer" + }, "buyer_organization": { "$ref": "#/definitions/tender.OrganizationResponse" }, @@ -7801,9 +7928,15 @@ const docTemplate = `{ "submission_deadline": { "type": "integer" }, + "submission_url": { + "type": "string" + }, "tender_deadline": { "type": "integer" }, + "tender_id": { + "type": "string" + }, "title": { "type": "string" } @@ -7875,20 +8008,6 @@ const docTemplate = `{ "tender_approval.CompanyTenderApprovalStatsResponse": { "type": "object", "properties": { - "approvals_by_status": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, - "approvals_by_submission": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, "approved_tenders": { "type": "integer" }, @@ -8002,20 +8121,6 @@ const docTemplate = `{ "tender_approval.TenderApprovalStatsResponse": { "type": "object", "properties": { - "approvals_by_status": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, - "approvals_by_submission": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, "approved_tenders": { "type": "integer" }, diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 1170b6f..4c332b5 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -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}": { "get": { "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": { "type": "object", "properties": { @@ -7753,6 +7877,9 @@ "tender.TenderResponse": { "type": "object", "properties": { + "application_deadline": { + "type": "integer" + }, "buyer_organization": { "$ref": "#/definitions/tender.OrganizationResponse" }, @@ -7795,9 +7922,15 @@ "submission_deadline": { "type": "integer" }, + "submission_url": { + "type": "string" + }, "tender_deadline": { "type": "integer" }, + "tender_id": { + "type": "string" + }, "title": { "type": "string" } @@ -7869,20 +8002,6 @@ "tender_approval.CompanyTenderApprovalStatsResponse": { "type": "object", "properties": { - "approvals_by_status": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, - "approvals_by_submission": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, "approved_tenders": { "type": "integer" }, @@ -7996,20 +8115,6 @@ "tender_approval.TenderApprovalStatsResponse": { "type": "object", "properties": { - "approvals_by_status": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, - "approvals_by_submission": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - } - }, "approved_tenders": { "type": "integer" }, diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 3b1d7b5..356a62b 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -748,6 +748,20 @@ definitions: is_verified: type: boolean 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: properties: feedback: @@ -860,6 +874,8 @@ definitions: type: object tender.TenderResponse: properties: + application_deadline: + type: integer buyer_organization: $ref: '#/definitions/tender.OrganizationResponse' country_code: @@ -888,8 +904,12 @@ definitions: $ref: '#/definitions/tender.TenderStatus' submission_deadline: type: integer + submission_url: + type: string tender_deadline: type: integer + tender_id: + type: string title: type: string type: object @@ -941,16 +961,6 @@ definitions: - ApprovalStatusRejected tender_approval.CompanyTenderApprovalStatsResponse: properties: - approvals_by_status: - additionalProperties: - format: int64 - type: integer - type: object - approvals_by_submission: - additionalProperties: - format: int64 - type: integer - type: object approved_tenders: type: integer company_id: @@ -1026,16 +1036,6 @@ definitions: type: object tender_approval.TenderApprovalStatsResponse: properties: - approvals_by_status: - additionalProperties: - format: int64 - type: integer - type: object - approvals_by_submission: - additionalProperties: - format: int64 - type: integer - type: object approved_tenders: type: integer last_updated: @@ -4786,6 +4786,62 @@ paths: summary: Get feedback by ID tags: - 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}: get: consumes: diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 09afc57..a66cd92 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -152,6 +152,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende feedbackGP.GET("", feedbackHandler.PublicListFeedback) feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetFeedbackByTenderID) feedbackGP.GET("/:id", feedbackHandler.PublicGetFeedback) + feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyFeedbackStats) } // Public Tender Approvals Routes diff --git a/internal/feedback/form.go b/internal/feedback/form.go index e2f2139..5393e95 100644 --- a/internal/feedback/form.go +++ b/internal/feedback/form.go @@ -30,6 +30,15 @@ type FeedbackResponse struct { 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 func (f *ListFeedbackForm) GetDateFromTime() *time.Time { if f.DateFrom == nil { diff --git a/internal/feedback/handler.go b/internal/feedback/handler.go index 69d02d4..f1962dd 100644 --- a/internal/feedback/handler.go +++ b/internal/feedback/handler.go @@ -378,3 +378,29 @@ func (h *Handler) PublicGetFeedbackByTenderID(c echo.Context) error { return response.Success(c, feedback, "Feedback retrieved successfully") } + +// GetCompanyFeedbackStats retrieves feedback statistics for the authenticated user's company +// @Summary Get company feedback statistics +// @Description Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations. +// @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") +} diff --git a/internal/feedback/repository.go b/internal/feedback/repository.go index 1c6267f..f3ea9bd 100644 --- a/internal/feedback/repository.go +++ b/internal/feedback/repository.go @@ -23,6 +23,7 @@ type Repository interface { 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) + GetCompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) } // feedbackRepository implements FeedbackRepository interface using MongoDB ORM @@ -412,6 +413,79 @@ func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, compan 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 func (r *feedbackRepository) buildSearchFilter(criteria FeedbackSearchCriteria) bson.M { filter := bson.M{} diff --git a/internal/feedback/service.go b/internal/feedback/service.go index 756f334..1987adf 100644 --- a/internal/feedback/service.go +++ b/internal/feedback/service.go @@ -13,6 +13,7 @@ type FeedbackService interface { ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListResponse, error) DeleteFeedback(ctx context.Context, id string) error GetFeedbackByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) + CompanyFeedbackStats(ctx context.Context, companyID string) (*CompanyFeedbackStatsResponse, error) } // feedbackService implements FeedbackService interface @@ -152,3 +153,27 @@ func (s *feedbackService) GetFeedbackByTenderID(ctx context.Context, tenderID, c } 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 +}