0cce9ef1b5
- Introduced the `tender_submission` domain, including entity, repository, service, and handler implementations for managing tender submissions. - Added new routes for both admin and public access to tender submissions, allowing for listing, ensuring, and retrieving submissions by ID and tender. - Enhanced the tender approval service to synchronize submission workflows with approval changes, ensuring proper state management. - Implemented validation and response structures for tender submission operations, improving API consistency and usability. - Added unit tests for tender submission status transitions and workflow logic, ensuring robust functionality. This update enhances the tender management system by providing comprehensive support for tender submissions, improving overall workflow and user experience.
294 lines
9.2 KiB
Go
294 lines
9.2 KiB
Go
package tender_submission
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/mongo"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
|
)
|
|
|
|
// Repository defines data access for tender submissions.
|
|
type Repository interface {
|
|
Create(ctx context.Context, submission *TenderSubmission) error
|
|
GetByID(ctx context.Context, id string) (*TenderSubmission, error)
|
|
GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderSubmission, error)
|
|
Update(ctx context.Context, submission *TenderSubmission) error
|
|
Delete(ctx context.Context, id string) error
|
|
ListByCompany(ctx context.Context, companyID string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error)
|
|
Search(ctx context.Context, tenderID, companyID *string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error)
|
|
GetCompanyStats(ctx context.Context, companyID string) (*CompanySubmissionStatsResponse, error)
|
|
GetGlobalStats(ctx context.Context) (*AdminSubmissionStatsResponse, error)
|
|
}
|
|
|
|
type tenderSubmissionRepository struct {
|
|
ormRepo mongo.Repository[TenderSubmission]
|
|
logger logger.Logger
|
|
}
|
|
|
|
func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
|
|
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("stage_idx", bson.D{{Key: "stage", 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}}),
|
|
}
|
|
|
|
if err := mongoManager.CreateIndexes("tender_submissions", indexes); err != nil {
|
|
logger.Warn("Failed to create tender submission indexes", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
ormRepo := mongo.NewRepository[TenderSubmission](mongoManager.GetCollection("tender_submissions"), logger)
|
|
return &tenderSubmissionRepository{ormRepo: ormRepo, logger: logger}
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) Create(ctx context.Context, submission *TenderSubmission) error {
|
|
now := time.Now().Unix()
|
|
submission.SetCreatedAt(now)
|
|
submission.SetUpdatedAt(now)
|
|
|
|
if err := r.ormRepo.Create(ctx, submission); err != nil {
|
|
r.logger.Error("Failed to create tender submission", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"tender_id": submission.TenderID,
|
|
"company_id": submission.CompanyID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("Tender submission created", map[string]interface{}{
|
|
"submission_id": submission.GetID(),
|
|
"tender_id": submission.TenderID,
|
|
"company_id": submission.CompanyID,
|
|
"status": submission.Status,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) GetByID(ctx context.Context, id string) (*TenderSubmission, error) {
|
|
submission, err := r.ormRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
|
return nil, ErrTenderSubmissionNotFound
|
|
}
|
|
r.logger.Error("Failed to get tender submission by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"submission_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
return submission, nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) GetByTenderAndCompany(ctx context.Context, tenderID, companyID string) (*TenderSubmission, error) {
|
|
filter := bson.M{"tender_id": tenderID, "company_id": companyID}
|
|
submission, err := r.ormRepo.FindOne(ctx, filter)
|
|
if err != nil {
|
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
|
return nil, ErrTenderSubmissionNotFound
|
|
}
|
|
r.logger.Error("Failed to get tender submission by tender and company", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"tender_id": tenderID,
|
|
"company_id": companyID,
|
|
})
|
|
return nil, err
|
|
}
|
|
return submission, nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) Update(ctx context.Context, submission *TenderSubmission) error {
|
|
submission.SetUpdatedAt(time.Now().Unix())
|
|
if err := r.ormRepo.Update(ctx, submission); err != nil {
|
|
r.logger.Error("Failed to update tender submission", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"submission_id": submission.GetID(),
|
|
})
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) Delete(ctx context.Context, id string) error {
|
|
if err := r.ormRepo.Delete(ctx, id); err != nil {
|
|
r.logger.Error("Failed to delete tender submission", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"submission_id": id,
|
|
})
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) ListByCompany(ctx context.Context, companyID string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) {
|
|
companyIDPtr := companyID
|
|
return r.Search(ctx, nil, &companyIDPtr, statuses, stages, from, to, limit, offset, sortBy, sortOrder)
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) Search(ctx context.Context, tenderID, companyID *string, statuses []SubmissionStatus, stages []SubmissionStage, from, to *int64, limit, offset int, sortBy, sortOrder string) ([]*TenderSubmission, int64, error) {
|
|
filter := bson.M{}
|
|
if tenderID != nil && *tenderID != "" {
|
|
filter["tender_id"] = *tenderID
|
|
}
|
|
if companyID != nil && *companyID != "" {
|
|
filter["company_id"] = *companyID
|
|
}
|
|
if len(statuses) > 0 {
|
|
filter["status"] = bson.M{"$in": statuses}
|
|
}
|
|
if len(stages) > 0 {
|
|
filter["stage"] = bson.M{"$in": stages}
|
|
}
|
|
if from != nil || to != nil {
|
|
dateFilter := bson.M{}
|
|
if from != nil {
|
|
dateFilter["$gte"] = *from
|
|
}
|
|
if to != nil {
|
|
dateFilter["$lte"] = *to
|
|
}
|
|
filter["created_at"] = dateFilter
|
|
}
|
|
|
|
if sortBy == "" {
|
|
sortBy = "updated_at"
|
|
}
|
|
if sortOrder == "" {
|
|
sortOrder = "desc"
|
|
}
|
|
|
|
pagination := mongo.NewPaginationBuilder().Limit(limit).Skip(offset)
|
|
if sortOrder == "asc" {
|
|
pagination = pagination.SortAsc(sortBy)
|
|
} else {
|
|
pagination = pagination.SortDesc(sortBy)
|
|
}
|
|
|
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
|
|
if err != nil {
|
|
r.logger.Error("Failed to search tender submissions", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, 0, err
|
|
}
|
|
|
|
items := make([]*TenderSubmission, len(result.Items))
|
|
for i := range result.Items {
|
|
items[i] = &result.Items[i]
|
|
}
|
|
return items, result.TotalCount, nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) GetCompanyStats(ctx context.Context, companyID string) (*CompanySubmissionStatsResponse, error) {
|
|
pipeline := mongodriver.Pipeline{
|
|
{{Key: "$match", Value: bson.M{"company_id": companyID}}},
|
|
{{Key: "$facet", Value: bson.M{
|
|
"total": bson.A{bson.M{"$count": "count"}},
|
|
"by_stage": bson.A{bson.M{"$group": bson.M{"_id": "$stage", "count": bson.M{"$sum": 1}}}},
|
|
"by_status": bson.A{bson.M{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}}},
|
|
}}},
|
|
}
|
|
|
|
results, err := r.ormRepo.Aggregate(ctx, pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats := &CompanySubmissionStatsResponse{
|
|
CompanyID: companyID,
|
|
ByStage: map[string]int64{},
|
|
ByStatus: map[string]int64{},
|
|
LastUpdated: time.Now().Unix(),
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
return stats, nil
|
|
}
|
|
|
|
facet := results[0]
|
|
if totalArr, ok := facet["total"].(bson.A); ok && len(totalArr) > 0 {
|
|
if doc, ok := totalArr[0].(bson.M); ok {
|
|
stats.Total = int64Value(doc["count"])
|
|
}
|
|
}
|
|
stats.ByStage = countMapFromFacet(facet["by_stage"])
|
|
stats.ByStatus = countMapFromFacet(facet["by_status"])
|
|
return stats, nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) GetGlobalStats(ctx context.Context) (*AdminSubmissionStatsResponse, error) {
|
|
pipeline := mongodriver.Pipeline{
|
|
{{Key: "$facet", Value: bson.M{
|
|
"total": bson.A{bson.M{"$count": "count"}},
|
|
"by_stage": bson.A{bson.M{"$group": bson.M{"_id": "$stage", "count": bson.M{"$sum": 1}}}},
|
|
"by_status": bson.A{bson.M{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}}},
|
|
}}},
|
|
}
|
|
|
|
results, err := r.ormRepo.Aggregate(ctx, pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats := &AdminSubmissionStatsResponse{
|
|
ByStage: map[string]int64{},
|
|
ByStatus: map[string]int64{},
|
|
LastUpdated: time.Now().Unix(),
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
return stats, nil
|
|
}
|
|
|
|
facet := results[0]
|
|
if totalArr, ok := facet["total"].(bson.A); ok && len(totalArr) > 0 {
|
|
if doc, ok := totalArr[0].(bson.M); ok {
|
|
stats.Total = int64Value(doc["count"])
|
|
}
|
|
}
|
|
stats.ByStage = countMapFromFacet(facet["by_stage"])
|
|
stats.ByStatus = countMapFromFacet(facet["by_status"])
|
|
return stats, nil
|
|
}
|
|
|
|
func countMapFromFacet(raw interface{}) map[string]int64 {
|
|
out := map[string]int64{}
|
|
arr, ok := raw.(bson.A)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, item := range arr {
|
|
doc, ok := item.(bson.M)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, _ := doc["_id"].(string)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
out[key] = int64Value(doc["count"])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func int64Value(v interface{}) int64 {
|
|
switch n := v.(type) {
|
|
case int32:
|
|
return int64(n)
|
|
case int64:
|
|
return n
|
|
case float64:
|
|
return int64(n)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|