ee830f8c1b
- Introduced a new search functionality for companies, allowing advanced filtering capabilities including tags, business criteria, and location. - Updated the API documentation to reflect changes in the search endpoint, enhancing clarity for API consumers. - Refactored existing company-related API endpoints for consistency, including renaming and restructuring routes. - Enhanced response structures to return company entities directly, simplifying the response handling in API endpoints. - Removed unused query parameters and handlers, streamlining the company management functionality.
363 lines
10 KiB
Go
363 lines
10 KiB
Go
package tender
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
"tm/internal/company"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/response"
|
|
)
|
|
|
|
// TenderService interface defines tender business logic operations
|
|
type TenderService 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)
|
|
|
|
// Maintenance operations
|
|
UpdateExpiredTenders(ctx context.Context) error
|
|
CleanupOldData(ctx context.Context, olderThan time.Duration) error
|
|
}
|
|
|
|
// tenderService implements TenderService interface
|
|
type tenderService struct {
|
|
repository TenderRepository
|
|
companyService company.Service
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewTenderService creates a new tender service
|
|
func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) TenderService {
|
|
return &tenderService{
|
|
repository: repository,
|
|
companyService: companyService,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetTenderByID retrieves a tender by ID
|
|
func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) {
|
|
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
tender, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
|
}
|
|
|
|
return tender.ToTenderResponse(), nil
|
|
}
|
|
|
|
// UpdateTender updates an existing tender
|
|
func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) {
|
|
s.logger.Info("Updating tender", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
})
|
|
|
|
// Get existing tender
|
|
tender, err := s.repository.GetByID(ctx, req.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to find tender for update", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to find tender: %w", err)
|
|
}
|
|
|
|
// Update fields if provided
|
|
if req.Title != nil {
|
|
tender.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
tender.Description = *req.Description
|
|
}
|
|
if req.Status != nil {
|
|
tender.Status = *req.Status
|
|
}
|
|
if req.EstimatedValue != nil {
|
|
tender.EstimatedValue = *req.EstimatedValue
|
|
}
|
|
if req.Currency != nil {
|
|
tender.Currency = *req.Currency
|
|
}
|
|
if req.TenderDeadline != nil {
|
|
tender.TenderDeadline = *req.TenderDeadline
|
|
}
|
|
|
|
if err := s.repository.Update(ctx, tender); err != nil {
|
|
s.logger.Error("Failed to update tender", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to update tender: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Tender updated successfully", map[string]interface{}{
|
|
"tender_id": tender.ID,
|
|
})
|
|
|
|
return tender, nil
|
|
}
|
|
|
|
// DeleteTender deletes a tender
|
|
func (s *tenderService) DeleteTender(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
if err := s.repository.Delete(ctx, id); err != nil {
|
|
s.logger.Error("Failed to delete tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to delete tender: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Tender deleted successfully", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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{}{
|
|
"limit": req.Limit,
|
|
"offset": req.Offset,
|
|
"company_id": req.CompanyID,
|
|
"from": req.From,
|
|
"to": req.To,
|
|
})
|
|
|
|
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)
|
|
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)
|
|
}
|
|
|
|
// If no company ID provided, return tenders without match percentage
|
|
if req.CompanyID == nil || *req.CompanyID == "" {
|
|
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: req.Limit,
|
|
Offset: req.Offset,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Get company CPV codes for match calculation
|
|
comp, err := s.companyService.GetByID(ctx, *req.CompanyID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get company for match calculation", map[string]interface{}{
|
|
"company_id": *req.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())
|
|
}
|
|
|
|
return &ListTendersResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: &response.Meta{
|
|
Total: int(total),
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
var companyCPVCodes []string
|
|
if comp.Tags != nil && comp.Tags.CPVCodes != nil {
|
|
companyCPVCodes = comp.Tags.CPVCodes
|
|
}
|
|
|
|
s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
|
|
"company_id": *req.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())
|
|
}
|
|
|
|
return &ListTendersResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: &response.Meta{
|
|
Total: int(total),
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
},
|
|
}, 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) {
|
|
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.GetByID(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, from, to)
|
|
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{}{})
|
|
|
|
stats, err := s.repository.GetStatistics(ctx)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve tender statistics", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to retrieve statistics: %w", err)
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// UpdateExpiredTenders updates status of expired tenders
|
|
func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error {
|
|
s.logger.Info("Updating expired tenders", map[string]interface{}{})
|
|
|
|
// Get tenders that should be expired
|
|
expiredTenders, err := s.repository.GetExpiredTenders(ctx, time.Now().Unix())
|
|
if err != nil {
|
|
s.logger.Error("Failed to get expired tenders", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to get expired tenders: %w", err)
|
|
}
|
|
|
|
updatedCount := 0
|
|
for _, tender := range expiredTenders {
|
|
if tender.Status != TenderStatusExpired {
|
|
tender.UpdateStatus(TenderStatusExpired)
|
|
if err := s.repository.Update(ctx, &tender); err != nil {
|
|
s.logger.Error("Failed to update expired tender", map[string]interface{}{
|
|
"tender_id": tender.ID,
|
|
"error": err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
updatedCount++
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Expired tenders updated", map[string]interface{}{
|
|
"total_expired": len(expiredTenders),
|
|
"updated": updatedCount,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupOldData removes old data based on retention policy
|
|
func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration) error {
|
|
s.logger.Info("Cleaning up old data", map[string]interface{}{
|
|
"older_than": olderThan.String(),
|
|
})
|
|
|
|
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},
|
|
}
|
|
|
|
oldTenders, err := s.repository.Search(ctx, criteria)
|
|
if err != nil {
|
|
s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to find old tenders: %w", err)
|
|
}
|
|
|
|
cleanupCount := 0
|
|
for _, tender := range oldTenders {
|
|
if tender.CreatedAt < cutoffTime {
|
|
cleanupCount++
|
|
// Here you would delete the tender if implementing actual cleanup
|
|
// s.repository.Delete(ctx, tender.ID)
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Old data cleanup completed", map[string]interface{}{
|
|
"found_old_tenders": cleanupCount,
|
|
"cutoff_time": cutoffTime,
|
|
})
|
|
|
|
return nil
|
|
}
|