Refactor Tender Management API Endpoints and Update Documentation

- Renamed and restructured tender-related API endpoints for improved clarity and consistency, including changing the route from `/admin/tenders` to `/admin/v1/companies` and updating the associated methods.
- Introduced a new SearchForm structure for listing tenders with advanced filtering capabilities, enhancing the API's search functionality.
- Updated the service and repository layers to align with the new endpoint structure, ensuring proper handling of tender data.
- Enhanced Swagger and YAML documentation to reflect the changes in endpoint structure, including detailed descriptions and examples for the new search functionality.
- Improved error handling and response structures to provide clearer feedback to API consumers, ensuring a more robust and user-friendly experience.
This commit is contained in:
n.nakhostin
2025-09-08 14:20:50 +03:30
parent 3b265e567d
commit 3911180486
11 changed files with 1216 additions and 1382 deletions
+100 -240
View File
@@ -4,7 +4,8 @@ import (
"context"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
@@ -17,81 +18,73 @@ type TenderRepository interface {
GetByID(ctx context.Context, id string) (*Tender, error)
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error)
GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error)
Update(ctx context.Context, tender *Tender) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error)
// Bulk operations
CreateBatch(ctx context.Context, tenders []Tender) error
UpdateBatch(ctx context.Context, tenders []Tender) error
// Search and filtering
Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error)
GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error)
GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error)
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error)
// Statistics and aggregations
GetStatistics(ctx context.Context) (*TenderStatistics, 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 mongopkg.Repository[Tender]
mongoManager *mongopkg.ConnectionManager
ormRepo orm.Repository[Tender]
mongoManager *orm.ConnectionManager
logger logger.Logger
}
// NewTenderRepository creates a new tender repository
func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) TenderRepository {
func NewTenderRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository {
// Create indexes for tenders collection
tenderIndexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}),
*mongopkg.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}),
*mongopkg.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}),
*mongopkg.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}),
*mongopkg.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}),
*mongopkg.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}),
*mongopkg.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}),
*mongopkg.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}),
*mongopkg.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*mongopkg.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
tenderIndexes := []orm.Index{
*orm.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", 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
*mongopkg.NewIndex("status_country_idx", bson.D{
*orm.NewIndex("status_country_idx", bson.D{
{Key: "status", Value: 1},
{Key: "country_code", Value: 1},
}),
*mongopkg.NewIndex("notice_type_status_idx", bson.D{
*orm.NewIndex("notice_type_status_idx", bson.D{
{Key: "notice_type_code", Value: 1},
{Key: "status", Value: 1},
}),
// Index for CPV matching queries
*mongopkg.NewIndex("cpv_matching_idx", bson.D{
*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
*mongopkg.CreateTextIndex("search_idx", "title", "description"),
*orm.CreateTextIndex("search_idx", "title", "description"),
}
// Create indexes
if err := mongoManager.CreateIndexes("tenders", tenderIndexes); err != nil {
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 := mongopkg.NewRepository[Tender](mongoManager.GetCollection("tenders"), logger)
tenderOrmRepo := orm.NewRepository[Tender](mongoManager.GetCollection(collectionName()), logger)
return &tenderRepository{
ormRepo: tenderOrmRepo,
@@ -102,6 +95,9 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, 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{}{
@@ -136,7 +132,7 @@ func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, err
// 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 := mongopkg.Pagination{Limit: 1}
pagination := orm.Pagination{Limit: 1}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
@@ -148,7 +144,7 @@ func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNo
}
if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound
return nil, orm.ErrDocumentNotFound
}
return &result.Items[0], nil
@@ -157,7 +153,7 @@ func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNo
// 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 := mongopkg.Pagination{Limit: 1}
pagination := orm.Pagination{Limit: 1}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
@@ -169,7 +165,7 @@ func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticeP
}
if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound
return nil, orm.ErrDocumentNotFound
}
return &result.Items[0], nil
@@ -213,28 +209,31 @@ func (r *tenderRepository) Delete(ctx context.Context, id string) error {
return nil
}
// List retrieves tenders with pagination and filtering
func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error) {
filter := r.buildSearchFilter(criteria)
// 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)
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "publication_date",
SortOrder: -1,
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
}
if from != nil {
filter["publication_date"] = bson.M{"$gte": *from}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
if to != nil {
filter["publication_date"] = bson.M{"$lte": *to}
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if err != nil {
r.logger.Error("Failed to list tenders", map[string]interface{}{
r.logger.Error("Failed to search tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
@@ -243,123 +242,6 @@ func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriter
return result.Items, result.TotalCount, nil
}
// CreateBatch creates multiple tenders in a single operation
func (r *tenderRepository) CreateBatch(ctx context.Context, tenders []Tender) error {
if len(tenders) == 0 {
return nil
}
for i := range tenders {
if err := r.ormRepo.Create(ctx, &tenders[i]); err != nil {
r.logger.Error("Failed to create tender in batch", map[string]interface{}{
"tender_id": tenders[i].ID,
"error": err.Error(),
})
return err
}
}
r.logger.Info("Tender batch created successfully", map[string]interface{}{
"count": len(tenders),
})
return nil
}
// UpdateBatch updates multiple tenders
func (r *tenderRepository) UpdateBatch(ctx context.Context, tenders []Tender) error {
if len(tenders) == 0 {
return nil
}
now := time.Now().Unix()
for i := range tenders {
tenders[i].UpdatedAt = now
if err := r.ormRepo.Update(ctx, &tenders[i]); err != nil {
r.logger.Error("Failed to update tender in batch", map[string]interface{}{
"tender_id": tenders[i].ID,
"error": err.Error(),
})
return err
}
}
r.logger.Info("Tender batch updated successfully", map[string]interface{}{
"count": len(tenders),
})
return nil
}
// Search searches tenders based on criteria
func (r *tenderRepository) Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error) {
filter := r.buildSearchFilter(criteria)
pagination := mongopkg.Pagination{
Limit: 100,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to search tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetByCountryCode retrieves tenders by country code
func (r *tenderRepository) GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error) {
filter := bson.M{"country_code": countryCode}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tenders by country code", map[string]interface{}{
"country_code": countryCode,
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetByStatus retrieves tenders by status
func (r *tenderRepository) GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error) {
filter := bson.M{"status": status}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "created_at",
SortOrder: -1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get tenders by status", map[string]interface{}{
"status": status,
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetExpiredTenders retrieves tenders that are expired
func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) {
filter := bson.M{
@@ -374,7 +256,7 @@ func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int
},
}
pagination := mongopkg.Pagination{
pagination := orm.Pagination{
Limit: 1000, // Large limit for maintenance operations
SortField: "tender_deadline",
SortOrder: 1,
@@ -392,28 +274,6 @@ func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int
return result.Items, nil
}
// GetActiveTenders retrieves active tenders
func (r *tenderRepository) GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error) {
filter := bson.M{"status": TenderStatusActive}
pagination := mongopkg.Pagination{
Limit: limit,
Skip: offset,
SortField: "tender_deadline",
SortOrder: 1,
}
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get active tenders", map[string]interface{}{
"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{
@@ -557,88 +417,88 @@ func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) (
return counts, nil
}
// buildSearchFilter builds MongoDB filter from search criteria
func (r *tenderRepository) buildSearchFilter(criteria TenderSearchCriteria) bson.M {
// buildSearchFilter builds MongoDB filter from search vvvform
func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M {
filter := bson.M{}
if criteria.Query != "" {
filter["$text"] = bson.M{"$search": criteria.Query}
if form.Search != nil {
filter["$text"] = bson.M{"$search": *form.Search}
}
if criteria.NoticeType != "" {
filter["notice_type_code"] = criteria.NoticeType
if form.NoticeType != nil {
filter["notice_type_code"] = form.NoticeType
}
if criteria.ProcurementType != "" {
filter["procurement_type_code"] = criteria.ProcurementType
if form.ProcurementType != nil {
filter["procurement_type_code"] = form.ProcurementType
}
if len(criteria.CountryCodes) > 0 {
filter["country_code"] = bson.M{"$in": criteria.CountryCodes}
if len(form.CountryCodes) > 0 {
filter["country_code"] = bson.M{"$in": form.CountryCodes}
}
if len(criteria.RegionCodes) > 0 {
filter["region_code"] = bson.M{"$in": criteria.RegionCodes}
if len(form.RegionCodes) > 0 {
filter["region_code"] = bson.M{"$in": form.RegionCodes}
}
if len(criteria.Classifications) > 0 {
if len(form.Classifications) > 0 {
filter["$or"] = []bson.M{
{"main_classification": bson.M{"$in": criteria.Classifications}},
{"additional_classifications": bson.M{"$in": criteria.Classifications}},
{"main_classification": bson.M{"$in": form.Classifications}},
{"additional_classifications": bson.M{"$in": form.Classifications}},
}
}
if criteria.MinEstimatedValue != nil || criteria.MaxEstimatedValue != nil {
if form.MinEstimatedValue != nil || form.MaxEstimatedValue != nil {
valueFilter := bson.M{}
if criteria.MinEstimatedValue != nil {
valueFilter["$gte"] = *criteria.MinEstimatedValue
if form.MinEstimatedValue != nil {
valueFilter["$gte"] = *form.MinEstimatedValue
}
if criteria.MaxEstimatedValue != nil {
valueFilter["$lte"] = *criteria.MaxEstimatedValue
if form.MaxEstimatedValue != nil {
valueFilter["$lte"] = *form.MaxEstimatedValue
}
filter["estimated_value"] = valueFilter
}
if criteria.Currency != "" {
filter["currency"] = criteria.Currency
if form.Currency != "" {
filter["currency"] = form.Currency
}
if criteria.DeadlineFrom != nil || criteria.DeadlineTo != nil {
if form.DeadlineFrom != nil || form.DeadlineTo != nil {
deadlineFilter := bson.M{}
if criteria.DeadlineFrom != nil {
deadlineFilter["$gte"] = *criteria.DeadlineFrom
if form.DeadlineFrom != nil {
deadlineFilter["$gte"] = *form.DeadlineFrom
}
if criteria.DeadlineTo != nil {
deadlineFilter["$lte"] = *criteria.DeadlineTo
if form.DeadlineTo != nil {
deadlineFilter["$lte"] = *form.DeadlineTo
}
filter["tender_deadline"] = deadlineFilter
}
if criteria.PublicationDateFrom != nil || criteria.PublicationDateTo != nil {
if form.PublicationDateFrom != nil || form.PublicationDateTo != nil {
pubDateFilter := bson.M{}
if criteria.PublicationDateFrom != nil {
pubDateFilter["$gte"] = *criteria.PublicationDateFrom
if form.PublicationDateFrom != nil {
pubDateFilter["$gte"] = *form.PublicationDateFrom
}
if criteria.PublicationDateTo != nil {
pubDateFilter["$lte"] = *criteria.PublicationDateTo
if form.PublicationDateTo != nil {
pubDateFilter["$lte"] = *form.PublicationDateTo
}
filter["publication_date"] = pubDateFilter
}
if len(criteria.Status) > 0 {
filter["status"] = bson.M{"$in": criteria.Status}
if len(form.Status) > 0 {
filter["status"] = bson.M{"$in": form.Status}
}
if len(criteria.Source) > 0 {
filter["source"] = bson.M{"$in": criteria.Source}
if len(form.Source) > 0 {
filter["source"] = bson.M{"$in": form.Source}
}
if criteria.BuyerOrganizationID != "" {
filter["buyer_organization.id"] = criteria.BuyerOrganizationID
if form.BuyerOrganizationID != nil {
filter["buyer_organization.id"] = *form.BuyerOrganizationID
}
if len(criteria.Languages) > 0 {
filter["notice_language_code"] = bson.M{"$in": criteria.Languages}
if len(form.Languages) > 0 {
filter["notice_language_code"] = bson.M{"$in": form.Languages}
}
return filter