Refactor Tender Management to Introduce Notice Entity and Repository
- Replaced the tender repository with a new notice repository, encapsulating notice-related data access methods. - Introduced the Notice entity to represent tender/contract notices, including relevant fields and methods for managing notice data. - Updated the TED scraper to utilize the new notice repository for creating and managing notices, enhancing the integration with the tender management system. - Implemented Ollama SDK initialization in the web bootstrap process, allowing for improved AI interactions. - Enhanced the tender service to include Ollama SDK for additional functionality, ensuring a more robust service layer.
This commit is contained in:
@@ -7,7 +7,7 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
"tm/pkg/config"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
@@ -78,7 +78,7 @@ func InitMongoDB(conf config.MongoConfig, log logger.Logger) *mongo.ConnectionMa
|
||||
// init TED scraper
|
||||
func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLogger logger.Logger, notify notification.SDK) {
|
||||
// Initialize tender repository
|
||||
tenderRepo := tender.NewRepository(mongoManager, appLogger)
|
||||
noticeRepo := notice.NewRepository(mongoManager, appLogger)
|
||||
|
||||
// Initialize TED scraper
|
||||
tedScraper := ted.NewTEDScraper(
|
||||
@@ -95,12 +95,12 @@ func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLog
|
||||
},
|
||||
appLogger,
|
||||
mongoManager,
|
||||
tenderRepo,
|
||||
noticeRepo,
|
||||
notify,
|
||||
)
|
||||
|
||||
// start TED scraper job
|
||||
schedule.NewCronScheduler(appLogger, mongoManager, tenderRepo).AddJob(schedule.Job{
|
||||
schedule.NewCronScheduler(appLogger, mongoManager, noticeRepo).AddJob(schedule.Job{
|
||||
Name: "TED Scraper Job",
|
||||
Func: func() {
|
||||
_ = tedScraper.Run(context.Background())
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
"tm/pkg/notification"
|
||||
"tm/pkg/ollama"
|
||||
"tm/pkg/redis"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -213,3 +214,38 @@ func InitNotificationService(conf config.NotificationConfig, log logger.Logger)
|
||||
|
||||
return *notificationSDK
|
||||
}
|
||||
|
||||
// Init Ollama Service
|
||||
func InitOllamaService(conf config.OllamaConfig, log logger.Logger) ollama.SDK {
|
||||
ollamaConfig := &ollama.Config{
|
||||
BaseURL: conf.BaseURL,
|
||||
Timeout: conf.Timeout,
|
||||
RetryAttempts: conf.RetryAttempts,
|
||||
RetryDelay: conf.RetryDelay,
|
||||
EnableLogging: conf.EnableLogging,
|
||||
UserAgent: conf.UserAgent,
|
||||
DefaultModel: conf.DefaultModel,
|
||||
DefaultTemperature: conf.DefaultTemperature,
|
||||
DefaultTopP: conf.DefaultTopP,
|
||||
DefaultMaxTokens: conf.DefaultMaxTokens,
|
||||
EnableStreaming: conf.EnableStreaming,
|
||||
}
|
||||
|
||||
ollamaSDK, err := ollama.New(ollamaConfig, log)
|
||||
if err != nil {
|
||||
log.Error("Failed to initialize Ollama SDK", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
log.Info("Ollama SDK initialized successfully", map[string]interface{}{
|
||||
"base_url": conf.BaseURL,
|
||||
"timeout": conf.Timeout,
|
||||
"retry_attempts": conf.RetryAttempts,
|
||||
"retry_delay": conf.RetryDelay,
|
||||
"enable_logging": conf.EnableLogging,
|
||||
"user_agent": conf.UserAgent,
|
||||
})
|
||||
|
||||
return *ollamaSDK
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Config struct {
|
||||
RateLimit config.RateLimitConfig
|
||||
Assets config.AssetsConfig
|
||||
Notification config.NotificationConfig
|
||||
Ollama config.OllamaConfig
|
||||
UserAuth AuthConfig
|
||||
CustomerAuth AuthConfig
|
||||
}
|
||||
|
||||
+4
-1
@@ -129,6 +129,9 @@ func main() {
|
||||
// Initialize notification service
|
||||
notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger)
|
||||
|
||||
// Initialize ollama service
|
||||
ollamaSDK := bootstrap.InitOllamaService(conf.Ollama, logger)
|
||||
|
||||
// Initialize authorization service
|
||||
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
|
||||
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
|
||||
@@ -155,7 +158,7 @@ func main() {
|
||||
categoryService := company_category.NewService(categoryRepository, logger)
|
||||
companyService := company.NewService(companyRepository, categoryService, logger)
|
||||
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator)
|
||||
tenderService := tender.NewService(tenderRepository, companyService, logger)
|
||||
tenderService := tender.NewService(tenderRepository, companyService, ollamaSDK, logger)
|
||||
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
|
||||
tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
|
||||
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/pkg/mongo"
|
||||
)
|
||||
|
||||
// Tender represents a tender/contract notice entity
|
||||
type Notice struct {
|
||||
mongo.Model `bson:",inline"`
|
||||
ContractNoticeID string `bson:"contract_notice_id" json:"contract_notice_id"`
|
||||
NoticePublicationID string `bson:"notice_publication_id" json:"notice_publication_id"`
|
||||
ContractFolderID string `bson:"contract_folder_id" json:"contract_folder_id"`
|
||||
NoticeTypeCode string `bson:"notice_type_code" json:"notice_type_code"`
|
||||
NoticeSubTypeCode string `bson:"notice_sub_type_code" json:"notice_sub_type_code"`
|
||||
NoticeLanguageCode string `bson:"notice_language_code" json:"notice_language_code"`
|
||||
IssueDate int64 `bson:"issue_date" json:"issue_date"`
|
||||
IssueTime int64 `bson:"issue_time" json:"issue_time"`
|
||||
TenderDeadline int64 `bson:"tender_deadline" json:"tender_deadline"`
|
||||
PublicationDate int64 `bson:"publication_date" json:"publication_date"`
|
||||
SubmissionDeadline int64 `bson:"submission_deadline" json:"submission_deadline"`
|
||||
ApplicationDeadline int64 `bson:"application_deadline" json:"application_deadline"`
|
||||
GazetteID string `bson:"gazette_id" json:"gazette_id"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Description string `bson:"description" json:"description"`
|
||||
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
||||
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
||||
MainClassification string `bson:"main_classification" json:"main_classification"`
|
||||
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
||||
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Duration string `bson:"duration" json:"duration"`
|
||||
DurationUnit string `bson:"duration_unit" json:"duration_unit"`
|
||||
PlaceOfPerformance string `bson:"place_of_performance" json:"place_of_performance"`
|
||||
CountryCode string `bson:"country_code" json:"country_code"`
|
||||
RegionCode string `bson:"region_code" json:"region_code"`
|
||||
CityName string `bson:"city_name" json:"city_name"`
|
||||
PostalCode string `bson:"postal_code" json:"postal_code"`
|
||||
DocumentURI string `bson:"document_uri" json:"document_uri"`
|
||||
TenderURL string `bson:"tender_url" json:"tender_url"`
|
||||
SubmissionURL string `bson:"submission_url" json:"submission_url"`
|
||||
BuyerOrganization *Organization `bson:"buyer_organization" json:"buyer_organization"`
|
||||
ReviewOrganization *Organization `bson:"review_organization,omitempty" json:"review_organization,omitempty"`
|
||||
Organizations []Organization `bson:"organizations" json:"organizations"`
|
||||
SelectionCriteria []SelectionCriterion `bson:"selection_criteria" json:"selection_criteria"`
|
||||
OfficialLanguages []string `bson:"official_languages" json:"official_languages"`
|
||||
Status TenderStatus `bson:"status" json:"status"`
|
||||
Source TenderSource `bson:"source" json:"source"`
|
||||
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}
|
||||
|
||||
// Status-specific fields
|
||||
CancellationReason string `bson:"cancellation_reason,omitempty" json:"cancellation_reason,omitempty"`
|
||||
CancellationDate int64 `bson:"cancellation_date,omitempty" json:"cancellation_date,omitempty"`
|
||||
AwardDate int64 `bson:"award_date,omitempty" json:"award_date,omitempty"`
|
||||
AwardedValue float64 `bson:"awarded_value,omitempty" json:"awarded_value,omitempty"`
|
||||
WinningTenderer *Organization `bson:"winning_tenderer,omitempty" json:"winning_tenderer,omitempty"`
|
||||
ContractNumber string `bson:"contract_number,omitempty" json:"contract_number,omitempty"`
|
||||
SuspensionReason string `bson:"suspension_reason,omitempty" json:"suspension_reason,omitempty"`
|
||||
SuspensionDate int64 `bson:"suspension_date,omitempty" json:"suspension_date,omitempty"`
|
||||
Modifications []TenderModification `bson:"modifications,omitempty" json:"modifications,omitempty"`
|
||||
AwardedEntities []Awarded `bson:"awarded,omitempty" json:"awarded,omitempty"`
|
||||
}
|
||||
|
||||
// Organization represents organization information
|
||||
type Organization struct {
|
||||
ID string `bson:"id" json:"id"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
CompanyID string `bson:"company_id,omitempty" json:"company_id,omitempty"`
|
||||
WebsiteURI string `bson:"website_uri,omitempty" json:"website_uri,omitempty"`
|
||||
ContactName string `bson:"contact_name,omitempty" json:"contact_name,omitempty"`
|
||||
ContactTelephone string `bson:"contact_telephone,omitempty" json:"contact_telephone,omitempty"`
|
||||
ContactEmail string `bson:"contact_email,omitempty" json:"contact_email,omitempty"`
|
||||
ContactFax string `bson:"contact_fax,omitempty" json:"contact_fax,omitempty"`
|
||||
Address Address `bson:"address" json:"address"`
|
||||
Role string `bson:"role,omitempty" json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// Address represents address information
|
||||
type Address struct {
|
||||
StreetName string `bson:"street_name,omitempty" json:"street_name,omitempty"`
|
||||
CityName string `bson:"city_name,omitempty" json:"city_name,omitempty"`
|
||||
PostalZone string `bson:"postal_zone,omitempty" json:"postal_zone,omitempty"`
|
||||
CountrySubentityCode string `bson:"country_subentity_code,omitempty" json:"country_subentity_code,omitempty"`
|
||||
Department string `bson:"department,omitempty" json:"department,omitempty"`
|
||||
Region string `bson:"region,omitempty" json:"region,omitempty"`
|
||||
CountryCode string `bson:"country_code" json:"country_code"`
|
||||
}
|
||||
|
||||
// SelectionCriterion represents selection criteria
|
||||
type SelectionCriterion struct {
|
||||
TypeCode string `bson:"type_code" json:"type_code"`
|
||||
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
||||
LanguageID string `bson:"language_id,omitempty" json:"language_id,omitempty"`
|
||||
}
|
||||
|
||||
// ProcessingMetadata contains metadata about how the tender was processed
|
||||
type ProcessingMetadata struct {
|
||||
ScrapedAt int64 `bson:"scraped_at" json:"scraped_at"`
|
||||
ProcessedAt int64 `bson:"processed_at" json:"processed_at"`
|
||||
ProcessingVersion string `bson:"processing_version" json:"processing_version"`
|
||||
ParsingErrors []string `bson:"parsing_errors,omitempty" json:"parsing_errors,omitempty"`
|
||||
ValidationErrors []string `bson:"validation_errors,omitempty" json:"validation_errors,omitempty"`
|
||||
EnrichmentData map[string]string `bson:"enrichment_data,omitempty" json:"enrichment_data,omitempty"`
|
||||
TranslatedData map[string]string `bson:"translated_data,omitempty" json:"translated_data,omitempty"`
|
||||
TranslatedAt int64 `bson:"translated_at,omitempty" json:"translated_at,omitempty"`
|
||||
Processed bool `bson:"processed" json:"processed"`
|
||||
}
|
||||
|
||||
// TenderModification represents a modification made to a tender
|
||||
type TenderModification struct {
|
||||
ModificationDate int64 `bson:"modification_date" json:"modification_date"` // Unix milliseconds
|
||||
ModificationReason string `bson:"modification_reason" json:"modification_reason"`
|
||||
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
||||
LanguageID string `bson:"language_id,omitempty" json:"language_id,omitempty"`
|
||||
}
|
||||
|
||||
// AwardedEntity represents an entity that has been awarded a contract
|
||||
type Awarded struct {
|
||||
Name string `bson:"name" json:"name"`
|
||||
Address string `bson:"address,omitempty" json:"address,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
Amount float64 `bson:"amount" json:"amount"`
|
||||
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
|
||||
Share float64 `bson:"share,omitempty" json:"share,omitempty"` // Percentage share for multiple winners
|
||||
AwardDate int64 `bson:"award_date" json:"award_date"` // Unix milliseconds
|
||||
ContractID string `bson:"contract_id,omitempty" json:"contract_id,omitempty"` // Contract reference ID
|
||||
TenderID string `bson:"tender_id,omitempty" json:"tender_id,omitempty"` // Tender reference ID
|
||||
LotID string `bson:"lot_id,omitempty" json:"lot_id,omitempty"` // Lot reference ID
|
||||
CompanyID string `bson:"company_id,omitempty" json:"company_id,omitempty"` // Company registration ID
|
||||
OrganizationID string `bson:"organization_id,omitempty" json:"organization_id,omitempty"` // Organization reference ID
|
||||
}
|
||||
|
||||
// TenderStatus represents the status of a tender
|
||||
type TenderStatus string
|
||||
|
||||
const (
|
||||
TenderStatusActive TenderStatus = "active"
|
||||
TenderStatusExpired TenderStatus = "expired"
|
||||
TenderStatusCancelled TenderStatus = "cancelled"
|
||||
TenderStatusAwarded TenderStatus = "awarded"
|
||||
TenderStatusDraft TenderStatus = "draft"
|
||||
TenderStatusClosed TenderStatus = "closed"
|
||||
TenderStatusModified TenderStatus = "modified"
|
||||
TenderStatusSuspended TenderStatus = "suspended"
|
||||
TenderStatusPublished TenderStatus = "published"
|
||||
)
|
||||
|
||||
// TenderSource represents the source of tender data
|
||||
type TenderSource string
|
||||
|
||||
const (
|
||||
TenderSourceTEDScraper TenderSource = "ted_scraper"
|
||||
TenderSourceManual TenderSource = "manual"
|
||||
TenderSourceAPI TenderSource = "api"
|
||||
TenderSourceBulkImport TenderSource = "bulk_import"
|
||||
)
|
||||
|
||||
// TenderStatistics represents tender statistics
|
||||
type TenderStatistics struct {
|
||||
TotalTenders int64 `json:"total_tenders"`
|
||||
ActiveTenders int64 `json:"active_tenders"`
|
||||
ExpiredTenders int64 `json:"expired_tenders"`
|
||||
TendersByCountry map[string]int64 `json:"tenders_by_country"`
|
||||
TendersByType map[string]int64 `json:"tenders_by_type"`
|
||||
TendersByClassification map[string]int64 `json:"tenders_by_classification"`
|
||||
AverageEstimatedValue float64 `json:"average_estimated_value"`
|
||||
TotalEstimatedValue float64 `json:"total_estimated_value"`
|
||||
LastUpdated int64 `json:"last_updated"` // Unix milliseconds
|
||||
}
|
||||
|
||||
// GetID returns the tender ID
|
||||
func (t *Notice) GetID() string {
|
||||
return t.ID.Hex()
|
||||
}
|
||||
|
||||
// IsActive returns true if the tender is active
|
||||
func (t *Notice) IsActive() bool {
|
||||
return t.Status == TenderStatusActive
|
||||
}
|
||||
|
||||
// IsExpired returns true if the tender deadline has passed
|
||||
func (t *Notice) IsExpired() bool {
|
||||
if t.Status == TenderStatusExpired {
|
||||
return true
|
||||
}
|
||||
|
||||
if t.TenderDeadline == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if deadline (in milliseconds) is in the past
|
||||
now := time.Now().Unix()
|
||||
return t.TenderDeadline < now
|
||||
}
|
||||
|
||||
// GetTenderURL returns the TED tender URL
|
||||
func (t *Notice) GetTenderURL() string {
|
||||
if t.TenderURL != "" {
|
||||
return t.TenderURL
|
||||
}
|
||||
|
||||
if t.NoticePublicationID != "" {
|
||||
return generateTenderURL(t.NoticePublicationID)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// generateTenderURL generates the TED tender URL from NoticePublicationID
|
||||
func generateTenderURL(noticePublicationID string) string {
|
||||
// Remove leading zeros and format for URL
|
||||
// Example: 00522942-2025 -> https://ted.europa.eu/en/notice/-/detail/522942-2025
|
||||
parts := strings.Split(noticePublicationID, "-")
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Convert to int and back to string to remove leading zeros
|
||||
if num, err := strconv.Atoi(parts[0]); err == nil {
|
||||
cleanID := fmt.Sprintf("%d-%s", num, parts[1])
|
||||
return fmt.Sprintf("https://ted.europa.eu/en/notice/-/detail/%s", cleanID)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMainOrganization returns the buyer organization
|
||||
func (t *Notice) GetMainOrganization() *Organization {
|
||||
return t.BuyerOrganization
|
||||
}
|
||||
|
||||
// HasEstimatedValue returns true if the tender has an estimated value
|
||||
func (t *Notice) HasEstimatedValue() bool {
|
||||
return t.EstimatedValue > 0
|
||||
}
|
||||
|
||||
// GetFormattedEstimatedValue returns formatted estimated value with currency
|
||||
func (t *Notice) GetFormattedEstimatedValue() string {
|
||||
if !t.HasEstimatedValue() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if t.Currency != "" {
|
||||
return fmt.Sprintf("%.2f %s", t.EstimatedValue, t.Currency)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f", t.EstimatedValue)
|
||||
}
|
||||
|
||||
// UpdateStatus updates the tender status and updated timestamp
|
||||
func (t *Notice) UpdateStatus(status TenderStatus) {
|
||||
t.Status = status
|
||||
t.UpdatedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
// GetDaysUntilDeadline returns the number of days until the tender deadline
|
||||
func (t *Notice) GetDaysUntilDeadline() int {
|
||||
if t.TenderDeadline == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if t.TenderDeadline <= now {
|
||||
return 0 // Already expired
|
||||
}
|
||||
|
||||
diffMillis := t.TenderDeadline - now
|
||||
diffDays := diffMillis / (24 * 60 * 60 * 1000)
|
||||
return int(diffDays)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
orm "tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// NoticeRepository interface defines notice data access methods
|
||||
type Repository interface {
|
||||
Import(ctx context.Context, notice *Notice) error
|
||||
BulkImport(ctx context.Context, notices []Notice) error
|
||||
GetByID(ctx context.Context, id string) (*Notice, error)
|
||||
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Notice, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
func collectionName() string {
|
||||
return "notices"
|
||||
}
|
||||
|
||||
// noticeRepository implements NoticeRepository interface using MongoDB ORM
|
||||
type noticeRepository struct {
|
||||
ormRepo orm.Repository[Notice]
|
||||
mongoManager *orm.ConnectionManager
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewRepository creates a new notice repository
|
||||
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
|
||||
// Create indexes for notices collection
|
||||
noticeIndexes := []orm.Index{}
|
||||
|
||||
// Create indexes
|
||||
if err := mongoManager.CreateIndexes(collectionName(), noticeIndexes); err != nil {
|
||||
logger.Warn("Failed to create notice indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create ORM repositories
|
||||
noticeOrmRepo := orm.NewRepository[Notice](mongoManager.GetCollection(collectionName()), logger)
|
||||
|
||||
return ¬iceRepository{
|
||||
ormRepo: noticeOrmRepo,
|
||||
mongoManager: mongoManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Import imports a new notice
|
||||
func (r *noticeRepository) Import(ctx context.Context, notice *Notice) error {
|
||||
now := time.Now().Unix()
|
||||
notice.SetCreatedAt(now)
|
||||
|
||||
err := r.ormRepo.Create(ctx, notice)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create notice", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"notice_publication_id": notice.NoticePublicationID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Notice created successfully", map[string]interface{}{
|
||||
"notice_id": notice.ID,
|
||||
"notice_publication_id": notice.NoticePublicationID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkImport imports a new notice
|
||||
func (r *noticeRepository) BulkImport(ctx context.Context, notices []Notice) error {
|
||||
err := r.ormRepo.CreateMany(ctx, notices)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to bulk import notices", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a notice by ID
|
||||
func (r *noticeRepository) GetByID(ctx context.Context, id string) (*Notice, error) {
|
||||
notice, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get notice by ID", map[string]interface{}{
|
||||
"notice_id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return notice, nil
|
||||
}
|
||||
|
||||
// GetByContractNoticeID retrieves a notice by contract notice ID
|
||||
func (r *noticeRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Notice, error) {
|
||||
filter := bson.M{"contract_notice_id": contractNoticeID}
|
||||
pagination := orm.Pagination{Limit: 1}
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get notice by contract notice ID", map[string]interface{}{
|
||||
"contract_notice_id": contractNoticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Items) == 0 {
|
||||
return nil, orm.ErrDocumentNotFound
|
||||
}
|
||||
|
||||
return &result.Items[0], nil
|
||||
}
|
||||
|
||||
// Update updates an existing notice
|
||||
func (r *noticeRepository) Update(ctx context.Context, notice *Notice) error {
|
||||
notice.UpdatedAt = time.Now().Unix()
|
||||
|
||||
err := r.ormRepo.Update(ctx, notice)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update notice", map[string]interface{}{
|
||||
"notice_id": notice.ID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Notice updated successfully", map[string]interface{}{
|
||||
"notice_id": notice.ID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a notice by ID
|
||||
func (r *noticeRepository) Delete(ctx context.Context, id string) error {
|
||||
err := r.ormRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete notice", map[string]interface{}{
|
||||
"notice_id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Notice deleted successfully", map[string]interface{}{
|
||||
"notice_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Service defines business logic for notice operations
|
||||
type Service interface {
|
||||
Import(ctx context.Context, form *Notice) error
|
||||
BulkImport(ctx context.Context, form []Notice) error
|
||||
}
|
||||
|
||||
// noticeService implements the Service interface
|
||||
type noticeService struct {
|
||||
repository Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new notice service
|
||||
func NewService(repository Repository, logger logger.Logger) Service {
|
||||
return ¬iceService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Import imports a new notice
|
||||
func (s *noticeService) Import(ctx context.Context, form *Notice) error {
|
||||
return s.repository.Import(ctx, form)
|
||||
}
|
||||
|
||||
// BulkImport imports a new notice
|
||||
func (s *noticeService) BulkImport(ctx context.Context, form []Notice) error {
|
||||
return s.repository.BulkImport(ctx, form)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
"tm/internal/company"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/ollama"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
@@ -27,14 +28,16 @@ type Service interface {
|
||||
type tenderService struct {
|
||||
repository TenderRepository
|
||||
companyService company.Service
|
||||
ollamaSDK ollama.SDK
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new tender service
|
||||
func NewService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service {
|
||||
func NewService(repository TenderRepository, companyService company.Service, ollamaSDK ollama.SDK, logger logger.Logger) Service {
|
||||
return &tenderService{
|
||||
repository: repository,
|
||||
companyService: companyService,
|
||||
ollamaSDK: ollamaSDK,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ type NotificationConfig struct {
|
||||
UserAgent string `env:"NOTIFICATION_USER_AGENT"`
|
||||
}
|
||||
|
||||
// OllamaConfig represents the configuration for the Ollama SDK
|
||||
type OllamaConfig struct {
|
||||
BaseURL string `env:"OLLAMA_BASE_URL" default:"http://localhost:11434"`
|
||||
Timeout time.Duration `env:"OLLAMA_TIMEOUT" default:"300s"`
|
||||
RetryAttempts int `env:"OLLAMA_RETRY_ATTEMPTS" default:"3"`
|
||||
RetryDelay time.Duration `env:"OLLAMA_RETRY_DELAY" default:"2s"`
|
||||
EnableLogging bool `env:"OLLAMA_ENABLE_LOGGING" default:"true"`
|
||||
UserAgent string `env:"OLLAMA_USER_AGENT" default:"TenderManagement-OllamaSDK/1.0"`
|
||||
DefaultModel string `env:"OLLAMA_DEFAULT_MODEL" default:"llama2"`
|
||||
DefaultTemperature float64 `env:"OLLAMA_DEFAULT_TEMPERATURE" default:"0.7"`
|
||||
DefaultTopP float64 `env:"OLLAMA_DEFAULT_TOP_P" default:"0.9"`
|
||||
DefaultMaxTokens int `env:"OLLAMA_DEFAULT_MAX_TOKENS" default:"1000"`
|
||||
EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration with priority: OS environment variables > .env file
|
||||
// OS environment variables take precedence over .env file values
|
||||
func LoadConfig[T any](path string, config T) (T, error) {
|
||||
|
||||
@@ -47,6 +47,9 @@ type Repository[T any] interface {
|
||||
// Create inserts a new document
|
||||
Create(ctx context.Context, model *T) error
|
||||
|
||||
// CreateMany inserts multiple documents
|
||||
CreateMany(ctx context.Context, models []T) error
|
||||
|
||||
// Update updates an existing document
|
||||
Update(ctx context.Context, model *T) error
|
||||
|
||||
@@ -239,6 +242,32 @@ func (r *repository[T]) Create(ctx context.Context, model *T) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateMany inserts multiple documents
|
||||
func (r *repository[T]) CreateMany(ctx context.Context, models []T) error {
|
||||
// Set timestamps if the model implements Timestampable
|
||||
for _, v := range models {
|
||||
if timestampable, ok := any(v).(Timestampable); ok {
|
||||
now := time.Now().Unix()
|
||||
timestampable.SetCreatedAt(now)
|
||||
timestampable.SetUpdatedAt(now)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := r.collection.InsertMany(ctx, models)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create multiple documents", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to create multiple documents: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Documents created successfully", map[string]interface{}{
|
||||
"ids": result.InsertedIDs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates an existing document
|
||||
func (r *repository[T]) Update(ctx context.Context, model *T) error {
|
||||
// Set updated timestamp if the model implements Timestampable
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Service implements the OllamaService interface
|
||||
type Service struct {
|
||||
client *Client
|
||||
config *Config
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new Ollama service
|
||||
func NewService(client *Client) OllamaService {
|
||||
return &Service{
|
||||
client: client,
|
||||
config: client.config,
|
||||
logger: client.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate generates text using a specified model and prompt
|
||||
func (s *Service) Generate(ctx context.Context, req *GenerateRequest) (*GenerateResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating text", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"prompt_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Model == "" {
|
||||
req.Model = s.config.DefaultModel
|
||||
}
|
||||
|
||||
var resp GenerateResponse
|
||||
err := s.client.Post(ctx, "/api/generate", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Text generation failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrGenerationFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Text generation completed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"response_length": len(resp.Response),
|
||||
"eval_count": resp.EvalCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GenerateWithOptions generates text with additional options
|
||||
func (s *Service) GenerateWithOptions(ctx context.Context, req *GenerateRequest, opts *GenerateOptions) (*GenerateResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating text with options", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"prompt_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
// Apply options to request
|
||||
if opts != nil {
|
||||
if req.Options == nil {
|
||||
req.Options = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if opts.Temperature != nil {
|
||||
req.Options["temperature"] = *opts.Temperature
|
||||
}
|
||||
if opts.TopP != nil {
|
||||
req.Options["top_p"] = *opts.TopP
|
||||
}
|
||||
if opts.TopK != nil {
|
||||
req.Options["top_k"] = *opts.TopK
|
||||
}
|
||||
if opts.RepeatPenalty != nil {
|
||||
req.Options["repeat_penalty"] = *opts.RepeatPenalty
|
||||
}
|
||||
if opts.Seed != nil {
|
||||
req.Options["seed"] = *opts.Seed
|
||||
}
|
||||
if opts.Stop != nil {
|
||||
req.Options["stop"] = opts.Stop
|
||||
}
|
||||
if opts.Format != "" {
|
||||
req.Format = opts.Format
|
||||
}
|
||||
|
||||
// Merge additional options
|
||||
for k, v := range opts.Options {
|
||||
req.Options[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return s.Generate(ctx, req)
|
||||
}
|
||||
|
||||
// Chat performs a conversational chat with the model
|
||||
func (s *Service) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Starting chat", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"message_count": len(req.Messages),
|
||||
})
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Model == "" {
|
||||
req.Model = s.config.DefaultModel
|
||||
}
|
||||
|
||||
var resp ChatResponse
|
||||
err := s.client.Post(ctx, "/api/chat", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Chat failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrChatFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Chat completed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"response_length": len(resp.Message.Content),
|
||||
"eval_count": resp.EvalCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Embed generates embeddings for the given text
|
||||
func (s *Service) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating embeddings", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"text_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
var resp EmbedResponse
|
||||
err := s.client.Post(ctx, "/api/embeddings", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Embedding generation failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrEmbeddingFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Embeddings generated", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"embedding_dimension": len(resp.Embedding),
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// PullModel pulls a model from the registry
|
||||
func (s *Service) PullModel(ctx context.Context, model string) (*PullResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Pulling model", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &PullRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp PullResponse
|
||||
err := s.client.Post(ctx, "/api/pull", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Model pull failed", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrModelPullFailed(model, err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Model pulled successfully", map[string]interface{}{
|
||||
"model": model,
|
||||
"status": resp.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ListModels lists available models
|
||||
func (s *Service) ListModels(ctx context.Context) (*ListModelsResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Listing models", map[string]interface{}{})
|
||||
}
|
||||
|
||||
var resp ListModelsResponse
|
||||
err := s.client.Get(ctx, "/api/tags", &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Failed to list models", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list models: %w", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Models listed", map[string]interface{}{
|
||||
"model_count": len(resp.Models),
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteModel deletes a model
|
||||
func (s *Service) DeleteModel(ctx context.Context, model string) (*DeleteResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Deleting model", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &DeleteRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp DeleteResponse
|
||||
err := s.client.Delete(ctx, "/api/delete", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Model deletion failed", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrModelDeleteFailed(model, err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Model deleted successfully", map[string]interface{}{
|
||||
"model": model,
|
||||
"status": resp.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ShowModel shows model information
|
||||
func (s *Service) ShowModel(ctx context.Context, model string) (*ShowModelResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Showing model information", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &ShowModelRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp ShowModelResponse
|
||||
err := s.client.Post(ctx, "/api/show", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Failed to show model", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, fmt.Errorf("failed to show model %s: %w", model, err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Model information retrieved", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Health checks if the Ollama service is available
|
||||
func (s *Service) Health(ctx context.Context) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Checking Ollama health", map[string]interface{}{})
|
||||
}
|
||||
|
||||
var resp HealthResponse
|
||||
err := s.client.Get(ctx, "/api/version", &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Health check failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return ErrConnectionFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Health check passed", map[string]interface{}{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewChatBuilder creates a new chat builder for conversational interactions
|
||||
func (s *Service) NewChatBuilder() ChatBuilder {
|
||||
return &chatBuilder{
|
||||
service: s,
|
||||
request: &ChatRequest{
|
||||
Model: s.config.DefaultModel,
|
||||
Messages: make([]Message, 0),
|
||||
Options: ChatOptions{
|
||||
Temperature: &s.config.DefaultTemperature,
|
||||
TopP: &s.config.DefaultTopP,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// chatBuilder implements the ChatBuilder interface
|
||||
type chatBuilder struct {
|
||||
service OllamaService
|
||||
request *ChatRequest
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetModel(model string) ChatBuilder {
|
||||
b.request.Model = model
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetSystem(system string) ChatBuilder {
|
||||
// Add or update system message
|
||||
for i, msg := range b.request.Messages {
|
||||
if msg.Role == MessageRoleSystem {
|
||||
b.request.Messages[i].Content = system
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// Add new system message at the beginning
|
||||
systemMsg := Message{
|
||||
Role: MessageRoleSystem,
|
||||
Content: system,
|
||||
}
|
||||
b.request.Messages = append([]Message{systemMsg}, b.request.Messages...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) AddMessage(role MessageRole, content string) ChatBuilder {
|
||||
msg := Message{
|
||||
Role: role,
|
||||
Content: content,
|
||||
}
|
||||
b.request.Messages = append(b.request.Messages, msg)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetTemperature(temperature float64) ChatBuilder {
|
||||
b.request.Options.Temperature = &temperature
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetTopP(topP float64) ChatBuilder {
|
||||
b.request.Options.TopP = &topP
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetMaxTokens(maxTokens int) ChatBuilder {
|
||||
b.request.Options.NumPredict = &maxTokens
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetStream(stream bool) ChatBuilder {
|
||||
b.request.Stream = &stream
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetFormat(format string) ChatBuilder {
|
||||
b.request.Format = format
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetOptions(options map[string]interface{}) ChatBuilder {
|
||||
if b.request.Options.Options == nil {
|
||||
b.request.Options.Options = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range options {
|
||||
b.request.Options.Options[k] = v
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Build() (*ChatRequest, error) {
|
||||
if len(b.request.Messages) == 0 {
|
||||
return nil, ErrInvalidRequest("at least one message is required")
|
||||
}
|
||||
return b.request, nil
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Send(ctx context.Context) (*ChatResponse, error) {
|
||||
req, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.service.Chat(ctx, req)
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Continue(ctx context.Context, userMessage string) (*ChatResponse, error) {
|
||||
// Add user message
|
||||
b.AddMessage(MessageRoleUser, userMessage)
|
||||
|
||||
// Send chat request
|
||||
resp, err := b.Send(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add assistant response to conversation history
|
||||
b.AddMessage(MessageRoleAssistant, resp.Message.Content)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package schedule
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type CronScheduler struct {
|
||||
cron *cron.Cron
|
||||
mongoManager *mongo.ConnectionManager
|
||||
tenderRepo tender.TenderRepository
|
||||
noticeRepo notice.Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ type Job struct {
|
||||
}
|
||||
|
||||
// NewCronScheduler creates a new cron scheduler instance
|
||||
func NewCronScheduler(logger logger.Logger, mongoManager *mongo.ConnectionManager, tenderRepo tender.TenderRepository) *CronScheduler {
|
||||
func NewCronScheduler(logger logger.Logger, mongoManager *mongo.ConnectionManager, noticeRepo notice.Repository) *CronScheduler {
|
||||
// Create cron with timezone support and seconds precision
|
||||
c := cron.New(cron.WithLocation(time.UTC), cron.WithSeconds())
|
||||
|
||||
return &CronScheduler{
|
||||
cron: c,
|
||||
mongoManager: mongoManager,
|
||||
tenderRepo: tenderRepo,
|
||||
noticeRepo: noticeRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
+21
-21
@@ -4,11 +4,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
)
|
||||
|
||||
// mapToTenderFromParsedDoc maps a parsed TED document to tender entity
|
||||
func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceFileURL, sourceFileName string) *tender.Tender {
|
||||
func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||
switch parsedDoc.Type {
|
||||
case DocumentTypeContractNotice:
|
||||
if parsedDoc.ContractNotice == nil {
|
||||
@@ -45,7 +45,7 @@ func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceF
|
||||
}
|
||||
|
||||
// mapGenericNoticeToTender maps any TED document type to tender entity using the common interface
|
||||
func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, sourceFileName string) *tender.Tender {
|
||||
func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||
if doc == nil {
|
||||
s.logger.Error("Document is nil in mapGenericNoticeToTender", map[string]interface{}{})
|
||||
return nil
|
||||
@@ -53,7 +53,7 @@ func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, so
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
t := &tender.Tender{
|
||||
t := ¬ice.Notice{
|
||||
ContractNoticeID: doc.GetID(),
|
||||
ContractFolderID: "", // Not available in generic interface
|
||||
NoticeTypeCode: doc.GetNoticeType(),
|
||||
@@ -68,11 +68,11 @@ func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, so
|
||||
// Basic tender information
|
||||
Title: "Generic Notice " + doc.GetNoticeType(),
|
||||
Description: "Processed from " + doc.GetNoticeType() + " document",
|
||||
Status: tender.TenderStatusActive,
|
||||
Source: tender.TenderSourceTEDScraper,
|
||||
Status: notice.TenderStatusActive,
|
||||
Source: notice.TenderSourceTEDScraper,
|
||||
SourceFileURL: sourceFileURL,
|
||||
SourceFileName: sourceFileName,
|
||||
ProcessingMetadata: tender.ProcessingMetadata{
|
||||
ProcessingMetadata: notice.ProcessingMetadata{
|
||||
ScrapedAt: now,
|
||||
ProcessedAt: now,
|
||||
ProcessingVersion: "1.0",
|
||||
@@ -86,10 +86,10 @@ func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, so
|
||||
}
|
||||
|
||||
// mapToTender maps a TED ContractNotice to tender entity
|
||||
func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileName string) *tender.Tender {
|
||||
func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||
now := time.Now().Unix()
|
||||
|
||||
t := &tender.Tender{
|
||||
t := ¬ice.Notice{
|
||||
ContractNoticeID: cn.ID,
|
||||
ContractFolderID: cn.ContractFolderID,
|
||||
NoticeTypeCode: cn.GetNoticeSubType(),
|
||||
@@ -105,11 +105,11 @@ func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileNa
|
||||
MainClassification: cn.GetMainClassification(),
|
||||
PlaceOfPerformance: cn.GetPlaceOfPerformance(),
|
||||
DocumentURI: cn.GetDocumentURI(),
|
||||
Status: tender.TenderStatusActive,
|
||||
Source: tender.TenderSourceTEDScraper,
|
||||
Status: notice.TenderStatusActive,
|
||||
Source: notice.TenderSourceTEDScraper,
|
||||
SourceFileURL: sourceFileURL,
|
||||
SourceFileName: sourceFileName,
|
||||
ProcessingMetadata: tender.ProcessingMetadata{
|
||||
ProcessingMetadata: notice.ProcessingMetadata{
|
||||
ScrapedAt: now,
|
||||
ProcessedAt: now,
|
||||
ProcessingVersion: "1.0",
|
||||
@@ -215,7 +215,7 @@ func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileNa
|
||||
// Set selection criteria
|
||||
if criteria := cn.GetSelectionCriteria(); criteria != nil {
|
||||
for _, criterion := range criteria {
|
||||
t.SelectionCriteria = append(t.SelectionCriteria, tender.SelectionCriterion{
|
||||
t.SelectionCriteria = append(t.SelectionCriteria, notice.SelectionCriterion{
|
||||
TypeCode: criterion.CriterionTypeCode,
|
||||
Description: criterion.Description,
|
||||
LanguageID: criterion.LanguageID,
|
||||
@@ -235,10 +235,10 @@ func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileNa
|
||||
}
|
||||
|
||||
// mapContractAwardNoticeToTender maps a TED ContractAwardNotice to tender entity
|
||||
func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, sourceFileURL, sourceFileName string) *tender.Tender {
|
||||
func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||
now := time.Now().Unix()
|
||||
|
||||
t := &tender.Tender{
|
||||
t := ¬ice.Notice{
|
||||
ContractNoticeID: can.ID,
|
||||
ContractFolderID: can.ContractFolderID,
|
||||
NoticeTypeCode: can.NoticeTypeCode,
|
||||
@@ -252,11 +252,11 @@ func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, so
|
||||
ProcurementTypeCode: can.GetContractNature(),
|
||||
ProcedureCode: can.GetProcedureCode(),
|
||||
MainClassification: can.GetMainClassification(),
|
||||
Status: tender.TenderStatusAwarded, // Award notices are for completed tenders
|
||||
Source: tender.TenderSourceTEDScraper,
|
||||
Status: notice.TenderStatusAwarded, // Award notices are for completed tenders
|
||||
Source: notice.TenderSourceTEDScraper,
|
||||
SourceFileURL: sourceFileURL,
|
||||
SourceFileName: sourceFileName,
|
||||
ProcessingMetadata: tender.ProcessingMetadata{
|
||||
ProcessingMetadata: notice.ProcessingMetadata{
|
||||
ScrapedAt: now,
|
||||
ProcessedAt: now,
|
||||
ProcessingVersion: "1.0",
|
||||
@@ -315,13 +315,13 @@ func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, so
|
||||
}
|
||||
|
||||
// mapOrganization maps a TED Organization to tender Organization
|
||||
func (s *TEDScraper) mapOrganization(tedOrg *Organization, role string) *tender.Organization {
|
||||
func (s *TEDScraper) mapOrganization(tedOrg *Organization, role string) *notice.Organization {
|
||||
if tedOrg == nil || tedOrg.Company == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
company := tedOrg.Company
|
||||
org := &tender.Organization{
|
||||
org := ¬ice.Organization{
|
||||
Role: role,
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ func (s *TEDScraper) mapOrganization(tedOrg *Organization, role string) *tender.
|
||||
|
||||
// Set address
|
||||
if company.PostalAddress != nil {
|
||||
org.Address = tender.Address{
|
||||
org.Address = notice.Address{
|
||||
StreetName: company.PostalAddress.StreetName,
|
||||
CityName: company.PostalAddress.CityName,
|
||||
PostalZone: company.PostalAddress.PostalZone,
|
||||
|
||||
+34
-34
@@ -10,7 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
"tm/pkg/notification"
|
||||
@@ -37,7 +37,7 @@ type TEDScraper struct {
|
||||
httpClient *http.Client
|
||||
xmlParser *TEDParser
|
||||
mongoManager *mongo.ConnectionManager
|
||||
tenderRepo tender.TenderRepository
|
||||
tenderRepo notice.Repository
|
||||
}
|
||||
|
||||
// NewTEDScraper creates a new TED scraper instance
|
||||
@@ -45,7 +45,7 @@ func NewTEDScraper(
|
||||
config *Config,
|
||||
logger logger.Logger,
|
||||
mongoManager *mongo.ConnectionManager,
|
||||
tenderRepo tender.TenderRepository,
|
||||
tenderRepo notice.Repository,
|
||||
notify notification.SDK,
|
||||
) *TEDScraper {
|
||||
httpClient := &http.Client{
|
||||
@@ -275,7 +275,7 @@ func (s *TEDScraper) tarGzFileProcessor(ctx context.Context, reader io.Reader, o
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *TEDScraper) findTenderByContractNoticeID(ctx context.Context, contractNoticeID string) (*tender.Tender, error) {
|
||||
func (s *TEDScraper) findTenderByContractNoticeID(ctx context.Context, contractNoticeID string) (*notice.Notice, error) {
|
||||
existingTender, err := s.tenderRepo.GetByContractNoticeID(ctx, contractNoticeID)
|
||||
if err != nil {
|
||||
if err.Error() == "document not found" {
|
||||
@@ -308,37 +308,37 @@ func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, file
|
||||
// Map to tender entity
|
||||
t := s.mapToTenderFromParsedDoc(notice, "", fileName)
|
||||
|
||||
existingTender, err := s.findTenderByContractNoticeID(ctx, t.ContractNoticeID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Error checking for existing tender", map[string]interface{}{
|
||||
"contract_notice_id": t.ContractNoticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
// existingTender, err := s.findTenderByContractNoticeID(ctx, t.ContractNoticeID)
|
||||
// if err != nil {
|
||||
// s.logger.Warn("Error checking for existing tender", map[string]interface{}{
|
||||
// "contract_notice_id": t.ContractNoticeID,
|
||||
// "error": err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
if err == nil && existingTender != nil {
|
||||
// Update existing tender
|
||||
s.logger.Info("Found existing tender, updating", map[string]interface{}{
|
||||
"existing_tender_id": existingTender.ID.Hex(),
|
||||
"contract_notice_id": t.ContractNoticeID,
|
||||
})
|
||||
// if err == nil && existingTender != nil {
|
||||
// // Update existing tender
|
||||
// s.logger.Info("Found existing tender, updating", map[string]interface{}{
|
||||
// "existing_tender_id": existingTender.ID.Hex(),
|
||||
// "contract_notice_id": t.ContractNoticeID,
|
||||
// })
|
||||
|
||||
t.ID = existingTender.ID
|
||||
t.UpdatedAt = time.Now().Unix()
|
||||
err = s.tenderRepo.Update(ctx, t)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update existing tender", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to update existing tender: %w", err)
|
||||
}
|
||||
// t.ID = existingTender.ID
|
||||
// t.UpdatedAt = time.Now().Unix()
|
||||
// err = s.tenderRepo.Update(ctx, t)
|
||||
// if err != nil {
|
||||
// s.logger.Error("Failed to update existing tender", map[string]interface{}{
|
||||
// "tender_id": t.ID.Hex(),
|
||||
// "error": err.Error(),
|
||||
// })
|
||||
// return fmt.Errorf("failed to update existing tender: %w", err)
|
||||
// }
|
||||
|
||||
s.logger.Info("Successfully updated tender", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
})
|
||||
} else {
|
||||
// Create new tender
|
||||
// s.logger.Info("Successfully updated tender", map[string]interface{}{
|
||||
// "tender_id": t.ID.Hex(),
|
||||
// })
|
||||
// } else {
|
||||
// // Create new tender
|
||||
s.logger.Info("Creating new tender", map[string]interface{}{
|
||||
"contract_notice_id": t.ContractNoticeID,
|
||||
"tender_id": t.TenderID,
|
||||
@@ -347,7 +347,7 @@ func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, file
|
||||
t.CreatedAt = time.Now().Unix()
|
||||
t.UpdatedAt = t.CreatedAt
|
||||
|
||||
err = s.tenderRepo.Create(ctx, t)
|
||||
err = s.tenderRepo.Import(ctx, t)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create new tender", map[string]interface{}{
|
||||
"contract_notice_id": t.ContractNoticeID,
|
||||
@@ -361,7 +361,7 @@ func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, file
|
||||
"tender_id": t.TenderID,
|
||||
"contract_notice_id": t.ContractNoticeID,
|
||||
})
|
||||
}
|
||||
// }
|
||||
|
||||
// Log detailed parsed information based on notice type
|
||||
s.logger.Info("Successfully parsed notice", map[string]interface{}{
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
)
|
||||
|
||||
// parseDate parses various date formats used in TED XML
|
||||
@@ -146,7 +146,7 @@ func GenerateTenderURL(noticePublicationID string) string {
|
||||
|
||||
// generateTenderID generates a unique tender ID using the format:
|
||||
// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
|
||||
func GenerateTenderID(t *tender.Tender) string {
|
||||
func GenerateTenderID(t *notice.Notice) string {
|
||||
var parts []string
|
||||
|
||||
// Source (TED)
|
||||
|
||||
Reference in New Issue
Block a user