fa3d466579
- 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.
335 lines
13 KiB
Go
335 lines
13 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 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)
|
|
}
|