fa3d466579
- Added a new error for submission sync failures, improving error handling in the tender approval process. - Updated the `ToggleTenderApproval` method to handle the new error, providing clearer responses for sync issues. - Introduced a method to reopen terminal submissions when tender approvals are resubmitted, enhancing submission state management. - Implemented optimistic locking in the repository to prevent concurrent updates, improving data integrity. - Added unit tests for new submission handling logic, ensuring robust functionality and error management. This update strengthens the tender approval workflow and submission handling, ensuring better error reporting and state management.
347 lines
10 KiB
Go
347 lines
10 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"
|
|
)
|
|
|
|
const maxListLimit = 100
|
|
|
|
// 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
|
|
UpdateIfUpdatedAt(ctx context.Context, submission *TenderSubmission, expectedUpdatedAt int64) 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]
|
|
collection *mongodriver.Collection
|
|
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(),
|
|
})
|
|
}
|
|
|
|
collection := mongoManager.GetCollection("tender_submissions")
|
|
ormRepo := mongo.NewRepository[TenderSubmission](collection, logger)
|
|
return &tenderSubmissionRepository{ormRepo: ormRepo, collection: collection, 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 {
|
|
if mongo.IsDuplicateKeyError(err) {
|
|
return ErrTenderSubmissionExists
|
|
}
|
|
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 {
|
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
|
return ErrTenderSubmissionNotFound
|
|
}
|
|
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) UpdateIfUpdatedAt(ctx context.Context, submission *TenderSubmission, expectedUpdatedAt int64) error {
|
|
objectID, err := bson.ObjectIDFromHex(submission.GetID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newUpdatedAt := time.Now().Unix()
|
|
submission.SetUpdatedAt(newUpdatedAt)
|
|
|
|
filter := bson.M{"_id": objectID, "updated_at": expectedUpdatedAt}
|
|
result, err := r.collection.UpdateOne(ctx, filter, bson.M{"$set": submission})
|
|
if err != nil {
|
|
r.logger.Error("Failed to update tender submission with optimistic lock", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"submission_id": submission.GetID(),
|
|
})
|
|
return err
|
|
}
|
|
if result.MatchedCount == 0 {
|
|
return ErrConcurrentUpdate
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *tenderSubmissionRepository) Delete(ctx context.Context, id string) error {
|
|
if err := r.ormRepo.Delete(ctx, id); err != nil {
|
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
|
return ErrTenderSubmissionNotFound
|
|
}
|
|
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) {
|
|
limit = capListLimit(limit)
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
func capListLimit(limit int) int {
|
|
if limit <= 0 {
|
|
return 20
|
|
}
|
|
if limit > maxListLimit {
|
|
return maxListLimit
|
|
}
|
|
return limit
|
|
}
|