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 for the authenticated company. // @Summary Get tender submission by ID // @Description Retrieve a tender submission workflow with tender details for the authenticated company // @Tags TenderSubmissions // @Accept json // @Produce json // @Param id path string true "Submission ID" // @Success 200 {object} response.APIResponse{data=TenderSubmissionWithTenderResponse} // @Failure 401 {object} response.APIResponse // @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 { companyID, ok := c.Get("company_id").(string) if !ok || companyID == "" { return response.Unauthorized(c, "Unauthorized") } id := strings.TrimSpace(c.Param("id")) if id == "" { return response.BadRequest(c, "Submission ID is required", "") } result, err := h.service.GetByIDForCompanyWithTender(c.Request().Context(), id, companyID) 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") } 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()) } if errors.Is(err, ErrConcurrentUpdate) { return response.Conflict(c, "Tender submission was modified concurrently") } 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 { 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") } return response.Success(c, result, "Tender submission retrieved successfully") } // 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) }