Implement Tender ID and Project Name Generation in Notice Worker
- Refactored the NoticeWorker to generate unique tender IDs using the PBL naming convention (SCDYYNNN format) and project names based on client and opportunity details. - Introduced methods for generating tender IDs and project names, including logic for extracting client and opportunity names, ensuring consistency and clarity in naming. - Updated the Tender entity to include a new ProjectName field, enhancing the data structure for better project identification. - Added MongoDB indexing for the new project_name field to optimize query performance. - Improved error handling and logging during the tender ID generation process, ensuring robustness in the worker's functionality.
This commit is contained in:
+128
-29
@@ -2,6 +2,7 @@ package workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/internal/notice"
|
||||
@@ -10,6 +11,9 @@ import (
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
"tm/pkg/notification"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type NoticeWorker struct {
|
||||
@@ -309,44 +313,139 @@ func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) {
|
||||
t.WinningTenderer = winningTenderer
|
||||
t.Modifications = modifications
|
||||
t.AwardedEntities = awardedEntities
|
||||
t.TenderID = GenerateTenderID(n)
|
||||
t.TenderID = w.GenerateTenderID(context.Background(), n)
|
||||
t.ProjectName = w.GenerateProjectName(t)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// generateTenderID generates a unique tender ID using the format:
|
||||
// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
|
||||
func GenerateTenderID(t *notice.Notice) string {
|
||||
var parts []string
|
||||
// GenerateTenderID generates a unique tender ID using the PBL naming convention: SCDYYNNN
|
||||
// SCD = Source Code (3 letters), YY = Year (2 digits), NNN = Sequential number (3 digits)
|
||||
func (w *NoticeWorker) GenerateTenderID(ctx context.Context, t *notice.Notice) string {
|
||||
sourceCode := getSourceCode(t)
|
||||
year := getCurrentYear()
|
||||
sequentialNumber := w.getNextSequentialNumber(ctx, sourceCode, year)
|
||||
|
||||
// Source (TED)
|
||||
parts = append(parts, "1")
|
||||
|
||||
// TED code (Notice Publication ID, first 8 chars)
|
||||
tedCode := "00000000"
|
||||
if t.NoticePublicationID != "" {
|
||||
tedCode = PadOrTruncate(t.NoticePublicationID, 8)
|
||||
return fmt.Sprintf("%s%s%03d", sourceCode, year, sequentialNumber)
|
||||
}
|
||||
parts = append(parts, tedCode)
|
||||
|
||||
// Buyer code (first 8 chars of buyer organization ID, or 8 zeros if not available)
|
||||
// buyerCode := "00000000"
|
||||
// if t.BuyerOrganization != nil && t.BuyerOrganization.ID != "" {
|
||||
// buyerCode = padOrTruncate(t.BuyerOrganization.ID, 8)
|
||||
// }
|
||||
// parts = append(parts, buyerCode)
|
||||
|
||||
// Date (YYYYMMDD format from issue date)
|
||||
parts = append(parts, PadOrTruncate(UnixToDateString(t.IssueDate), 4))
|
||||
|
||||
// Location code (country code + region code, padded to 6 chars)
|
||||
locationCode := "EU"
|
||||
if t.CountryCode != "" {
|
||||
locationCode = t.CountryCode
|
||||
// getSourceCode determines the 3-letter source code based on tender characteristics
|
||||
func getSourceCode(t *notice.Notice) string {
|
||||
// Since all notices come from TED (Tenders Electronic Daily), use PTD for Public Tender
|
||||
return "PTD"
|
||||
}
|
||||
parts = append(parts, locationCode)
|
||||
|
||||
return strings.Join(parts, "")
|
||||
// getCurrentYear returns the last two digits of the current year
|
||||
func getCurrentYear() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%02d", now.Year()%100)
|
||||
}
|
||||
|
||||
// GenerateProjectName generates the project name using PBL naming convention:
|
||||
// <InternalProjectNumber>_<Client-OpportunityName>_<PartnerCode>
|
||||
func (w *NoticeWorker) GenerateProjectName(t *tender.Tender) string {
|
||||
clientName := w.getClientName(t)
|
||||
opportunityName := w.getOpportunityName(t)
|
||||
|
||||
// Clean and format the names (replace spaces with hyphens, remove special chars)
|
||||
clientName = cleanForProjectName(clientName)
|
||||
opportunityName = cleanForProjectName(opportunityName)
|
||||
|
||||
// If client name and opportunity name are the same, just use one of them
|
||||
if clientName == opportunityName {
|
||||
return fmt.Sprintf("%s_%s_PBL", t.TenderID, clientName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s-%s_PBL", t.TenderID, clientName, opportunityName)
|
||||
}
|
||||
|
||||
// getClientName extracts the client name from the tender
|
||||
func (w *NoticeWorker) getClientName(t *tender.Tender) string {
|
||||
if t.BuyerOrganization != nil && t.BuyerOrganization.Name != "" {
|
||||
return t.BuyerOrganization.Name
|
||||
}
|
||||
return "UnknownClient"
|
||||
}
|
||||
|
||||
// getOpportunityName extracts the opportunity name from the tender title
|
||||
func (w *NoticeWorker) getOpportunityName(t *tender.Tender) string {
|
||||
if t.Title != "" {
|
||||
// Take first few words of the title as opportunity name
|
||||
words := strings.Fields(t.Title)
|
||||
if len(words) > 3 {
|
||||
return strings.Join(words[:3], " ")
|
||||
}
|
||||
return t.Title
|
||||
}
|
||||
return "UnknownOpportunity"
|
||||
}
|
||||
|
||||
// cleanForProjectName cleans a string for use in project names (removes special chars, replaces spaces with hyphens)
|
||||
func cleanForProjectName(s string) string {
|
||||
// Replace spaces with hyphens
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
// Remove special characters except hyphens and alphanumeric
|
||||
clean := ""
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
clean += string(r)
|
||||
}
|
||||
}
|
||||
// Remove multiple consecutive hyphens
|
||||
for strings.Contains(clean, "--") {
|
||||
clean = strings.ReplaceAll(clean, "--", "-")
|
||||
}
|
||||
// Trim hyphens from start and end
|
||||
clean = strings.Trim(clean, "-")
|
||||
return clean
|
||||
}
|
||||
|
||||
// getNextSequentialNumber returns the next sequential number for the given source code and year
|
||||
func (w *NoticeWorker) getNextSequentialNumber(ctx context.Context, sourceCode, year string) int {
|
||||
// Query tenders with tender IDs that match the pattern: {sourceCode}{year}*
|
||||
// We need to find the highest NNN value for this source code and year
|
||||
|
||||
// Pattern to match: e.g., "PTD25" followed by 3 digits
|
||||
pattern := fmt.Sprintf("^%s%s\\d{3}$", sourceCode, year)
|
||||
|
||||
// Create aggregation pipeline to find the highest sequential number
|
||||
pipeline := mongodriver.Pipeline{
|
||||
bson.D{{"$match", bson.M{
|
||||
"tender_id": bson.M{"$regex": pattern},
|
||||
}}},
|
||||
bson.D{{"$project", bson.M{
|
||||
"sequential_num": bson.M{"$substr": bson.A{"$tender_id", 5, 3}}, // Extract last 3 chars (NNN)
|
||||
}}},
|
||||
bson.D{{"$group", bson.M{
|
||||
"_id": nil,
|
||||
"max_num": bson.M{"$max": bson.M{"$toInt": "$sequential_num"}},
|
||||
}}},
|
||||
}
|
||||
|
||||
// Execute aggregation
|
||||
cursor, err := w.Mongo.GetCollection("tenders").Aggregate(ctx, pipeline)
|
||||
if err != nil {
|
||||
w.Logger.Error("Failed to aggregate tenders for sequential numbering", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"source_code": sourceCode,
|
||||
"year": year,
|
||||
})
|
||||
return 1 // Return 1 as fallback
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
// Parse result
|
||||
var result struct {
|
||||
MaxNum int `bson:"max_num"`
|
||||
}
|
||||
|
||||
if cursor.Next(ctx) {
|
||||
if err := cursor.Decode(&result); err == nil {
|
||||
return result.MaxNum + 1
|
||||
}
|
||||
}
|
||||
|
||||
return 1 // No existing tenders found, start with 1
|
||||
}
|
||||
|
||||
// padOrTruncate pads a string with zeros or truncates it to the specified length
|
||||
|
||||
@@ -52,7 +52,8 @@ type Tender struct {
|
||||
SourceFileURL string `bson:"source_file_url" json:"source_file_url"`
|
||||
SourceFileName string `bson:"source_file_name" json:"source_file_name"`
|
||||
ProcessingMetadata ProcessingMetadata `bson:"processing_metadata" json:"processing_metadata"`
|
||||
TenderID string `bson:"tender_id" json:"tender_id"` // Implement logic to generate a unique tender_id using the following format: {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
|
||||
TenderID string `bson:"tender_id" json:"tender_id"` // PBL Project Number (SCDYYNNN format)
|
||||
ProjectName string `bson:"project_name" json:"project_name"` // PBL Project Name (<InternalProjectNumber>_<Client-OpportunityName>_<PartnerCode>)
|
||||
|
||||
// Status-specific fields
|
||||
CancellationReason string `bson:"cancellation_reason,omitempty" json:"cancellation_reason,omitempty"`
|
||||
|
||||
@@ -44,6 +44,7 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te
|
||||
// Create indexes for tenders collection
|
||||
tenderIndexes := []orm.Index{
|
||||
*orm.CreateUniqueIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
|
||||
*orm.NewIndex("project_name_idx", bson.D{{Key: "project_name", 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}}),
|
||||
|
||||
Reference in New Issue
Block a user