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

318 lines
12 KiB
Go

package tender_submission
import (
"errors"
"strings"
"tm/internal/customer"
"tm/pkg/logger"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for tender submission operations.
type Handler struct {
service Service
logger logger.Logger
}
func NewHandler(service Service, logger logger.Logger) *Handler {
return &Handler{service: service, logger: logger}
}
// ListCompanySubmissions lists tender submissions for the authenticated company.
// @Summary List company tender submissions
// @Description Retrieve paginated tender submission workflows for the authenticated company
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Param status query []string false "Filter by status"
// @Param stage query []string false "Filter by stage"
// @Param created_from query int false "Filter by created date from (Unix timestamp)"
// @Param created_to query int false "Filter by created date to (Unix timestamp)"
// @Param limit query int false "Page size (default 20, max 100)"
// @Param offset query int false "Pagination offset"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionListResponse}
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions [get]
func (h *Handler) ListCompanySubmissions(c echo.Context) error {
companyID, ok := c.Get("company_id").(string)
if !ok || companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
form, err := response.Parse[ListTenderSubmissionsForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ListByCompanyWithTender(c.Request().Context(), companyID, form)
if err != nil {
return response.InternalServerError(c, "Failed to list tender submissions")
}
return response.SuccessWithMeta(c, result.Submissions, result.Metadata, "Tender submissions retrieved successfully")
}
// EnsureSubmission ensures a submission workflow exists for an approved tender.
// @Summary Ensure tender submission workflow
// @Description Create or return the submission workflow for an approved tender
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Param tender_id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions/tender/{tender_id}/ensure [post]
func (h *Handler) EnsureSubmission(c echo.Context) error {
companyID, err := h.resolveCompanyID(c)
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)
tenderID := strings.TrimSpace(c.Param("tender_id"))
if tenderID == "" {
return response.BadRequest(c, "Tender ID is required", "")
}
result, err := h.service.EnsureForApprovedTender(c.Request().Context(), tenderID, companyID, customerID)
if err != nil {
if errors.Is(err, ErrTenderNotFound) {
return response.NotFound(c, "Tender not found")
}
if errors.Is(err, ErrApprovalRequired) {
return response.BadRequest(c, "Tender must be approved before starting submission workflow", "")
}
return response.InternalServerError(c, "Failed to ensure tender submission")
}
return response.Success(c, result, "Tender submission ensured successfully")
}
// GetSubmissionByID retrieves a tender submission by ID.
// @Summary Get tender submission by ID
// @Description Retrieve a tender submission workflow with tender details
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Param id path string true "Submission ID"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions/{id} [get]
func (h *Handler) GetSubmissionByID(c echo.Context) error {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
return response.BadRequest(c, "Submission ID is required", "")
}
result, err := h.service.GetByIDWithTender(c.Request().Context(), id)
if err != nil {
if errors.Is(err, ErrTenderSubmissionNotFound) {
return response.NotFound(c, "Tender submission not found")
}
return response.InternalServerError(c, "Failed to get tender submission")
}
companyID, _ := c.Get("company_id").(string)
if companyID != "" && result.CompanyID != companyID {
return response.NotFound(c, "Tender submission not found")
}
return response.Success(c, result, "Tender submission retrieved successfully")
}
// GetSubmissionByTender retrieves a submission by tender and company.
// @Summary Get tender submission by tender
// @Description Retrieve the submission workflow for a specific tender and company
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Param tender_id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions/tender/{tender_id} [get]
func (h *Handler) GetSubmissionByTender(c echo.Context) error {
companyID, ok := c.Get("company_id").(string)
if !ok || companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
tenderID := strings.TrimSpace(c.Param("tender_id"))
if tenderID == "" {
return response.BadRequest(c, "Tender ID is required", "")
}
result, err := h.service.GetByTenderAndCompanyWithTender(c.Request().Context(), tenderID, companyID)
if err != nil {
if errors.Is(err, ErrTenderNotFound) || errors.Is(err, ErrTenderSubmissionNotFound) {
return response.NotFound(c, "Tender submission not found")
}
return response.InternalServerError(c, "Failed to get tender submission")
}
return response.Success(c, result, "Tender submission retrieved successfully")
}
// UpdateSubmissionStatus updates the workflow status of a submission.
// @Summary Update tender submission status
// @Description Move a tender submission to the next workflow step
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Param id path string true "Submission ID"
// @Param body body UpdateSubmissionStatusForm true "Status update"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 422 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions/{id}/status [patch]
func (h *Handler) UpdateSubmissionStatus(c echo.Context) error {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
return response.BadRequest(c, "Submission ID is required", "")
}
form, err := response.Parse[UpdateSubmissionStatusForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
companyID, err := h.resolveCompanyID(c)
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)
result, err := h.service.UpdateStatus(c.Request().Context(), id, form, companyID, customerID)
if err != nil {
if errors.Is(err, ErrTenderSubmissionNotFound) {
return response.NotFound(c, "Tender submission not found")
}
var transitionErr *InvalidStatusTransitionError
if errors.As(err, &transitionErr) {
return response.ValidationError(c, "Invalid status transition", err.Error())
}
if errors.Is(err, ErrTerminalStatus) {
return response.ValidationError(c, "Submission is in a terminal state", err.Error())
}
return response.InternalServerError(c, "Failed to update tender submission status")
}
return response.Success(c, result, "Tender submission status updated successfully")
}
// GetCompanySubmissionStats returns submission statistics for the company.
// @Summary Get company tender submission statistics
// @Description Retrieve submission counts grouped by stage and status
// @Tags TenderSubmissions
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CompanySubmissionStatsResponse}
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tender-submissions/stats [get]
func (h *Handler) GetCompanySubmissionStats(c echo.Context) error {
companyID, ok := c.Get("company_id").(string)
if !ok || companyID == "" {
return response.Unauthorized(c, "Unauthorized")
}
stats, err := h.service.GetCompanyStats(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to get submission statistics")
}
return response.Success(c, stats, "Submission statistics retrieved successfully")
}
// AdminListSubmissions lists tender submissions for admin users.
// @Summary Admin list tender submissions
// @Description Retrieve paginated tender submissions across companies
// @Tags Admin-TenderSubmissions
// @Accept json
// @Produce json
// @Param company_id query string false "Filter by company ID"
// @Param tender_id query string false "Filter by tender ID"
// @Param status query []string false "Filter by status"
// @Param stage query []string false "Filter by stage"
// @Param limit query int false "Page size"
// @Param offset query int false "Pagination offset"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionListResponse}
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /admin/v1/tender-submissions [get]
func (h *Handler) AdminListSubmissions(c echo.Context) error {
form, err := response.Parse[ListTenderSubmissionsForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.AdminListWithTender(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to list tender submissions")
}
return response.SuccessWithMeta(c, result.Submissions, result.Metadata, "Tender submissions retrieved successfully")
}
// AdminGetSubmissionByID retrieves a submission by ID for admin users.
// @Summary Admin get tender submission by ID
// @Description Retrieve a tender submission workflow with tender details
// @Tags Admin-TenderSubmissions
// @Accept json
// @Produce json
// @Param id path string true "Submission ID"
// @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /admin/v1/tender-submissions/{id} [get]
func (h *Handler) AdminGetSubmissionByID(c echo.Context) error {
return h.GetSubmissionByID(c)
}
// AdminGetSubmissionStats returns global submission statistics.
// @Summary Admin get tender submission statistics
// @Description Retrieve global submission counts grouped by stage and status
// @Tags Admin-TenderSubmissions
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=AdminSubmissionStatsResponse}
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /admin/v1/tender-submissions/stats [get]
func (h *Handler) AdminGetSubmissionStats(c echo.Context) error {
stats, err := h.service.GetGlobalStats(c.Request().Context())
if err != nil {
return response.InternalServerError(c, "Failed to get submission statistics")
}
return response.Success(c, stats, "Submission statistics retrieved successfully")
}
func (h *Handler) resolveCompanyID(c echo.Context) (string, error) {
requestedCompanyID := strings.TrimSpace(c.QueryParam("company_id"))
activeCompanyID, _ := c.Get("company_id").(string)
assignedCompanyIDs, _ := c.Get("company_ids").([]string)
return customer.PickAssignedCompanyID(activeCompanyID, assignedCompanyIDs, requestedCompanyID)
}