Refactor Tender Management API Endpoints and Update Documentation
- Renamed and restructured tender-related API endpoints for improved clarity and consistency, including changing the route from `/admin/tenders` to `/admin/v1/companies` and updating the associated methods. - Introduced a new SearchForm structure for listing tenders with advanced filtering capabilities, enhancing the API's search functionality. - Updated the service and repository layers to align with the new endpoint structure, ensuring proper handling of tender data. - Enhanced Swagger and YAML documentation to reflect the changes in endpoint structure, including detailed descriptions and examples for the new search functionality. - Improved error handling and response structures to provide clearer feedback to API consumers, ensuring a more robust and user-friendly experience.
This commit is contained in:
+60
-87
@@ -3,7 +3,6 @@ package tender
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
"tm/internal/company"
|
||||
"tm/pkg/logger"
|
||||
@@ -11,17 +10,17 @@ import (
|
||||
)
|
||||
|
||||
// TenderService interface defines tender business logic operations
|
||||
type TenderService interface {
|
||||
type Service interface {
|
||||
// Tender operations
|
||||
GetTenderByID(ctx context.Context, id string) (*TenderResponse, error)
|
||||
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, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error)
|
||||
GetByID(ctx context.Context, id string) (*TenderResponse, error)
|
||||
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
||||
Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
||||
|
||||
// Maintenance operations
|
||||
UpdateExpiredTenders(ctx context.Context) error
|
||||
CleanupOldData(ctx context.Context, olderThan time.Duration) error
|
||||
CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error
|
||||
}
|
||||
|
||||
// tenderService implements TenderService interface
|
||||
@@ -32,7 +31,7 @@ type tenderService struct {
|
||||
}
|
||||
|
||||
// NewTenderService creates a new tender service
|
||||
func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) TenderService {
|
||||
func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service {
|
||||
return &tenderService{
|
||||
repository: repository,
|
||||
companyService: companyService,
|
||||
@@ -41,7 +40,7 @@ func NewTenderService(repository TenderRepository, companyService company.Servic
|
||||
}
|
||||
|
||||
// GetTenderByID retrieves a tender by ID
|
||||
func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) {
|
||||
func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse, error) {
|
||||
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
})
|
||||
@@ -55,11 +54,11 @@ func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderRe
|
||||
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
||||
}
|
||||
|
||||
return tender.ToTenderResponse(), nil
|
||||
return tender.ToResponse(), nil
|
||||
}
|
||||
|
||||
// UpdateTender updates an existing tender
|
||||
func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) {
|
||||
func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) {
|
||||
s.logger.Info("Updating tender", map[string]interface{}{
|
||||
"tender_id": req.ID,
|
||||
})
|
||||
@@ -106,11 +105,11 @@ func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderReques
|
||||
"tender_id": tender.ID,
|
||||
})
|
||||
|
||||
return tender, nil
|
||||
return tender.ToResponse(), nil
|
||||
}
|
||||
|
||||
// DeleteTender deletes a tender
|
||||
func (s *tenderService) DeleteTender(ctx context.Context, id string) error {
|
||||
// Delete deletes a tender
|
||||
func (s *tenderService) Delete(ctx context.Context, id string) error {
|
||||
s.logger.Info("Deleting tender", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
})
|
||||
@@ -131,23 +130,16 @@ func (s *tenderService) DeleteTender(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// ListTenders retrieves tenders with pagination and filtering
|
||||
func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) {
|
||||
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
||||
s.logger.Info("Listing tenders", map[string]interface{}{
|
||||
"limit": req.Limit,
|
||||
"offset": req.Offset,
|
||||
"company_id": req.CompanyID,
|
||||
"from": req.From,
|
||||
"to": req.To,
|
||||
"limit": pagination.Limit,
|
||||
"offset": pagination.Offset,
|
||||
"company_id": form.CompanyID,
|
||||
"from": form.DeadlineFrom,
|
||||
"to": form.DeadlineTo,
|
||||
})
|
||||
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 20
|
||||
}
|
||||
if req.Limit > 100 {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
tenders, total, err := s.repository.List(ctx, req.Criteria, req.Limit, req.Offset, req.From, req.To)
|
||||
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -156,43 +148,36 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest)
|
||||
}
|
||||
|
||||
// If no company ID provided, return tenders without match percentage
|
||||
if req.CompanyID == nil || *req.CompanyID == "" {
|
||||
if form.CompanyID == nil || *form.CompanyID == "" {
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderResponses = append(tenderResponses, *tender.ToTenderResponse())
|
||||
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
||||
}
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
},
|
||||
return &SearchResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: pagination.Response(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get company CPV codes for match calculation
|
||||
comp, err := s.companyService.GetByID(ctx, *req.CompanyID)
|
||||
comp, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company for match calculation", map[string]interface{}{
|
||||
"company_id": *req.CompanyID,
|
||||
"company_id": *form.CompanyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Continue without match percentage if company not found
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderResponses = append(tenderResponses, *tender.ToTenderResponse())
|
||||
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
||||
}
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
return &SearchResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: pagination.Response(total),
|
||||
},
|
||||
}, nil
|
||||
nil
|
||||
}
|
||||
|
||||
var companyCPVCodes []string
|
||||
@@ -201,52 +186,42 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest)
|
||||
}
|
||||
|
||||
s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
|
||||
"company_id": *req.CompanyID,
|
||||
"company_id": *form.CompanyID,
|
||||
"company_cpv_codes": len(companyCPVCodes),
|
||||
"total_tenders": len(tenders),
|
||||
})
|
||||
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderResponses = append(tenderResponses, *tender.ToTenderResponse())
|
||||
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
||||
}
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
return &SearchResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: pagination.Response(total),
|
||||
},
|
||||
}, nil
|
||||
nil
|
||||
}
|
||||
|
||||
// ListTenders retrieves tenders with pagination and filtering
|
||||
func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error) {
|
||||
// Recommend retrieves tenders with pagination and filtering
|
||||
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
||||
s.logger.Info("Recommend tenders", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"company_id": form.CompanyID,
|
||||
})
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
company, err := s.companyService.GetByID(ctx, *companyID)
|
||||
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company", map[string]interface{}{
|
||||
"company_id": *companyID,
|
||||
"company_id": form.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, from, to)
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
form.Classifications = company.Tags.CPVCodes
|
||||
|
||||
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -256,21 +231,17 @@ func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string,
|
||||
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderResponses = append(tenderResponses, *tender.ToTenderResponse())
|
||||
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
||||
}
|
||||
|
||||
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))),
|
||||
},
|
||||
return &SearchResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: pagination.Response(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete deletes a tender
|
||||
|
||||
// GetTenderStatistics retrieves tender statistics
|
||||
func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) {
|
||||
s.logger.Info("Retrieving tender statistics", map[string]interface{}{})
|
||||
@@ -323,20 +294,22 @@ func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// CleanupOldData removes old data based on retention policy
|
||||
func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration) error {
|
||||
func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error {
|
||||
s.logger.Info("Cleaning up old data", map[string]interface{}{
|
||||
"older_than": olderThan.String(),
|
||||
"limit": pagination.Limit,
|
||||
"offset": pagination.Offset,
|
||||
})
|
||||
|
||||
cutoffTime := time.Now().Add(-olderThan).Unix()
|
||||
|
||||
// For now, we'll just log what would be cleaned up
|
||||
// In production, you might want to actually delete old data
|
||||
criteria := TenderSearchCriteria{
|
||||
Status: []TenderStatus{TenderStatusExpired, TenderStatusCancelled, TenderStatusAwarded},
|
||||
form := &SearchForm{
|
||||
Status: []string{string(TenderStatusExpired), string(TenderStatusCancelled), string(TenderStatusAwarded)},
|
||||
}
|
||||
|
||||
oldTenders, err := s.repository.Search(ctx, criteria)
|
||||
oldTenders, _, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
||||
Reference in New Issue
Block a user