package tender_approval import ( "context" "errors" "strings" "time" "tm/pkg/logger" "tm/pkg/mongo" "go.mongodb.org/mongo-driver/v2/bson" mongodriver "go.mongodb.org/mongo-driver/v2/mongo" ) // Repository defines the interface for tender approval data operations type Repository interface { Create(ctx context.Context, tenderApproval *TenderApproval) error GetByID(ctx context.Context, id string) (*TenderApproval, error) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) GetByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, limit int, offset int, from *int64, to *int64) ([]*TenderApproval, int64, error) GetByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) Update(ctx context.Context, tenderApproval *TenderApproval) error Delete(ctx context.Context, id string) error List(ctx context.Context, limit, offset int) ([]*TenderApproval, error) Search(ctx context.Context, tenderID *string, companyID *string, status []ApprovalStatus, submissionMode []SubmissionMode, createdAtFrom *int64, createdAtTo *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderApproval, error) Count(ctx context.Context) (int64, error) CountByStatus(ctx context.Context, status ApprovalStatus) (int64, error) CountBySubmissionMode(ctx context.Context, submissionMode SubmissionMode) (int64, error) CountByTenderID(ctx context.Context, tenderID string) (int64, error) CountByCompanyID(ctx context.Context, companyID string) (int64, error) GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error) GetCompanyTenderApprovalStats(ctx context.Context, companyID string) (*CompanyTenderApprovalStatsResponse, error) SearchByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error) ListRejectedTenderIDsByCompanyID(ctx context.Context, companyID string) ([]string, error) } // tenderApprovalRepository implements the Repository interface using the MongoDB ORM type tenderApprovalRepository struct { ormRepo mongo.Repository[TenderApproval] logger logger.Logger } // NewTenderApprovalRepository creates a new tender approval repository func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository { // Create indexes using the ORM's index management indexes := []mongo.Index{ *mongo.CreateUniqueIndex("tender_company_idx", bson.D{{Key: "tender_id", Value: 1}, {Key: "company_id", Value: 1}}), *mongo.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}), *mongo.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}), *mongo.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), *mongo.NewIndex("submission_mode_idx", bson.D{{Key: "submission_mode", Value: 1}}), *mongo.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), *mongo.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}), } // Create indexes err := mongoManager.CreateIndexes("tender_approvals", indexes) if err != nil { logger.Warn("Failed to create tender approval indexes", map[string]interface{}{ "error": err.Error(), }) } // Create ORM repository ormRepo := mongo.NewRepository[TenderApproval](mongoManager.GetCollection("tender_approvals"), logger) return &tenderApprovalRepository{ ormRepo: ormRepo, logger: logger, } } // Create creates a new tender approval func (r *tenderApprovalRepository) Create(ctx context.Context, tenderApproval *TenderApproval) error { // Set created/updated timestamps using Unix timestamps now := time.Now().Unix() tenderApproval.SetCreatedAt(now) tenderApproval.SetUpdatedAt(now) // Use ORM to create tender approval err := r.ormRepo.Create(ctx, tenderApproval) if err != nil { r.logger.Error("Failed to create tender approval", map[string]interface{}{ "error": err.Error(), "tender_id": tenderApproval.TenderID, "company_id": tenderApproval.CompanyID, }) return err } r.logger.Info("Tender approval created successfully", map[string]interface{}{ "tender_approval_id": tenderApproval.ID, "tender_id": tenderApproval.TenderID, "company_id": tenderApproval.CompanyID, "status": tenderApproval.Status, }) return nil } // GetByID retrieves a tender approval by ID func (r *tenderApprovalRepository) GetByID(ctx context.Context, id string) (*TenderApproval, error) { tenderApproval, err := r.ormRepo.FindByID(ctx, id) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, ErrTenderApprovalNotFound } r.logger.Error("Failed to get tender approval by ID", map[string]interface{}{ "error": err.Error(), "tender_approval_id": id, }) return nil, err } return tenderApproval, nil } // GetByTenderAndCompany retrieves a tender approval by tender ID and company ID func (r *tenderApprovalRepository) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) { filter := bson.M{ "tender_id": tenderID, "company_id": companyID, } tenderApproval, err := r.ormRepo.FindOne(ctx, filter) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, ErrTenderApprovalNotFound } r.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{ "error": err.Error(), "tender_id": tenderID, "company_id": companyID, }) return nil, err } return tenderApproval, nil } // GetByTenderID retrieves all tender approvals for a specific tender func (r *tenderApprovalRepository) GetByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) { filter := bson.M{"tender_id": tenderID} result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().SortDesc("created_at").Build()) if err != nil { r.logger.Error("Failed to get tender approvals by tender ID", map[string]interface{}{ "error": err.Error(), "tender_id": tenderID, }) return nil, err } // Convert []TenderApproval to []*TenderApproval tenderApprovals := make([]*TenderApproval, len(result.Items)) for i := range result.Items { tenderApprovals[i] = &result.Items[i] } return tenderApprovals, nil } // GetByCompanyID retrieves all tender approvals for a specific company with optional filters func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, limit int, offset int, from *int64, to *int64) ([]*TenderApproval, int64, error) { filter := bson.M{"company_id": companyID} pagination := mongo.Pagination{ Limit: limit, Skip: offset, SortField: "created_at", SortOrder: -1, } // Add submission mode filter if provided if submissionMode != nil { filter["submission_mode"] = *submissionMode } // Add status filter if provided if status != nil { filter["status"] = *status } if from != nil { filter["created_at"] = bson.M{"$gte": *from} } if to != nil { filter["created_at"] = bson.M{"$lte": *to} } result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { r.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, 0, err } // Convert []TenderApproval to []*TenderApproval tenderApprovals := make([]*TenderApproval, len(result.Items)) for i := range result.Items { tenderApprovals[i] = &result.Items[i] } return tenderApprovals, result.TotalCount, nil } // GetByIDs retrieves multiple tender approvals by their IDs func (r *tenderApprovalRepository) GetByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) { if len(ids) == 0 { return []*TenderApproval{}, nil } // Convert string IDs to ObjectIDs for MongoDB query objectIDs := make([]interface{}, len(ids)) for i, id := range ids { objectID, err := bson.ObjectIDFromHex(id) if err != nil { r.logger.Warn("Invalid tender approval ID format", map[string]interface{}{ "id": id, "error": err.Error(), }) continue } objectIDs[i] = objectID } if len(objectIDs) == 0 { return []*TenderApproval{}, nil } filter := bson.M{"_id": bson.M{"$in": objectIDs}} result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().Limit(len(ids)).Build()) if err != nil { r.logger.Error("Failed to get tender approvals by IDs", map[string]interface{}{ "error": err.Error(), "ids": ids, }) return nil, err } // Convert []TenderApproval to []*TenderApproval tenderApprovals := make([]*TenderApproval, len(result.Items)) for i := range result.Items { tenderApprovals[i] = &result.Items[i] } r.logger.Info("Retrieved tender approvals by IDs", map[string]interface{}{ "requested_count": len(ids), "found_count": len(tenderApprovals), }) return tenderApprovals, nil } // Update updates a tender approval func (r *tenderApprovalRepository) Update(ctx context.Context, tenderApproval *TenderApproval) error { // Set updated timestamp using Unix timestamp tenderApproval.SetUpdatedAt(time.Now().Unix()) // Use ORM to update tender approval err := r.ormRepo.Update(ctx, tenderApproval) if err != nil { r.logger.Error("Failed to update tender approval", map[string]interface{}{ "error": err.Error(), "tender_approval_id": tenderApproval.ID, }) return err } r.logger.Info("Tender approval updated successfully", map[string]interface{}{ "tender_approval_id": tenderApproval.ID, "tender_id": tenderApproval.TenderID, "company_id": tenderApproval.CompanyID, }) return nil } // Delete deletes a tender approval func (r *tenderApprovalRepository) Delete(ctx context.Context, id string) error { // Get tender approval first tenderApproval, err := r.GetByID(ctx, id) if err != nil { return err } // Delete from database err = r.ormRepo.Delete(ctx, id) if err != nil { r.logger.Error("Failed to delete tender approval", map[string]interface{}{ "error": err.Error(), "tender_approval_id": id, }) return err } r.logger.Info("Tender approval deleted successfully", map[string]interface{}{ "tender_approval_id": id, "tender_id": tenderApproval.TenderID, "company_id": tenderApproval.CompanyID, }) return nil } // List retrieves tender approvals with pagination func (r *tenderApprovalRepository) List(ctx context.Context, limit, offset int) ([]*TenderApproval, error) { // Build pagination pagination := mongo.NewPaginationBuilder(). Limit(limit). Skip(offset). SortDesc("created_at"). Build() // Use ORM to find tender approvals result, err := r.ormRepo.FindAll(ctx, bson.M{}, pagination) if err != nil { r.logger.Error("Failed to list tender approvals", map[string]interface{}{ "error": err.Error(), "limit": limit, "offset": offset, }) return nil, err } // Convert []TenderApproval to []*TenderApproval tenderApprovals := make([]*TenderApproval, len(result.Items)) for i := range result.Items { tenderApprovals[i] = &result.Items[i] } return tenderApprovals, nil } // Search retrieves tender approvals with search and filters func (r *tenderApprovalRepository) Search(ctx context.Context, tenderID *string, companyID *string, status []ApprovalStatus, submissionMode []SubmissionMode, createdAtFrom *int64, createdAtTo *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderApproval, error) { // Build filter filter := bson.M{} if tenderID != nil { filter["tender_id"] = *tenderID } if companyID != nil { filter["company_id"] = *companyID } if len(status) > 0 { statusStrings := make([]string, len(status)) for i, s := range status { statusStrings[i] = string(s) } filter["status"] = bson.M{"$in": statusStrings} } if len(submissionMode) > 0 { modeStrings := make([]string, len(submissionMode)) for i, m := range submissionMode { modeStrings[i] = string(m) } filter["submission_mode"] = bson.M{"$in": modeStrings} } // Date range filters if createdAtFrom != nil || createdAtTo != nil { dateFilter := bson.M{} if createdAtFrom != nil { dateFilter["$gte"] = *createdAtFrom } if createdAtTo != nil { dateFilter["$lte"] = *createdAtTo } filter["created_at"] = dateFilter } // Build pagination pagination := mongo.NewPaginationBuilder(). Limit(limit). Skip(offset) // Set sort if sortBy != "" { if sortOrder == "desc" { pagination.SortDesc(sortBy) } else { pagination.SortAsc(sortBy) } } else { pagination.SortDesc("created_at") // Default sort } // Use ORM to find tender approvals result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build()) if err != nil { r.logger.Error("Failed to search tender approvals", map[string]interface{}{ "error": err.Error(), }) return nil, err } // Convert []TenderApproval to []*TenderApproval tenderApprovals := make([]*TenderApproval, len(result.Items)) for i := range result.Items { tenderApprovals[i] = &result.Items[i] } return tenderApprovals, nil } // Count counts all tender approvals func (r *tenderApprovalRepository) Count(ctx context.Context) (int64, error) { count, err := r.ormRepo.Count(ctx, bson.M{}) if err != nil { r.logger.Error("Failed to count tender approvals", map[string]interface{}{ "error": err.Error(), }) return 0, err } return count, nil } // CountByStatus counts tender approvals by status func (r *tenderApprovalRepository) CountByStatus(ctx context.Context, status ApprovalStatus) (int64, error) { filter := bson.M{"status": string(status)} count, err := r.ormRepo.Count(ctx, filter) if err != nil { r.logger.Error("Failed to count tender approvals by status", map[string]interface{}{ "error": err.Error(), "status": status, }) return 0, err } return count, nil } // CountBySubmissionMode counts tender approvals by submission mode func (r *tenderApprovalRepository) CountBySubmissionMode(ctx context.Context, submissionMode SubmissionMode) (int64, error) { filter := bson.M{"submission_mode": string(submissionMode)} count, err := r.ormRepo.Count(ctx, filter) if err != nil { r.logger.Error("Failed to count tender approvals by submission mode", map[string]interface{}{ "error": err.Error(), "submission_mode": submissionMode, }) return 0, err } return count, nil } // CountByTenderID counts tender approvals for a specific tender func (r *tenderApprovalRepository) CountByTenderID(ctx context.Context, tenderID string) (int64, error) { filter := bson.M{"tender_id": tenderID} count, err := r.ormRepo.Count(ctx, filter) if err != nil { r.logger.Error("Failed to count tender approvals by tender ID", map[string]interface{}{ "error": err.Error(), "tender_id": tenderID, }) return 0, err } return count, nil } // CountByCompanyID counts tender approvals for a specific company func (r *tenderApprovalRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) { filter := bson.M{"company_id": companyID} count, err := r.ormRepo.Count(ctx, filter) if err != nil { r.logger.Error("Failed to count tender approvals by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return 0, err } return count, nil } // GetTenderApprovalStats returns tender approval statistics func (r *tenderApprovalRepository) GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error) { // Get basic counts totalApprovals, _ := r.Count(ctx) submittedTenders, _ := r.CountByStatus(ctx, ApprovalStatusSubmitted) rejectedTenders, _ := r.CountByStatus(ctx, ApprovalStatusRejected) stats := &TenderApprovalStatsResponse{ TotalApprovals: totalApprovals, SubmittedTenders: submittedTenders, RejectedTenders: rejectedTenders, LastUpdated: time.Now().Unix(), } return stats, nil } // GetCompanyTenderApprovalStats returns company-specific tender approval statistics func (r *tenderApprovalRepository) GetCompanyTenderApprovalStats(ctx context.Context, companyID string) (*CompanyTenderApprovalStatsResponse, error) { // Get company-specific counts totalApprovals, _ := r.CountByCompanyID(ctx, companyID) // Count by status for this company submittedFilter := bson.M{"company_id": companyID, "status": string(ApprovalStatusSubmitted)} submittedTenders, _ := r.ormRepo.Count(ctx, submittedFilter) rejectedFilter := bson.M{"company_id": companyID, "status": string(ApprovalStatusRejected)} rejectedTenders, _ := r.ormRepo.Count(ctx, rejectedFilter) // Count by submission mode for this company selfApplyFilter := bson.M{"company_id": companyID, "status": string(ApprovalStatusSubmitted), "submission_mode": string(SubmissionModeSelfApply)} selfApplyCount, _ := r.ormRepo.Count(ctx, selfApplyFilter) partnershipFilter := bson.M{"company_id": companyID, "status": string(ApprovalStatusSubmitted), "submission_mode": string(SubmissionModePartnership)} partnershipCount, _ := r.ormRepo.Count(ctx, partnershipFilter) stats := &CompanyTenderApprovalStatsResponse{ CompanyID: companyID, TotalApprovals: totalApprovals, SubmittedTenders: submittedTenders, RejectedTenders: rejectedTenders, SelfApplyCount: selfApplyCount, PartnershipCount: partnershipCount, LastUpdated: time.Now().Unix(), } return stats, nil } // ListRejectedTenderIDsByCompanyID returns distinct tender IDs rejected by a company. func (r *tenderApprovalRepository) ListRejectedTenderIDsByCompanyID(ctx context.Context, companyID string) ([]string, error) { pipeline := mongodriver.Pipeline{ {{Key: "$match", Value: bson.M{ "company_id": companyID, "status": string(ApprovalStatusRejected), }}}, {{Key: "$group", Value: bson.M{"_id": "$tender_id"}}}, } results, err := r.ormRepo.Aggregate(ctx, pipeline) if err != nil { r.logger.Error("Failed to list rejected tender IDs by company", map[string]interface{}{ "company_id": companyID, "error": err.Error(), }) return nil, err } return aggregateStringIDs(results), nil } // SearchByCriteria searches tender approvals using search criteria func (r *tenderApprovalRepository) SearchByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error) { // Convert criteria to search parameters var status []ApprovalStatus if criteria.Status != nil { status = criteria.Status } var submissionMode []SubmissionMode if criteria.SubmissionMode != nil { submissionMode = criteria.SubmissionMode } // Use the search method return r.Search(ctx, criteria.TenderID, criteria.CompanyID, status, submissionMode, criteria.CreatedAtFrom, criteria.CreatedAtTo, criteria.Limit, criteria.Offset, "created_at", "desc") } func aggregateStringIDs(results []bson.M) []string { ids := make([]string, 0, len(results)) for _, result := range results { switch id := result["_id"].(type) { case string: if strings.TrimSpace(id) != "" { ids = append(ids, id) } } } return ids }