Files
tm_back/internal/tender_approval/repository.go
T
Mazyar 81a94b4879
continuous-integration/drone/push Build is passing
Enhance tender and company context handling with new features and error management
- Introduced `PickAssignedCompanyID` function to determine the appropriate company ID for company-scoped operations, improving company assignment logic.
- Added `ResolveMongoID` method in the tender service to map MongoDB IDs and AI procedure references to canonical tender IDs, enhancing ID resolution.
- Updated `ToggleTenderApproval` to handle company ID validation and improve error handling, ensuring robust processing of tender approvals.
- Enhanced logging for error scenarios in the tender approval service, providing better insights into failures during operations.
- Refactored the `GetByID` and `GetByTenderAndCompany` methods in the tender approval repository to utilize custom error types for improved clarity.

This update significantly improves the handling of company and tender operations, enhancing error management and overall service reliability.
2026-07-08 02:55:55 +03:30

590 lines
19 KiB
Go

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
}