diff --git a/pom.xml b/pom.xml index 17fd7ad..91232ce 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,18 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-test + + + junit + junit + + + org.mockito + mockito-core + @@ -57,6 +69,14 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + diff --git a/src/main/java/com/zoomcare/candidatechallenge/controllers/EmployeeController.java b/src/main/java/com/zoomcare/candidatechallenge/controllers/EmployeeController.java new file mode 100644 index 0000000..2a0831e --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/controllers/EmployeeController.java @@ -0,0 +1,54 @@ +package com.zoomcare.candidatechallenge.controllers; + +import com.zoomcare.candidatechallenge.models.Employee; +import com.zoomcare.candidatechallenge.services.EmployeeService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Endpoints for obtaining information about an employee or collections of employees. + */ +@RestController +@RequestMapping("/employees") +public class EmployeeController { + + private final EmployeeService employeeService; + + public EmployeeController(EmployeeService employeeService) { + this.employeeService = employeeService; + } + + /** + * Retrieves the employee with the given ID. + * + * @param id the ID of the employee to retrieve + * @return a {@code ResponseEntity} representing the requested employee. The entity will contain the + * employee data, or a {@code NOT_FOUND} status if an employee with the given ID could not be found. + */ + @GetMapping("/{id}") + public ResponseEntity getEmployeeById(@PathVariable long id) { + Employee employee = employeeService.getEmployeeById(id); + if (employee == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(employee, HttpStatus.OK); + } + + /** + * Retrieves all top-level employees (employees with no managers). + * + * @return a {@code ResponseEntity} representing the collection of top-level employees. The entity will contain + * the employee data. + */ + @GetMapping("/top-level-employees") + public ResponseEntity> getAllTopLevelEmployees() { + List topLevelEmployees = employeeService.getAllTopLevelEmployees(); + return new ResponseEntity<>(topLevelEmployees, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/models/Employee.java b/src/main/java/com/zoomcare/candidatechallenge/models/Employee.java new file mode 100644 index 0000000..bbde9c2 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/models/Employee.java @@ -0,0 +1,61 @@ +package com.zoomcare.candidatechallenge.models; + +import org.springframework.data.annotation.Transient; + +import java.util.ArrayList; +import java.util.List; + +/** + * Domain representation of an employee as it is stored in the database. + */ +public class Employee { + private Long id; + private Long supervisorId; + @Transient private List properties; + @Transient private List directReports; + + public Employee(Long id, Long supervisorId) { + this.id = id; + this.supervisorId = supervisorId; + } + + public Employee() { + this.properties = new ArrayList<>(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSupervisorId() { + return supervisorId; + } + + public void setSupervisorId(Long supervisorId) { + this.supervisorId = supervisorId; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + + public void addEmployeeProperty(Property property) { + this.properties.add(property); + } + + public List getDirectReports() { + return directReports; + } + + public void setDirectReports(List directReports) { + this.directReports = directReports; + } +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/models/Property.java b/src/main/java/com/zoomcare/candidatechallenge/models/Property.java new file mode 100644 index 0000000..9d6e467 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/models/Property.java @@ -0,0 +1,42 @@ +package com.zoomcare.candidatechallenge.models; + +/** + * Domain representation of a property of an employee as it is stored in the database. + */ +public class Property { + private Long employeeId; + private String key; + private String value; + + public Property(Long employeeId, String key, String value) { + this.employeeId = employeeId; + this.key = key; + this.value = value; + } + + public Property() {} + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/repositories/EmployeeRepository.java b/src/main/java/com/zoomcare/candidatechallenge/repositories/EmployeeRepository.java new file mode 100644 index 0000000..8775cab --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/repositories/EmployeeRepository.java @@ -0,0 +1,24 @@ +package com.zoomcare.candidatechallenge.repositories; + +import com.zoomcare.candidatechallenge.models.Employee; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Access layer for the employee table. + */ +@Repository +public interface EmployeeRepository extends CrudRepository { + @Query("SELECT * FROM Employee WHERE id = :id") + Optional findById(Long id); + + @Query("SELECT * FROM Employee WHERE supervisor_id = :supervisorId") + Optional> findDirectReports(Long supervisorId); + + @Query("SELECT * FROM Employee WHERE supervisor_id IS NULL") + Optional> findBySupervisorIdIsNull(); +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/repositories/PropertyRepository.java b/src/main/java/com/zoomcare/candidatechallenge/repositories/PropertyRepository.java new file mode 100644 index 0000000..f218d66 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/repositories/PropertyRepository.java @@ -0,0 +1,16 @@ +package com.zoomcare.candidatechallenge.repositories; + +import com.zoomcare.candidatechallenge.models.Property; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + +/** + * Access layer for the property table. + */ +public interface PropertyRepository extends CrudRepository { + @Query("SELECT * FROM Property WHERE employee_id = :employeeId") + Optional> findByEmployeeId(Long employeeId); +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/services/EmployeeService.java b/src/main/java/com/zoomcare/candidatechallenge/services/EmployeeService.java new file mode 100644 index 0000000..20f4dba --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/services/EmployeeService.java @@ -0,0 +1,62 @@ +package com.zoomcare.candidatechallenge.services; + +import com.zoomcare.candidatechallenge.models.Employee; +import com.zoomcare.candidatechallenge.models.Property; +import com.zoomcare.candidatechallenge.repositories.EmployeeRepository; +import com.zoomcare.candidatechallenge.repositories.PropertyRepository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Handles employee information requests. + */ +@Service +public class EmployeeService { + + private final EmployeeRepository employeeRepository; + private final PropertyRepository propertyRepository; + + @Autowired + public EmployeeService(EmployeeRepository employeeRepository, PropertyRepository propertyRepository) { + this.employeeRepository = employeeRepository; + this.propertyRepository = propertyRepository; + } + + public Employee getEmployeeById(Long id) { + Employee employee = employeeRepository.findById(id).orElse(null); + if (employee == null) { + return null; + } + + List properties = propertyRepository.findByEmployeeId(id).orElse(Collections.emptyList()); + employee.setProperties(properties); + + List directReports = employeeRepository.findDirectReports(id).orElse(Collections.emptyList()); + employee.setDirectReports(directReports); + + List employees = new ArrayList<>(directReports); + while (!employees.isEmpty()) { + Employee currentEmployee = employees.remove(0); + List currentProperties = propertyRepository.findByEmployeeId(currentEmployee.getId()).orElse(Collections.emptyList()); + currentEmployee.setProperties(currentProperties); + directReports = employeeRepository.findDirectReports(currentEmployee.getId()).orElse(Collections.emptyList()); + currentEmployee.setDirectReports(directReports); + employees.addAll(directReports); + } + + return employee; + } + + public List getAllTopLevelEmployees() { + List topLevelEmployees = employeeRepository.findBySupervisorIdIsNull().orElse(Collections.emptyList()); + return topLevelEmployees.stream() + .map(employee -> getEmployeeById(employee.getId())) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/zoomcare/candidatechallenge/tests/EmployeeServiceTest.java b/src/main/java/com/zoomcare/candidatechallenge/tests/EmployeeServiceTest.java new file mode 100644 index 0000000..95da703 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/tests/EmployeeServiceTest.java @@ -0,0 +1,105 @@ +package com.zoomcare.candidatechallenge.tests; + +import com.zoomcare.candidatechallenge.models.Employee; +import com.zoomcare.candidatechallenge.models.Property; +import com.zoomcare.candidatechallenge.repositories.EmployeeRepository; +import com.zoomcare.candidatechallenge.repositories.PropertyRepository; +import com.zoomcare.candidatechallenge.services.EmployeeService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +public class EmployeeServiceTest { + + @Mock + private EmployeeRepository employeeRepository; + + @Mock + private PropertyRepository propertyRepository; + + private EmployeeService employeeService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + employeeService = new EmployeeService(employeeRepository, propertyRepository); + } + + @Test + public void testGetEmployeeById() { + Long id = 1L; + Employee employee = new Employee(); + employee.setId(id); + employee.setSupervisorId(null); + when(employeeRepository.findById(id)).thenReturn(Optional.of(employee)); + + List properties = new ArrayList<>(); + Property property = new Property(); + property.setEmployeeId(id); + property.setKey("testdata"); + property.setValue("true"); + properties.add(property); + when(propertyRepository.findByEmployeeId(anyLong())).thenReturn(Optional.of(properties)); + + Employee directReport1 = new Employee(); + directReport1.setId(2L); + directReport1.setSupervisorId(id); + Employee directReport2 = new Employee(); + directReport2.setId(3L); + directReport2.setSupervisorId(id); + List directReports = new ArrayList<>(); + directReports.add(directReport1); + directReports.add(directReport2); + when(employeeRepository.findDirectReports(id)).thenReturn(Optional.of(directReports)); + + Employee result = employeeService.getEmployeeById(id); + assertEquals(id, result.getId()); + assertEquals(2, result.getDirectReports().size()); + assertEquals(1, result.getProperties().size()); + assertEquals("testdata", result.getProperties().get(0).getKey()); + assertEquals("true", result.getProperties().get(0).getValue()); + } + + @Test + public void testGetAllTopLevelEmployees() { + Employee topLevelEmployee = new Employee(); + Long id = 1L; + topLevelEmployee.setId(id); + topLevelEmployee.setSupervisorId(null); + when(employeeRepository.findBySupervisorIdIsNull()).thenReturn(Optional.of(List.of(topLevelEmployee))); + when(employeeRepository.findById(id)).thenReturn(Optional.of(topLevelEmployee)); + + List properties = new ArrayList<>(); + Property property = new Property(); + property.setEmployeeId(id); + property.setKey("testdata"); + property.setValue("true"); + properties.add(property); + when(propertyRepository.findByEmployeeId(anyLong())).thenReturn(Optional.of(properties)); + + Employee directReport1 = new Employee(); + directReport1.setId(2L); + directReport1.setSupervisorId(id); + Employee directReport2 = new Employee(); + directReport2.setId(3L); + directReport2.setSupervisorId(id); + List directReports = new ArrayList<>(); + directReports.add(directReport1); + directReports.add(directReport2); + when(employeeRepository.findDirectReports(id)).thenReturn(Optional.of(directReports)); + + List result = employeeService.getAllTopLevelEmployees(); + List expectedResult = List.of(topLevelEmployee); + + assertEquals(expectedResult, result); + } +} \ No newline at end of file