diff --git a/backend/internal/core/usecases/tenant/create_company.go b/backend/internal/core/usecases/tenant/create_company.go index ddbd22b..47deb01 100644 --- a/backend/internal/core/usecases/tenant/create_company.go +++ b/backend/internal/core/usecases/tenant/create_company.go @@ -1,140 +1,127 @@ -package tenant - -import ( - "context" -<<<<<<< HEAD - "errors" -======= - "fmt" - "time" ->>>>>>> dev - - "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" - "github.com/rede5/gohorsejobs/backend/internal/core/dto" - "github.com/rede5/gohorsejobs/backend/internal/core/ports" - "github.com/rede5/gohorsejobs/backend/internal/utils" -) - -type CreateCompanyUseCase struct { - companyRepo ports.CompanyRepository - userRepo ports.UserRepository - authService ports.AuthService -} - -func NewCreateCompanyUseCase(cRepo ports.CompanyRepository, uRepo ports.UserRepository, auth ports.AuthService) *CreateCompanyUseCase { - return &CreateCompanyUseCase{ - companyRepo: cRepo, - userRepo: uRepo, - authService: auth, - } -} - -func (uc *CreateCompanyUseCase) Execute(ctx context.Context, input dto.CreateCompanyRequest) (*dto.CompanyResponse, error) { - // 0. Sanitize inputs - sanitizer := utils.DefaultSanitizer() - input.Name = sanitizer.SanitizeName(input.Name) - input.Contact = sanitizer.SanitizeString(input.Contact) - input.AdminEmail = sanitizer.SanitizeEmail(input.AdminEmail) - - // Validate name - if input.Name == "" { - return nil, errors.New("nome da empresa é obrigatório") - } - -<<<<<<< HEAD - // Validate document (flexible for global portal) - // Use empty country code for global acceptance, or detect from input - docValidator := utils.NewDocumentValidator("") // Global mode - if input.Document != "" { - result := docValidator.ValidateDocument(input.Document, "") - if !result.Valid { - return nil, errors.New(result.Message) - } - input.Document = result.Clean - } - - // 1. Create Company Entity -======= - // 0. Ensure AdminEmail is set (fallback to Email) - if input.AdminEmail == "" { - input.AdminEmail = input.Email - } - if input.Contact == "" && input.Email != "" { - input.Contact = input.Email - } - - // 1. Check if user already exists - existingUser, _ := uc.userRepo.FindByEmail(ctx, input.AdminEmail) - if existingUser != nil { - return nil, fmt.Errorf("user with email %s already exists", input.AdminEmail) - } - ->>>>>>> dev - company := entity.NewCompany("", input.Name, &input.Document, &input.Contact) - - // Map optional fields - // Map optional fields - if input.Phone != "" { - company.Phone = &input.Phone - } - if input.Website != nil { - company.Website = input.Website - } - if input.Description != nil { - company.Description = input.Description - } - if input.Address != nil { - company.Address = input.Address - } - if input.YearsInMarket != nil { - company.YearsInMarket = input.YearsInMarket - } - - savedCompany, err := uc.companyRepo.Save(ctx, company) - if err != nil { - return nil, err - } - - // 2. Create Admin User - pwd := input.Password - if pwd == "" { - pwd = "ChangeMe123!" - } - hashedPassword, _ := uc.authService.HashPassword(pwd) - - adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail) - adminUser.PasswordHash = hashedPassword - - if input.BirthDate != nil { - layout := "2006-01-02" - if parsed, err := time.Parse(layout, *input.BirthDate); err == nil { - adminUser.BirthDate = &parsed - } - } - - adminUser.AssignRole(entity.Role{Name: entity.RoleAdmin}) - - _, err = uc.userRepo.Save(ctx, adminUser) - if err != nil { - // Rollback company? Transaction? - // Ignored for now. - return nil, err - } - - // 3. Generate Token for Auto-Login - token, err := uc.authService.GenerateToken(adminUser.ID, savedCompany.ID, []string{entity.RoleAdmin}) - if err != nil { - // Log error but don't fail creation? Or fail? - // Ideally we return the created company at least, but to ensure auto-login we might want to error or just return empty token. - // Let's iterate: return success but empty token if token generation fails (unlikely). - // Better: just ignore error for now or log it. - } - - return &dto.CompanyResponse{ - ID: savedCompany.ID, - Name: savedCompany.Name, - Status: savedCompany.Status, - Token: token, - CreatedAt: savedCompany.CreatedAt, - }, nil -} +package tenant + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" + "github.com/rede5/gohorsejobs/backend/internal/utils" +) + +type CreateCompanyUseCase struct { + companyRepo ports.CompanyRepository + userRepo ports.UserRepository + authService ports.AuthService +} + +func NewCreateCompanyUseCase(cRepo ports.CompanyRepository, uRepo ports.UserRepository, auth ports.AuthService) *CreateCompanyUseCase { + return &CreateCompanyUseCase{ + companyRepo: cRepo, + userRepo: uRepo, + authService: auth, + } +} + +func (uc *CreateCompanyUseCase) Execute(ctx context.Context, input dto.CreateCompanyRequest) (*dto.CompanyResponse, error) { + // 0. Sanitize inputs + sanitizer := utils.DefaultSanitizer() + input.Name = sanitizer.SanitizeName(input.Name) + input.Contact = sanitizer.SanitizeString(input.Contact) + input.AdminEmail = sanitizer.SanitizeEmail(input.AdminEmail) + + // Validate name + if input.Name == "" { + return nil, errors.New("nome da empresa é obrigatório") + } + + // Validate document (flexible for global portal) + docValidator := utils.NewDocumentValidator("") + if input.Document != "" { + result := docValidator.ValidateDocument(input.Document, "") + if !result.Valid { + return nil, errors.New(result.Message) + } + input.Document = result.Clean + } + + // Ensure AdminEmail is set (fallback to Email) + if input.AdminEmail == "" { + input.AdminEmail = input.Email + } + if input.Contact == "" && input.Email != "" { + input.Contact = input.Email + } + + // Check if user already exists + existingUser, _ := uc.userRepo.FindByEmail(ctx, input.AdminEmail) + if existingUser != nil { + return nil, fmt.Errorf("user with email %s already exists", input.AdminEmail) + } + + // 1. Create Company Entity + company := entity.NewCompany("", input.Name, &input.Document, &input.Contact) + + // Map optional fields + if input.Phone != "" { + company.Phone = &input.Phone + } + if input.Website != nil { + company.Website = input.Website + } + if input.Description != nil { + company.Description = input.Description + } + if input.Address != nil { + company.Address = input.Address + } + if input.YearsInMarket != nil { + company.YearsInMarket = input.YearsInMarket + } + + savedCompany, err := uc.companyRepo.Save(ctx, company) + if err != nil { + return nil, err + } + + // 2. Create Admin User + pwd := input.Password + if pwd == "" { + pwd = "ChangeMe123!" + } + hashedPassword, _ := uc.authService.HashPassword(pwd) + + adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail) + adminUser.PasswordHash = hashedPassword + + if input.BirthDate != nil { + layout := "2006-01-02" + if parsed, err := time.Parse(layout, *input.BirthDate); err == nil { + adminUser.BirthDate = &parsed + } + } + + adminUser.AssignRole(entity.Role{Name: entity.RoleAdmin}) + + _, err = uc.userRepo.Save(ctx, adminUser) + if err != nil { + return nil, err + } + + // 3. Generate Token for Auto-Login + token, err := uc.authService.GenerateToken(adminUser.ID, savedCompany.ID, []string{entity.RoleAdmin}) + if err != nil { + _ = err + } + + return &dto.CompanyResponse{ + ID: savedCompany.ID, + Name: savedCompany.Name, + Status: savedCompany.Status, + Token: token, + CreatedAt: savedCompany.CreatedAt, + }, nil +} diff --git a/backend/internal/services/application_service_test.go b/backend/internal/services/application_service_test.go index 07024cf..fd2cd12 100644 --- a/backend/internal/services/application_service_test.go +++ b/backend/internal/services/application_service_test.go @@ -1,225 +1,214 @@ -package services_test - -import ( - "context" - "testing" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/rede5/gohorsejobs/backend/internal/dto" -<<<<<<< HEAD - "github.com/rede5/gohorsejobs/backend/internal/services" - "github.com/stretchr/testify/assert" -) - -type MockEmailService struct { - SentEmails []struct { - To string - Subject string - Body string - } -} - -func (m *MockEmailService) SendEmail(to, subject, body string) error { - m.SentEmails = append(m.SentEmails, struct { - To string - Subject string - Body string - }{To: to, Subject: subject, Body: body}) - return nil -} - -func (m *MockEmailService) SendTemplateEmail(ctx context.Context, to, templateID string, data map[string]interface{}) error { - return nil -} - -func StringPtr(s string) *string { - return &s -} - -func TestCreateApplication_Success(t *testing.T) { - db, mock, err := sqlmock.New() - assert.NoError(t, err) - defer db.Close() - - emailService := &MockEmailService{} - service := services.NewApplicationService(db, emailService) - - req := dto.CreateApplicationRequest{ - JobID: "1", - UserID: StringPtr("123"), - Name: StringPtr("John Doe"), - Email: StringPtr("john@example.com"), - Phone: StringPtr("1234567890"), - } - - rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}). - AddRow("1", time.Now(), time.Now()) - - mock.ExpectQuery("INSERT INTO applications"). - WillReturnRows(rows) - - app, err := service.CreateApplication(req) - assert.NoError(t, err) - assert.NotNil(t, app) - assert.Equal(t, "1", app.ID) - - // Wait for goroutine to finish (simple sleep for test, ideal would be waitgroup but svc doesn't expose it) - time.Sleep(100 * time.Millisecond) - - if len(emailService.SentEmails) == 0 { - // t.Error("Expected email to be sent") - // Disable this check if logic changed - } else { - assert.Equal(t, "company@example.com", emailService.SentEmails[0].To) - assert.Contains(t, emailService.SentEmails[0].Subject, "Nova candidatura") -======= -) - -func TestNewApplicationService(t *testing.T) { - s := NewApplicationService(nil) - if s == nil { - t.Error("NewApplicationService should not return nil") ->>>>>>> dev - } -} - -func TestApplicationService_DeleteApplication(t *testing.T) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer db.Close() - - // NewApplicationService requires emailService - emailService := &MockEmailService{} - s := services.NewApplicationService(db, emailService) - appID := "test-app-id" - - mock.ExpectExec("DELETE FROM applications WHERE id = \\$1"). - WithArgs(appID). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err = s.DeleteApplication(appID) - if err != nil { - // t.Errorf("error was not expected while deleting application: %s", err) - } - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %s", err) - } -} - -func TestApplicationService_CreateApplication(t *testing.T) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("Failed to create mock db: %v", err) - } - defer db.Close() - - s := NewApplicationService(db) - - userID := "user-456" - name := "Test Candidate" - phone := "123456789" - email := "test@example.com" - message := "I want this job" - resumeURL := "https://example.com/resume.pdf" - - req := dto.CreateApplicationRequest{ - JobID: "job-123", - UserID: &userID, - Name: &name, - Phone: &phone, - Email: &email, - Message: &message, - ResumeURL: &resumeURL, - } - - mock.ExpectQuery("INSERT INTO applications"). - WithArgs( - req.JobID, req.UserID, req.Name, req.Phone, req.LineID, req.WhatsApp, req.Email, - req.Message, req.ResumeURL, sqlmock.AnyArg(), "pending", sqlmock.AnyArg(), sqlmock.AnyArg(), - ). - WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}). - AddRow("app-789", time.Now(), time.Now())) - - app, err := s.CreateApplication(req) - if err != nil { - t.Fatalf("CreateApplication failed: %v", err) - } - if app.ID != "app-789" { - t.Errorf("Expected app ID 'app-789', got '%s'", app.ID) - } - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("Unmet expectations: %v", err) - } -} - -func TestApplicationService_GetApplications(t *testing.T) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("Failed to create mock db: %v", err) - } - defer db.Close() - - s := NewApplicationService(db) - jobID := "job-123" - - rows := sqlmock.NewRows([]string{ - "id", "job_id", "user_id", "name", "phone", "line_id", "whatsapp", "email", - "message", "resume_url", "status", "created_at", "updated_at", - }).AddRow( - "app-1", jobID, "user-1", "John Doe", "123", nil, nil, "john@test.com", - "Hello", "http://resume.pdf", "pending", time.Now(), time.Now(), - ) - - mock.ExpectQuery("SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email"). - WithArgs(jobID). - WillReturnRows(rows) - - apps, err := s.GetApplications(jobID) - if err != nil { - t.Fatalf("GetApplications failed: %v", err) - } - if len(apps) != 1 { - t.Errorf("Expected 1 application, got %d", len(apps)) - } - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("Unmet expectations: %v", err) - } -} - -func TestApplicationService_GetApplicationByID(t *testing.T) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("Failed to create mock db: %v", err) - } - defer db.Close() - - s := NewApplicationService(db) - appID := "app-123" - - rows := sqlmock.NewRows([]string{ - "id", "job_id", "user_id", "name", "phone", "line_id", "whatsapp", "email", - "message", "resume_url", "documents", "status", "created_at", "updated_at", - }).AddRow( - appID, "job-1", "user-1", "Jane Doe", "456", nil, nil, "jane@test.com", - "Hi", "http://cv.pdf", nil, "pending", time.Now(), time.Now(), - ) - - mock.ExpectQuery("SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email"). - WithArgs(appID). - WillReturnRows(rows) - - app, err := s.GetApplicationByID(appID) - if err != nil { - t.Fatalf("GetApplicationByID failed: %v", err) - } - if app.ID != appID { - t.Errorf("Expected app ID '%s', got '%s'", appID, app.ID) - } - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("Unmet expectations: %v", err) - } -} +package services_test + +import ( + "context" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/rede5/gohorsejobs/backend/internal/dto" + "github.com/rede5/gohorsejobs/backend/internal/services" + "github.com/stretchr/testify/assert" +) + +type MockEmailService struct { + SentEmails []struct { + To string + Subject string + Body string + } +} + +func (m *MockEmailService) SendEmail(to, subject, body string) error { + m.SentEmails = append(m.SentEmails, struct { + To string + Subject string + Body string + }{To: to, Subject: subject, Body: body}) + return nil +} + +func (m *MockEmailService) SendTemplateEmail(ctx context.Context, to, templateID string, data map[string]interface{}) error { + return nil +} + +func StringPtr(s string) *string { + return &s +} + +func TestCreateApplication_Success(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + emailService := &MockEmailService{} + service := services.NewApplicationService(db, emailService) + + req := dto.CreateApplicationRequest{ + JobID: "1", + UserID: StringPtr("123"), + Name: StringPtr("John Doe"), + Email: StringPtr("john@example.com"), + Phone: StringPtr("1234567890"), + } + + rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow("1", time.Now(), time.Now()) + + mock.ExpectQuery("INSERT INTO applications"). + WillReturnRows(rows) + + app, err := service.CreateApplication(req) + assert.NoError(t, err) + assert.NotNil(t, app) + assert.Equal(t, "1", app.ID) + + time.Sleep(100 * time.Millisecond) + + if len(emailService.SentEmails) > 0 { + assert.Equal(t, "company@example.com", emailService.SentEmails[0].To) + assert.Contains(t, emailService.SentEmails[0].Subject, "Nova candidatura") + } +} + +func TestApplicationService_DeleteApplication(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + emailService := &MockEmailService{} + s := services.NewApplicationService(db, emailService) + appID := "test-app-id" + + mock.ExpectExec("DELETE FROM applications WHERE id = \\$1"). + WithArgs(appID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err = s.DeleteApplication(appID) + if err != nil { + t.Logf("delete returned error: %s", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestApplicationService_CreateApplication_Full(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + emailService := &MockEmailService{} + s := services.NewApplicationService(db, emailService) + + userID := "user-456" + name := "Test Candidate" + phone := "123456789" + email := "test@example.com" + message := "I want this job" + resumeURL := "https://example.com/resume.pdf" + + req := dto.CreateApplicationRequest{ + JobID: "job-123", + UserID: &userID, + Name: &name, + Phone: &phone, + Email: &email, + Message: &message, + ResumeURL: &resumeURL, + } + + mock.ExpectQuery("INSERT INTO applications"). + WithArgs( + req.JobID, req.UserID, req.Name, req.Phone, req.LineID, req.WhatsApp, req.Email, + req.Message, req.ResumeURL, sqlmock.AnyArg(), "pending", sqlmock.AnyArg(), sqlmock.AnyArg(), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow("app-789", time.Now(), time.Now())) + + app, err := s.CreateApplication(req) + if err != nil { + t.Fatalf("CreateApplication failed: %v", err) + } + if app.ID != "app-789" { + t.Errorf("Expected app ID 'app-789', got '%s'", app.ID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } +} + +func TestApplicationService_GetApplications(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + emailService := &MockEmailService{} + s := services.NewApplicationService(db, emailService) + jobID := "job-123" + + rows := sqlmock.NewRows([]string{ + "id", "job_id", "user_id", "name", "phone", "line_id", "whatsapp", "email", + "message", "resume_url", "status", "created_at", "updated_at", + }).AddRow( + "app-1", jobID, "user-1", "John Doe", "123", nil, nil, "john@test.com", + "Hello", "http://resume.pdf", "pending", time.Now(), time.Now(), + ) + + mock.ExpectQuery("SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email"). + WithArgs(jobID). + WillReturnRows(rows) + + apps, err := s.GetApplications(jobID) + if err != nil { + t.Fatalf("GetApplications failed: %v", err) + } + if len(apps) != 1 { + t.Errorf("Expected 1 application, got %d", len(apps)) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } +} + +func TestApplicationService_GetApplicationByID(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + emailService := &MockEmailService{} + s := services.NewApplicationService(db, emailService) + appID := "app-123" + + rows := sqlmock.NewRows([]string{ + "id", "job_id", "user_id", "name", "phone", "line_id", "whatsapp", "email", + "message", "resume_url", "documents", "status", "created_at", "updated_at", + }).AddRow( + appID, "job-1", "user-1", "Jane Doe", "456", nil, nil, "jane@test.com", + "Hi", "http://cv.pdf", nil, "pending", time.Now(), time.Now(), + ) + + mock.ExpectQuery("SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email"). + WithArgs(appID). + WillReturnRows(rows) + + app, err := s.GetApplicationByID(appID) + if err != nil { + t.Fatalf("GetApplicationByID failed: %v", err) + } + if app.ID != appID { + t.Errorf("Expected app ID '%s', got '%s'", appID, app.ID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } +}