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:
n.nakhostin
2025-08-18 19:00:41 +03:30
parent a8ef631e43
commit 94d0fbef38
14 changed files with 4615 additions and 16 deletions
+2 -2
View File
@@ -177,8 +177,8 @@ type UpdateCustomerMobileForm struct {
// Customer Authentication DTOs
type LoginForm struct {
Username string `json:"username" valid:"required"`
Password string `json:"password" valid:"required"`
Username string `json:"username" valid:"required" example:"nakhostin"`
Password string `json:"password" valid:"required" example:"Nima.1998"`
}
type RefreshTokenForm struct {
+2
View File
@@ -41,6 +41,7 @@ type TenderResponse struct {
Duration string `json:"duration"`
DurationUnit string `json:"duration_unit"`
TenderDeadline int64 `json:"tender_deadline"`
SubmissionDeadline int64 `json:"submission_deadline"`
CountryCode string `json:"country_code"`
BuyerOrganization *OrganizationResponse `json:"buyer_organization"`
Status TenderStatus `json:"status"`
@@ -65,6 +66,7 @@ func (t *Tender) ToTenderResponse() *TenderResponse {
Duration: t.Duration,
DurationUnit: t.DurationUnit,
TenderDeadline: t.TenderDeadline,
SubmissionDeadline: t.SubmissionDeadline,
CountryCode: t.CountryCode,
BuyerOrganization: &OrganizationResponse{Name: t.BuyerOrganization.Name},
Status: t.Status,
+125
View File
@@ -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,
}
}
+17
View File
@@ -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")
)
+70
View File
@@ -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"`
}
+437
View File
@@ -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")
}
+519
View File
@@ -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")
}
+527
View File
@@ -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
}
+176
View File
@@ -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
}