Files
tm_back/internal/tender/service.go
T
n.nakhostin a342da193e Refactor Feedback Module for Consistency and Clarity
- Updated feedback repository and service methods to use a consistent naming convention, changing `NewTenderRepository` and similar methods to `NewRepository`.
- Refactored feedback handler methods to improve clarity by renaming methods such as `ListFeedback` to `Search` and `GetFeedback` to `Get`.
- Enhanced feedback response structures to include detailed company and tender information, improving the clarity of feedback data returned to API consumers.
- Updated Swagger documentation to reflect changes in feedback response structures and endpoint paths, ensuring accurate representation of the API for consumers.
2025-09-24 12:20:19 +03:30

336 lines
9.8 KiB
Go

package tender
import (
"context"
"fmt"
"time"
"tm/internal/company"
"tm/pkg/logger"
"tm/pkg/response"
)
// TenderService interface defines tender business logic operations
type Service interface {
// Tender operations
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, pagination *response.Pagination) error
}
// tenderService implements TenderService interface
type tenderService struct {
repository TenderRepository
companyService company.Service
logger logger.Logger
}
// NewService creates a new tender service
func NewService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service {
return &tenderService{
repository: repository,
companyService: companyService,
logger: logger,
}
}
// GetTenderByID retrieves a tender by ID
func (s *tenderService) GetByID(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.ToResponse(), nil
}
// UpdateTender updates an existing tender
func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, 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.ToResponse(), nil
}
// 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,
})
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) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
s.logger.Info("Listing tenders", map[string]interface{}{
"limit": pagination.Limit,
"offset": pagination.Offset,
"company_id": form.CompanyID,
"from": form.DeadlineFrom,
"to": form.DeadlineTo,
})
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(),
})
return nil, fmt.Errorf("failed to list tenders: %w", err)
}
// If no company ID provided, return tenders without match percentage
if form.CompanyID == nil || *form.CompanyID == "" {
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
}
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.Response(total),
}, nil
}
// Get company CPV codes for match calculation
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": *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.ToResponse())
}
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.Response(total),
},
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": *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.ToResponse())
}
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.Response(total),
},
nil
}
// 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": form.CompanyID,
})
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{
"company_id": form.CompanyID,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get company: %w", err)
}
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(),
})
return nil, fmt.Errorf("failed to list tenders: %w", err)
}
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
}
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{}{})
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, 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
form := &SearchForm{
Status: []string{string(TenderStatusExpired), string(TenderStatusCancelled), string(TenderStatusAwarded)},
}
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(),
})
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
}