From 81a94b4879da17f8e6a430b83cd014ce2ad06c15 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Wed, 8 Jul 2026 02:55:55 +0330 Subject: [PATCH] Enhance tender and company context handling with new features and error management - Introduced `PickAssignedCompanyID` function to determine the appropriate company ID for company-scoped operations, improving company assignment logic. - Added `ResolveMongoID` method in the tender service to map MongoDB IDs and AI procedure references to canonical tender IDs, enhancing ID resolution. - Updated `ToggleTenderApproval` to handle company ID validation and improve error handling, ensuring robust processing of tender approvals. - Enhanced logging for error scenarios in the tender approval service, providing better insights into failures during operations. - Refactored the `GetByID` and `GetByTenderAndCompany` methods in the tender approval repository to utilize custom error types for improved clarity. This update significantly improves the handling of company and tender operations, enhancing error management and overall service reliability. --- internal/customer/company_context.go | 26 +++++++++- internal/tender/repository.go | 11 +++- internal/tender/service.go | 31 ++++++++++++ internal/tender_approval/form.go | 1 + internal/tender_approval/handler.go | 23 +++++++-- internal/tender_approval/repository.go | 4 +- internal/tender_approval/service.go | 69 +++++++++++++++++++------- 7 files changed, 137 insertions(+), 28 deletions(-) diff --git a/internal/customer/company_context.go b/internal/customer/company_context.go index ebfc339..bfa9039 100644 --- a/internal/customer/company_context.go +++ b/internal/customer/company_context.go @@ -6,7 +6,10 @@ import ( "strings" ) -var errCompanyNotAssigned = errors.New("company is not assigned to customer") +// ErrCompanyNotAssigned is returned when the requested company is not assigned to the customer. +var ErrCompanyNotAssigned = errors.New("company is not assigned to customer") + +var errCompanyNotAssigned = ErrCompanyNotAssigned func normalizeAssignedCompanyIDs(assigned []string) []string { normalized := make([]string, 0, len(assigned)) @@ -56,6 +59,27 @@ func pickActiveCompanyID(assigned []string, tokenCompanyID, requestedCompanyID s return assigned[0], nil } +// PickAssignedCompanyID chooses the company for a company-scoped write (e.g. tender approval). +// When requestedCompanyID is set it must be in assigned; otherwise activeCompanyID is used. +func PickAssignedCompanyID(activeCompanyID string, assigned []string, requestedCompanyID string) (string, error) { + activeCompanyID = strings.TrimSpace(activeCompanyID) + requestedCompanyID = strings.TrimSpace(requestedCompanyID) + + if requestedCompanyID != "" { + for _, id := range normalizeAssignedCompanyIDs(assigned) { + if id == requestedCompanyID { + return requestedCompanyID, nil + } + } + return "", ErrCompanyNotAssigned + } + + if activeCompanyID == "" { + return "", errors.New("company ID is required") + } + return activeCompanyID, nil +} + // ResolveCompanyContext resolves the active company and assigned companies for an authenticated customer. func (s *customerService) ResolveCompanyContext(ctx context.Context, customerID, tokenCompanyID, requestedCompanyID string) (string, []string, error) { customer, err := s.repository.GetByID(ctx, customerID) diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 3987164..1bedf77 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -249,12 +249,19 @@ func (r *tenderRepository) GetByIDs(ctx context.Context, ids []string) ([]Tender objectIDs := make([]bson.ObjectID, 0, len(batch)) for _, id := range batch { - objectID, err := bson.ObjectIDFromHex(id) + objectID, err := bson.ObjectIDFromHex(strings.TrimSpace(id)) if err != nil { - return nil, err + r.logger.Warn("Skipping invalid tender ID in batch lookup", map[string]interface{}{ + "tender_id": id, + "error": err.Error(), + }) + continue } objectIDs = append(objectIDs, objectID) } + if len(objectIDs) == 0 { + continue + } pagination := orm.Pagination{ Limit: len(objectIDs), diff --git a/internal/tender/service.go b/internal/tender/service.go index c02b081..0d4aa38 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -55,6 +55,8 @@ type Service interface { // Tender operations GetByID(ctx context.Context, id string, language string) (*TenderResponse, error) GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) + // ResolveMongoID maps a MongoDB id or AI procedure ref to the canonical tender MongoDB id. + ResolveMongoID(ctx context.Context, tenderRef string) (string, error) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) Delete(ctx context.Context, id string) error Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error) @@ -181,6 +183,35 @@ func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string] return out, nil } +// ResolveMongoID maps a MongoDB id or AI procedure ref (PROC_{folder}/{notice}) to the canonical tender id. +func (s *tenderService) ResolveMongoID(ctx context.Context, tenderRef string) (string, error) { + tenderRef = strings.TrimSpace(tenderRef) + if tenderRef == "" { + return "", fmt.Errorf("tender reference is required") + } + + if IsMongoObjectIDRef(tenderRef) { + if _, err := s.repository.GetByID(ctx, tenderRef); err != nil { + return "", fmt.Errorf("tender not found") + } + return tenderRef, nil + } + + if folder, notice, ok := ParseAIProcedureRef(tenderRef); ok { + t, err := s.repository.GetByProcedureReference(ctx, folder, notice) + if err != nil || t == nil { + return "", fmt.Errorf("tender not found") + } + return t.GetID(), nil + } + + if _, err := s.repository.GetByID(ctx, tenderRef); err == nil { + return tenderRef, nil + } + + return "", fmt.Errorf("tender not found") +} + // buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary. func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse { resp := tender.ToResponseWithLanguage("") diff --git a/internal/tender_approval/form.go b/internal/tender_approval/form.go index f2593d1..7f392fe 100644 --- a/internal/tender_approval/form.go +++ b/internal/tender_approval/form.go @@ -81,6 +81,7 @@ type CompanyTenderApprovalStatsResponse struct { // ToggleTenderApprovalForm represents the form for toggling a tender approval type ToggleTenderApprovalForm struct { TenderID string `json:"tender_id" valid:"required"` + CompanyID *string `json:"company_id,omitempty" valid:"optional"` Status ApprovalStatus `json:"status" valid:"required,in(submitted|rejected)"` SubmissionMode SubmissionMode `json:"submission_mode" valid:"required,in(self-apply|partnership)"` } diff --git a/internal/tender_approval/handler.go b/internal/tender_approval/handler.go index a973e3b..43ae803 100644 --- a/internal/tender_approval/handler.go +++ b/internal/tender_approval/handler.go @@ -1,7 +1,10 @@ package tender_approval import ( + "errors" "strconv" + "strings" + "tm/internal/customer" "tm/pkg/logger" "tm/pkg/response" @@ -42,14 +45,26 @@ func (h *Handler) ToggleTenderApproval(c echo.Context) error { return response.ValidationError(c, "Invalid request data", err.Error()) } - // Get company ID from context - companyID := c.Get("company_id").(string) - if companyID == "" { - return response.Unauthorized(c, "Unauthorized") + requestedCompanyID := strings.TrimSpace(c.QueryParam("company_id")) + if form.CompanyID != nil && strings.TrimSpace(*form.CompanyID) != "" { + requestedCompanyID = strings.TrimSpace(*form.CompanyID) + } + + activeCompanyID, _ := c.Get("company_id").(string) + assignedCompanyIDs, _ := c.Get("company_ids").([]string) + companyID, err := customer.PickAssignedCompanyID(activeCompanyID, assignedCompanyIDs, requestedCompanyID) + if err != nil { + if errors.Is(err, customer.ErrCompanyNotAssigned) { + return response.Forbidden(c, "Company is not assigned to customer") + } + return response.BadRequest(c, "Company ID is required", "") } tenderApproval, err := h.service.ToggleTenderApproval(c.Request().Context(), form, companyID) if err != nil { + if errors.Is(err, ErrTenderNotFound) { + return response.NotFound(c, "Tender not found") + } if err.Error() == "tender approval already exists for this tender and company" { return response.Conflict(c, err.Error()) } diff --git a/internal/tender_approval/repository.go b/internal/tender_approval/repository.go index 8c4a4be..cad62a7 100644 --- a/internal/tender_approval/repository.go +++ b/internal/tender_approval/repository.go @@ -104,7 +104,7 @@ func (r *tenderApprovalRepository) GetByID(ctx context.Context, id string) (*Ten tenderApproval, err := r.ormRepo.FindByID(ctx, id) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { - return nil, errors.New("tender approval not found") + return nil, ErrTenderApprovalNotFound } r.logger.Error("Failed to get tender approval by ID", map[string]interface{}{ "error": err.Error(), @@ -126,7 +126,7 @@ func (r *tenderApprovalRepository) GetByTenderAndCompany(ctx context.Context, te tenderApproval, err := r.ormRepo.FindOne(ctx, filter) if err != nil { if errors.Is(err, mongo.ErrDocumentNotFound) { - return nil, errors.New("tender approval not found") + return nil, ErrTenderApprovalNotFound } r.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{ "error": err.Error(), diff --git a/internal/tender_approval/service.go b/internal/tender_approval/service.go index f0e89cf..58c0d77 100644 --- a/internal/tender_approval/service.go +++ b/internal/tender_approval/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "math" + "strings" "time" "tm/internal/tender" "tm/pkg/logger" @@ -853,10 +854,34 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex } func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *ToggleTenderApprovalForm, companyID string) (*TenderApprovalWithTenderResponse, error) { - existing, err := s.repository.GetByTenderAndCompany(ctx, req.TenderID, companyID) + 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: req.TenderID, + TenderID: tenderID, CompanyID: companyID, SubmissionMode: req.SubmissionMode, Status: req.Status, @@ -865,7 +890,7 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T err = s.repository.Create(ctx, approval) if err != nil { s.logger.Error("Failed to create new tender approval", map[string]interface{}{ - "tender_id": req.TenderID, + "tender_id": tenderID, "company_id": companyID, "status": req.Status, "submission_mode": req.SubmissionMode, @@ -874,13 +899,13 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T return nil, err } - return approval.ToResponseWithTender(nil), nil + 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 update tender approval", map[string]interface{}{ + s.logger.Error("Failed to delete tender approval during toggle", map[string]interface{}{ "error": err.Error(), }) return nil, err @@ -892,24 +917,17 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T existing.Status = "unrejected" } - return existing.ToResponseWithTender(nil), nil - } else if existing.Status == req.Status && existing.SubmissionMode != req.SubmissionMode { - 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 - } - return existing.ToResponseWithTender(nil), nil } - if existing.Status == ApprovalStatusSubmitted { + if existing.Status == ApprovalStatusRejected && req.Status == ApprovalStatusSubmitted { + existing.Submit() + existing.SubmissionMode = req.SubmissionMode + } else if existing.Status == ApprovalStatusSubmitted && req.Status == ApprovalStatusRejected { existing.Reject() } else { - existing.Submit() + existing.Status = req.Status + existing.SubmissionMode = req.SubmissionMode } err = s.repository.Update(ctx, existing) @@ -920,7 +938,20 @@ func (s *tenderApprovalService) ToggleTenderApproval(ctx context.Context, req *T return nil, err } - return existing.ToResponseWithTender(nil), nil + return s.toggleResponseWithTender(ctx, existing) +} + +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