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] = © return nil } func (m *mockRepository) GetByID(_ context.Context, id string) (*TenderSubmission, error) { for _, submission := range m.byTenderCompany { if submission.GetID() == id { copy := *submission return ©, 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 ©, 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] = © 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)] = © } 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) } }