Files
tm_back/internal/tender/repository.go
T
n.nakhostin c684a12155 Update API Documentation and Enhance Tender Query Parameters
- Changed references in API documentation from `main.HealthResponse` to `bootstrap.HealthResponse` for consistency.
- Added new query parameters `publication_from` and `publication_to` to the tender listing and recommendation endpoints to allow filtering by publication date.
- Updated the `ListTendersRequest` struct to include optional fields for `from` and `to` timestamps.
- Modified the `TenderRepository` and `TenderService` interfaces to support the new filtering parameters in the tender listing functionality.
- Enhanced the service and handler layers to process the new query parameters, improving the flexibility of tender retrieval.
2025-09-02 11:03:38 +03:30

646 lines
18 KiB
Go

package tender
import (
"context"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// TenderRepository interface defines tender data access methods
type TenderRepository interface {
// Tender CRUD operations
Create(ctx context.Context, tender *Tender) error
GetByID(ctx context.Context, id string) (*Tender, error)
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error)
GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error)
Update(ctx context.Context, tender *Tender) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error)
// Bulk operations
CreateBatch(ctx context.Context, tenders []Tender) error
UpdateBatch(ctx context.Context, tenders []Tender) error
// Search and filtering
Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error)
GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error)
GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error)
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error)
// Statistics and aggregations
GetStatistics(ctx context.Context) (*TenderStatistics, error)
GetTenderCountByCountry(ctx context.Context) (map[string]int64, error)
GetTenderCountByType(ctx context.Context) (map[string]int64, error)
GetTenderCountByClassification(ctx context.Context) (map[string]int64, error)
}
// tenderRepository implements TenderRepository interface using MongoDB ORM
type tenderRepository struct {
ormRepo mongopkg.Repository[Tender]
mongoManager *mongopkg.ConnectionManager
logger logger.Logger
}
// NewTenderRepository creates a new tender repository
func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) TenderRepository {
// Create indexes for tenders collection
tenderIndexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}),
*mongopkg.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}),
*mongopkg.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}),
*mongopkg.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}),
*mongopkg.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}),
*mongopkg.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}),
*mongopkg.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}),
*mongopkg.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}),
*mongopkg.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*mongopkg.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
// Compound indexes for common queries
*mongopkg.NewIndex("status_country_idx", bson.D{
{Key: "status", Value: 1},
{Key: "country_code", Value: 1},
}),
*mongopkg.NewIndex("notice_type_status_idx", bson.D{
{Key: "notice_type_code", Value: 1},
{Key: "status", Value: 1},
}),
// Index for CPV matching queries
*mongopkg.NewIndex("cpv_matching_idx", bson.D{
{Key: "status", Value: 1},
{Key: "main_classification", Value: 1},
{Key: "additional_classifications", Value: 1},
{Key: "publication_date", Value: -1},
}),
// Text index for search
*mongopkg.CreateTextIndex("search_idx", "title", "description"),
}
// Create indexes
if err := mongoManager.CreateIndexes("tenders", tenderIndexes); err != nil {
logger.Warn("Failed to create tender indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repositories
tenderOrmRepo := mongopkg.NewRepository[Tender](mongoManager.GetCollection("tenders"), logger)
return &tenderRepository{
ormRepo: tenderOrmRepo,
mongoManager: mongoManager,
logger: logger,
}
}
// Create creates a new tender
func (r *tenderRepository) Create(ctx context.Context, tender *Tender) error {
err := r.ormRepo.Create(ctx, tender)
if err != nil {
r.logger.Error("Failed to create tender", map[string]interface{}{
"error": err.Error(),
"notice_publication_id": tender.NoticePublicationID,
})
return err
}
r.logger.Info("Tender created successfully", map[string]interface{}{
"tender_id": tender.ID,
"notice_publication_id": tender.NoticePublicationID,
})
return nil
}
// GetByID retrieves a tender by ID
func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, error) {
tender, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
r.logger.Error("Failed to get tender by ID", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return nil, err
}
return tender, nil
}
// GetByContractNoticeID retrieves a tender by contract notice ID
func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) {
filter := bson.M{"contract_notice_id": contractNoticeID}
pagination := mongopkg.Pagination{Limit: 1}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tender by contract notice ID", map[string]interface{}{
"contract_notice_id": contractNoticeID,
"error": err.Error(),
})
return nil, err
}
if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound
}
return &result.Items[0], nil
}
// GetByNoticePublicationID retrieves a tender by notice publication ID
func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) {
filter := bson.M{"notice_publication_id": noticePublicationID}
pagination := mongopkg.Pagination{Limit: 1}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tender by notice publication ID", map[string]interface{}{
"notice_publication_id": noticePublicationID,
"error": err.Error(),
})
return nil, err
}
if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound
}
return &result.Items[0], nil
}
// Update updates an existing tender
func (r *tenderRepository) Update(ctx context.Context, tender *Tender) error {
tender.UpdatedAt = time.Now().Unix()
err := r.ormRepo.Update(ctx, tender)
if err != nil {
r.logger.Error("Failed to update tender", map[string]interface{}{
"tender_id": tender.ID,
"error": err.Error(),
})
return err
}
r.logger.Info("Tender updated successfully", map[string]interface{}{
"tender_id": tender.ID,
})
return nil
}
// Delete deletes a tender by ID
func (r *tenderRepository) Delete(ctx context.Context, id string) error {
err := r.ormRepo.Delete(ctx, id)
if err != nil {
r.logger.Error("Failed to delete tender", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return err
}
r.logger.Info("Tender deleted successfully", map[string]interface{}{
"tender_id": id,
})
return nil
}
// List retrieves tenders with pagination and filtering
func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error) {
filter := r.buildSearchFilter(criteria)
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "publication_date",
SortOrder: -1,
}
if from != nil {
filter["publication_date"] = bson.M{"$gte": *from}
}
if to != nil {
filter["publication_date"] = bson.M{"$lte": *to}
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to list tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
}
return result.Items, result.TotalCount, nil
}
// CreateBatch creates multiple tenders in a single operation
func (r *tenderRepository) CreateBatch(ctx context.Context, tenders []Tender) error {
if len(tenders) == 0 {
return nil
}
for i := range tenders {
if err := r.ormRepo.Create(ctx, &tenders[i]); err != nil {
r.logger.Error("Failed to create tender in batch", map[string]interface{}{
"tender_id": tenders[i].ID,
"error": err.Error(),
})
return err
}
}
r.logger.Info("Tender batch created successfully", map[string]interface{}{
"count": len(tenders),
})
return nil
}
// UpdateBatch updates multiple tenders
func (r *tenderRepository) UpdateBatch(ctx context.Context, tenders []Tender) error {
if len(tenders) == 0 {
return nil
}
now := time.Now().Unix()
for i := range tenders {
tenders[i].UpdatedAt = now
if err := r.ormRepo.Update(ctx, &tenders[i]); err != nil {
r.logger.Error("Failed to update tender in batch", map[string]interface{}{
"tender_id": tenders[i].ID,
"error": err.Error(),
})
return err
}
}
r.logger.Info("Tender batch updated successfully", map[string]interface{}{
"count": len(tenders),
})
return nil
}
// Search searches tenders based on criteria
func (r *tenderRepository) Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error) {
filter := r.buildSearchFilter(criteria)
pagination := mongopkg.Pagination{
Limit: 100,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to search tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetByCountryCode retrieves tenders by country code
func (r *tenderRepository) GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error) {
filter := bson.M{"country_code": countryCode}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tenders by country code", map[string]interface{}{
"country_code": countryCode,
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetByStatus retrieves tenders by status
func (r *tenderRepository) GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error) {
filter := bson.M{"status": status}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tenders by status", map[string]interface{}{
"status": status,
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetExpiredTenders retrieves tenders that are expired
func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) {
filter := bson.M{
"$or": []bson.M{
{"status": TenderStatusExpired},
{
"tender_deadline": bson.M{
"$lt": beforeDate,
},
"status": bson.M{"$ne": TenderStatusExpired},
},
},
}
pagination := mongopkg.Pagination{
Limit: 1000, // Large limit for maintenance operations
SortField: "tender_deadline",
SortOrder: 1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get expired tenders", map[string]interface{}{
"before_date": beforeDate,
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetActiveTenders retrieves active tenders
func (r *tenderRepository) GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error) {
filter := bson.M{"status": TenderStatusActive}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "tender_deadline",
SortOrder: 1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get active tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetStatistics calculates tender statistics using aggregation
func (r *tenderRepository) GetStatistics(ctx context.Context) (*TenderStatistics, error) {
stats := &TenderStatistics{
LastUpdated: time.Now().Unix(),
}
// Get total count
total, err := r.ormRepo.Count(ctx, bson.M{})
if err != nil {
return nil, err
}
stats.TotalTenders = total
// Get active count
active, err := r.ormRepo.Count(ctx, bson.M{"status": TenderStatusActive})
if err != nil {
return nil, err
}
stats.ActiveTenders = active
// Get expired count
expired, err := r.ormRepo.Count(ctx, bson.M{"status": TenderStatusExpired})
if err != nil {
return nil, err
}
stats.ExpiredTenders = expired
// Get statistics by country, type, and classification
stats.TendersByCountry, _ = r.GetTenderCountByCountry(ctx)
stats.TendersByType, _ = r.GetTenderCountByType(ctx)
stats.TendersByClassification, _ = r.GetTenderCountByClassification(ctx)
// Calculate value statistics using aggregation
pipeline := mongo.Pipeline{
{
{Key: "$group", Value: bson.M{
"_id": nil,
"total_estimated_value": bson.M{"$sum": "$estimated_value"},
"count": bson.M{"$sum": 1},
}},
},
}
result, err := r.ormRepo.Aggregate(ctx, pipeline)
if err == nil && len(result) > 0 {
if totalValue, ok := result[0]["total_estimated_value"].(float64); ok {
stats.TotalEstimatedValue = totalValue
if count, ok := result[0]["count"].(int32); ok && count > 0 {
stats.AverageEstimatedValue = totalValue / float64(count)
}
}
}
return stats, nil
}
// GetTenderCountByCountry gets tender count grouped by country
func (r *tenderRepository) GetTenderCountByCountry(ctx context.Context) (map[string]int64, error) {
pipeline := mongo.Pipeline{
{
{Key: "$group", Value: bson.M{
"_id": "$country_code",
"count": bson.M{"$sum": 1},
}},
},
}
results, err := r.ormRepo.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
counts := make(map[string]int64)
for _, result := range results {
if country, ok := result["_id"].(string); ok {
if count, ok := result["count"].(int32); ok {
counts[country] = int64(count)
}
}
}
return counts, nil
}
// GetTenderCountByType gets tender count grouped by type
func (r *tenderRepository) GetTenderCountByType(ctx context.Context) (map[string]int64, error) {
pipeline := mongo.Pipeline{
{
{Key: "$group", Value: bson.M{
"_id": "$notice_type_code",
"count": bson.M{"$sum": 1},
}},
},
}
results, err := r.ormRepo.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
counts := make(map[string]int64)
for _, result := range results {
if noticeType, ok := result["_id"].(string); ok {
if count, ok := result["count"].(int32); ok {
counts[noticeType] = int64(count)
}
}
}
return counts, nil
}
// GetTenderCountByClassification gets tender count grouped by classification
func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) (map[string]int64, error) {
pipeline := mongo.Pipeline{
{
{Key: "$group", Value: bson.M{
"_id": "$main_classification",
"count": bson.M{"$sum": 1},
}},
},
{
{Key: "$limit", Value: 20},
},
}
results, err := r.ormRepo.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
counts := make(map[string]int64)
for _, result := range results {
if classification, ok := result["_id"].(string); ok {
if count, ok := result["count"].(int32); ok {
counts[classification] = int64(count)
}
}
}
return counts, nil
}
// buildSearchFilter builds MongoDB filter from search criteria
func (r *tenderRepository) buildSearchFilter(criteria TenderSearchCriteria) bson.M {
filter := bson.M{}
if criteria.Query != "" {
filter["$text"] = bson.M{"$search": criteria.Query}
}
if criteria.NoticeType != "" {
filter["notice_type_code"] = criteria.NoticeType
}
if criteria.ProcurementType != "" {
filter["procurement_type_code"] = criteria.ProcurementType
}
if len(criteria.CountryCodes) > 0 {
filter["country_code"] = bson.M{"$in": criteria.CountryCodes}
}
if len(criteria.RegionCodes) > 0 {
filter["region_code"] = bson.M{"$in": criteria.RegionCodes}
}
if len(criteria.Classifications) > 0 {
filter["$or"] = []bson.M{
{"main_classification": bson.M{"$in": criteria.Classifications}},
{"additional_classifications": bson.M{"$in": criteria.Classifications}},
}
}
if criteria.MinEstimatedValue != nil || criteria.MaxEstimatedValue != nil {
valueFilter := bson.M{}
if criteria.MinEstimatedValue != nil {
valueFilter["$gte"] = *criteria.MinEstimatedValue
}
if criteria.MaxEstimatedValue != nil {
valueFilter["$lte"] = *criteria.MaxEstimatedValue
}
filter["estimated_value"] = valueFilter
}
if criteria.Currency != "" {
filter["currency"] = criteria.Currency
}
if criteria.DeadlineFrom != nil || criteria.DeadlineTo != nil {
deadlineFilter := bson.M{}
if criteria.DeadlineFrom != nil {
deadlineFilter["$gte"] = *criteria.DeadlineFrom
}
if criteria.DeadlineTo != nil {
deadlineFilter["$lte"] = *criteria.DeadlineTo
}
filter["tender_deadline"] = deadlineFilter
}
if criteria.PublicationDateFrom != nil || criteria.PublicationDateTo != nil {
pubDateFilter := bson.M{}
if criteria.PublicationDateFrom != nil {
pubDateFilter["$gte"] = *criteria.PublicationDateFrom
}
if criteria.PublicationDateTo != nil {
pubDateFilter["$lte"] = *criteria.PublicationDateTo
}
filter["publication_date"] = pubDateFilter
}
if len(criteria.Status) > 0 {
filter["status"] = bson.M{"$in": criteria.Status}
}
if len(criteria.Source) > 0 {
filter["source"] = bson.M{"$in": criteria.Source}
}
if criteria.BuyerOrganizationID != "" {
filter["buyer_organization.id"] = criteria.BuyerOrganizationID
}
if len(criteria.Languages) > 0 {
filter["notice_language_code"] = bson.M{"$in": criteria.Languages}
}
return filter
}