Add Tender Approval Management Functionality and API Enhancements
- Introduced a new tender approval management system, including the creation of tender approval entities, services, and repositories. - Implemented CRUD operations for tender approvals, allowing companies to approve or reject tenders. - Developed administrative and public API endpoints for listing, retrieving, and updating tender approvals with comprehensive filtering options. - Enhanced Swagger documentation to accurately reflect the new endpoints, including detailed descriptions, parameters, and response formats. - Updated existing routes to include tender approval management, ensuring adherence to Clean Architecture principles and maintaining a clear separation of concerns in the handler layer.
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// 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) ([]*TenderApproval, 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)
|
||||
SearchByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, 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 NewTenderApprovalRepository(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, errors.New("tender approval not found")
|
||||
}
|
||||
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, errors.New("tender approval not found")
|
||||
}
|
||||
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) ([]*TenderApproval, error) {
|
||||
filter := bson.M{"company_id": companyID}
|
||||
|
||||
// Add submission mode filter if provided
|
||||
if submissionMode != nil {
|
||||
filter["submission_mode"] = *submissionMode
|
||||
}
|
||||
|
||||
// Add status filter if provided
|
||||
if status != nil {
|
||||
filter["status"] = *status
|
||||
}
|
||||
|
||||
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 company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// 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 := primitive.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)
|
||||
approvedTenders, _ := r.CountByStatus(ctx, ApprovalStatusApproved)
|
||||
rejectedTenders, _ := r.CountByStatus(ctx, ApprovalStatusRejected)
|
||||
|
||||
// For this implementation, we'll return basic stats
|
||||
// In a real implementation, you'd use MongoDB aggregation pipelines
|
||||
stats := &TenderApprovalStatsResponse{
|
||||
TotalApprovals: totalApprovals,
|
||||
ApprovedTenders: approvedTenders,
|
||||
RejectedTenders: rejectedTenders,
|
||||
ApprovalsByStatus: make(map[string]int64),
|
||||
ApprovalsBySubmission: make(map[string]int64),
|
||||
LastUpdated: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add status breakdown
|
||||
stats.ApprovalsByStatus["approved"] = approvedTenders
|
||||
stats.ApprovalsByStatus["rejected"] = rejectedTenders
|
||||
|
||||
// Add submission mode breakdown
|
||||
selfApplyCount, _ := r.CountBySubmissionMode(ctx, SubmissionModeSelfApply)
|
||||
partnershipCount, _ := r.CountBySubmissionMode(ctx, SubmissionModePartnership)
|
||||
stats.ApprovalsBySubmission["self-apply"] = selfApplyCount
|
||||
stats.ApprovalsBySubmission["partnership"] = partnershipCount
|
||||
|
||||
return stats, 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")
|
||||
}
|
||||
Reference in New Issue
Block a user