Files
Mazyar fa3d466579 Enhance tender approval and submission handling
- Added a new error for submission sync failures, improving error handling in the tender approval process.
- Updated the `ToggleTenderApproval` method to handle the new error, providing clearer responses for sync issues.
- Introduced a method to reopen terminal submissions when tender approvals are resubmitted, enhancing submission state management.
- Implemented optimistic locking in the repository to prevent concurrent updates, improving data integrity.
- Added unit tests for new submission handling logic, ensuring robust functionality and error management.

This update strengthens the tender approval workflow and submission handling, ensuring better error reporting and state management.
2026-07-12 21:41:53 +03:30

1030 lines
38 KiB
Go

package tender_approval
import (
"context"
"errors"
"math"
"strings"
"time"
"tm/internal/tender"
"tm/pkg/logger"
"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
CreateTenderApproval(ctx context.Context, form *CreateTenderApprovalForm, companyID string) (*TenderApproval, error)
GetTenderApprovalByID(ctx context.Context, id string) (*TenderApproval, error)
GetTenderApprovalByIDWithTender(ctx context.Context, id string) (*TenderApprovalWithTenderResponse, error)
GetTenderApprovalByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error)
GetTenderApprovalByTenderAndCompanyWithTender(ctx context.Context, tenderID, companyID string) (*TenderApprovalWithTenderResponse, error)
GetTenderApprovalsByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error)
GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) ([]*TenderApproval, int64, error)
GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) (*TenderApprovalWithTenderListResponse, error)
GetTenderApprovalsByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error)
UpdateTenderApproval(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApproval, error)
UpdateTenderApprovalWithTender(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApprovalWithTenderResponse, error)
DeleteTenderApproval(ctx context.Context, id string) error
// Tender approval listing and search
ListTenderApprovals(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponse, error)
ListTenderApprovalsWithTender(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponseWithTender, error)
SearchTenderApprovals(ctx context.Context, form *TenderApprovalSearchForm) (*TenderApprovalListResponse, error)
SearchTenderApprovalsByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error)
// Business operations
ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID, customerID string) (*TenderApprovalWithTenderResponse, error)
// Statistics and analytics
GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error)
GetCompanyTenderApprovalStats(ctx context.Context, companyID string) (*CompanyTenderApprovalStatsResponse, error)
GetMultipleCompanyTenderApprovalStats(ctx context.Context, companyIDs []string) ([]*CompanyTenderApprovalStatsResponse, error)
GetAggregatedCompanyStats(ctx context.Context) (*AggregatedCompanyStatsResponse, error)
// Search and filtering helpers
GetTenderApprovalsByStatus(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApproval, int64, error)
GetTenderApprovalsByStatusWithTender(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApprovalWithTenderResponse, int64, error)
GetTenderApprovalsBySubmissionMode(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApproval, int64, error)
GetTenderApprovalsBySubmissionModeWithTender(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApprovalWithTenderResponse, int64, error)
}
// tenderApprovalService implements the Service interface
type tenderApprovalService struct {
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, submissionHook SubmissionLifecycleHook) Service {
return &tenderApprovalService{
repository: repository,
tenderService: tenderService,
logger: logger,
submissionHook: submissionHook,
}
}
// CreateTenderApproval creates a new tender approval
func (s *tenderApprovalService) CreateTenderApproval(ctx context.Context, form *CreateTenderApprovalForm, companyID string) (*TenderApproval, error) {
// Check if tender approval already exists for this tender and company
existingApproval, _ := s.repository.GetByTenderAndCompany(ctx, form.TenderID, companyID)
if existingApproval != nil {
return nil, errors.New("tender approval already exists for this tender and company")
}
// Create tender approval
tenderApproval := &TenderApproval{
TenderID: form.TenderID,
CompanyID: companyID,
SubmissionMode: SubmissionMode(form.SubmissionMode),
Status: ApprovalStatus(form.Status),
}
// Save to database
err := s.repository.Create(ctx, tenderApproval)
if err != nil {
s.logger.Error("Failed to create tender approval", map[string]interface{}{
"error": err.Error(),
"tender_id": form.TenderID,
"company_id": companyID,
})
return nil, err
}
s.logger.Info("Tender approval created successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
"submission_mode": tenderApproval.SubmissionMode,
"status": tenderApproval.Status,
})
return tenderApproval, nil
}
// GetTenderApprovalByID retrieves a tender approval by ID
func (s *tenderApprovalService) GetTenderApprovalByID(ctx context.Context, id string) (*TenderApproval, error) {
tenderApproval, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender approval by ID", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, err
}
s.logger.Info("Tender approval retrieved successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return tenderApproval, nil
}
// GetTenderApprovalByIDWithTender retrieves a tender approval by ID with tender details
func (s *tenderApprovalService) GetTenderApprovalByIDWithTender(ctx context.Context, id string) (*TenderApprovalWithTenderResponse, error) {
tenderApproval, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender approval by ID", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, err
}
// Get tender details
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID,
"error": err.Error(),
})
// Return approval without tender details rather than failing completely
return tenderApproval.ToResponseWithTender(nil), nil
}
// Convert tender response to tender details
tenderDetails := &TenderDetails{
ID: tenderResponse.ID,
NoticePublicationID: tenderResponse.NoticePublicationID,
PublicationDate: tenderResponse.PublicationDate,
Title: tenderResponse.Title,
Description: tenderResponse.Description,
ProcurementTypeCode: tenderResponse.ProcurementTypeCode,
ProcedureCode: tenderResponse.ProcedureCode,
EstimatedValue: tenderResponse.EstimatedValue,
Currency: tenderResponse.Currency,
Duration: tenderResponse.Duration,
DurationUnit: tenderResponse.DurationUnit,
TenderDeadline: tenderResponse.TenderDeadline,
SubmissionDeadline: tenderResponse.SubmissionDeadline,
CountryCode: tenderResponse.CountryCode,
BuyerOrganization: &OrganizationResponse{Name: tenderResponse.BuyerOrganization.Name},
Status: string(tenderResponse.Status),
}
s.logger.Info("Tender approval with tender details retrieved successfully", map[string]interface{}{
"tender_approval_id": id,
})
return tenderApproval.ToResponseWithTender(tenderDetails), nil
}
// GetTenderApprovalByTenderAndCompany retrieves a tender approval by tender ID and company ID
func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) {
tenderApproval, err := s.repository.GetByTenderAndCompany(ctx, tenderID, companyID)
if err != nil {
s.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{
"error": err.Error(),
"tender_id": tenderID,
"company_id": companyID,
})
return nil, err
}
s.logger.Info("Tender approval retrieved successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return tenderApproval, nil
}
// GetTenderApprovalByTenderAndCompanyWithTender retrieves a tender approval by tender ID and company ID with tender details
func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompanyWithTender(ctx context.Context, tenderID, companyID string) (*TenderApprovalWithTenderResponse, error) {
tenderApproval, err := s.repository.GetByTenderAndCompany(ctx, tenderID, companyID)
if err != nil {
s.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{
"error": err.Error(),
"tender_id": tenderID,
"company_id": companyID,
})
return nil, err
}
// Get tender details
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID,
})
// Return approval without tender details rather than failing completely
return tenderApproval.ToResponseWithTender(nil), nil
}
// Convert tender response to tender details
tenderDetails := &TenderDetails{
ID: tenderResponse.ID,
NoticePublicationID: tenderResponse.NoticePublicationID,
PublicationDate: tenderResponse.PublicationDate,
Title: tenderResponse.Title,
Description: tenderResponse.Description,
ProcurementTypeCode: tenderResponse.ProcurementTypeCode,
ProcedureCode: tenderResponse.ProcedureCode,
EstimatedValue: tenderResponse.EstimatedValue,
Currency: tenderResponse.Currency,
Duration: tenderResponse.Duration,
DurationUnit: tenderResponse.DurationUnit,
TenderDeadline: tenderResponse.TenderDeadline,
SubmissionDeadline: tenderResponse.SubmissionDeadline,
CountryCode: tenderResponse.CountryCode,
BuyerOrganization: &OrganizationResponse{Name: tenderResponse.BuyerOrganization.Name},
Status: string(tenderResponse.Status),
}
s.logger.Info("Tender approval with tender details retrieved successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return tenderApproval.ToResponseWithTender(tenderDetails), nil
}
// GetTenderApprovalsByTenderID retrieves all tender approvals for a specific tender
func (s *tenderApprovalService) GetTenderApprovalsByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) {
tenderApprovals, err := s.repository.GetByTenderID(ctx, tenderID)
if err != nil {
s.logger.Error("Failed to get tender approvals by tender ID", map[string]interface{}{
"error": err.Error(),
"tender_id": tenderID,
})
return nil, err
}
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
"tender_id": tenderID,
"count": len(tenderApprovals),
})
return tenderApprovals, nil
}
// GetTenderApprovalsByCompanyID retrieves all tender approvals for a specific company with optional filters
func (s *tenderApprovalService) GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit int, offset int) ([]*TenderApproval, int64, error) {
tenderApprovals, total, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, limit, offset, from, to)
if err != nil {
s.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, 0, err
}
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
"company_id": companyID,
"count": len(tenderApprovals),
})
return tenderApprovals, total, nil
}
// GetTenderApprovalsByCompanyIDWithTender retrieves all tender approvals for a specific company with tender details
func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus, from *int64, to *int64, limit, offset int) (*TenderApprovalWithTenderListResponse, error) {
tenderApprovals, total, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status, limit, offset, from, to)
if err != nil {
s.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, err
}
tenders := s.loadTendersByIDs(ctx, uniqueTenderIDs(tenderApprovals))
responses := &TenderApprovalWithTenderListResponse{
Approvals: mapApprovalsWithTender(tenderApprovals, tenders, listTenderDetailsFromResponse),
Metadata: &response.Meta{},
}
responses.Metadata = &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
Pages: int(math.Ceil(float64(total) / float64(limit))),
Page: int(math.Ceil(float64(offset) / float64(limit))),
}
s.logger.Info("Tender approvals with tender details retrieved successfully", map[string]interface{}{
"company_id": companyID,
"count": len(responses.Approvals),
})
return responses, nil
}
// GetTenderApprovalsByIDs retrieves multiple tender approvals by their IDs
func (s *tenderApprovalService) GetTenderApprovalsByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) {
if len(ids) == 0 {
return []*TenderApproval{}, nil
}
tenderApprovals, err := s.repository.GetByIDs(ctx, ids)
if err != nil {
s.logger.Error("Failed to get tender approvals by IDs", map[string]interface{}{
"error": err.Error(),
"ids": ids,
})
return nil, err
}
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
"requested_count": len(ids),
"found_count": len(tenderApprovals),
})
return tenderApprovals, nil
}
// UpdateTenderApproval updates a tender approval
func (s *tenderApprovalService) UpdateTenderApproval(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApproval, error) {
// Get existing tender approval
tenderApproval, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender approval for update", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, errors.New("tender approval not found")
}
// Update fields if provided
if form.SubmissionMode != nil {
tenderApproval.SubmissionMode = SubmissionMode(*form.SubmissionMode)
}
if form.Status != nil {
tenderApproval.Status = ApprovalStatus(*form.Status)
}
// Set updated timestamp
tenderApproval.UpdatedAt = time.Now().Unix()
// Update in database
err = s.repository.Update(ctx, tenderApproval)
if err != nil {
s.logger.Error("Failed to update tender approval", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, errors.New("failed to update tender approval")
}
s.logger.Info("Tender approval updated successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return tenderApproval, nil
}
// UpdateTenderApprovalWithTender updates a tender approval and returns it with tender details
func (s *tenderApprovalService) UpdateTenderApprovalWithTender(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApprovalWithTenderResponse, error) {
// Get existing tender approval
tenderApproval, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender approval for update", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, errors.New("tender approval not found")
}
// Update fields if provided
if form.SubmissionMode != nil {
tenderApproval.SubmissionMode = SubmissionMode(*form.SubmissionMode)
}
if form.Status != nil {
tenderApproval.Status = ApprovalStatus(*form.Status)
}
// Set updated timestamp
tenderApproval.UpdatedAt = time.Now().Unix()
// Update in database
err = s.repository.Update(ctx, tenderApproval)
if err != nil {
s.logger.Error("Failed to update tender approval", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return nil, errors.New("failed to update tender approval")
}
// Get tender details
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID,
"error": err.Error(),
})
// Return approval without tender details rather than failing completely
return tenderApproval.ToResponseWithTender(nil), nil
}
// Convert tender response to tender details
tenderDetails := &TenderDetails{
ID: tenderResponse.ID,
NoticePublicationID: tenderResponse.NoticePublicationID,
PublicationDate: tenderResponse.PublicationDate,
Title: tenderResponse.Title,
Description: tenderResponse.Description,
ProcurementTypeCode: tenderResponse.ProcurementTypeCode,
ProcedureCode: tenderResponse.ProcedureCode,
EstimatedValue: tenderResponse.EstimatedValue,
Currency: tenderResponse.Currency,
Duration: tenderResponse.Duration,
DurationUnit: tenderResponse.DurationUnit,
TenderDeadline: tenderResponse.TenderDeadline,
SubmissionDeadline: tenderResponse.SubmissionDeadline,
CountryCode: tenderResponse.CountryCode,
BuyerOrganization: &OrganizationResponse{Name: tenderResponse.BuyerOrganization.Name},
Status: string(tenderResponse.Status),
}
s.logger.Info("Tender approval updated successfully", map[string]interface{}{
"tender_approval_id": tenderApproval.ID,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return tenderApproval.ToResponseWithTender(tenderDetails), nil
}
// DeleteTenderApproval deletes a tender approval
func (s *tenderApprovalService) DeleteTenderApproval(ctx context.Context, id string) error {
// Get tender approval to check if exists
tenderApproval, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("tender approval not found")
}
// Delete tender approval
err = s.repository.Delete(ctx, id)
if err != nil {
s.logger.Error("Failed to delete tender approval", map[string]interface{}{
"error": err.Error(),
"tender_approval_id": id,
})
return errors.New("failed to delete tender approval")
}
s.logger.Info("Tender approval deleted successfully", map[string]interface{}{
"tender_approval_id": id,
"tender_id": tenderApproval.TenderID,
"company_id": tenderApproval.CompanyID,
})
return nil
}
// ListTenderApprovals lists tender approvals with search and filters
func (s *tenderApprovalService) ListTenderApprovals(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponse, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
sortBy := "created_at"
if form.SortBy != nil {
sortBy = *form.SortBy
}
sortOrder := "desc"
if form.SortOrder != nil {
sortOrder = *form.SortOrder
}
// Convert status strings to ApprovalStatus
var status []ApprovalStatus
if form.Status != nil {
status = make([]ApprovalStatus, len(form.Status))
for i, s := range form.Status {
status[i] = ApprovalStatus(s)
}
}
// Convert submission mode strings to SubmissionMode
var submissionMode []SubmissionMode
if form.SubmissionMode != nil {
submissionMode = make([]SubmissionMode, len(form.SubmissionMode))
for i, m := range form.SubmissionMode {
submissionMode[i] = SubmissionMode(m)
}
}
// Get tender approvals
tenderApprovals, err := s.repository.Search(ctx, form.TenderID, form.CompanyID, status, submissionMode, form.CreatedAtFrom, form.CreatedAtTo, limit, offset, sortBy, sortOrder)
if err != nil {
s.logger.Error("Failed to list tender approvals", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to list tender approvals")
}
// Convert to responses
var tenderApprovalResponses []*TenderApprovalResponse
for _, approval := range tenderApprovals {
tenderApprovalResponses = append(tenderApprovalResponses, approval.ToResponse())
}
// TODO: Implement proper count for pagination metadata
total := int64(len(tenderApprovals)) // This should be a separate count query
totalPages := (total + int64(limit) - 1) / int64(limit)
return &TenderApprovalListResponse{
TenderApprovals: tenderApprovalResponses,
Total: total,
Limit: limit,
Offset: offset,
TotalPages: int(totalPages),
}, nil
}
// SearchTenderApprovals searches tender approvals with advanced filters
func (s *tenderApprovalService) SearchTenderApprovals(ctx context.Context, form *TenderApprovalSearchForm) (*TenderApprovalListResponse, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
// Convert status strings to ApprovalStatus
var status []ApprovalStatus
if form.Status != nil {
status = make([]ApprovalStatus, len(form.Status))
for i, s := range form.Status {
status[i] = ApprovalStatus(s)
}
}
// Convert submission mode strings to SubmissionMode
var submissionMode []SubmissionMode
if form.SubmissionMode != nil {
submissionMode = make([]SubmissionMode, len(form.SubmissionMode))
for i, m := range form.SubmissionMode {
submissionMode[i] = SubmissionMode(m)
}
}
// Get tender approvals using search
tenderApprovals, err := s.repository.Search(ctx, form.TenderID, form.CompanyID, status, submissionMode, form.CreatedAtFrom, form.CreatedAtTo, limit, offset, "created_at", "desc")
if err != nil {
s.logger.Error("Failed to search tender approvals", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search tender approvals")
}
// Convert to responses
var tenderApprovalResponses []*TenderApprovalResponse
for _, approval := range tenderApprovals {
tenderApprovalResponses = append(tenderApprovalResponses, approval.ToResponse())
}
// TODO: Implement proper count for pagination metadata
total := int64(len(tenderApprovals)) // This should be a separate count query
totalPages := (total + int64(limit) - 1) / int64(limit)
return &TenderApprovalListResponse{
TenderApprovals: tenderApprovalResponses,
Total: total,
Limit: limit,
Offset: offset,
TotalPages: int(totalPages),
}, nil
}
// SearchTenderApprovalsByCriteria searches tender approvals using search criteria
func (s *tenderApprovalService) SearchTenderApprovalsByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error) {
tenderApprovals, err := s.repository.SearchByCriteria(ctx, criteria)
if err != nil {
s.logger.Error("Failed to search tender approvals by criteria", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search tender approvals by criteria")
}
return tenderApprovals, nil
}
// GetTenderApprovalStats returns tender approval statistics
func (s *tenderApprovalService) GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error) {
stats, err := s.repository.GetTenderApprovalStats(ctx)
if err != nil {
s.logger.Error("Failed to get tender approval stats", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to get tender approval statistics")
}
return stats, nil
}
// GetMultipleCompanyTenderApprovalStats returns tender approval statistics for multiple companies
func (s *tenderApprovalService) GetMultipleCompanyTenderApprovalStats(ctx context.Context, companyIDs []string) ([]*CompanyTenderApprovalStatsResponse, error) {
if len(companyIDs) == 0 {
return []*CompanyTenderApprovalStatsResponse{}, nil
}
var allStats []*CompanyTenderApprovalStatsResponse
for _, companyID := range companyIDs {
stats, err := s.GetCompanyTenderApprovalStats(ctx, companyID)
if err != nil {
s.logger.Warn("Failed to get stats for company", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
// Continue with other companies rather than failing completely
continue
}
allStats = append(allStats, stats)
}
s.logger.Info("Multiple company tender approval stats retrieved successfully", map[string]interface{}{
"requested_count": len(companyIDs),
"retrieved_count": len(allStats),
})
return allStats, nil
}
// GetAggregatedCompanyStats returns aggregated statistics across all companies
func (s *tenderApprovalService) GetAggregatedCompanyStats(ctx context.Context) (*AggregatedCompanyStatsResponse, error) {
// Get general stats first
generalStats, err := s.GetTenderApprovalStats(ctx)
if err != nil {
s.logger.Error("Failed to get general tender approval stats", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to get general tender approval statistics")
}
// For now, we'll return basic aggregated stats
// In a real implementation, you might want to get stats for all companies
// and aggregate them properly
stats := &AggregatedCompanyStatsResponse{
TotalCompanies: 1, // This should be calculated from actual company data
TotalApprovals: generalStats.TotalApprovals,
TotalSubmittedTenders: generalStats.SubmittedTenders,
TotalRejectedTenders: generalStats.RejectedTenders,
CompanyStats: []*CompanyTenderApprovalStatsResponse{}, // Empty for now
LastUpdated: time.Now().Unix(),
}
s.logger.Info("Aggregated company stats retrieved successfully", map[string]interface{}{
"total_approvals": stats.TotalApprovals,
"total_companies": stats.TotalCompanies,
})
return stats, nil
}
// GetTenderApprovalsByStatus retrieves tender approvals by status with pagination
func (s *tenderApprovalService) GetTenderApprovalsByStatus(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApproval, int64, error) {
// Use search method with status filter
tenderApprovals, err := s.repository.Search(ctx, nil, nil, []ApprovalStatus{status}, nil, nil, nil, limit, offset, "created_at", "desc")
if err != nil {
s.logger.Error("Failed to get tender approvals by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return nil, 0, errors.New("failed to get tender approvals by status")
}
// Get total count
total, err := s.repository.CountByStatus(ctx, status)
if err != nil {
s.logger.Error("Failed to count tender approvals by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return tenderApprovals, 0, nil // Return approvals even if count fails
}
return tenderApprovals, total, nil
}
// GetTenderApprovalsBySubmissionMode retrieves tender approvals by submission mode with pagination
func (s *tenderApprovalService) GetTenderApprovalsBySubmissionMode(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApproval, int64, error) {
// Use search method with submission mode filter
tenderApprovals, err := s.repository.Search(ctx, nil, nil, nil, []SubmissionMode{submissionMode}, nil, nil, limit, offset, "created_at", "desc")
if err != nil {
s.logger.Error("Failed to get tender approvals by submission mode", map[string]interface{}{
"error": err.Error(),
"submission_mode": submissionMode,
})
return nil, 0, errors.New("failed to get tender approvals by submission mode")
}
// Get total count
total, err := s.repository.CountBySubmissionMode(ctx, submissionMode)
if err != nil {
s.logger.Error("Failed to count tender approvals by submission mode", map[string]interface{}{
"error": err.Error(),
"submission_mode": submissionMode,
})
return tenderApprovals, 0, nil
}
return tenderApprovals, total, nil
}
// GetTenderApprovalsByStatusWithTender retrieves tender approvals by status with tender details
func (s *tenderApprovalService) GetTenderApprovalsByStatusWithTender(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApprovalWithTenderResponse, int64, error) {
tenderApprovals, total, err := s.GetTenderApprovalsByStatus(ctx, status, limit, offset)
if err != nil {
return nil, 0, err
}
tenders := s.loadTendersByIDs(ctx, uniqueTenderIDs(tenderApprovals))
responses := mapApprovalsWithTender(tenderApprovals, tenders, fullTenderDetailsFromResponse)
return responses, total, nil
}
// GetTenderApprovalsBySubmissionModeWithTender retrieves tender approvals by submission mode with tender details
func (s *tenderApprovalService) GetTenderApprovalsBySubmissionModeWithTender(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApprovalWithTenderResponse, int64, error) {
tenderApprovals, total, err := s.GetTenderApprovalsBySubmissionMode(ctx, submissionMode, limit, offset)
if err != nil {
return nil, 0, err
}
tenders := s.loadTendersByIDs(ctx, uniqueTenderIDs(tenderApprovals))
responses := mapApprovalsWithTender(tenderApprovals, tenders, fullTenderDetailsFromResponse)
return responses, total, nil
}
// ListTenderApprovalsWithTender lists tender approvals with search and filters, including tender details
func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponseWithTender, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
sortBy := "created_at"
if form.SortBy != nil {
sortBy = *form.SortBy
}
sortOrder := "desc"
if form.SortOrder != nil {
sortOrder = *form.SortOrder
}
// Convert status strings to ApprovalStatus
var status []ApprovalStatus
if form.Status != nil {
for _, s := range form.Status {
status = append(status, ApprovalStatus(s))
}
}
// Convert submission mode strings to SubmissionMode
var submissionMode []SubmissionMode
if form.SubmissionMode != nil {
for _, sm := range form.SubmissionMode {
submissionMode = append(submissionMode, SubmissionMode(sm))
}
}
// Search tender approvals
tenderApprovals, err := s.repository.Search(ctx, form.TenderID, form.CompanyID, status, submissionMode, form.CreatedAtFrom, form.CreatedAtTo, limit, offset, sortBy, sortOrder)
if err != nil {
s.logger.Error("Failed to search tender approvals", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search tender approvals")
}
// Get total count for pagination
total, err := s.repository.Count(ctx)
if err != nil {
s.logger.Error("Failed to count tender approvals", map[string]interface{}{
"error": err.Error(),
})
total = int64(len(tenderApprovals)) // Use actual count if total count fails
}
tenders := s.loadTendersByIDs(ctx, uniqueTenderIDs(tenderApprovals))
responses := mapApprovalsWithTender(tenderApprovals, tenders, fullTenderDetailsFromResponse)
// Calculate pagination metadata
pages := int(total) / limit
if int(total)%limit > 0 {
pages++
}
page := (offset / limit) + 1
meta := &TenderApprovalListResponseWithTender{
TenderApprovals: responses,
Metadata: &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
Page: page,
Pages: pages,
},
}
return meta, nil
}
func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID, customerID string) (*TenderApprovalWithTenderResponse, error) {
companyID = strings.TrimSpace(companyID)
if companyID == "" {
return nil, ErrInvalidCompanyID
}
tenderID, err := s.tenderService.ResolveMongoID(ctx, req.TenderID)
if err != nil {
s.logger.Warn("Failed to resolve tender for approval toggle", map[string]interface{}{
"tender_ref": req.TenderID,
"company_id": companyID,
"error": err.Error(),
})
return nil, ErrTenderNotFound
}
existing, err := s.repository.GetByTenderAndCompany(ctx, tenderID, companyID)
if err != nil {
if !errors.Is(err, ErrTenderApprovalNotFound) {
s.logger.Error("Failed to load tender approval for toggle", map[string]interface{}{
"tender_id": tenderID,
"company_id": companyID,
"error": err.Error(),
})
return nil, err
}
approval := &TenderApproval{
TenderID: tenderID,
CompanyID: companyID,
SubmissionMode: req.SubmissionMode,
Status: req.Status,
}
err = s.repository.Create(ctx, approval)
if err != nil {
s.logger.Error("Failed to create new tender approval", map[string]interface{}{
"tender_id": tenderID,
"company_id": companyID,
"status": req.Status,
"submission_mode": req.SubmissionMode,
"error": err.Error(),
})
return nil, err
}
if err := s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, true); err != nil {
if deleteErr := s.repository.Delete(ctx, approval.GetID()); deleteErr != nil {
s.logger.Error("Failed to rollback tender approval after submission sync failure", map[string]interface{}{
"tender_approval_id": approval.GetID(),
"error": deleteErr.Error(),
})
}
return nil, ErrSubmissionSyncFailed
}
return s.toggleResponseWithTender(ctx, approval)
}
if existing.Status == req.Status {
err = s.repository.Delete(ctx, existing.GetID())
if err != nil {
s.logger.Error("Failed to delete tender approval during toggle", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
if req.Status == ApprovalStatusSubmitted {
existing.Status = "unsubmitted"
} else {
existing.Status = "unrejected"
}
if err := s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, false); err != nil {
return nil, ErrSubmissionSyncFailed
}
return existing.ToResponseWithTender(nil), nil
}
if existing.Status == ApprovalStatusRejected && req.Status == ApprovalStatusSubmitted {
existing.Submit()
existing.SubmissionMode = req.SubmissionMode
} else if existing.Status == ApprovalStatusSubmitted && req.Status == ApprovalStatusRejected {
existing.Reject()
existing.SubmissionMode = req.SubmissionMode
} else {
existing.Status = req.Status
existing.SubmissionMode = req.SubmissionMode
}
err = s.repository.Update(ctx, existing)
if err != nil {
s.logger.Error("Failed to update tender approval", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
if err := s.syncSubmissionAfterToggle(ctx, tenderID, companyID, customerID, req.Status, true); err != nil {
return nil, ErrSubmissionSyncFailed
}
return s.toggleResponseWithTender(ctx, existing)
}
func (s *tenderApprovalService) syncSubmissionAfterToggle(ctx context.Context, tenderID, companyID, customerID string, status ApprovalStatus, active bool) error {
if s.submissionHook == nil {
return nil
}
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(),
})
}
return err
}
func (s *tenderApprovalService) toggleResponseWithTender(ctx context.Context, approval *TenderApproval) (*TenderApprovalWithTenderResponse, error) {
if approval == nil {
return nil, ErrInvalidData
}
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
if err != nil {
return approval.ToResponseWithTender(nil), nil
}
return approval.ToResponseWithTender(listTenderDetailsFromResponse(tenderResponse)), nil
}
// GetCompanyTenderApprovalStats returns company-specific tender approval statistics
func (s *tenderApprovalService) GetCompanyTenderApprovalStats(ctx context.Context, companyID string) (*CompanyTenderApprovalStatsResponse, error) {
stats, err := s.repository.GetCompanyTenderApprovalStats(ctx, companyID)
if err != nil {
s.logger.Error("Failed to get company tender approval stats", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("failed to get company tender approval statistics")
}
s.logger.Info("Company tender approval stats retrieved successfully", map[string]interface{}{
"company_id": companyID,
"total_approvals": stats.TotalApprovals,
"submitted_tenders": stats.SubmittedTenders,
"rejected_tenders": stats.RejectedTenders,
"self_apply_count": stats.SelfApplyCount,
"partnership_count": stats.PartnershipCount,
})
return stats, nil
}