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 }