package tender import ( "context" "time" "tm/pkg/logger" orm "tm/pkg/mongo" "tm/pkg/response" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/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) GetTenderCountByCountry(ctx context.Context) (map[string]int64, error) GetTenderCountByType(ctx context.Context) (map[string]int64, error) GetTenderCountByClassification(ctx context.Context) (map[string]int64, error) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) Update(ctx context.Context, tender *Tender) error Delete(ctx context.Context, id string) error GetStatistics(ctx context.Context) (*TenderStatistics, error) } func collectionName() string { return "tenders" } // tenderRepository implements TenderRepository interface using MongoDB ORM type tenderRepository struct { ormRepo orm.Repository[Tender] mongoManager *orm.ConnectionManager logger logger.Logger } // NewTenderRepository creates a new tender repository func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository { // Create indexes for tenders collection tenderIndexes := []orm.Index{ *orm.CreateUniqueIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}), *orm.NewIndex("project_name_idx", bson.D{{Key: "project_name", Value: 1}}), *orm.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}), *orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), *orm.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}), *orm.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}), *orm.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}), *orm.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}), *orm.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}), *orm.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}), *orm.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}), *orm.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}), *orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), *orm.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}), // Compound indexes for common queries *orm.NewIndex("status_country_idx", bson.D{ {Key: "status", Value: 1}, {Key: "country_code", Value: 1}, }), *orm.NewIndex("notice_type_status_idx", bson.D{ {Key: "notice_type_code", Value: 1}, {Key: "status", Value: 1}, }), // Index for CPV matching queries *orm.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 *orm.CreateTextIndex("search_idx", "title", "description"), } // Create indexes if err := mongoManager.CreateIndexes(collectionName(), tenderIndexes); err != nil { logger.Warn("Failed to create tender indexes", map[string]interface{}{ "error": err.Error(), }) } // Create ORM repositories tenderOrmRepo := orm.NewRepository[Tender](mongoManager.GetCollection(collectionName()), logger) return &tenderRepository{ ormRepo: tenderOrmRepo, mongoManager: mongoManager, logger: logger, } } // Create creates a new tender func (r *tenderRepository) Create(ctx context.Context, tender *Tender) error { now := time.Now().Unix() tender.SetCreatedAt(now) 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 := orm.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, orm.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 := orm.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, orm.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 } // Search searches tenders based on criteria func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) { filter := r.buildSearchFilter(form) // Build sort sort := "created_at" if pagination.SortBy != "" { sort = pagination.SortBy } sortOrder := "desc" if pagination.SortOrder != "" { sortOrder = pagination.SortOrder } // Build pagination paginationBuilder := orm.NewPaginationBuilder(). Limit(pagination.Limit). Skip(pagination.Offset). SortBy(sort, sortOrder). Build() result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) if err != nil { r.logger.Error("Failed to search tenders", map[string]interface{}{ "error": err.Error(), }) return nil, 0, err } return result.Items, result.TotalCount, 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 := orm.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 } // 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 form func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M { filter := bson.M{} if form.Search != nil { filter["$text"] = bson.M{"$search": *form.Search} } if form.NoticeType != nil { filter["notice_type_code"] = form.NoticeType } if form.ProcurementType != nil { filter["procurement_type_code"] = form.ProcurementType } if len(form.CountryCodes) > 0 { filter["country_code"] = bson.M{"$in": form.CountryCodes} } if len(form.RegionCodes) > 0 { filter["region_code"] = bson.M{"$in": form.RegionCodes} } if len(form.Classifications) > 0 { filter["$or"] = []bson.M{ {"main_classification": bson.M{"$in": form.Classifications}}, {"additional_classifications": bson.M{"$in": form.Classifications}}, } } if form.MinEstimatedValue != nil || form.MaxEstimatedValue != nil { valueFilter := bson.M{} if form.MinEstimatedValue != nil { valueFilter["$gte"] = *form.MinEstimatedValue } if form.MaxEstimatedValue != nil { valueFilter["$lte"] = *form.MaxEstimatedValue } filter["estimated_value"] = valueFilter } if form.Currency != "" { filter["currency"] = form.Currency } if form.DeadlineFrom != nil || form.DeadlineTo != nil { deadlineFilter := bson.M{} if form.DeadlineFrom != nil { deadlineFilter["$gte"] = *form.DeadlineFrom } if form.DeadlineTo != nil { deadlineFilter["$lte"] = *form.DeadlineTo } filter["tender_deadline"] = deadlineFilter } if form.PublicationDateFrom != nil || form.PublicationDateTo != nil { pubDateFilter := bson.M{} if form.PublicationDateFrom != nil { pubDateFilter["$gte"] = *form.PublicationDateFrom } if form.PublicationDateTo != nil { pubDateFilter["$lte"] = *form.PublicationDateTo } filter["publication_date"] = pubDateFilter } if len(form.Status) > 0 { filter["status"] = bson.M{"$in": form.Status} } if len(form.Source) > 0 { filter["source"] = bson.M{"$in": form.Source} } if form.BuyerOrganizationID != nil { filter["buyer_organization.id"] = *form.BuyerOrganizationID } if len(form.Languages) > 0 { filter["notice_language_code"] = bson.M{"$in": form.Languages} } return filter }