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.
This commit is contained in:
Mazyar
2026-07-12 14:54:42 +03:30
parent 10385e997b
commit 0cce9ef1b5
12 changed files with 1598 additions and 18 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ func (h *Handler) ToggleTenderApproval(c echo.Context) error {
return response.BadRequest(c, "Company ID is required", "")
}
tenderApproval, err := h.service.ToggleTenderApproval(c.Request().Context(), form, companyID)
customerID, _ := customer.GetCustomerIDFromContext(c)
tenderApproval, err := h.service.ToggleTenderApproval(c.Request().Context(), form, companyID, customerID)
if err != nil {
if errors.Is(err, ErrTenderNotFound) {
return response.NotFound(c, "Tender not found")
+46 -9
View File
@@ -11,6 +11,13 @@ import (
"tm/pkg/response"
)
// SubmissionLifecycleHook synchronizes tender submission workflow with approval changes.
type SubmissionLifecycleHook interface {
OnApprovalSubmitted(ctx context.Context, tenderID, companyID, customerID string) error
OnApprovalRemoved(ctx context.Context, tenderID, companyID string) error
OnApprovalRejected(ctx context.Context, tenderID, companyID, changedBy string) error
}
// Service defines business logic for tender approval operations
type Service interface {
// Core tender approval operations
@@ -34,7 +41,7 @@ type Service interface {
SearchTenderApprovalsByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error)
// Business operations
ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID string) (*TenderApprovalWithTenderResponse, error)
ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID, customerID string) (*TenderApprovalWithTenderResponse, error)
// Statistics and analytics
GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error)
@@ -51,17 +58,19 @@ type Service interface {
// tenderApprovalService implements the Service interface
type tenderApprovalService struct {
repository Repository
tenderService tender.Service
logger logger.Logger
repository Repository
tenderService tender.Service
logger logger.Logger
submissionHook SubmissionLifecycleHook
}
// NewService creates a new tender approval service
func NewService(repository Repository, tenderService tender.Service, logger logger.Logger) Service {
func NewService(repository Repository, tenderService tender.Service, logger logger.Logger, submissionHook SubmissionLifecycleHook) Service {
return &tenderApprovalService{
repository: repository,
tenderService: tenderService,
logger: logger,
repository: repository,
tenderService: tenderService,
logger: logger,
submissionHook: submissionHook,
}
}
@@ -853,7 +862,7 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex
return meta, nil
}
func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID string) (*TenderApprovalWithTenderResponse, error) {
func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID, customerID string) (*TenderApprovalWithTenderResponse, error) {
companyID = strings.TrimSpace(companyID)
if companyID == "" {
return nil, ErrInvalidCompanyID
@@ -899,6 +908,7 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T
return nil, err
}
s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, true)
return s.toggleResponseWithTender(ctx, approval)
}
@@ -917,6 +927,7 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T
existing.Status = "unrejected"
}
s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, false)
return existing.ToResponseWithTender(nil), nil
}
@@ -939,9 +950,35 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T
return nil, err
}
s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, true)
return s.toggleResponseWithTender(ctx, existing)
}
func (s *tenderApprovalService) syncSubmissionAfterToggle(ctx context.Context, tenderID, companyID, customerID string, status ApprovalStatus, active bool) {
if s.submissionHook == nil {
return
}
var err error
if !active {
err = s.submissionHook.OnApprovalRemoved(ctx, tenderID, companyID)
} else if status == ApprovalStatusSubmitted {
err = s.submissionHook.OnApprovalSubmitted(ctx, tenderID, companyID, customerID)
} else if status == ApprovalStatusRejected {
err = s.submissionHook.OnApprovalRejected(ctx, tenderID, companyID, customerID)
}
if err != nil {
s.logger.Error("Failed to sync tender submission workflow after approval toggle", map[string]interface{}{
"tender_id": tenderID,
"company_id": companyID,
"status": status,
"active": active,
"error": err.Error(),
})
}
}
func (s *tenderApprovalService) toggleResponseWithTender(ctx context.Context, approval *TenderApproval) (*TenderApprovalWithTenderResponse, error) {
if approval == nil {
return nil, ErrInvalidData