Files
Mazyar 0cce9ef1b5 Add tender submission functionality and related routes
- Introduced the `tender_submission` domain, including entity, repository, service, and handler implementations for managing tender submissions.
- Added new routes for both admin and public access to tender submissions, allowing for listing, ensuring, and retrieving submissions by ID and tender.
- Enhanced the tender approval service to synchronize submission workflows with approval changes, ensuring proper state management.
- Implemented validation and response structures for tender submission operations, improving API consistency and usability.
- Added unit tests for tender submission status transitions and workflow logic, ensuring robust functionality.

This update enhances the tender management system by providing comprehensive support for tender submissions, improving overall workflow and user experience.
2026-07-12 14:54:42 +03:30

423 lines
17 KiB
Go

package router
import (
"tm/internal/assets"
"tm/internal/ai_pipeline"
"tm/internal/auditlog"
"tm/internal/cms"
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/contact"
"tm/internal/customer"
"tm/internal/dashboard"
"tm/internal/document_scraper"
"tm/internal/feedback"
"tm/internal/inquiry"
"tm/internal/kanban"
"tm/internal/notification"
"tm/internal/tender"
"tm/internal/tender_approval"
"tm/internal/tender_submission"
"tm/internal/user"
"tm/pkg/filestore"
"tm/pkg/security"
"github.com/labstack/echo/v4"
)
// SetupCSPSecurity registers global CSP middleware and the CSP report endpoint once.
func SetupCSPSecurity(e *echo.Echo) {
adminCSPConfig := security.DefaultCSPConfig()
adminCSPConfig.ScriptSrc = []string{"'self'"} // No unsafe-inline for admin
adminCSPConfig.StyleSrc = []string{"'self'"} // No unsafe-inline for admin
publicCSPConfig := security.DefaultCSPConfig()
e.Use(security.CSPMiddlewareForPaths("/admin", adminCSPConfig, "/api/v1", publicCSPConfig))
e.POST("/api/v1/csp-report", security.CSPReportHandler)
}
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, tenderSubmissionHandler *tender_submission.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, aiPipelineHandler *ai_pipeline.Handler, auditLogHandler *auditlog.Handler, xssPolicy *security.XSSPolicy) {
adminV1 := e.Group("/admin/v1")
// Admin Users Routes
adminUsersGP := adminV1.Group("/users")
{
adminUsersGP.Use(userHandler.AuthMiddleware())
adminUsersGP.POST("", userHandler.CreateUser)
adminUsersGP.GET("", userHandler.ListUsers)
adminUsersGP.GET("/:id", userHandler.GetUserByID)
adminUsersGP.PUT("/:id", userHandler.UpdateUser)
adminUsersGP.DELETE("/:id", userHandler.DeleteUser)
adminUsersGP.PUT("/:id/status", userHandler.UpdateUserStatus)
adminUsersGP.POST("/:id/reset-password", userHandler.ResetUserPassword, userHandler.AdminMiddleware())
}
// Admin Audit Logs Routes
auditLogsGP := adminV1.Group("/audit-logs")
{
auditLogsGP.Use(userHandler.AuthMiddleware(), userHandler.AdminMiddleware())
auditLogsGP.GET("", auditLogHandler.Search)
}
// Admin user profile
profileGP := adminV1.Group("/profile")
{
userAuthorizationGP := profileGP.Group("")
userAuthorizationGP.POST("/login", userHandler.Login)
userAuthorizationGP.POST("/refresh-token", userHandler.RefreshToken)
userAuthorizationGP.POST("/reset-password", userHandler.ResetPassword)
userProfileGP := profileGP.Group("")
userProfileGP.Use(userHandler.AuthMiddleware())
userProfileGP.GET("", userHandler.GetProfile)
userProfileGP.PUT("", userHandler.UpdateProfile)
userProfileGP.PUT("/change-password", userHandler.ChangePassword)
userProfileGP.DELETE("/logout", userHandler.Logout)
}
// Admin Companies Routes
companiesGP := adminV1.Group("/companies")
{
companiesGP.Use(userHandler.AuthMiddleware())
companiesGP.POST("", companyHandler.Create)
companiesGP.GET("", companyHandler.Search)
companiesGP.GET("/:id/recommended-tenders", tenderHandler.AdminRecommendTenders)
companiesGP.GET("/:id", companyHandler.Get)
companiesGP.PUT("/:id", companyHandler.Update)
companiesGP.DELETE("/:id", companyHandler.Delete)
companiesGP.PATCH("/:id/status", companyHandler.UpdateStatus)
companiesGP.PATCH("/:id/verification", companyHandler.UpdateVerification)
companiesGP.POST("/:id/documents", companyHandler.UploadDocuments)
companiesGP.POST("/:id/links", companyHandler.AddLinks)
}
// Admin Categories Routes
categoriesGP := adminV1.Group("/company-categories")
{
categoriesGP.Use(userHandler.AuthMiddleware())
categoriesGP.POST("", categoryHandler.Create)
categoriesGP.GET("", categoryHandler.Search)
categoriesGP.GET("/:id", categoryHandler.Get)
categoriesGP.PUT("/:id", categoryHandler.Update)
categoriesGP.DELETE("/:id", categoryHandler.Delete)
categoriesGP.PATCH("/:id/publish", categoryHandler.TogglePublish)
}
// Admin Customers Routes
customersGP := adminV1.Group("/customers")
{
customersGP.Use(userHandler.AuthMiddleware())
customersGP.POST("", customerHandler.CreateCustomer)
customersGP.GET("", customerHandler.Search)
customersGP.GET("/:id", customerHandler.GetCustomerByID)
customersGP.PUT("/:id", customerHandler.UpdateCustomer)
customersGP.DELETE("/:id", customerHandler.DeleteCustomer)
customersGP.PUT("/:id/status", customerHandler.UpdateStatus)
customersGP.POST("/:id/reset-password", customerHandler.ResetCustomerPassword)
customersGP.PATCH("/:id/role", customerHandler.AssignRole)
customersGP.PATCH("/:id/companies", customerHandler.AssignCompanies)
}
// Admin Tenders Routes
tendersGP := adminV1.Group("/tenders")
{
tendersGP.Use(userHandler.AuthMiddleware())
tendersGP.GET("", tenderHandler.Search)
tendersGP.GET("/ai-summaries", tenderHandler.GetAllAISummaries)
tendersGP.GET("/scraped-documents", tenderHandler.ListTendersWithScrapedDocuments)
tendersGP.GET("/:id", tenderHandler.Get)
tendersGP.PUT("/:id", tenderHandler.Update)
tendersGP.DELETE("/:id", tenderHandler.Delete)
tendersGP.GET("/:id/documents", tenderHandler.GetDocuments)
tendersGP.GET("/:id/documents/download", tenderHandler.DownloadDocuments)
tendersGP.GET("/:id/ai-summary", tenderHandler.GetAISummary)
tendersGP.POST("/:id/ai-summarize", tenderHandler.TriggerAISummarize)
tendersGP.POST("/:id/ai-analyze", tenderHandler.TriggerAIAnalyze)
tendersGP.POST("/:id/ai-translate", tenderHandler.TriggerAITranslate)
}
// Admin Feedback Routes
feedbackGP := adminV1.Group("/feedback")
{
feedbackGP.Use(userHandler.AuthMiddleware())
feedbackGP.GET("", feedbackHandler.Search)
feedbackGP.GET("/:id", feedbackHandler.Get)
feedbackGP.DELETE("/:id", feedbackHandler.Delete)
}
// Admin Tender-Approvals Routes
tenderApprovalGP := adminV1.Group("/tender-approvals")
{
tenderApprovalGP.Use(userHandler.AuthMiddleware())
tenderApprovalGP.GET("", tenderApprovalHandler.ListTenderApprovals)
tenderApprovalGP.GET("/:id", tenderApprovalHandler.GetTenderApproval)
tenderApprovalGP.GET("/stats", tenderApprovalHandler.GetTenderApprovalStats)
tenderApprovalGP.GET("/submission-mode/:submission_mode", tenderApprovalHandler.GetTenderApprovalsBySubmissionMode)
tenderApprovalGP.GET("/status/:status", tenderApprovalHandler.GetTenderApprovalsByStatus)
}
// Admin Tender-Submissions Routes
tenderSubmissionGP := adminV1.Group("/tender-submissions")
{
tenderSubmissionGP.Use(userHandler.AuthMiddleware())
tenderSubmissionGP.GET("", tenderSubmissionHandler.AdminListSubmissions)
tenderSubmissionGP.GET("/stats", tenderSubmissionHandler.AdminGetSubmissionStats)
tenderSubmissionGP.GET("/:id", tenderSubmissionHandler.AdminGetSubmissionByID)
}
// Admin Inquiry Routes
inquiryGP := adminV1.Group("/inquiries")
{
inquiryGP.Use(userHandler.AuthMiddleware())
inquiryGP.GET("", inquiryHandler.SearchInquiries)
inquiryGP.GET("/:id", inquiryHandler.GetInquiryByID)
inquiryGP.PUT("/:id/status", inquiryHandler.UpdateInquiryStatus)
inquiryGP.DELETE("/:id", inquiryHandler.DeleteInquiry)
}
// Admin Flags Routes
flagsGP := adminV1.Group("/flags")
{
flagsGP.GET("/:country_code", flagHandler.AdminGetFlagSVG)
}
// Admin Notifications Routes
notificationsGP := adminV1.Group("/notifications")
{
notificationsGP.Use(userHandler.AuthMiddleware())
notificationsGP.POST("", notificationHandler.Send)
notificationsGP.GET("", notificationHandler.GetNotifications)
notificationsGP.GET("/view/:id", notificationHandler.ViewNotification)
notificationsGP.GET("/mark-seen/:id", notificationHandler.MarkSeen)
notificationsGP.GET("/my", notificationHandler.AdminNotifications)
}
// Admin Contact Routes
contactsGP := adminV1.Group("/contacts")
{
contactsGP.Use(userHandler.AuthMiddleware())
contactsGP.GET("", contactHandler.Search)
contactsGP.GET("/stats", contactHandler.GetStats)
contactsGP.GET("/:id", contactHandler.GetByID)
contactsGP.PUT("/:id/status", contactHandler.UpdateStatus)
contactsGP.DELETE("/:id", contactHandler.Delete)
}
// Admin CMS Routes
cmsGP := adminV1.Group("/cms")
{
cmsGP.Use(userHandler.AuthMiddleware())
cmsGP.POST("", cmsHandler.Create)
cmsGP.GET("", cmsHandler.Search)
cmsGP.GET("/:id", cmsHandler.GetByID)
cmsGP.PUT("/:id", cmsHandler.Update)
cmsGP.DELETE("/:id", cmsHandler.Delete)
}
// Kanban Routes — paths are /admin/v1/kanban/* (RegisterRoutes expects this /kanban group)
kanbanGP := adminV1.Group("/kanban")
{
kanbanGP.Use(userHandler.AuthMiddleware())
kanbanHandler.RegisterRoutes(kanbanGP)
}
// File Store Routes
filesGP := adminV1.Group("/files")
{
filesGP.Use(userHandler.AuthMiddleware())
filesGP.POST("/upload", fileStoreHandler.Upload)
filesGP.POST("/upload/batch", fileStoreHandler.UploadBatch)
filesGP.GET("", fileStoreHandler.ListFiles)
filesGP.GET("/:file_id/info", fileStoreHandler.GetInfo)
filesGP.GET("/:file_id/download", fileStoreHandler.Download)
filesGP.DELETE("/:file_id", fileStoreHandler.Delete)
}
// Dashboard Routes
dashboardGP := adminV1.Group("/dashboard")
{
dashboardGP.Use(userHandler.AuthMiddleware())
dashboardGP.GET("/summary", dashboardHandler.Summary)
dashboardGP.GET("/trend", dashboardHandler.Trend)
dashboardGP.GET("/countries", dashboardHandler.Countries)
dashboardGP.GET("/notice-types", dashboardHandler.NoticeTypes)
dashboardGP.GET("/closing-soon", dashboardHandler.ClosingSoon)
dashboardGP.GET("/recent", dashboardHandler.Recent)
dashboardGP.GET("/statistics", dashboardHandler.Statistics)
}
// AI Pipeline Routes (proxy to Opplens AI service)
aiPipelineGP := adminV1.Group("/ai-pipeline")
{
aiPipelineGP.Use(userHandler.AuthMiddleware())
aiPipelineGP.POST("/scrape/documents", aiPipelineHandler.ScrapeDocuments)
aiPipelineGP.POST("/scrape/documents/batch", aiPipelineHandler.ScrapeDocumentsBatch)
aiPipelineGP.GET("/scrape/portals", aiPipelineHandler.GetScrapePortals)
aiPipelineGP.POST("/summarize/batch", aiPipelineHandler.SummarizeBatch)
aiPipelineGP.POST("/translate/batch", aiPipelineHandler.TranslateBatch)
aiPipelineGP.POST("/sync", aiPipelineHandler.Sync)
aiPipelineGP.POST("/run", aiPipelineHandler.Run)
aiPipelineGP.POST("/run/batch", aiPipelineHandler.RunBatch)
aiPipelineGP.POST("/analyze", aiPipelineHandler.Analyze)
aiPipelineGP.POST("/daily-run", aiPipelineHandler.DailyRun)
aiPipelineGP.POST("/auto", aiPipelineHandler.Auto)
aiPipelineGP.GET("/last-run", aiPipelineHandler.GetLastRun)
aiPipelineGP.GET("/last-auto-run", aiPipelineHandler.GetLastAutoRun)
aiPipelineGP.GET("/report", aiPipelineHandler.GetReport)
aiPipelineGP.GET("/stats", aiPipelineHandler.GetStats)
aiPipelineGP.GET("/minio-stats", aiPipelineHandler.GetMinioStats)
}
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, tenderSubmissionHandler *tender_submission.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, xssPolicy *security.XSSPolicy) {
v1 := e.Group("/api/v1")
customerGP := v1.Group("/profile")
{
customerGP.POST("/login", customerHandler.Login)
customerGP.POST("/refresh-token", customerHandler.RefreshToken)
// Forgot password routes
customerGP.POST("/forgot-password", customerHandler.RequestResetPassword)
customerGP.POST("/verify-otp", customerHandler.VerifyOTP)
customerGP.POST("/reset-password", customerHandler.ResetPassword)
profileGP := customerGP.Group("")
profileGP.Use(customerHandler.AuthMiddleware())
profileGP.GET("", customerHandler.GetProfile)
profileGP.PUT("/keywords", customerHandler.UpdateProfileKeywords)
profileGP.DELETE("/logout", customerHandler.Logout)
}
// Public Tenders Routes
tendersGP := v1.Group("/tenders")
tendersGP.Use(customerHandler.AuthMiddleware(), customerHandler.CompanyContextMiddleware())
{
tendersGP.GET("", tenderHandler.GetPublicTenders)
tendersGP.GET("/recommend", tenderHandler.RecommendTenders)
tendersGP.GET("/details/:id", tenderHandler.GetPublicTenderDetails)
tendersGP.GET("/:id/documents", tenderHandler.GetDocuments)
tendersGP.GET("/:id/documents/download", tenderHandler.DownloadDocuments)
tendersGP.GET("/:id/document-summaries", tenderHandler.GetDocumentSummaries)
tendersGP.GET("/:id/ai-summary", tenderHandler.GetPublicAISummary)
tendersGP.POST("/:id/ai-translate", tenderHandler.TriggerPublicAITranslate)
}
// Public Feedback Routes
feedbackGP := v1.Group("/feedback")
{
feedbackGP.Use(customerHandler.AuthMiddleware())
feedbackGP.POST("", feedbackHandler.Toggle)
feedbackGP.GET("", feedbackHandler.PublicList)
feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetByTenderID)
feedbackGP.GET("/:id", feedbackHandler.PublicGet)
feedbackGP.GET("/stats/customer", feedbackHandler.GetCustomerStats)
feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyStats)
}
// Public Tender Approvals Routes
tenderApprovalGP := v1.Group("/tender-approvals")
{
tenderApprovalGP.Use(customerHandler.AuthMiddleware(), customerHandler.CompanyContextMiddleware())
tenderApprovalGP.POST("", tenderApprovalHandler.ToggleTenderApproval)
tenderApprovalGP.GET("", tenderApprovalHandler.PublicGetTenderApprovals)
tenderApprovalGP.GET("/:id", tenderApprovalHandler.GetPublicTenderApproval)
tenderApprovalGP.GET("/tender/:tender_id", tenderApprovalHandler.GetTenderApprovalByTenderAndCompany)
tenderApprovalGP.GET("/stats", tenderApprovalHandler.GetCompanyTenderApprovalStats)
}
// Public Tender Submissions Routes
tenderSubmissionGP := v1.Group("/tender-submissions")
{
tenderSubmissionGP.Use(customerHandler.AuthMiddleware(), customerHandler.CompanyContextMiddleware())
tenderSubmissionGP.GET("", tenderSubmissionHandler.ListCompanySubmissions)
tenderSubmissionGP.GET("/stats", tenderSubmissionHandler.GetCompanySubmissionStats)
tenderSubmissionGP.POST("/tender/:tender_id/ensure", tenderSubmissionHandler.EnsureSubmission)
tenderSubmissionGP.GET("/tender/:tender_id", tenderSubmissionHandler.GetSubmissionByTender)
tenderSubmissionGP.PATCH("/:id/status", tenderSubmissionHandler.UpdateSubmissionStatus)
tenderSubmissionGP.GET("/:id", tenderSubmissionHandler.GetSubmissionByID)
}
// Public Company Routes
companiesGP := v1.Group("/companies")
{
companiesGP.Use(customerHandler.AuthMiddleware(), customerHandler.CompanyContextMiddleware())
companiesGP.GET("", companyHandler.MyCompany)
}
// AI tender recommendation routes
recommendationGP := v1.Group("")
recommendationGP.Use(customerHandler.AuthMiddleware(), customerHandler.CompanyContextMiddleware())
{
recommendationGP.POST("/onboarding", companyHandler.StartOnboarding)
recommendationGP.POST("/recommend", companyHandler.RecommendTenders)
}
// Public Inquiry Routes
inquiryGP := v1.Group("/inquiries")
{
inquiryGP.POST("", inquiryHandler.CreateInquiry)
}
// Public Flags Routes
flagsGP := v1.Group("/flags")
{
flagsGP.GET("/:country_code", flagHandler.GetFlagSVG)
}
// Public Notifications Routes
notificationsGP := v1.Group("/notifications")
{
notificationsGP.Use(customerHandler.AuthMiddleware())
notificationsGP.GET("", notificationHandler.CustomerNotifications)
notificationsGP.GET("/view/:id", notificationHandler.PublicViewNotification)
notificationsGP.GET("/mark", notificationHandler.PublicAllMarkSeen)
notificationsGP.GET("/mark/:id", notificationHandler.PublicMarkSeen)
}
// Public Contact Routes
contactsPublicGP := v1.Group("/contacts")
{
contactsPublicGP.POST("", contactHandler.Create)
}
// Public CMS Routes
cmsGP := v1.Group("/cms")
{
cmsGP.GET("/:key", cmsHandler.GetByKey)
}
// Kanban (mobile bid workflow board) — paths are /api/v1/kanban/*
kanbanGP := v1.Group("/kanban")
{
kanbanGP.Use(customerHandler.AuthMiddleware())
kanbanHandler.RegisterRoutes(kanbanGP)
}
// File Store Routes
publicFilesGP := v1.Group("/files")
{
publicFilesGP.Use(customerHandler.AuthMiddleware())
publicFilesGP.POST("/upload", fileStoreHandler.Upload)
publicFilesGP.POST("/upload/batch", fileStoreHandler.UploadBatch)
publicFilesGP.GET("", fileStoreHandler.ListFiles)
publicFilesGP.GET("/:file_id/info", fileStoreHandler.GetInfo)
publicFilesGP.GET("/:file_id/download", fileStoreHandler.Download)
publicFilesGP.DELETE("/:file_id", fileStoreHandler.Delete)
}
}
func RegisterDocumentScraperRoutes(e *echo.Echo, docScraperHandler *document_scraper.Handler, apiKey string) {
v1 := e.Group("/api/v1/scraper")
// Apply API key middleware to all document scraper routes
v1.Use(document_scraper.APIKeyMiddleware(apiKey))
// Document Scraper Routes
{
v1.GET("/tenders", docScraperHandler.ListPendingTenders)
v1.GET("/tenders/:notice_id", docScraperHandler.GetTenderByNoticeID)
}
}