diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 00c554a..0162c32 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -5374,7 +5374,7 @@ const docTemplate = `{ } } }, - "/api/v1/tenders/{id}": { + "/api/v1/tenders/read/{id}": { "get": { "security": [ { @@ -5432,6 +5432,88 @@ const docTemplate = `{ } } } + }, + "/api/v1/tenders/recommend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tenders", + "parameters": [ + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 50)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by country code", + "name": "country_code", + "in": "query" + }, + { + "type": "number", + "description": "Minimum estimated value", + "name": "min_estimated_value", + "in": "query" + }, + { + "type": "number", + "description": "Maximum estimated value", + "name": "max_estimated_value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 3befcfd..fd3a269 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -5368,7 +5368,7 @@ } } }, - "/api/v1/tenders/{id}": { + "/api/v1/tenders/read/{id}": { "get": { "security": [ { @@ -5426,6 +5426,88 @@ } } } + }, + "/api/v1/tenders/recommend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tenders", + "parameters": [ + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 50)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by country code", + "name": "country_code", + "in": "query" + }, + { + "type": "number", + "description": "Minimum estimated value", + "name": "min_estimated_value", + "in": "query" + }, + { + "type": "number", + "description": "Maximum estimated value", + "name": "max_estimated_value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index baba529..a55a19b 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -4493,7 +4493,7 @@ paths: summary: Get public tenders tags: - Tenders - /api/v1/tenders/{id}: + /api/v1/tenders/read/{id}: get: description: Retrieve detailed information about a specific tender for mobile application users @@ -4529,6 +4529,57 @@ paths: summary: Get public tender details tags: - Tenders + /api/v1/tenders/recommend: + get: + description: Retrieve active tenders for mobile application users with automatic + company-based matching from customer profile + parameters: + - description: 'Number of items per page (default: 20, max: 50)' + in: query + name: limit + type: integer + - description: 'Number of items to skip (default: 0)' + in: query + name: offset + type: integer + - description: Filter by country code + in: query + name: country_code + type: string + - description: Minimum estimated value + in: query + name: min_estimated_value + type: number + - description: Maximum estimated value + in: query + name: max_estimated_value + type: number + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + additionalProperties: true + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get public tenders + tags: + - Tenders securityDefinitions: BearerAuth: description: 'Type "Bearer" followed by a space and JWT token. Example: "Bearer diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 8a7ebcf..9032192 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -128,7 +128,8 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende tendersGP.Use(customerHandler.AuthMiddleware()) { tendersGP.GET("", tenderHandler.GetPublicTenders) - tendersGP.GET("/:id", tenderHandler.GetPublicTenderDetails) + tendersGP.GET("/recommend", tenderHandler.RecommendTenders) + tendersGP.GET("/details/:id", tenderHandler.GetPublicTenderDetails) } // Public Feedback Routes diff --git a/internal/tender/form.go b/internal/tender/form.go index b3c83e8..b954179 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -27,13 +27,6 @@ type ListTendersResponse struct { Metadata *response.Meta `json:"metadata"` } -// SearchTendersResponse represents the response for searching tenders -type SearchTendersResponse struct { - Tenders []Tender `json:"tenders"` - Query string `json:"query"` - Total int `json:"total"` -} - // TenderResponse represents a tender in API responses type TenderResponse struct { ID string `json:"id"` diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 5f7ad16..f8804b4 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -285,6 +285,65 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { return response.Success(c, result, "Tenders retrieved successfully") } +// GetPublicTenders retrieves public tenders for mobile app +// @Summary Get public tenders +// @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile +// @Tags Tenders +// @Produce json +// @Param limit query int false "Number of items per page (default: 20, max: 50)" +// @Param offset query int false "Number of items to skip (default: 0)" +// @Param country_code query string false "Filter by country code" +// @Param min_estimated_value query number false "Minimum estimated value" +// @Param max_estimated_value query number false "Maximum estimated value" +// @Success 200 {object} response.APIResponse{data=map[string]interface{}} +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Security BearerAuth +// @Router /api/v1/tenders/recommend [get] +func (h *TenderHandler) RecommendTenders(c echo.Context) error { + // Parse pagination parameters (more restrictive for public API) + limit := 20 + if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { + limit = parsed + } + + offset := 0 + if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { + offset = parsed + } + + // Build search criteria for public API (only active tenders) + criteria := TenderSearchCriteria{ + Status: []TenderStatus{TenderStatusActive}, + } + + if countryCode := c.QueryParam("country_code"); countryCode != "" { + criteria.CountryCodes = []string{countryCode} + } + + // Parse numeric parameters + if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil { + criteria.MinEstimatedValue = &parsed + } + + if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil { + criteria.MaxEstimatedValue = &parsed + } + + // Get company ID from customer context for automatic matching + var companyID *string + if customerCompanyID, ok := c.Get("company_id").(string); ok { + companyID = &customerCompanyID + } + + result, err := h.service.RecommendTenders(c.Request().Context(), companyID, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve tenders") + } + + return response.Success(c, result, "Tenders retrieved successfully") +} + // GetPublicTenderDetails retrieves public tender details for mobile app // @Summary Get public tender details // @Description Retrieve detailed information about a specific tender for mobile application users @@ -295,7 +354,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security BearerAuth -// @Router /api/v1/tenders/{id} [get] +// @Router /api/v1/tenders/details/{id} [get] func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error { id := c.Param("id") if id == "" { diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 1c399c9..119fd11 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -220,7 +220,7 @@ func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriter pagination := mongopkg.Pagination{ Limit: limit, Skip: offset, - SortField: "created_at", + SortField: "publication_date", SortOrder: -1, } diff --git a/internal/tender/service.go b/internal/tender/service.go index 8c72d2e..6111006 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -3,6 +3,7 @@ package tender import ( "context" "fmt" + "math" "time" "tm/internal/company" "tm/pkg/logger" @@ -16,6 +17,7 @@ type TenderService interface { UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) DeleteTender(ctx context.Context, id string) error ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) + RecommendTenders(ctx context.Context, companyID *string, limit, offset int) (*ListTendersResponse, error) // Maintenance operations UpdateExpiredTenders(ctx context.Context) error @@ -128,13 +130,6 @@ func (s *tenderService) DeleteTender(ctx context.Context, id string) error { return nil } -// TenderWithMatch represents a tender with calculated match percentage for sorting -type TenderWithMatch struct { - Tender *Tender - MatchPercentage int - DaysUntilDeadline int -} - // ListTenders retrieves tenders with pagination and filtering func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) { s.logger.Info("Listing tenders", map[string]interface{}{ @@ -224,6 +219,56 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) }, nil } +// ListTenders retrieves tenders with pagination and filtering +func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string, limit, offset int) (*ListTendersResponse, error) { + s.logger.Info("Recommend tenders", map[string]interface{}{ + "company_id": companyID, + }) + + if limit <= 0 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + + company, err := s.companyService.GetCompanyByID(ctx, *companyID) + if err != nil { + s.logger.Error("Failed to get company", map[string]interface{}{ + "company_id": *companyID, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to get company: %w", err) + } + + tenders, total, err := s.repository.List(ctx, TenderSearchCriteria{ + Status: []TenderStatus{TenderStatusActive}, + Classifications: company.Tags.CPVCodes, + }, limit, offset) + if err != nil { + s.logger.Error("Failed to list tenders", map[string]interface{}{ + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to list tenders: %w", err) + } + + tenderResponses := make([]TenderResponse, 0, len(tenders)) + for _, tender := range tenders { + tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + } + + return &ListTendersResponse{ + Tenders: tenderResponses, + 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))), + }, + }, nil +} + // GetTenderStatistics retrieves tender statistics func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) { s.logger.Info("Retrieving tender statistics", map[string]interface{}{})