From eb69a842f057bfdf16c8dd012e2fb2c85df2a3e3 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Sat, 6 Sep 2025 12:07:32 +0330 Subject: [PATCH] Update Configuration and Enhance Tender Approval Pagination - Changed the server port from 8081 to 8082 for the web application. - Updated MongoDB URI to include authentication credentials for improved security. - Added new query parameters `limit` and `offset` for pagination in tender approval retrieval, enhancing the API's flexibility. - Modified the response structure to include metadata for pagination, improving the clarity of responses. - Updated API documentation to reflect the new pagination parameters and response format, ensuring consistency for API consumers. --- cmd/web/config.yaml | 6 ++-- cmd/web/docs/docs.go | 38 +++++++++++++++++--- cmd/web/docs/swagger.json | 38 +++++++++++++++++--- cmd/web/docs/swagger.yaml | 28 ++++++++++++--- cmd/web/main.go | 2 +- internal/tender_approval/entity.go | 13 ++++--- internal/tender_approval/handler.go | 22 ++++++++++-- internal/tender_approval/repository.go | 17 ++++++--- internal/tender_approval/service.go | 49 +++++++++++++++++--------- pkg/response/response.go | 10 +++--- 10 files changed, 171 insertions(+), 52 deletions(-) diff --git a/cmd/web/config.yaml b/cmd/web/config.yaml index ae435a3..cb094e2 100644 --- a/cmd/web/config.yaml +++ b/cmd/web/config.yaml @@ -1,13 +1,13 @@ server: host: "0.0.0.0" - port: 8081 + port: 8082 timeout: 30s read_timeout: 10s write_timeout: 10s database: mongodb: - uri: "mongodb://localhost:27017" + uri: "mongodb://admin:admin1234@localhost:27017" name: "tender_management" timeout: 10s max_pool_size: 100 @@ -16,7 +16,7 @@ cache: redis: host: "localhost" port: 6379 - password: "" + password: "admin1234" db: 0 pool_size: 10 diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index a30be1a..e66e328 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -6006,6 +6006,23 @@ const docTemplate = `{ ], "summary": "Get tender approvals by company ID", "parameters": [ + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of approvals per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of approvals to skip for pagination", + "name": "offset", + "in": "query" + }, { "enum": [ "self-apply", @@ -6051,10 +6068,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderResponse" - } + "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderListResponse" } } } @@ -8171,6 +8185,20 @@ const docTemplate = `{ } } }, + "tender_approval.TenderApprovalWithTenderListResponse": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/response.Meta" + }, + "tenders": { + "type": "array", + "items": { + "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderResponse" + } + } + } + }, "tender_approval.TenderApprovalWithTenderResponse": { "type": "object", "properties": { @@ -8585,7 +8613,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "2.0.0", - Host: "localhost:8081", + Host: "localhost:8082", BasePath: "/", Schemes: []string{}, Title: "Tender Management API", diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 17e8ed8..beac4d2 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -15,7 +15,7 @@ }, "version": "2.0.0" }, - "host": "localhost:8081", + "host": "localhost:8082", "basePath": "/", "paths": { "/admin/tenders": { @@ -6000,6 +6000,23 @@ ], "summary": "Get tender approvals by company ID", "parameters": [ + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of approvals per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of approvals to skip for pagination", + "name": "offset", + "in": "query" + }, { "enum": [ "self-apply", @@ -6045,10 +6062,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderResponse" - } + "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderListResponse" } } } @@ -8165,6 +8179,20 @@ } } }, + "tender_approval.TenderApprovalWithTenderListResponse": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/response.Meta" + }, + "tenders": { + "type": "array", + "items": { + "$ref": "#/definitions/tender_approval.TenderApprovalWithTenderResponse" + } + } + } + }, "tender_approval.TenderApprovalWithTenderResponse": { "type": "object", "properties": { diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index be01cc0..03077d4 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1045,6 +1045,15 @@ definitions: total_approvals: type: integer type: object + tender_approval.TenderApprovalWithTenderListResponse: + properties: + metadata: + $ref: '#/definitions/response.Meta' + tenders: + items: + $ref: '#/definitions/tender_approval.TenderApprovalWithTenderResponse' + type: array + type: object tender_approval.TenderApprovalWithTenderResponse: properties: company_id: @@ -1288,7 +1297,7 @@ definitions: username: type: string type: object -host: localhost:8081 +host: localhost:8082 info: contact: email: api-support@tender-management.com @@ -5061,6 +5070,19 @@ paths: description: Retrieve all tender approvals for a specific company with optional filtering parameters: + - default: 20 + description: Number of approvals per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of approvals to skip for pagination + in: query + minimum: 0 + name: offset + type: integer - description: Filter by submission mode Enums(self-apply, partnership) enum: - self-apply @@ -5093,9 +5115,7 @@ paths: - $ref: '#/definitions/response.APIResponse' - properties: data: - items: - $ref: '#/definitions/tender_approval.TenderApprovalWithTenderResponse' - type: array + $ref: '#/definitions/tender_approval.TenderApprovalWithTenderListResponse' type: object "400": description: Bad request - Invalid company ID diff --git a/cmd/web/main.go b/cmd/web/main.go index fb498f3..ff0a158 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -12,7 +12,7 @@ package main // @license.name MIT // @license.url https://opensource.org/licenses/MIT -// @host localhost:8081 +// @host localhost:8082 // @BasePath / // @securityDefinitions.apikey BearerAuth diff --git a/internal/tender_approval/entity.go b/internal/tender_approval/entity.go index 03ba7c4..d166589 100644 --- a/internal/tender_approval/entity.go +++ b/internal/tender_approval/entity.go @@ -3,6 +3,7 @@ package tender_approval import ( "time" "tm/pkg/mongo" + "tm/pkg/response" ) // ApprovalStatus represents the approval status of a tender by a company @@ -63,6 +64,12 @@ type TenderApprovalWithTenderResponse struct { Tender *TenderDetails `json:"tender,omitempty"` } +// TenderApprovalWithTenderResponse represents the response data for tender approval with tender details +type TenderApprovalWithTenderListResponse struct { + Approvals []*TenderApprovalWithTenderResponse `json:"tenders"` + Metadata *response.Meta `json:"metadata"` +} + // TenderDetails represents essential tender information for the response type ( TenderDetails struct { @@ -103,11 +110,7 @@ type TenderApprovalSearchCriteria struct { // TenderApprovalListResponseWithTender represents a paginated list of tender approvals with tender details type TenderApprovalListResponseWithTender struct { TenderApprovals []*TenderApprovalWithTenderResponse `json:"tender_approvals"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Page int `json:"page"` - Pages int `json:"pages"` + Metadata *response.Meta `json:"metadata"` } // TenderApprovalStatistics represents statistics for tender approvals diff --git a/internal/tender_approval/handler.go b/internal/tender_approval/handler.go index 2bfc701..08547b0 100644 --- a/internal/tender_approval/handler.go +++ b/internal/tender_approval/handler.go @@ -129,16 +129,34 @@ func (h *Handler) GetTenderApprovalByTenderAndCompany(c echo.Context) error { // @Tags TenderApprovals // @Accept json // @Produce json +// @Param limit query integer false "Number of approvals per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of approvals to skip for pagination" minimum(0) default(0) // @Param submission_mode query string false "Filter by submission mode Enums(self-apply, partnership)" // @Param status query string false "Filter by status Enums(submitted, rejected)" // @Param created_from query integer false "Filter by created date from (Unix timestamp)" // @Param created_to query integer false "Filter by created date to (Unix timestamp)" -// @Success 200 {object} response.APIResponse{data=[]TenderApprovalWithTenderResponse} "Tender approvals retrieved successfully" +// @Success 200 {object} response.APIResponse{data=TenderApprovalWithTenderListResponse} "Tender approvals retrieved successfully" // @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" // @Failure 500 {object} response.APIResponse "Internal server error" // @Security BearerAuth // @Router /api/v1/tender-approvals [get] func (h *Handler) PublicGetTenderApprovals(c echo.Context) error { + + // Parse pagination parameters + limit := 20 + if l := c.QueryParam("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { + limit = parsed + } + } + + offset := 0 + if o := c.QueryParam("offset"); o != "" { + if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { + offset = parsed + } + } + companyID := c.Get("company_id").(string) if companyID == "" { return response.Unauthorized(c, "Unauthorized") @@ -182,7 +200,7 @@ func (h *Handler) PublicGetTenderApprovals(c echo.Context) error { } } - tenderApprovalsWithTender, err := h.service.GetTenderApprovalsByCompanyIDWithTender(c.Request().Context(), companyID, submissionMode, status, from, to) + tenderApprovalsWithTender, err := h.service.GetTenderApprovalsByCompanyIDWithTender(c.Request().Context(), companyID, submissionMode, status, from, to, limit, offset) if err != nil { return response.InternalServerError(c, "Failed to get tender approvals") } diff --git a/internal/tender_approval/repository.go b/internal/tender_approval/repository.go index 4b8254b..8e091ce 100644 --- a/internal/tender_approval/repository.go +++ b/internal/tender_approval/repository.go @@ -17,7 +17,7 @@ type Repository interface { GetByID(ctx context.Context, id string) (*TenderApproval, error) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) GetByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) - GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApproval, error) + GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, limit int, offset int, from *int64, to *int64) ([]*TenderApproval, int64, error) GetByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) Update(ctx context.Context, tenderApproval *TenderApproval) error Delete(ctx context.Context, id string) error @@ -160,9 +160,16 @@ func (r *tenderApprovalRepository) GetByTenderID(ctx context.Context, tenderID s } // GetByCompanyID retrieves all tender approvals for a specific company with optional filters -func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApproval, error) { +func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, limit int, offset int, from *int64, to *int64) ([]*TenderApproval, int64, error) { filter := bson.M{"company_id": companyID} + pagination := mongo.Pagination{ + Limit: limit, + Skip: offset, + SortField: "created_at", + SortOrder: -1, + } + // Add submission mode filter if provided if submissionMode != nil { filter["submission_mode"] = *submissionMode @@ -180,13 +187,13 @@ func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID filter["created_at"] = bson.M{"$lte": *to} } - result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().SortDesc("created_at").Build()) + result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { r.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) - return nil, err + return nil, 0, err } // Convert []TenderApproval to []*TenderApproval @@ -195,7 +202,7 @@ func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID tenderApprovals[i] = &result.Items[i] } - return tenderApprovals, nil + return tenderApprovals, result.TotalCount, nil } // GetByIDs retrieves multiple tender approvals by their IDs diff --git a/internal/tender_approval/service.go b/internal/tender_approval/service.go index fa300b2..5147242 100644 --- a/internal/tender_approval/service.go +++ b/internal/tender_approval/service.go @@ -3,9 +3,11 @@ package tender_approval import ( "context" "errors" + "math" "time" "tm/internal/tender" "tm/pkg/logger" + "tm/pkg/response" ) // Service defines business logic for tender approval operations @@ -17,8 +19,8 @@ type Service interface { GetTenderApprovalByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) GetTenderApprovalByTenderAndCompanyWithTender(ctx context.Context, tenderID, companyID string) (*TenderApprovalWithTenderResponse, error) GetTenderApprovalsByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) - GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApproval, error) - GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApprovalWithTenderResponse, error) + GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) ([]*TenderApproval, int64, error) + GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) (*TenderApprovalWithTenderListResponse, error) GetTenderApprovalsByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) UpdateTenderApproval(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApproval, error) UpdateTenderApprovalWithTender(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApprovalWithTenderResponse, error) @@ -261,14 +263,14 @@ func (s *tenderApprovalService) GetTenderApprovalsByTenderID(ctx context.Context } // GetTenderApprovalsByCompanyID retrieves all tender approvals for a specific company with optional filters -func (s *tenderApprovalService) GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApproval, error) { - tenderApprovals, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, from, to) +func (s *tenderApprovalService) GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) ([]*TenderApproval, int64, error) { + tenderApprovals, total, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, limit, offset, from, to) if err != nil { s.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) - return nil, err + return nil, 0, err } s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{ @@ -276,12 +278,12 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyID(ctx context.Contex "count": len(tenderApprovals), }) - return tenderApprovals, nil + return tenderApprovals, total, nil } // GetTenderApprovalsByCompanyIDWithTender retrieves all tender approvals for a specific company with tender details -func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64) ([]*TenderApprovalWithTenderResponse, error) { - tenderApprovals, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, from, to) +func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit, offset int) (*TenderApprovalWithTenderListResponse, error) { + tenderApprovals, total, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, limit, offset, from, to) if err != nil { s.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{ "error": err.Error(), @@ -291,7 +293,10 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont } // Convert to response with tender details - var responses []*TenderApprovalWithTenderResponse + responses := &TenderApprovalWithTenderListResponse{ + Approvals: []*TenderApprovalWithTenderResponse{}, + Metadata: &response.Meta{}, + } for _, approval := range tenderApprovals { // Get tender details tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) @@ -301,7 +306,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont "error": err.Error(), }) // Continue without tender details rather than failing completely - responses = append(responses, approval.ToResponseWithTender(nil)) + responses.Approvals = append(responses.Approvals, approval.ToResponseWithTender(nil)) continue } @@ -319,12 +324,20 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont Status: string(tenderResponse.Status), } - responses = append(responses, approval.ToResponseWithTender(tenderDetails)) + responses.Approvals = append(responses.Approvals, approval.ToResponseWithTender(tenderDetails)) + } + + responses.Metadata = &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + Pages: int(math.Ceil(float64(total) / float64(limit))), + Page: int(math.Ceil(float64(offset) / float64(limit))), } s.logger.Info("Tender approvals with tender details retrieved successfully", map[string]interface{}{ "company_id": companyID, - "count": len(responses), + "count": len(responses.Approvals), }) return responses, nil @@ -960,11 +973,13 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex meta := &TenderApprovalListResponseWithTender{ TenderApprovals: responses, - Total: total, - Limit: limit, - Offset: offset, - Page: page, - Pages: pages, + Metadata: &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + Page: page, + Pages: pages, + }, } return meta, nil diff --git a/pkg/response/response.go b/pkg/response/response.go index 12bdc14..33d5e59 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -25,11 +25,11 @@ type APIError struct { // Meta represents metadata for paginated responses type Meta struct { - Total int `json:"total,omitempty"` - Limit int `json:"limit,omitempty"` - Offset int `json:"offset,omitempty"` - Page int `json:"page,omitempty"` - Pages int `json:"pages,omitempty"` + Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Page int `json:"page"` + Pages int `json:"pages"` } // Success returns a successful response