diff --git a/pom.xml b/pom.xml
index 17fd7ad..351e267 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
org.flywaydb
@@ -49,6 +53,25 @@
spring-boot-starter-test
test
+
+
+ org.modelmapper
+ modelmapper
+ 3.1.1
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.28
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
diff --git a/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java b/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java
new file mode 100644
index 0000000..5369d8b
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java
@@ -0,0 +1,20 @@
+package com.zoomcare.candidatechallenge.config;
+
+import com.zoomcare.candidatechallenge.repository.EmployeeRepository;
+import com.zoomcare.candidatechallenge.service.EmployeeService;
+import org.modelmapper.ModelMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CandidateChallengeConfig {
+ @Bean
+ public ModelMapper modelMapper() {
+ return new ModelMapper();
+ }
+
+ @Bean
+ public EmployeeService employeeService(ModelMapper modelMapper, EmployeeRepository employeeRepository) {
+ return new EmployeeService(employeeRepository, modelMapper);
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java b/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java
new file mode 100644
index 0000000..04b26bc
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java
@@ -0,0 +1,37 @@
+package com.zoomcare.candidatechallenge.controller;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.service.EmployeeService;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+@RestController
+@RequestMapping("/api/employees")
+public class EmployeeController {
+ @Autowired
+ private EmployeeService employeeService;
+
+ @GetMapping("/{id}")
+ ResponseEntity getById(@PathVariable Long id) {
+ Optional employeeDTO = employeeService.findById(id);
+ if (!employeeDTO.isPresent()) {
+ return ResponseEntity.notFound().build();
+ }
+ return ResponseEntity.ok(employeeDTO.get());
+ }
+
+ @GetMapping("/topLevel")
+ ResponseEntity> getTopLevelEmployees() {
+ List employeeDTOs = employeeService.getTopLevelEmployee();
+ if (employeeDTOs.isEmpty()) {
+ return ResponseEntity.notFound().build();
+ }
+ return ResponseEntity.ok(employeeDTOs);
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java
new file mode 100644
index 0000000..3eeaf1a
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java
@@ -0,0 +1,18 @@
+package com.zoomcare.candidatechallenge.dto;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class EmployeeDTO {
+ private Long id;
+ private Long supervisorId;
+ private List properties;
+ private List directReports;
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java
new file mode 100644
index 0000000..7dfbc00
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java
@@ -0,0 +1,11 @@
+package com.zoomcare.candidatechallenge.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class PropertyDTO {
+ private PropertyPKDTO id;
+ private String value;
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java
new file mode 100644
index 0000000..fcda728
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java
@@ -0,0 +1,10 @@
+package com.zoomcare.candidatechallenge.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class PropertyPKDTO {
+ private String key;
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java b/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java
new file mode 100644
index 0000000..efa3a91
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java
@@ -0,0 +1,59 @@
+package com.zoomcare.candidatechallenge.model;
+
+import java.io.Serializable;
+import java.util.List;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+import lombok.Builder;
+
+@Builder
+@Entity
+public class Employee implements Serializable {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Column(name = "supervisor_id")
+ private Long supervisorId;
+
+ @OneToMany(fetch = FetchType.LAZY)
+ @JoinColumn(name = "employee_id")
+ private List properties;
+
+ @OneToMany(fetch = FetchType.LAZY)
+ @JoinColumn(name = "supervisor_id")
+ private List directReports;
+
+ public Long getSupervisorId() {
+ return supervisorId;
+ }
+
+ public void setSupervisorId(Long supervisorId) {
+ this.supervisorId = supervisorId;
+ }
+
+ public List getDirectReports() {
+ return directReports;
+ }
+
+ public void setDirectReports(List directReports) {
+ this.directReports = directReports;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public void setProperties(List properties) {
+ this.properties = properties;
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/Property.java b/src/main/java/com/zoomcare/candidatechallenge/model/Property.java
new file mode 100644
index 0000000..d93a408
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/Property.java
@@ -0,0 +1,30 @@
+package com.zoomcare.candidatechallenge.model;
+
+import java.io.Serializable;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import lombok.Builder;
+
+@Entity
+@Builder
+public class Property implements Serializable {
+ @EmbeddedId
+ private PropertyPK id;
+ private String value;
+
+ public PropertyPK getId() {
+ return id;
+ }
+
+ public void setId(PropertyPK id) {
+ this.id = id;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java b/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java
new file mode 100644
index 0000000..db193e8
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java
@@ -0,0 +1,32 @@
+package com.zoomcare.candidatechallenge.model;
+
+import java.io.Serializable;
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import lombok.Builder;
+
+@Embeddable
+@Builder
+public class PropertyPK implements Serializable {
+
+ private String key;
+
+ @Column(name = "employee_id")
+ private Long employeeId;
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public Long getEmployeeId() {
+ return employeeId;
+ }
+
+ public void setEmployeeId(Long employeeId) {
+ this.employeeId = employeeId;
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java
new file mode 100644
index 0000000..8694f0a
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java
@@ -0,0 +1,11 @@
+package com.zoomcare.candidatechallenge.repository;
+
+import com.zoomcare.candidatechallenge.model.Employee;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface EmployeeRepository extends JpaRepository {
+ Optional findById(Long id);
+ List findAllBySupervisorIdIsNull();
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java b/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java
new file mode 100644
index 0000000..a2c1818
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java
@@ -0,0 +1,36 @@
+package com.zoomcare.candidatechallenge.service;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.model.Employee;
+import com.zoomcare.candidatechallenge.repository.EmployeeRepository;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.modelmapper.ModelMapper;
+import org.springframework.stereotype.Service;
+
+@Service
+public class EmployeeService {
+ private final EmployeeRepository employeeRepository;
+ private final ModelMapper modelMapper;
+
+ public EmployeeService(EmployeeRepository employeeRepository, ModelMapper modelMapper) {
+ this.employeeRepository = employeeRepository;
+ this.modelMapper = modelMapper;
+ }
+
+ public Optional findById(Long id) {
+ return employeeRepository.findById(id).map(this::convertToDto);
+ }
+
+ public List getTopLevelEmployee() {
+ return employeeRepository.findAllBySupervisorIdIsNull().stream()
+ .map(this::convertToDto)
+ .collect(Collectors.toList());
+ }
+
+ public EmployeeDTO convertToDto(Employee employee) {
+ return modelMapper.map(employee, EmployeeDTO.class);
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 4408d17..69ee7c7 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,4 +1,6 @@
spring:
+ main:
+ allow-bean-definition-overriding: true
h2:
console:
enabled: true
diff --git a/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java b/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java
new file mode 100644
index 0000000..b31af1c
--- /dev/null
+++ b/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java
@@ -0,0 +1,110 @@
+package com.zoomcare.candidatechallenge.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.model.Employee;
+import com.zoomcare.candidatechallenge.model.Property;
+import com.zoomcare.candidatechallenge.model.PropertyPK;
+import com.zoomcare.candidatechallenge.repository.EmployeeRepository;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.modelmapper.ModelMapper;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EmployeeServiceTest {
+ private final Long EMPLOYEE_ID = 2L;
+ private final Long SUPERVISOR_ID = 1L;
+ private final String PROPERTY_KEY = "key";
+ private Employee employee;
+ private Employee supervisorEmployee;
+
+ @Mock
+ private EmployeeRepository employeeRepository;
+
+ private PropertyPK propertyPK;
+ private Property property;
+ private ModelMapper modelMapper;
+
+ private EmployeeService employeeService;
+
+ @Before
+ public void setUp() {
+ modelMapper = new ModelMapper();
+ employeeService = new EmployeeService(employeeRepository, modelMapper);
+
+ propertyPK = PropertyPK.builder()
+ .employeeId(EMPLOYEE_ID)
+ .key(PROPERTY_KEY)
+ .build();
+
+ property = Property.builder()
+ .id(propertyPK)
+ .value("value")
+ .build();
+
+ employee = Employee.builder()
+ .id(EMPLOYEE_ID)
+ .supervisorId(SUPERVISOR_ID)
+ .properties(Arrays.asList(property))
+ .build();
+
+ supervisorEmployee = Employee.builder()
+ .id(1l)
+ .directReports(Arrays.asList(employee))
+ .build();
+
+ }
+
+ @Test
+ public void findByIdFindsEmployeeTest() {
+ when(employeeRepository.findById(EMPLOYEE_ID)).thenReturn(Optional.of(employee));
+
+ Optional result = employeeService.findById(EMPLOYEE_ID);
+
+ assertTrue(result.isPresent());
+ EmployeeDTO employeeDTO = result.get();
+ assertEquals(EMPLOYEE_ID, employeeDTO.getId());
+ assertEquals(propertyPK.getKey(), employeeDTO.getProperties().get(0).getId().getKey());
+ assertEquals(property.getValue(), employeeDTO.getProperties().get(0).getValue());
+ assertEquals(employee.getSupervisorId(), employeeDTO.getSupervisorId());
+ assertEquals(employee.getProperties().get(0).getValue(), employeeDTO.getProperties().get(0).getValue());
+ }
+
+ @Test
+ public void findByIdEmployeeNotFoundTest() {
+ Optional result = employeeService.findById(3L);
+
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ public void getTopLevelEmployeeFoundTest() {
+ when(employeeRepository.findAllBySupervisorIdIsNull()).thenReturn(Arrays.asList(supervisorEmployee));
+
+ List supervisors = employeeService.getTopLevelEmployee();
+
+ assertFalse(supervisors.isEmpty());
+ EmployeeDTO supervisor = supervisors.get(0);
+ assertEquals(SUPERVISOR_ID, supervisor.getId());
+ assertEquals(supervisorEmployee.getSupervisorId(), supervisor.getSupervisorId());
+ assertEquals(supervisorEmployee.getDirectReports().size(), supervisor.getDirectReports().size());
+ assertEquals(EMPLOYEE_ID, supervisor.getDirectReports().get(0).getId());
+ }
+
+ @Test
+ public void getTopLevelEmployeeNotFoundTest() {
+ List supervisors = employeeService.getTopLevelEmployee();
+
+ assertTrue(supervisors.isEmpty());
+ }
+}
\ No newline at end of file