package tender_submission import ( "context" "errors" "time" "tm/pkg/logger" "tm/pkg/mongo" "go.mongodb.org/mongo-driver/v2/bson" mongodriver "go.mongodb.org/mongo-driver/v2/mongo" ) const maxListLimit = 100 // Repository defines data access for tender submissions. type Repository interface { Create(ctx context.Context, submission *TenderSubmission) error GetByID(ctx context.Context, id string) (*TenderSubmission, error) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderSubmission, error) Update(ctx context.Context, submission *TenderSubmission) error UpdateIfUpdatedAt(ctx context.Context, submission *TenderSubmission, expectedUpdatedAt int64) error Delete(ctx context.Context, id string) error ListByCompany(ctx context.Context, companyID string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) Search(ctx context.Context, tenderID, companyID *string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) GetCompanyStats(ctx context.Context, companyID string) (*CompanySubmissionStatsResponse, error) GetGlobalStats(ctx context.Context) (*AdminSubmissionStatsResponse, error) } type tenderSubmissionRepository struct { ormRepo mongo.Repository[TenderSubmission] collection *mongodriver.Collection logger logger.Logger } func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository { 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("stage_idx", bson.D{{Key: "stage", 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}}), } if err := mongoManager.CreateIndexes("tender_submissions", indexes); err != nil { logger.Warn("Failed to create tender submission indexes", map[string]interface{}{ "error": err.Error(), }) } collection := mongoManager.GetCollection("tender_submissions") ormRepo := mongo.NewRepository[TenderSubmission](collection, logger) return &tenderSubmissionRepository{ormRepo: ormRepo, collection: collection, logger: logger} } func (r *tenderSubmissionRepository) Create(ctx context.Context, submission *TenderSubmission) error { now := time.Now().Unix() submission.SetCreatedAt(now) submission.SetUpdatedAt(now) if err := r.ormRepo.Create(ctx, submission); err != nil { if mongo.IsDuplicateKeyError(err) { return ErrTenderSubmissionExists } r.logger.Error("Failed to create tender submission", map[string]interface{}{ "error": err.Error(), "tender_id": submission.TenderID, "company_id": submission.CompanyID, }) return err } r.logger.Info("Tender submission created", map[string]interface{}{ "submission_id": submission.GetID(), "tender_id": submission.TenderID, "company_id": submission.CompanyID, "status": submission.Status, }) return nil } func (r *tenderSubmissionRepository) GetByID(ctx context.Context, id string) (*TenderSubmission, error) { submission, err := r.ormRepo.FindByID(ctx, id) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, ErrTenderSubmissionNotFound } r.logger.Error("Failed to get tender submission by ID", map[string]interface{}{ "error": err.Error(), "submission_id": id, }) return nil, err } return submission, nil } func (r *tenderSubmissionRepository) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderSubmission, error) { filter := bson.M{"tender_id": tenderID, "company_id": companyID} submission, err := r.ormRepo.FindOne(ctx, filter) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, ErrTenderSubmissionNotFound } r.logger.Error("Failed to get tender submission by tender and company", map[string]interface{}{ "error": err.Error(), "tender_id": tenderID, "company_id": companyID, }) return nil, err } return submission, nil } func (r *tenderSubmissionRepository) Update(ctx context.Context, submission *TenderSubmission) error { submission.SetUpdatedAt(time.Now().Unix()) if err := r.ormRepo.Update(ctx, submission); err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return ErrTenderSubmissionNotFound } r.logger.Error("Failed to update tender submission", map[string]interface{}{ "error": err.Error(), "submission_id": submission.GetID(), }) return err } return nil } func (r *tenderSubmissionRepository) UpdateIfUpdatedAt(ctx context.Context, submission *TenderSubmission, expectedUpdatedAt int64) error { objectID, err := bson.ObjectIDFromHex(submission.GetID()) if err != nil { return err } newUpdatedAt := time.Now().Unix() submission.SetUpdatedAt(newUpdatedAt) filter := bson.M{"_id": objectID, "updated_at": expectedUpdatedAt} result, err := r.collection.UpdateOne(ctx, filter, bson.M{"$set": submission}) if err != nil { r.logger.Error("Failed to update tender submission with optimistic lock", map[string]interface{}{ "error": err.Error(), "submission_id": submission.GetID(), }) return err } if result.MatchedCount == 0 { return ErrConcurrentUpdate } return nil } func (r *tenderSubmissionRepository) Delete(ctx context.Context, id string) error { if err := r.ormRepo.Delete(ctx, id); err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { return ErrTenderSubmissionNotFound } r.logger.Error("Failed to delete tender submission", map[string]interface{}{ "error": err.Error(), "submission_id": id, }) return err } return nil } func (r *tenderSubmissionRepository) ListByCompany(ctx context.Context, companyID string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) { companyIDPtr := companyID return r.Search(ctx, nil, &companyIDPtr, statuses, stages, from, to, limit, offset, sortBy, sortOrder) } func (r *tenderSubmissionRepository) Search(ctx context.Context, tenderID, companyID *string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) { limit = capListLimit(limit) if offset < 0 { offset = 0 } filter := bson.M{} if tenderID != nil && *tenderID != "" { filter["tender_id"] = *tenderID } if companyID != nil && *companyID != "" { filter["company_id"] = *companyID } if len(statuses) > 0 { filter["status"] = bson.M{"$in": statuses} } if len(stages) > 0 { filter["stage"] = bson.M{"$in": stages} } if from != nil || to != nil { dateFilter := bson.M{} if from != nil { dateFilter["$gte"] = *from } if to != nil { dateFilter["$lte"] = *to } filter["created_at"] = dateFilter } if sortBy == "" { sortBy = "updated_at" } if sortOrder == "" { sortOrder = "desc" } pagination := mongo.NewPaginationBuilder().Limit(limit).Skip(offset) if sortOrder == "asc" { pagination = pagination.SortAsc(sortBy) } else { pagination = pagination.SortDesc(sortBy) } result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build()) if err != nil { r.logger.Error("Failed to search tender submissions", map[string]interface{}{ "error": err.Error(), }) return nil, 0, err } items := make([]*TenderSubmission, len(result.Items)) for i := range result.Items { items[i] = &result.Items[i] } return items, result.TotalCount, nil } func (r *tenderSubmissionRepository) GetCompanyStats(ctx context.Context, companyID string) (*CompanySubmissionStatsResponse, error) { pipeline := mongodriver.Pipeline{ {{Key: "$match", Value: bson.M{"company_id": companyID}}}, {{Key: "$facet", Value: bson.M{ "total": bson.A{bson.M{"$count": "count"}}, "by_stage": bson.A{bson.M{"$group": bson.M{"_id": "$stage", "count": bson.M{"$sum": 1}}}}, "by_status": bson.A{bson.M{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}}}, }}}, } results, err := r.ormRepo.Aggregate(ctx, pipeline) if err != nil { return nil, err } stats := &CompanySubmissionStatsResponse{ CompanyID: companyID, ByStage: map[string]int64{}, ByStatus: map[string]int64{}, LastUpdated: time.Now().Unix(), } if len(results) == 0 { return stats, nil } facet := results[0] if totalArr, ok := facet["total"].(bson.A); ok && len(totalArr) > 0 { if doc, ok := totalArr[0].(bson.M); ok { stats.Total = int64Value(doc["count"]) } } stats.ByStage = countMapFromFacet(facet["by_stage"]) stats.ByStatus = countMapFromFacet(facet["by_status"]) return stats, nil } func (r *tenderSubmissionRepository) GetGlobalStats(ctx context.Context) (*AdminSubmissionStatsResponse, error) { pipeline := mongodriver.Pipeline{ {{Key: "$facet", Value: bson.M{ "total": bson.A{bson.M{"$count": "count"}}, "by_stage": bson.A{bson.M{"$group": bson.M{"_id": "$stage", "count": bson.M{"$sum": 1}}}}, "by_status": bson.A{bson.M{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}}}, }}}, } results, err := r.ormRepo.Aggregate(ctx, pipeline) if err != nil { return nil, err } stats := &AdminSubmissionStatsResponse{ ByStage: map[string]int64{}, ByStatus: map[string]int64{}, LastUpdated: time.Now().Unix(), } if len(results) == 0 { return stats, nil } facet := results[0] if totalArr, ok := facet["total"].(bson.A); ok && len(totalArr) > 0 { if doc, ok := totalArr[0].(bson.M); ok { stats.Total = int64Value(doc["count"]) } } stats.ByStage = countMapFromFacet(facet["by_stage"]) stats.ByStatus = countMapFromFacet(facet["by_status"]) return stats, nil } func countMapFromFacet(raw interface{}) map[string]int64 { out := map[string]int64{} arr, ok := raw.(bson.A) if !ok { return out } for _, item := range arr { doc, ok := item.(bson.M) if !ok { continue } key, _ := doc["_id"].(string) if key == "" { continue } out[key] = int64Value(doc["count"]) } return out } func int64Value(v interface{}) int64 { switch n := v.(type) { case int32: return int64(n) case int64: return n case float64: return int64(n) default: return 0 } } func capListLimit(limit int) int { if limit <= 0 { return 20 } if limit > maxListLimit { return maxListLimit } return limit }