Enhance tender approval and submission handling

- 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.
This commit is contained in:
Mazyar
2026-07-12 21:41:53 +03:30
parent 0cce9ef1b5
commit fa3d466579
9 changed files with 436 additions and 21 deletions
+57 -4
View File
@@ -11,12 +11,15 @@ import (
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)
@@ -25,8 +28,9 @@ type Repository interface {
}
type tenderSubmissionRepository struct {
ormRepo mongo.Repository[TenderSubmission]
logger logger.Logger
ormRepo mongo.Repository[TenderSubmission]
collection *mongodriver.Collection
logger logger.Logger
}
func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
@@ -46,8 +50,9 @@ func NewRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger)
})
}
ormRepo := mongo.NewRepository[TenderSubmission](mongoManager.GetCollection("tender_submissions"), logger)
return &tenderSubmissionRepository{ormRepo: ormRepo, logger: logger}
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 {
@@ -56,6 +61,9 @@ func (r *tenderSubmissionRepository) Create(ctx context.Context, submission *Ten
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,
@@ -108,6 +116,9 @@ func (r *tenderSubmissionRepository) GetByTenderAndCompany(ctx context.Context,
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(),
@@ -117,8 +128,35 @@ func (r *tenderSubmissionRepository) Update(ctx context.Context, submission *Ten
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,
@@ -134,6 +172,11 @@ func (r *tenderSubmissionRepository) ListByCompany(ctx context.Context, companyI
}
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
@@ -291,3 +334,13 @@ func int64Value(v interface{}) int64 {
return 0
}
}
func capListLimit(limit int) int {
if limit <= 0 {
return 20
}
if limit > maxListLimit {
return maxListLimit
}
return limit
}