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