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.
262 lines
8.3 KiB
Go
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] = ©
|
|
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)
|
|
}
|
|
}
|