Files
tm_back/internal/tender_submission/service_test.go
T
Mazyar fa3d466579 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.
2026-07-12 21:41:53 +03:30

262 lines
8.3 KiB
Go

package tender_submission
import (
"context"
"errors"
"testing"
"tm/pkg/logger"
)
type mockRepository struct {
byTenderCompany map[string]*TenderSubmission
createErr error
updateErr error
deleteErr error
}
func submissionKey(tenderID, companyID string) string {
return tenderID + ":" + companyID
}
func newMockRepository() *mockRepository {
return &mockRepository{byTenderCompany: map[string]*TenderSubmission{}}
}
func (m *mockRepository) Create(_ context.Context, submission *TenderSubmission) error {
if m.createErr != nil {
return m.createErr
}
key := submissionKey(submission.TenderID, submission.CompanyID)
if _, exists := m.byTenderCompany[key]; exists {
return ErrTenderSubmissionExists
}
copy := *submission
m.byTenderCompany[key] = &copy
return nil
}
func (m *mockRepository) GetByID(_ context.Context, id string) (*TenderSubmission, error) {
for _, submission := range m.byTenderCompany {
if submission.GetID() == id {
copy := *submission
return &copy, nil
}
}
return nil, ErrTenderSubmissionNotFound
}
func (m *mockRepository) GetByTenderAndCompany(_ context.Context, tenderID, companyID string) (*TenderSubmission, error) {
submission, ok := m.byTenderCompany[submissionKey(tenderID, companyID)]
if !ok {
return nil, ErrTenderSubmissionNotFound
}
copy := *submission
return &copy, nil
}
func (m *mockRepository) Update(_ context.Context, submission *TenderSubmission) error {
if m.updateErr != nil {
return m.updateErr
}
key := submissionKey(submission.TenderID, submission.CompanyID)
if _, ok := m.byTenderCompany[key]; !ok {
return ErrTenderSubmissionNotFound
}
copy := *submission
m.byTenderCompany[key] = &copy
return nil
}
func (m *mockRepository) UpdateIfUpdatedAt(ctx context.Context, submission *TenderSubmission, expectedUpdatedAt int64) error {
current, err := m.GetByTenderAndCompany(ctx, submission.TenderID, submission.CompanyID)
if err != nil {
return err
}
if current.UpdatedAt != expectedUpdatedAt {
return ErrConcurrentUpdate
}
return m.Update(ctx, submission)
}
func (m *mockRepository) Delete(_ context.Context, id string) error {
if m.deleteErr != nil {
return m.deleteErr
}
for key, submission := range m.byTenderCompany {
if submission.GetID() == id {
delete(m.byTenderCompany, key)
return nil
}
}
return ErrTenderSubmissionNotFound
}
func (m *mockRepository) ListByCompany(context.Context, string, []SubmissionStatus, []SubmissionStage, *int64, *int64, int, int, string, string) ([]*TenderSubmission, int64, error) {
return nil, 0, nil
}
func (m *mockRepository) Search(context.Context, *string, *string, []SubmissionStatus, []SubmissionStage, *int64, *int64, int, int, string, string) ([]*TenderSubmission, int64, error) {
return nil, 0, nil
}
func (m *mockRepository) GetCompanyStats(context.Context, string) (*CompanySubmissionStatsResponse, error) {
return nil, nil
}
func (m *mockRepository) GetGlobalStats(context.Context) (*AdminSubmissionStatsResponse, error) {
return nil, nil
}
func (m *mockRepository) seed(submission *TenderSubmission) {
copy := *submission
m.byTenderCompany[submissionKey(submission.TenderID, submission.CompanyID)] = &copy
}
func newTestService(repo Repository) *tenderSubmissionService {
return &tenderSubmissionService{
repository: repo,
approvalReader: nil,
tenderService: nil,
logger: testLogger{},
}
}
type testLogger struct{}
func (testLogger) Debug(string, map[string]interface{}) {}
func (testLogger) Info(string, map[string]interface{}) {}
func (testLogger) Warn(string, map[string]interface{}) {}
func (testLogger) Error(string, map[string]interface{}) {}
func (testLogger) Fatal(string, map[string]interface{}) {}
func (testLogger) WithFields(map[string]interface{}) logger.Logger {
return testLogger{}
}
func (testLogger) Sync() error { return nil }
func TestOnApprovalSubmittedReopensTerminalSubmission(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
existing := NewTenderSubmission("tender-1", "company-1", "customer-1")
existing.AddStatusChange(StatusNotApplied, "approval_rejected", "customer-1", "Rejected")
repo.seed(existing)
if err := service.OnApprovalSubmitted(context.Background(), "tender-1", "company-1", "customer-2"); err != nil {
t.Fatalf("OnApprovalSubmitted() error = %v", err)
}
updated, err := repo.GetByTenderAndCompany(context.Background(), "tender-1", "company-1")
if err != nil {
t.Fatalf("GetByTenderAndCompany() error = %v", err)
}
if updated.Status != StatusOpportunity {
t.Fatalf("status = %q, want %q", updated.Status, StatusOpportunity)
}
}
func TestOnApprovalSubmittedCreatesWhenMissing(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
if err := service.OnApprovalSubmitted(context.Background(), "tender-1", "company-1", "customer-1"); err != nil {
t.Fatalf("OnApprovalSubmitted() error = %v", err)
}
created, err := repo.GetByTenderAndCompany(context.Background(), "tender-1", "company-1")
if err != nil {
t.Fatalf("GetByTenderAndCompany() error = %v", err)
}
if created.Status != StatusOpportunity {
t.Fatalf("status = %q, want %q", created.Status, StatusOpportunity)
}
}
func TestOnApprovalRejectedUsesValidatedTransition(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
existing := NewTenderSubmission("tender-1", "company-1", "customer-1")
existing.AddStatusChange(StatusValidating, "", "customer-1", "Accepted")
repo.seed(existing)
if err := service.OnApprovalRejected(context.Background(), "tender-1", "company-1", "customer-1"); err != nil {
t.Fatalf("OnApprovalRejected() error = %v", err)
}
updated, err := repo.GetByTenderAndCompany(context.Background(), "tender-1", "company-1")
if err != nil {
t.Fatalf("GetByTenderAndCompany() error = %v", err)
}
if updated.Status != StatusNotApplied {
t.Fatalf("status = %q, want %q", updated.Status, StatusNotApplied)
}
}
func TestOnApprovalRejectedSkipsInvalidTransitionFromSentWaiting(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
existing := NewTenderSubmission("tender-1", "company-1", "customer-1")
existing.AddStatusChange(StatusValidating, "", "customer-1", "Accepted")
existing.AddStatusChange(StatusApplying, "", "customer-1", "Ready")
existing.AddStatusChange(StatusSentWaiting, "", "customer-1", "Submitted")
repo.seed(existing)
if err := service.OnApprovalRejected(context.Background(), "tender-1", "company-1", "customer-1"); err != nil {
t.Fatalf("OnApprovalRejected() error = %v", err)
}
updated, err := repo.GetByTenderAndCompany(context.Background(), "tender-1", "company-1")
if err != nil {
t.Fatalf("GetByTenderAndCompany() error = %v", err)
}
if updated.Status != StatusSentWaiting {
t.Fatalf("status = %q, want %q", updated.Status, StatusSentWaiting)
}
}
func TestOnApprovalRemovedDeletesOpportunityOnly(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
opportunity := NewTenderSubmission("tender-1", "company-1", "customer-1")
repo.seed(opportunity)
if err := service.OnApprovalRemoved(context.Background(), "tender-1", "company-1"); err != nil {
t.Fatalf("OnApprovalRemoved() error = %v", err)
}
if _, err := repo.GetByTenderAndCompany(context.Background(), "tender-1", "company-1"); !errors.Is(err, ErrTenderSubmissionNotFound) {
t.Fatalf("expected submission to be deleted, got err = %v", err)
}
inProgress := NewTenderSubmission("tender-2", "company-1", "customer-1")
inProgress.AddStatusChange(StatusValidating, "", "customer-1", "Accepted")
repo.seed(inProgress)
if err := service.OnApprovalRemoved(context.Background(), "tender-2", "company-1"); err != nil {
t.Fatalf("OnApprovalRemoved() error = %v", err)
}
updated, err := repo.GetByTenderAndCompany(context.Background(), "tender-2", "company-1")
if err != nil {
t.Fatalf("GetByTenderAndCompany() error = %v", err)
}
if updated.Status != StatusValidating {
t.Fatalf("status = %q, want %q", updated.Status, StatusValidating)
}
}
func TestGetByIDForCompanyWithTenderEnforcesOwnership(t *testing.T) {
repo := newMockRepository()
service := newTestService(repo)
submission := NewTenderSubmission("tender-1", "company-1", "customer-1")
repo.seed(submission)
_, err := service.GetByIDForCompanyWithTender(context.Background(), submission.GetID(), "company-2")
if !errors.Is(err, ErrTenderSubmissionNotFound) {
t.Fatalf("expected not found for other company, got %v", err)
}
}