Files
tm_back/internal/tender_approval/handler.go
T
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

441 lines
18 KiB
Go

package tender_approval
import (
"errors"
"strconv"
"strings"
"tm/internal/customer"
"tm/pkg/logger"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for tender approval operations
type Handler struct {
service Service
logger logger.Logger
}
// NewHandler creates a new tender approval handler
func NewHandler(service Service, logger logger.Logger) *Handler {
return &Handler{
service: service,
logger: logger,
}
}
// CreateTenderApproval creates a new tender approval
// @Summary Create a new tender approval
// @Description Create a new tender approval for a company to approve or reject a tender
// @Tags TenderApprovals
// @Accept json
// @Produce json
// @Param tender_approval body CreateTenderApprovalForm true "Tender approval information"
// @Success 201 {object} response.APIResponse{data=TenderApprovalResponse} "Tender approval created successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 409 {object} response.APIResponse "Conflict - Tender approval already exists for this tender and company"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/tender-approvals [post]
func (h *Handler) ToggleTenderApproval(c echo.Context) error {
form, err := response.Parse[ToggleTenderApprovalForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
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", "")
}
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")
}
if errors.Is(err, ErrSubmissionSyncFailed) {
return response.InternalServerError(c, "Failed to sync tender submission workflow")
}
if err.Error() == "tender approval already exists for this tender and company" {
return response.Conflict(c, err.Error())
}
return response.InternalServerError(c, "Failed to create tender approval")
}
return response.Success(c, tenderApproval, "Tender approval updated successfully")
}
// GetTenderApproval retrieves a tender approval by ID
// @Summary Get tender approval by ID
// @Description Retrieve detailed tender approval information by ID
// @Tags TenderApprovals
// @Accept json
// @Produce json
// @Param id path string true "Tender Approval ID"
// @Success 200 {object} response.APIResponse{data=TenderApprovalResponse} "Tender approval retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid tender approval ID"
// @Failure 404 {object} response.APIResponse "Not found - Tender approval not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/tender-approvals/{id} [get]
func (h *Handler) GetPublicTenderApproval(c echo.Context) error {
id := c.Param("id")
// Get the approval with tender details
tenderApprovalWithTender, err := h.service.GetTenderApprovalByIDWithTender(c.Request().Context(), id)
if err != nil {
if err.Error() == "tender approval not found" {
return response.NotFound(c, "Tender approval not found")
}
return response.InternalServerError(c, "Failed to get tender approval")
}
return response.Success(c, tenderApprovalWithTender, "Tender approval retrieved successfully")
}
// GetTenderApprovalByTenderAndCompany retrieves a tender approval by tender ID and company ID
// @Summary Get tender approval by tender and company
// @Description Retrieve tender approval information for a specific tender and company combination
// @Tags TenderApprovals
// @Accept json
// @Produce json
// @Param tender_id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=TenderApprovalResponse} "Tender approval retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid tender ID or company ID"
// @Failure 404 {object} response.APIResponse "Not found - Tender approval not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/tender-approvals/tender/{tender_id} [get]
func (h *Handler) GetTenderApprovalByTenderAndCompany(c echo.Context) error {
companyID := c.Get("company_id").(string)
if companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
tenderID := c.Param("tender_id")
if tenderID == "" {
return response.BadRequest(c, "Missing required parameters", "tender_id required")
}
tenderApprovalWithTender, err := h.service.GetTenderApprovalByTenderAndCompanyWithTender(c.Request().Context(), tenderID, companyID)
if err != nil {
if err.Error() == "tender approval not found" {
return response.NotFound(c, "Tender approval not found")
}
return response.InternalServerError(c, "Failed to get tender approval")
}
return response.Success(c, tenderApprovalWithTender, "Tender approval retrieved successfully")
}
// GetTenderApprovalsByCompanyID retrieves all tender approvals for a specific company
// @Summary Get tender approvals by company ID
// @Description Retrieve all tender approvals for a specific company with optional filtering
// @Tags TenderApprovals
// @Accept json
// @Produce json
// @Param limit query integer false "Number of approvals per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of approvals to skip for pagination" minimum(0) default(0)
// @Param submission_mode query string false "Filter by submission mode Enums(self-apply, partnership)"
// @Param status query string false "Filter by status Enums(submitted, rejected). Defaults to submitted when omitted."
// @Param created_from query integer false "Filter by created date from (Unix timestamp)"
// @Param created_to query integer false "Filter by created date to (Unix timestamp)"
// @Success 200 {object} response.APIResponse{data=TenderApprovalWithTenderListResponse} "Tender approvals retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/tender-approvals [get]
func (h *Handler) PublicGetTenderApprovals(c echo.Context) error {
// Parse pagination parameters
limit := 20
if l := c.QueryParam("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
offset := 0
if o := c.QueryParam("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
companyID := c.Get("company_id").(string)
if companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
// Get query parameters for filtering
submissionModeStr := c.QueryParam("submission_mode")
statusStr := c.QueryParam("status")
// Parse submission mode filter
var submissionMode *SubmissionMode
if submissionModeStr != "" {
mode := SubmissionMode(submissionModeStr)
if mode != SubmissionModeSelfApply && mode != SubmissionModePartnership {
return response.BadRequest(c, "Invalid submission_mode value", "Must be 'self-apply' or 'partnership'")
}
submissionMode = &mode
}
// Parse status filter
var status *ApprovalStatus
if statusStr != "" {
statusVal := ApprovalStatus(statusStr)
if statusVal != ApprovalStatusSubmitted && statusVal != ApprovalStatusRejected {
return response.BadRequest(c, "Invalid status value", "Must be 'submitted' or 'rejected'")
}
status = &statusVal
}
var from *int64
if fromParam := c.QueryParam("created_from"); fromParam != "" {
if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil {
from = &parsed
}
}
var to *int64
if toParam := c.QueryParam("created_to"); toParam != "" {
if parsed, err := strconv.ParseInt(toParam, 10, 64); err == nil {
to = &parsed
}
}
tenderApprovalsWithTender, err := h.service.GetTenderApprovalsByCompanyIDWithTender(c.Request().Context(), companyID, submissionMode, status, from, to, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to get tender approvals")
}
return response.Success(c, tenderApprovalsWithTender, "Tender approvals retrieved successfully")
}
// **************** Admin Routes ****************
// ListTenderApprovals lists tender approvals with filters and pagination
// @Summary List tender approvals with filters and pagination
// @Description Retrieve a paginated list of tender approvals with filtering options
// @Tags Admin-TenderApprovals
// @Accept json
// @Produce json
// @Param tender_id query string false "Filter by tender ID"
// @Param company_id query string false "Filter by company ID"
// @Param status query array false "Filter by status"
// @Param submission_mode query array false "Filter by submission mode"
// @Param created_at_from query integer false "Filter by creation date from (Unix timestamp)"
// @Param created_at_to query integer false "Filter by creation date to (Unix timestamp)"
// @Param limit query integer false "Number of approvals per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of approvals to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(created_at, updated_at, status, submission_mode) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=TenderApprovalListResponse} "Tender approvals retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/tender-approvals [get]
func (h *Handler) ListTenderApprovals(c echo.Context) error {
form, err := response.Parse[ListTenderApprovalsForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ListTenderApprovalsWithTender(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to list tender approvals")
}
return response.Success(c, result, "Tender approvals retrieved successfully")
}
// GetTenderApprovalStats returns tender approval statistics
// @Summary Get tender approval statistics
// @Description Get comprehensive tender approval statistics for dashboard
// @Tags Admin-TenderApprovals
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=TenderApprovalStatsResponse} "Tender approval statistics retrieved successfully"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/tender-approvals/stats [get]
func (h *Handler) GetTenderApprovalStats(c echo.Context) error {
stats, err := h.service.GetTenderApprovalStats(c.Request().Context())
if err != nil {
return response.InternalServerError(c, "Failed to get tender approval statistics")
}
return response.Success(c, stats, "Tender approval statistics retrieved successfully")
}
// GetCompanyTenderApprovalStats returns company-specific tender approval statistics
// @Summary Get company tender approval statistics
// @Description Get comprehensive tender approval statistics for a specific company
// @Tags TenderApprovals
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CompanyTenderApprovalStatsResponse} "Company tender approval statistics retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/tender-approvals/stats [get]
func (h *Handler) GetCompanyTenderApprovalStats(c echo.Context) error {
companyID := c.Get("company_id").(string)
if companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
stats, err := h.service.GetCompanyTenderApprovalStats(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to get company tender approval statistics")
}
return response.Success(c, stats, "Company tender approval statistics retrieved successfully")
}
// GetTenderApprovalsByStatus retrieves tender approvals by status
// @Summary Get tender approvals by status
// @Description Retrieve tender approvals filtered by their status (submitted, rejected)
// @Tags Admin-TenderApprovals
// @Accept json
// @Produce json
// @Param status path string true "Tender approval status" Enums(submitted, rejected)
// @Param limit query integer false "Number of approvals per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of approvals to skip for pagination" minimum(0) default(0)
// @Success 200 {object} response.APIResponse{data=[]TenderApprovalResponse,meta=response.Meta} "Tender approvals retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid tender approval status"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/tender-approvals/status/{status} [get]
func (h *Handler) GetTenderApprovalsByStatus(c echo.Context) error {
statusStr := c.Param("status")
status := ApprovalStatus(statusStr)
// Validate status
if status != ApprovalStatusSubmitted && status != ApprovalStatusRejected {
return response.BadRequest(c, "Invalid tender approval status", "Must be submitted or rejected")
}
limit := 20
if limitStr := c.QueryParam("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
offset := 0
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
tenderApprovalsWithTender, total, err := h.service.GetTenderApprovalsByStatusWithTender(c.Request().Context(), status, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve tender approvals by status")
}
meta := &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
}
return response.SuccessWithMeta(c, tenderApprovalsWithTender, meta, "Tender approvals retrieved successfully")
}
// GetTenderApprovalsBySubmissionMode retrieves tender approvals by submission mode
// @Summary Get tender approvals by submission mode
// @Description Retrieve tender approvals filtered by their submission mode (self-apply, partnership)
// @Tags Admin-TenderApprovals
// @Accept json
// @Produce json
// @Param submission_mode path string true "Tender approval submission mode" Enums(self-apply, partnership)
// @Param limit query integer false "Number of approvals per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of approvals to skip for pagination" minimum(0) default(0)
// @Success 200 {object} response.APIResponse{data=[]TenderApprovalResponse,meta=response.Meta} "Tender approvals retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid tender approval submission mode"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/tender-approvals/submission-mode/{submission_mode} [get]
func (h *Handler) GetTenderApprovalsBySubmissionMode(c echo.Context) error {
submissionModeStr := c.Param("submission_mode")
submissionMode := SubmissionMode(submissionModeStr)
// Validate submission mode
if submissionMode != SubmissionModeSelfApply && submissionMode != SubmissionModePartnership {
return response.BadRequest(c, "Invalid tender approval submission mode", "Must be self-apply or partnership")
}
limit := 20
if limitStr := c.QueryParam("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
offset := 0
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
tenderApprovalsWithTender, total, err := h.service.GetTenderApprovalsBySubmissionModeWithTender(c.Request().Context(), submissionMode, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve tender approvals by submission mode")
}
meta := &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
}
return response.SuccessWithMeta(c, tenderApprovalsWithTender, meta, "Tender approvals retrieved successfully")
}
// GetTenderApproval retrieves a tender approval by ID
// @Summary Get tender approval by ID
// @Description Retrieve detailed tender approval information by ID
// @Tags Admin-TenderApprovals
// @Accept json
// @Produce json
// @Param id path string true "Tender Approval ID"
// @Success 200 {object} response.APIResponse{data=TenderApprovalResponse} "Tender approval retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid tender approval ID"
// @Failure 404 {object} response.APIResponse "Not found - Tender approval not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/tender-approvals/{id} [get]
func (h *Handler) GetTenderApproval(c echo.Context) error {
id := c.Param("id")
// Get the approval with tender details
tenderApprovalWithTender, err := h.service.GetTenderApprovalByIDWithTender(c.Request().Context(), id)
if err != nil {
if err.Error() == "tender approval not found" {
return response.NotFound(c, "Tender approval not found")
}
return response.InternalServerError(c, "Failed to get tender approval")
}
return response.Success(c, tenderApprovalWithTender, "Tender approval retrieved successfully")
}