Add Tender Approval Management Functionality and API Enhancements
- Introduced a new tender approval management system, including the creation of tender approval entities, services, and repositories. - Implemented CRUD operations for tender approvals, allowing companies to approve or reject tenders. - Developed administrative and public API endpoints for listing, retrieving, and updating tender approvals with comprehensive filtering options. - Enhanced Swagger documentation to accurately reflect the new endpoints, including detailed descriptions, parameters, and response formats. - Updated existing routes to include tender approval management, ensuring adherence to Clean Architecture principles and maintaining a clear separation of concerns in the handler layer.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tm/pkg/mongo"
|
||||
)
|
||||
|
||||
// ApprovalStatus represents the approval status of a tender by a company
|
||||
type ApprovalStatus string
|
||||
|
||||
const (
|
||||
ApprovalStatusApproved ApprovalStatus = "approved" // Company approved the tender
|
||||
ApprovalStatusRejected ApprovalStatus = "rejected" // Company rejected the tender
|
||||
)
|
||||
|
||||
// SubmissionMode represents the submission mode of a tender by a company
|
||||
type SubmissionMode string
|
||||
|
||||
const (
|
||||
SubmissionModeSelfApply SubmissionMode = "self-apply" // Company applies for the tender on their own
|
||||
SubmissionModePartnership SubmissionMode = "partnership" // Company applies for the tender with a partner
|
||||
)
|
||||
|
||||
// TenderApproval represents a company's approval decision for a specific tender
|
||||
type TenderApproval struct {
|
||||
mongo.Model `bson:",inline"`
|
||||
|
||||
// Core relationships
|
||||
TenderID string `bson:"tender_id" json:"tender_id"` // Reference to the tender
|
||||
CompanyID string `bson:"company_id" json:"company_id"` // Reference to the company
|
||||
|
||||
// Approval decision
|
||||
SubmissionMode SubmissionMode `bson:"submission_mode" json:"submission_mode"`
|
||||
Status ApprovalStatus `bson:"status" json:"status"` // Current approval status
|
||||
}
|
||||
|
||||
// TenderApprovalRequest represents a request to create or update a tender approval
|
||||
type TenderApprovalRequest struct {
|
||||
TenderID string `json:"tender_id" validate:"required"`
|
||||
CompanyID string `json:"company_id" validate:"required"`
|
||||
SubmissionMode SubmissionMode `json:"submission_mode" validate:"required"`
|
||||
Status ApprovalStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// TenderApprovalResponse represents the response data for tender approval
|
||||
type TenderApprovalResponse struct {
|
||||
ID string `json:"id"`
|
||||
TenderID string `json:"tender_id"`
|
||||
CompanyID string `json:"company_id"`
|
||||
SubmissionMode string `json:"submission_mode"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// TenderApprovalSearchCriteria represents search criteria for tender approvals
|
||||
type TenderApprovalSearchCriteria struct {
|
||||
TenderID *string `json:"tender_id,omitempty"`
|
||||
CompanyID *string `json:"company_id,omitempty"`
|
||||
Status []ApprovalStatus `json:"status,omitempty"`
|
||||
SubmissionMode []SubmissionMode `json:"submission_mode,omitempty"`
|
||||
CreatedAtFrom *int64 `json:"created_at_from,omitempty"`
|
||||
CreatedAtTo *int64 `json:"created_at_to,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
}
|
||||
|
||||
// TenderApprovalStatistics represents statistics for tender approvals
|
||||
type TenderApprovalStatistics struct {
|
||||
TotalApprovals int64 `json:"total_approvals"`
|
||||
LastUpdated int64 `json:"last_updated"`
|
||||
}
|
||||
|
||||
// GetID returns the tender approval ID
|
||||
func (ta *TenderApproval) GetID() string {
|
||||
return ta.ID.Hex()
|
||||
}
|
||||
|
||||
// IsApproved returns true if the tender was approved
|
||||
func (ta *TenderApproval) IsApproved() bool {
|
||||
return ta.Status == ApprovalStatusApproved
|
||||
}
|
||||
|
||||
// IsRejected returns true if the tender was rejected
|
||||
func (ta *TenderApproval) IsRejected() bool {
|
||||
return ta.Status == ApprovalStatusRejected
|
||||
}
|
||||
|
||||
// HasDecision returns true if a decision has been made
|
||||
func (ta *TenderApproval) HasDecision() bool {
|
||||
return ta.Status == ApprovalStatusApproved || ta.Status == ApprovalStatusRejected
|
||||
}
|
||||
|
||||
// Approve approves the tender with optional notes
|
||||
func (ta *TenderApproval) Approve(notes *string) {
|
||||
ta.Status = ApprovalStatusApproved
|
||||
ta.CreatedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Reject rejects the tender with a reason
|
||||
func (ta *TenderApproval) Reject(reason string) {
|
||||
ta.Status = ApprovalStatusRejected
|
||||
ta.CreatedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
|
||||
func (ta *TenderApproval) SetCreatedAt(timestamp int64) {
|
||||
ta.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
|
||||
func (ta *TenderApproval) SetUpdatedAt(timestamp int64) {
|
||||
ta.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
// ToResponse converts TenderApproval to TenderApprovalResponse
|
||||
func (ta *TenderApproval) ToResponse() *TenderApprovalResponse {
|
||||
return &TenderApprovalResponse{
|
||||
ID: ta.ID.Hex(),
|
||||
TenderID: ta.TenderID,
|
||||
CompanyID: ta.CompanyID,
|
||||
SubmissionMode: string(ta.SubmissionMode),
|
||||
Status: string(ta.Status),
|
||||
CreatedAt: ta.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package tender_approval
|
||||
|
||||
import "errors"
|
||||
|
||||
// Common error messages for tender approval operations
|
||||
var (
|
||||
ErrTenderApprovalNotFound = errors.New("tender approval not found")
|
||||
ErrTenderApprovalExists = errors.New("tender approval already exists for this tender and company")
|
||||
ErrInvalidStatus = errors.New("invalid tender approval status")
|
||||
ErrInvalidSubmissionMode = errors.New("invalid submission mode")
|
||||
ErrTenderNotFound = errors.New("tender not found")
|
||||
ErrCompanyNotFound = errors.New("company not found")
|
||||
ErrInvalidTenderID = errors.New("invalid tender ID")
|
||||
ErrInvalidCompanyID = errors.New("invalid company ID")
|
||||
ErrUnauthorized = errors.New("unauthorized to perform this action")
|
||||
ErrInvalidData = errors.New("invalid tender approval data")
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
package tender_approval
|
||||
|
||||
// CreateTenderApprovalForm represents the form for creating a new tender approval
|
||||
type CreateTenderApprovalForm struct {
|
||||
TenderID string `json:"tender_id" valid:"required"`
|
||||
SubmissionMode SubmissionMode `json:"submission_mode" valid:"required,in(self-apply|partnership)"`
|
||||
Status ApprovalStatus `json:"status,omitempty" valid:"optional,in(approved|rejected)"`
|
||||
}
|
||||
|
||||
// UpdateTenderApprovalForm represents the form for updating a tender approval
|
||||
type UpdateTenderApprovalForm struct {
|
||||
SubmissionMode *string `json:"submission_mode,omitempty" valid:"optional,in(self-apply|partnership)"`
|
||||
Status *string `json:"status,omitempty" valid:"optional,in(approved|rejected)"`
|
||||
}
|
||||
|
||||
// ListTenderApprovalsForm represents the form for listing tender approvals with filters
|
||||
type ListTenderApprovalsForm struct {
|
||||
TenderID *string `query:"tender_id" valid:"optional"`
|
||||
CompanyID *string `query:"company_id" valid:"optional"`
|
||||
Status []string `query:"status" valid:"optional"`
|
||||
SubmissionMode []string `query:"submission_mode" valid:"optional"`
|
||||
CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"`
|
||||
CreatedAtTo *int64 `query:"created_at_to" valid:"optional"`
|
||||
Limit *int `query:"limit" valid:"optional,range(1|100)"`
|
||||
Offset *int `query:"offset" valid:"optional,min(0)"`
|
||||
SortBy *string `query:"sort_by" valid:"optional,in(created_at|updated_at|status|submission_mode)"`
|
||||
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
|
||||
}
|
||||
|
||||
// ApproveTenderForm represents the form for approving a tender
|
||||
type ApproveTenderForm struct {
|
||||
Notes *string `json:"notes,omitempty" valid:"optional,length(0|1000)"`
|
||||
}
|
||||
|
||||
// RejectTenderForm represents the form for rejecting a tender
|
||||
type RejectTenderForm struct {
|
||||
Reason string `json:"reason" valid:"required,length(10|500)"`
|
||||
}
|
||||
|
||||
// TenderApprovalListResponse represents the response for listing tender approvals
|
||||
type TenderApprovalListResponse struct {
|
||||
TenderApprovals []*TenderApprovalResponse `json:"tender_approvals"`
|
||||
Total int64 `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// TenderApproval search and matching DTOs
|
||||
type TenderApprovalSearchForm struct {
|
||||
Query *string `query:"q" valid:"optional"`
|
||||
TenderID *string `query:"tender_id" valid:"optional"`
|
||||
CompanyID *string `query:"company_id" valid:"optional"`
|
||||
Status []string `query:"status" valid:"optional"`
|
||||
SubmissionMode []string `query:"submission_mode" valid:"optional"`
|
||||
CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"`
|
||||
CreatedAtTo *int64 `query:"created_at_to" valid:"optional"`
|
||||
Limit *int `query:"limit" valid:"optional,range(1|100)"`
|
||||
Offset *int `query:"offset" valid:"optional,min(0)"`
|
||||
}
|
||||
|
||||
// TenderApproval statistics DTOs
|
||||
type TenderApprovalStatsResponse struct {
|
||||
TotalApprovals int64 `json:"total_approvals"`
|
||||
ApprovedTenders int64 `json:"approved_tenders"`
|
||||
RejectedTenders int64 `json:"rejected_tenders"`
|
||||
ApprovalsByStatus map[string]int64 `json:"approvals_by_status"`
|
||||
ApprovalsBySubmission map[string]int64 `json:"approvals_by_submission"`
|
||||
LastUpdated int64 `json:"last_updated"`
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"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) CreateTenderApproval(c echo.Context) error {
|
||||
form, err := response.Parse[CreateTenderApprovalForm](c)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
tenderApproval, err := h.service.CreateTenderApproval(c.Request().Context(), form, companyID)
|
||||
if err != nil {
|
||||
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.Created(c, tenderApproval.ToResponse(), "Tender approval created successfully")
|
||||
}
|
||||
|
||||
// UpdateTenderApproval updates a tender approval
|
||||
// @Summary Update tender approval
|
||||
// @Description Update tender approval information
|
||||
// @Tags TenderApprovals
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Tender Approval ID"
|
||||
// @Param tender_approval body UpdateTenderApprovalForm true "Tender approval update information"
|
||||
// @Success 200 {object} response.APIResponse{data=TenderApprovalResponse} "Tender approval updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Tender approval not found"
|
||||
// @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/{id} [put]
|
||||
func (h *Handler) UpdateTenderApproval(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[UpdateTenderApprovalForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
tenderApproval, err := h.service.UpdateTenderApproval(c.Request().Context(), id, form)
|
||||
if err != nil {
|
||||
if err.Error() == "tender approval not found" {
|
||||
return response.NotFound(c, "Tender approval not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update tender approval")
|
||||
}
|
||||
|
||||
return response.Success(c, tenderApproval.ToResponse(), "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")
|
||||
|
||||
tenderApproval, err := h.service.GetTenderApprovalByID(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, tenderApproval.ToResponse(), "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 query 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 {
|
||||
tenderID := c.QueryParam("tender_id")
|
||||
companyID := c.Get("company_id").(string)
|
||||
|
||||
if companyID == "" {
|
||||
return response.Unauthorized(c, "Unauthorized")
|
||||
}
|
||||
|
||||
if tenderID == "" {
|
||||
return response.BadRequest(c, "Missing required parameters", "tender_id and company_id are required")
|
||||
}
|
||||
|
||||
tenderApproval, err := h.service.GetTenderApprovalByTenderAndCompany(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, tenderApproval.ToResponse(), "Tender approval retrieved successfully")
|
||||
}
|
||||
|
||||
// DeleteTenderApproval deletes a tender approval
|
||||
// @Summary Delete tender approval
|
||||
// @Description Delete a tender approval
|
||||
// @Tags TenderApprovals
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Tender Approval ID"
|
||||
// @Success 200 {object} response.APIResponse "Tender approval deleted 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} [delete]
|
||||
func (h *Handler) DeleteTenderApproval(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.service.DeleteTenderApproval(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 delete tender approval")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Tender approval deleted successfully",
|
||||
}, "Tender approval deleted 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 submission_mode query string false "Filter by submission mode Enums(self-apply, partnership)"
|
||||
// @Param status query string false "Filter by status Enums(approved, rejected)"
|
||||
// @Success 200 {object} response.APIResponse{data=[]TenderApprovalResponse} "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 {
|
||||
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 != ApprovalStatusApproved && statusVal != ApprovalStatusRejected {
|
||||
return response.BadRequest(c, "Invalid status value", "Must be 'approved' or 'rejected'")
|
||||
}
|
||||
status = &statusVal
|
||||
}
|
||||
|
||||
tenderApprovals, err := h.service.GetTenderApprovalsByCompanyID(c.Request().Context(), companyID, submissionMode, status)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to get tender approvals")
|
||||
}
|
||||
|
||||
var responses []*TenderApprovalResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
responses = append(responses, approval.ToResponse())
|
||||
}
|
||||
|
||||
return response.Success(c, responses, "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.ListTenderApprovals(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")
|
||||
}
|
||||
|
||||
// GetTenderApprovalsByStatus retrieves tender approvals by status
|
||||
// @Summary Get tender approvals by status
|
||||
// @Description Retrieve tender approvals filtered by their status (approved, rejected)
|
||||
// @Tags Admin-TenderApprovals
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status path string true "Tender approval status" Enums(approved, 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 != ApprovalStatusApproved && status != ApprovalStatusRejected {
|
||||
return response.BadRequest(c, "Invalid tender approval status", "Must be approved 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
|
||||
}
|
||||
}
|
||||
|
||||
tenderApprovals, total, err := h.service.GetTenderApprovalsByStatus(c.Request().Context(), status, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tender approvals by status")
|
||||
}
|
||||
|
||||
var responses []*TenderApprovalResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
responses = append(responses, approval.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, responses, 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
|
||||
}
|
||||
}
|
||||
|
||||
tenderApprovals, total, err := h.service.GetTenderApprovalsBySubmissionMode(c.Request().Context(), submissionMode, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tender approvals by submission mode")
|
||||
}
|
||||
|
||||
var responses []*TenderApprovalResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
responses = append(responses, approval.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, responses, 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 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")
|
||||
|
||||
tenderApproval, err := h.service.GetTenderApprovalByID(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, tenderApproval.ToResponse(), "Tender approval retrieved successfully")
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Repository defines the interface for tender approval data operations
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, tenderApproval *TenderApproval) error
|
||||
GetByID(ctx context.Context, id string) (*TenderApproval, error)
|
||||
GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error)
|
||||
GetByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error)
|
||||
GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus) ([]*TenderApproval, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error)
|
||||
Update(ctx context.Context, tenderApproval *TenderApproval) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context, limit, offset int) ([]*TenderApproval, error)
|
||||
Search(ctx context.Context, tenderID *string, companyID *string, status []ApprovalStatus, submissionMode []SubmissionMode, createdAtFrom *int64, createdAtTo *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderApproval, error)
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountByStatus(ctx context.Context, status ApprovalStatus) (int64, error)
|
||||
CountBySubmissionMode(ctx context.Context, submissionMode SubmissionMode) (int64, error)
|
||||
CountByTenderID(ctx context.Context, tenderID string) (int64, error)
|
||||
CountByCompanyID(ctx context.Context, companyID string) (int64, error)
|
||||
GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error)
|
||||
SearchByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error)
|
||||
}
|
||||
|
||||
// tenderApprovalRepository implements the Repository interface using the MongoDB ORM
|
||||
type tenderApprovalRepository struct {
|
||||
ormRepo mongo.Repository[TenderApproval]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewTenderApprovalRepository creates a new tender approval repository
|
||||
func NewTenderApprovalRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []mongo.Index{
|
||||
*mongo.CreateUniqueIndex("tender_company_idx", bson.D{{Key: "tender_id", Value: 1}, {Key: "company_id", Value: 1}}),
|
||||
*mongo.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
|
||||
*mongo.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
|
||||
*mongo.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||
*mongo.NewIndex("submission_mode_idx", bson.D{{Key: "submission_mode", Value: 1}}),
|
||||
*mongo.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*mongo.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
err := mongoManager.CreateIndexes("tender_approvals", indexes)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create tender approval indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create ORM repository
|
||||
ormRepo := mongo.NewRepository[TenderApproval](mongoManager.GetCollection("tender_approvals"), logger)
|
||||
|
||||
return &tenderApprovalRepository{
|
||||
ormRepo: ormRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new tender approval
|
||||
func (r *tenderApprovalRepository) Create(ctx context.Context, tenderApproval *TenderApproval) error {
|
||||
// Set created/updated timestamps using Unix timestamps
|
||||
now := time.Now().Unix()
|
||||
tenderApproval.SetCreatedAt(now)
|
||||
tenderApproval.SetUpdatedAt(now)
|
||||
|
||||
// Use ORM to create tender approval
|
||||
err := r.ormRepo.Create(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Tender approval created successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
"status": tenderApproval.Status,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a tender approval by ID
|
||||
func (r *tenderApprovalRepository) GetByID(ctx context.Context, id string) (*TenderApproval, error) {
|
||||
tenderApproval, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||
return nil, errors.New("tender approval not found")
|
||||
}
|
||||
r.logger.Error("Failed to get tender approval by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetByTenderAndCompany retrieves a tender approval by tender ID and company ID
|
||||
func (r *tenderApprovalRepository) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) {
|
||||
filter := bson.M{
|
||||
"tender_id": tenderID,
|
||||
"company_id": companyID,
|
||||
}
|
||||
|
||||
tenderApproval, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||
return nil, errors.New("tender approval not found")
|
||||
}
|
||||
r.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderID,
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetByTenderID retrieves all tender approvals for a specific tender
|
||||
func (r *tenderApprovalRepository) GetByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) {
|
||||
filter := bson.M{"tender_id": tenderID}
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().SortDesc("created_at").Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get tender approvals by tender ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []TenderApproval to []*TenderApproval
|
||||
tenderApprovals := make([]*TenderApproval, len(result.Items))
|
||||
for i := range result.Items {
|
||||
tenderApprovals[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// GetByCompanyID retrieves all tender approvals for a specific company with optional filters
|
||||
func (r *tenderApprovalRepository) GetByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus) ([]*TenderApproval, error) {
|
||||
filter := bson.M{"company_id": companyID}
|
||||
|
||||
// Add submission mode filter if provided
|
||||
if submissionMode != nil {
|
||||
filter["submission_mode"] = *submissionMode
|
||||
}
|
||||
|
||||
// Add status filter if provided
|
||||
if status != nil {
|
||||
filter["status"] = *status
|
||||
}
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().SortDesc("created_at").Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []TenderApproval to []*TenderApproval
|
||||
tenderApprovals := make([]*TenderApproval, len(result.Items))
|
||||
for i := range result.Items {
|
||||
tenderApprovals[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// GetByIDs retrieves multiple tender approvals by their IDs
|
||||
func (r *tenderApprovalRepository) GetByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*TenderApproval{}, nil
|
||||
}
|
||||
|
||||
// Convert string IDs to ObjectIDs for MongoDB query
|
||||
objectIDs := make([]interface{}, len(ids))
|
||||
for i, id := range ids {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
r.logger.Warn("Invalid tender approval ID format", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
objectIDs[i] = objectID
|
||||
}
|
||||
|
||||
if len(objectIDs) == 0 {
|
||||
return []*TenderApproval{}, nil
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, mongo.NewPaginationBuilder().Limit(len(ids)).Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get tender approvals by IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"ids": ids,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []TenderApproval to []*TenderApproval
|
||||
tenderApprovals := make([]*TenderApproval, len(result.Items))
|
||||
for i := range result.Items {
|
||||
tenderApprovals[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
r.logger.Info("Retrieved tender approvals by IDs", map[string]interface{}{
|
||||
"requested_count": len(ids),
|
||||
"found_count": len(tenderApprovals),
|
||||
})
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// Update updates a tender approval
|
||||
func (r *tenderApprovalRepository) Update(ctx context.Context, tenderApproval *TenderApproval) error {
|
||||
// Set updated timestamp using Unix timestamp
|
||||
tenderApproval.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Use ORM to update tender approval
|
||||
err := r.ormRepo.Update(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Tender approval updated successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a tender approval
|
||||
func (r *tenderApprovalRepository) Delete(ctx context.Context, id string) error {
|
||||
// Get tender approval first
|
||||
tenderApproval, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
err = r.ormRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Tender approval deleted successfully", map[string]interface{}{
|
||||
"tender_approval_id": id,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves tender approvals with pagination
|
||||
func (r *tenderApprovalRepository) List(ctx context.Context, limit, offset int) ([]*TenderApproval, error) {
|
||||
// Build pagination
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Use ORM to find tender approvals
|
||||
result, err := r.ormRepo.FindAll(ctx, bson.M{}, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list tender approvals", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []TenderApproval to []*TenderApproval
|
||||
tenderApprovals := make([]*TenderApproval, len(result.Items))
|
||||
for i := range result.Items {
|
||||
tenderApprovals[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// Search retrieves tender approvals with search and filters
|
||||
func (r *tenderApprovalRepository) Search(ctx context.Context, tenderID *string, companyID *string, status []ApprovalStatus, submissionMode []SubmissionMode, createdAtFrom *int64, createdAtTo *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderApproval, error) {
|
||||
// Build filter
|
||||
filter := bson.M{}
|
||||
|
||||
if tenderID != nil {
|
||||
filter["tender_id"] = *tenderID
|
||||
}
|
||||
|
||||
if companyID != nil {
|
||||
filter["company_id"] = *companyID
|
||||
}
|
||||
|
||||
if len(status) > 0 {
|
||||
statusStrings := make([]string, len(status))
|
||||
for i, s := range status {
|
||||
statusStrings[i] = string(s)
|
||||
}
|
||||
filter["status"] = bson.M{"$in": statusStrings}
|
||||
}
|
||||
|
||||
if len(submissionMode) > 0 {
|
||||
modeStrings := make([]string, len(submissionMode))
|
||||
for i, m := range submissionMode {
|
||||
modeStrings[i] = string(m)
|
||||
}
|
||||
filter["submission_mode"] = bson.M{"$in": modeStrings}
|
||||
}
|
||||
|
||||
// Date range filters
|
||||
if createdAtFrom != nil || createdAtTo != nil {
|
||||
dateFilter := bson.M{}
|
||||
if createdAtFrom != nil {
|
||||
dateFilter["$gte"] = *createdAtFrom
|
||||
}
|
||||
if createdAtTo != nil {
|
||||
dateFilter["$lte"] = *createdAtTo
|
||||
}
|
||||
filter["created_at"] = dateFilter
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset)
|
||||
|
||||
// Set sort
|
||||
if sortBy != "" {
|
||||
if sortOrder == "desc" {
|
||||
pagination.SortDesc(sortBy)
|
||||
} else {
|
||||
pagination.SortAsc(sortBy)
|
||||
}
|
||||
} else {
|
||||
pagination.SortDesc("created_at") // Default sort
|
||||
}
|
||||
|
||||
// Use ORM to find tender approvals
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search tender approvals", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []TenderApproval to []*TenderApproval
|
||||
tenderApprovals := make([]*TenderApproval, len(result.Items))
|
||||
for i := range result.Items {
|
||||
tenderApprovals[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// Count counts all tender approvals
|
||||
func (r *tenderApprovalRepository) Count(ctx context.Context) (int64, error) {
|
||||
count, err := r.ormRepo.Count(ctx, bson.M{})
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count tender approvals", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByStatus counts tender approvals by status
|
||||
func (r *tenderApprovalRepository) CountByStatus(ctx context.Context, status ApprovalStatus) (int64, error) {
|
||||
filter := bson.M{"status": string(status)}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count tender approvals by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountBySubmissionMode counts tender approvals by submission mode
|
||||
func (r *tenderApprovalRepository) CountBySubmissionMode(ctx context.Context, submissionMode SubmissionMode) (int64, error) {
|
||||
filter := bson.M{"submission_mode": string(submissionMode)}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count tender approvals by submission mode", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"submission_mode": submissionMode,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByTenderID counts tender approvals for a specific tender
|
||||
func (r *tenderApprovalRepository) CountByTenderID(ctx context.Context, tenderID string) (int64, error) {
|
||||
filter := bson.M{"tender_id": tenderID}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count tender approvals by tender ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderID,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByCompanyID counts tender approvals for a specific company
|
||||
func (r *tenderApprovalRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) {
|
||||
filter := bson.M{"company_id": companyID}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count tender approvals by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalStats returns tender approval statistics
|
||||
func (r *tenderApprovalRepository) GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error) {
|
||||
// Get basic counts
|
||||
totalApprovals, _ := r.Count(ctx)
|
||||
approvedTenders, _ := r.CountByStatus(ctx, ApprovalStatusApproved)
|
||||
rejectedTenders, _ := r.CountByStatus(ctx, ApprovalStatusRejected)
|
||||
|
||||
// For this implementation, we'll return basic stats
|
||||
// In a real implementation, you'd use MongoDB aggregation pipelines
|
||||
stats := &TenderApprovalStatsResponse{
|
||||
TotalApprovals: totalApprovals,
|
||||
ApprovedTenders: approvedTenders,
|
||||
RejectedTenders: rejectedTenders,
|
||||
ApprovalsByStatus: make(map[string]int64),
|
||||
ApprovalsBySubmission: make(map[string]int64),
|
||||
LastUpdated: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add status breakdown
|
||||
stats.ApprovalsByStatus["approved"] = approvedTenders
|
||||
stats.ApprovalsByStatus["rejected"] = rejectedTenders
|
||||
|
||||
// Add submission mode breakdown
|
||||
selfApplyCount, _ := r.CountBySubmissionMode(ctx, SubmissionModeSelfApply)
|
||||
partnershipCount, _ := r.CountBySubmissionMode(ctx, SubmissionModePartnership)
|
||||
stats.ApprovalsBySubmission["self-apply"] = selfApplyCount
|
||||
stats.ApprovalsBySubmission["partnership"] = partnershipCount
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// SearchByCriteria searches tender approvals using search criteria
|
||||
func (r *tenderApprovalRepository) SearchByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error) {
|
||||
// Convert criteria to search parameters
|
||||
var status []ApprovalStatus
|
||||
if criteria.Status != nil {
|
||||
status = criteria.Status
|
||||
}
|
||||
|
||||
var submissionMode []SubmissionMode
|
||||
if criteria.SubmissionMode != nil {
|
||||
submissionMode = criteria.SubmissionMode
|
||||
}
|
||||
|
||||
// Use the search method
|
||||
return r.Search(ctx, criteria.TenderID, criteria.CompanyID, status, submissionMode, criteria.CreatedAtFrom, criteria.CreatedAtTo, criteria.Limit, criteria.Offset, "created_at", "desc")
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Service defines business logic for tender approval operations
|
||||
type Service interface {
|
||||
// Core tender approval operations
|
||||
CreateTenderApproval(ctx context.Context, form *CreateTenderApprovalForm, companyID string) (*TenderApproval, error)
|
||||
GetTenderApprovalByID(ctx context.Context, id string) (*TenderApproval, error)
|
||||
GetTenderApprovalByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error)
|
||||
GetTenderApprovalsByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error)
|
||||
GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus) ([]*TenderApproval, error)
|
||||
GetTenderApprovalsByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error)
|
||||
UpdateTenderApproval(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApproval, error)
|
||||
DeleteTenderApproval(ctx context.Context, id string) error
|
||||
|
||||
// Tender approval listing and search
|
||||
ListTenderApprovals(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponse, error)
|
||||
SearchTenderApprovals(ctx context.Context, form *TenderApprovalSearchForm) (*TenderApprovalListResponse, error)
|
||||
SearchTenderApprovalsByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error)
|
||||
|
||||
// Business operations
|
||||
ApproveTender(ctx context.Context, id string, form *ApproveTenderForm) (*TenderApproval, error)
|
||||
RejectTender(ctx context.Context, id string, form *RejectTenderForm) (*TenderApproval, error)
|
||||
|
||||
// Statistics and analytics
|
||||
GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error)
|
||||
|
||||
// Search and filtering helpers
|
||||
GetTenderApprovalsByStatus(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApproval, int64, error)
|
||||
GetTenderApprovalsBySubmissionMode(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApproval, int64, error)
|
||||
}
|
||||
|
||||
// tenderApprovalService implements the Service interface
|
||||
type tenderApprovalService struct {
|
||||
repository Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewTenderApprovalService creates a new tender approval service
|
||||
func NewTenderApprovalService(repository Repository, logger logger.Logger) Service {
|
||||
return &tenderApprovalService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTenderApproval creates a new tender approval
|
||||
func (s *tenderApprovalService) CreateTenderApproval(ctx context.Context, form *CreateTenderApprovalForm, companyID string) (*TenderApproval, error) {
|
||||
// Check if tender approval already exists for this tender and company
|
||||
existingApproval, _ := s.repository.GetByTenderAndCompany(ctx, form.TenderID, companyID)
|
||||
if existingApproval != nil {
|
||||
return nil, errors.New("tender approval already exists for this tender and company")
|
||||
}
|
||||
|
||||
// Create tender approval
|
||||
tenderApproval := &TenderApproval{
|
||||
TenderID: form.TenderID,
|
||||
CompanyID: companyID,
|
||||
SubmissionMode: SubmissionMode(form.SubmissionMode),
|
||||
Status: ApprovalStatus(form.Status),
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err := s.repository.Create(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": form.TenderID,
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approval created successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
"submission_mode": tenderApproval.SubmissionMode,
|
||||
"status": tenderApproval.Status,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalByID retrieves a tender approval by ID
|
||||
func (s *tenderApprovalService) GetTenderApprovalByID(ctx context.Context, id string) (*TenderApproval, error) {
|
||||
tenderApproval, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approval by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approval retrieved successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalByTenderAndCompany retrieves a tender approval by tender ID and company ID
|
||||
func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderApproval, error) {
|
||||
tenderApproval, err := s.repository.GetByTenderAndCompany(ctx, tenderID, companyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approval by tender and company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderID,
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approval retrieved successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalsByTenderID retrieves all tender approvals for a specific tender
|
||||
func (s *tenderApprovalService) GetTenderApprovalsByTenderID(ctx context.Context, tenderID string) ([]*TenderApproval, error) {
|
||||
tenderApprovals, err := s.repository.GetByTenderID(ctx, tenderID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approvals by tender ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_id": tenderID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
|
||||
"tender_id": tenderID,
|
||||
"count": len(tenderApprovals),
|
||||
})
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalsByCompanyID retrieves all tender approvals for a specific company with optional filters
|
||||
func (s *tenderApprovalService) GetTenderApprovalsByCompanyID(ctx context.Context, companyID string, submissionMode *SubmissionMode, status *ApprovalStatus) ([]*TenderApproval, error) {
|
||||
tenderApprovals, err := s.repository.GetByCompanyID(ctx, companyID, submissionMode, status)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approvals by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"count": len(tenderApprovals),
|
||||
})
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalsByIDs retrieves multiple tender approvals by their IDs
|
||||
func (s *tenderApprovalService) GetTenderApprovalsByIDs(ctx context.Context, ids []string) ([]*TenderApproval, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*TenderApproval{}, nil
|
||||
}
|
||||
|
||||
tenderApprovals, err := s.repository.GetByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approvals by IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"ids": ids,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approvals retrieved successfully", map[string]interface{}{
|
||||
"requested_count": len(ids),
|
||||
"found_count": len(tenderApprovals),
|
||||
})
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// UpdateTenderApproval updates a tender approval
|
||||
func (s *tenderApprovalService) UpdateTenderApproval(ctx context.Context, id string, form *UpdateTenderApprovalForm) (*TenderApproval, error) {
|
||||
// Get existing tender approval
|
||||
tenderApproval, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approval for update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, errors.New("tender approval not found")
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if form.SubmissionMode != nil {
|
||||
tenderApproval.SubmissionMode = SubmissionMode(*form.SubmissionMode)
|
||||
}
|
||||
|
||||
if form.Status != nil {
|
||||
tenderApproval.Status = ApprovalStatus(*form.Status)
|
||||
}
|
||||
|
||||
// Set updated timestamp
|
||||
tenderApproval.UpdatedAt = time.Now().Unix()
|
||||
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, errors.New("failed to update tender approval")
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approval updated successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// DeleteTenderApproval deletes a tender approval
|
||||
func (s *tenderApprovalService) DeleteTenderApproval(ctx context.Context, id string) error {
|
||||
// Get tender approval to check if exists
|
||||
tenderApproval, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("tender approval not found")
|
||||
}
|
||||
|
||||
// Delete tender approval
|
||||
err = s.repository.Delete(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete tender approval", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return errors.New("failed to delete tender approval")
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approval deleted successfully", map[string]interface{}{
|
||||
"tender_approval_id": id,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTenderApprovals lists tender approvals with search and filters
|
||||
func (s *tenderApprovalService) ListTenderApprovals(ctx context.Context, form *ListTenderApprovalsForm) (*TenderApprovalListResponse, error) {
|
||||
// Set defaults
|
||||
limit := 20
|
||||
if form.Limit != nil {
|
||||
limit = *form.Limit
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if form.Offset != nil {
|
||||
offset = *form.Offset
|
||||
}
|
||||
|
||||
sortBy := "created_at"
|
||||
if form.SortBy != nil {
|
||||
sortBy = *form.SortBy
|
||||
}
|
||||
|
||||
sortOrder := "desc"
|
||||
if form.SortOrder != nil {
|
||||
sortOrder = *form.SortOrder
|
||||
}
|
||||
|
||||
// Convert status strings to ApprovalStatus
|
||||
var status []ApprovalStatus
|
||||
if form.Status != nil {
|
||||
status = make([]ApprovalStatus, len(form.Status))
|
||||
for i, s := range form.Status {
|
||||
status[i] = ApprovalStatus(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert submission mode strings to SubmissionMode
|
||||
var submissionMode []SubmissionMode
|
||||
if form.SubmissionMode != nil {
|
||||
submissionMode = make([]SubmissionMode, len(form.SubmissionMode))
|
||||
for i, m := range form.SubmissionMode {
|
||||
submissionMode[i] = SubmissionMode(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Get tender approvals
|
||||
tenderApprovals, err := s.repository.Search(ctx, form.TenderID, form.CompanyID, status, submissionMode, form.CreatedAtFrom, form.CreatedAtTo, limit, offset, sortBy, sortOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list tender approvals", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to list tender approvals")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var tenderApprovalResponses []*TenderApprovalResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
tenderApprovalResponses = append(tenderApprovalResponses, approval.ToResponse())
|
||||
}
|
||||
|
||||
// TODO: Implement proper count for pagination metadata
|
||||
total := int64(len(tenderApprovals)) // This should be a separate count query
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
return &TenderApprovalListResponse{
|
||||
TenderApprovals: tenderApprovalResponses,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
TotalPages: int(totalPages),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchTenderApprovals searches tender approvals with advanced filters
|
||||
func (s *tenderApprovalService) SearchTenderApprovals(ctx context.Context, form *TenderApprovalSearchForm) (*TenderApprovalListResponse, error) {
|
||||
// Set defaults
|
||||
limit := 20
|
||||
if form.Limit != nil {
|
||||
limit = *form.Limit
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if form.Offset != nil {
|
||||
offset = *form.Offset
|
||||
}
|
||||
|
||||
// Convert status strings to ApprovalStatus
|
||||
var status []ApprovalStatus
|
||||
if form.Status != nil {
|
||||
status = make([]ApprovalStatus, len(form.Status))
|
||||
for i, s := range form.Status {
|
||||
status[i] = ApprovalStatus(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert submission mode strings to SubmissionMode
|
||||
var submissionMode []SubmissionMode
|
||||
if form.SubmissionMode != nil {
|
||||
submissionMode = make([]SubmissionMode, len(form.SubmissionMode))
|
||||
for i, m := range form.SubmissionMode {
|
||||
submissionMode[i] = SubmissionMode(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Get tender approvals using search
|
||||
tenderApprovals, err := s.repository.Search(ctx, form.TenderID, form.CompanyID, status, submissionMode, form.CreatedAtFrom, form.CreatedAtTo, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search tender approvals", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to search tender approvals")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var tenderApprovalResponses []*TenderApprovalResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
tenderApprovalResponses = append(tenderApprovalResponses, approval.ToResponse())
|
||||
}
|
||||
|
||||
// TODO: Implement proper count for pagination metadata
|
||||
total := int64(len(tenderApprovals)) // This should be a separate count query
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
return &TenderApprovalListResponse{
|
||||
TenderApprovals: tenderApprovalResponses,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
TotalPages: int(totalPages),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchTenderApprovalsByCriteria searches tender approvals using search criteria
|
||||
func (s *tenderApprovalService) SearchTenderApprovalsByCriteria(ctx context.Context, criteria *TenderApprovalSearchCriteria) ([]*TenderApproval, error) {
|
||||
tenderApprovals, err := s.repository.SearchByCriteria(ctx, criteria)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search tender approvals by criteria", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to search tender approvals by criteria")
|
||||
}
|
||||
|
||||
return tenderApprovals, nil
|
||||
}
|
||||
|
||||
// ApproveTender approves a tender
|
||||
func (s *tenderApprovalService) ApproveTender(ctx context.Context, id string, form *ApproveTenderForm) (*TenderApproval, error) {
|
||||
// Get tender approval to check if exists
|
||||
tenderApproval, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New("tender approval not found")
|
||||
}
|
||||
|
||||
// Approve the tender
|
||||
tenderApproval.Approve(form.Notes)
|
||||
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to approve tender", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, errors.New("failed to approve tender")
|
||||
}
|
||||
|
||||
s.logger.Info("Tender approved successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// RejectTender rejects a tender
|
||||
func (s *tenderApprovalService) RejectTender(ctx context.Context, id string, form *RejectTenderForm) (*TenderApproval, error) {
|
||||
// Get tender approval to check if exists
|
||||
tenderApproval, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New("tender approval not found")
|
||||
}
|
||||
|
||||
// Reject the tender
|
||||
tenderApproval.Reject(form.Reason)
|
||||
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, tenderApproval)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to reject tender", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tender_approval_id": id,
|
||||
})
|
||||
return nil, errors.New("failed to reject tender")
|
||||
}
|
||||
|
||||
s.logger.Info("Tender rejected successfully", map[string]interface{}{
|
||||
"tender_approval_id": tenderApproval.ID,
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
"company_id": tenderApproval.CompanyID,
|
||||
"reason": form.Reason,
|
||||
})
|
||||
|
||||
return tenderApproval, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalStats returns tender approval statistics
|
||||
func (s *tenderApprovalService) GetTenderApprovalStats(ctx context.Context) (*TenderApprovalStatsResponse, error) {
|
||||
stats, err := s.repository.GetTenderApprovalStats(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approval stats", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to get tender approval statistics")
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalsByStatus retrieves tender approvals by status with pagination
|
||||
func (s *tenderApprovalService) GetTenderApprovalsByStatus(ctx context.Context, status ApprovalStatus, limit, offset int) ([]*TenderApproval, int64, error) {
|
||||
// Use search method with status filter
|
||||
tenderApprovals, err := s.repository.Search(ctx, nil, nil, []ApprovalStatus{status}, nil, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approvals by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get tender approvals by status")
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := s.repository.CountByStatus(ctx, status)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count tender approvals by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return tenderApprovals, 0, nil // Return approvals even if count fails
|
||||
}
|
||||
|
||||
return tenderApprovals, total, nil
|
||||
}
|
||||
|
||||
// GetTenderApprovalsBySubmissionMode retrieves tender approvals by submission mode with pagination
|
||||
func (s *tenderApprovalService) GetTenderApprovalsBySubmissionMode(ctx context.Context, submissionMode SubmissionMode, limit, offset int) ([]*TenderApproval, int64, error) {
|
||||
// Use search method with submission mode filter
|
||||
tenderApprovals, err := s.repository.Search(ctx, nil, nil, nil, []SubmissionMode{submissionMode}, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender approvals by submission mode", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"submission_mode": submissionMode,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get tender approvals by submission mode")
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := s.repository.CountBySubmissionMode(ctx, submissionMode)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count tender approvals by submission mode", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"submission_mode": submissionMode,
|
||||
})
|
||||
return tenderApprovals, 0, nil
|
||||
}
|
||||
|
||||
return tenderApprovals, total, nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package tender_approval
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validation functions for tender approval domain
|
||||
|
||||
// IsValidTenderID validates if the tender ID format is correct
|
||||
func IsValidTenderID(tenderID string) bool {
|
||||
if tenderID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Tender ID should be a non-empty string
|
||||
// You can add more specific validation rules here based on your tender ID format
|
||||
return len(tenderID) > 0 && len(tenderID) <= 100
|
||||
}
|
||||
|
||||
// IsValidCompanyID validates if the company ID format is correct
|
||||
func IsValidCompanyID(companyID string) bool {
|
||||
if companyID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Company ID should be a non-empty string
|
||||
// You can add more specific validation rules here based on your company ID format
|
||||
return len(companyID) > 0 && len(companyID) <= 100
|
||||
}
|
||||
|
||||
// IsValidSubmissionMode validates if the submission mode is valid
|
||||
func IsValidSubmissionMode(mode string) bool {
|
||||
validModes := []string{
|
||||
string(SubmissionModeSelfApply),
|
||||
string(SubmissionModePartnership),
|
||||
}
|
||||
|
||||
for _, validMode := range validModes {
|
||||
if strings.EqualFold(mode, validMode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidStatus validates if the approval status is valid
|
||||
func IsValidStatus(status string) bool {
|
||||
validStatuses := []string{
|
||||
string(ApprovalStatusApproved),
|
||||
string(ApprovalStatusRejected),
|
||||
}
|
||||
|
||||
for _, validStatus := range validStatuses {
|
||||
if strings.EqualFold(status, validStatus) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidNotes validates if the notes field is valid
|
||||
func IsValidNotes(notes *string) bool {
|
||||
if notes == nil {
|
||||
return true // Notes are optional
|
||||
}
|
||||
|
||||
// Notes should be between 0 and 1000 characters
|
||||
return len(*notes) <= 1000
|
||||
}
|
||||
|
||||
// IsValidReason validates if the rejection reason is valid
|
||||
func IsValidReason(reason string) bool {
|
||||
// Reason should be between 10 and 500 characters
|
||||
if len(reason) < 10 || len(reason) > 500 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reason should not contain only whitespace
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidTimestamp validates if the timestamp is within reasonable range
|
||||
func IsValidTimestamp(timestamp int64) bool {
|
||||
// Timestamp should be positive and not too far in the future
|
||||
// You can adjust these limits based on your requirements
|
||||
if timestamp <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if timestamp is not too far in the future (e.g., 10 years from now)
|
||||
maxFutureTimestamp := int64(10 * 365 * 24 * 60 * 60) // 10 years in seconds
|
||||
return timestamp <= maxFutureTimestamp
|
||||
}
|
||||
|
||||
// IsValidLimit validates if the limit parameter is valid
|
||||
func IsValidLimit(limit int) bool {
|
||||
return limit > 0 && limit <= 100
|
||||
}
|
||||
|
||||
// IsValidOffset validates if the offset parameter is valid
|
||||
func IsValidOffset(offset int) bool {
|
||||
return offset >= 0
|
||||
}
|
||||
|
||||
// IsValidSortBy validates if the sort by parameter is valid
|
||||
func IsValidSortBy(sortBy string) bool {
|
||||
validSortFields := []string{
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"status",
|
||||
"submission_mode",
|
||||
}
|
||||
|
||||
for _, validField := range validSortFields {
|
||||
if strings.EqualFold(sortBy, validField) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidSortOrder validates if the sort order parameter is valid
|
||||
func IsValidSortOrder(sortOrder string) bool {
|
||||
validOrders := []string{"asc", "desc"}
|
||||
|
||||
for _, validOrder := range validOrders {
|
||||
if strings.EqualFold(sortOrder, validOrder) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SanitizeString removes potentially dangerous characters from input strings
|
||||
func SanitizeString(input string) string {
|
||||
// Remove HTML tags
|
||||
htmlTagRegex := regexp.MustCompile(`<[^>]*>`)
|
||||
sanitized := htmlTagRegex.ReplaceAllString(input, "")
|
||||
|
||||
// Remove script tags
|
||||
scriptRegex := regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`)
|
||||
sanitized = scriptRegex.ReplaceAllString(sanitized, "")
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = strings.TrimSpace(sanitized)
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// ValidateSearchQuery validates if the search query is valid
|
||||
func ValidateSearchQuery(query string) bool {
|
||||
if query == "" {
|
||||
return true // Empty query is valid
|
||||
}
|
||||
|
||||
// Query should not be too long
|
||||
if len(query) > 200 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Query should not contain only special characters
|
||||
if regexp.MustCompile(`^[^a-zA-Z0-9\s]+$`).MatchString(query) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user