diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationController.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationController.java new file mode 100644 index 0000000..bc70010 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationController.java @@ -0,0 +1,41 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.location; + +import java.util.List; + +import org.jspecify.annotations.NonNull; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import pl.milosnicyit.codewarehousebackend.location.Location; +import pl.milosnicyit.codewarehousebackend.location.LocationService; + +@RestController +@RequestMapping("/api/locations") +class LocationController { + + private final LocationService locationService; + + LocationController(LocationService locationService) { + this.locationService = locationService; + } + + @GetMapping + ResponseEntity> getLocations() { + return ResponseEntity.ok(locationService.getAllLocations()); + } + @PostMapping + ResponseEntity + createLocation(@RequestBody Location location) { + return ResponseEntity.ok(locationService.createLocation(location)); + } + @PatchMapping("/{id}") + ResponseEntity + updateLocation(@PathVariable Long id, @RequestBody @NonNull Location locationUpdate) { + return ResponseEntity.ok( + locationService.updateLocationName(id, locationUpdate.getName())); + } + @DeleteMapping("/{id}") + ResponseEntity deleteLocation(@PathVariable Long id) { + locationService.deleteLocation(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/Location.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/Location.java new file mode 100644 index 0000000..fcd0f8a --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/Location.java @@ -0,0 +1,29 @@ +package pl.milosnicyit.codewarehousebackend.location; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Location { + private Long id; + private String name; + private boolean active = true; + private boolean empty = true; + + public Location(Long id, String name) { + this.id = id; + this.name = name; + this.active = true; + this.empty = true; + } + + void deactivate() { + if (!this.empty) { + throw new IllegalStateException("Cannot delete location: it is not empty"); + } + this.active = false; + } +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationConfiguration.java new file mode 100644 index 0000000..9625676 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationConfiguration.java @@ -0,0 +1,16 @@ +package pl.milosnicyit.codewarehousebackend.location.adapter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import pl.milosnicyit.codewarehousebackend.location.LocationRepository; +import pl.milosnicyit.codewarehousebackend.location.LocationService; +import pl.milosnicyit.codewarehousebackend.location.LocationServiceImpl; + +@Configuration +class LocationConfiguration { + + @Bean + public LocationService locationService(LocationRepository locationRepository) { + return new LocationServiceImpl(locationRepository); + } +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationRepository.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationRepository.java new file mode 100644 index 0000000..3e0f191 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationRepository.java @@ -0,0 +1,12 @@ +package pl.milosnicyit.codewarehousebackend.location; + +import java.util.List; +import java.util.Optional; + +public interface LocationRepository { + List findAll(); + Optional findById(Long id); + Optional findByName(String name); + Location save(Location location); + void deleteById(Long id); +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationService.java new file mode 100644 index 0000000..1f45bef --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationService.java @@ -0,0 +1,10 @@ +package pl.milosnicyit.codewarehousebackend.location; + +import java.util.List; + +public interface LocationService { + List getAllLocations(); + Location createLocation(Location location); + Location updateLocationName(Long id, String newName); + void deleteLocation(Long id); +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceImpl.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceImpl.java new file mode 100644 index 0000000..20c7b44 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceImpl.java @@ -0,0 +1,45 @@ +package pl.milosnicyit.codewarehousebackend.location; + +import java.util.List; +import java.util.stream.Collectors; + +public class LocationServiceImpl implements LocationService { + + private final LocationRepository locationRepository; + + public LocationServiceImpl(LocationRepository locationRepository) { + this.locationRepository = locationRepository; + } + + @Override + public List getAllLocations() { + return locationRepository.findAll().stream() + .filter(Location::isActive) + .collect(Collectors.toList()); + } + + @Override + public Location createLocation(Location location) { + if (locationRepository.findByName(location.getName()).isPresent()) { + throw new IllegalArgumentException("Location with this name already exists"); + } + return locationRepository.save(location); + } + + @Override + public Location updateLocationName(Long id, String newName) { + Location existingLocation = locationRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Location not found")); + existingLocation.setName(newName); + return locationRepository.save(existingLocation); + } + + @Override + public void deleteLocation(Long id) { + Location location = locationRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Location not found")); + + location.deactivate(); + locationRepository.save(location); + } +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/LocationEntity.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/LocationEntity.java new file mode 100644 index 0000000..e4ea28f --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/LocationEntity.java @@ -0,0 +1,41 @@ +package pl.milosnicyit.codewarehousebackend.location.database; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import pl.milosnicyit.codewarehousebackend.location.Location; + +@Entity +@Table(name = "locations") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LocationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + @Column(nullable = false) + private boolean active = true; + + @Column(nullable = false) + private boolean empty = true; + + public static LocationEntity fromDomain(Location location) { + return new LocationEntity( + location.getId(), + location.getName(), + location.isActive(), + location.isEmpty() + ); + } + + public Location toDomain() { + return new Location(id, name, active, empty); + } +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationJpaRepository.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationJpaRepository.java new file mode 100644 index 0000000..16936e4 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationJpaRepository.java @@ -0,0 +1,11 @@ +package pl.milosnicyit.codewarehousebackend.location.database.wrapper; + +import org.springframework.data.jpa.repository.JpaRepository; + +import pl.milosnicyit.codewarehousebackend.location.database.LocationEntity; + +import java.util.Optional; + +interface LocationJpaRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationRepositoryWrapper.java b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationRepositoryWrapper.java new file mode 100644 index 0000000..ed17c72 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/location/database/wrapper/LocationRepositoryWrapper.java @@ -0,0 +1,49 @@ +package pl.milosnicyit.codewarehousebackend.location.database.wrapper; + +import org.springframework.stereotype.Repository; +import pl.milosnicyit.codewarehousebackend.location.Location; +import pl.milosnicyit.codewarehousebackend.location.LocationRepository; +import pl.milosnicyit.codewarehousebackend.location.database.LocationEntity; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +class LocationRepositoryWrapper implements LocationRepository { + + private final LocationJpaRepository jpaRepository; + + LocationRepositoryWrapper(LocationJpaRepository jpaRepository) { + this.jpaRepository = jpaRepository; + } + + @Override + public List findAll() { + return jpaRepository.findAll().stream() + .map(LocationEntity::toDomain) + .collect(Collectors.toList()); + } + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id).map(LocationEntity::toDomain); + } + + @Override + public Optional findByName(String name) { + return jpaRepository.findByName(name).map(LocationEntity::toDomain); + } + + @Override + public Location save(Location location) { + LocationEntity entity = LocationEntity.fromDomain(location); + LocationEntity savedEntity = jpaRepository.save(entity); + return savedEntity.toDomain(); + } + + @Override + public void deleteById(Long id) { + jpaRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationControllerE2ETest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationControllerE2ETest.java new file mode 100644 index 0000000..951bda8 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/location/LocationControllerE2ETest.java @@ -0,0 +1,50 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.location; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import pl.milosnicyit.codewarehousebackend.location.Location; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static pl.milosnicyit.codewarehousebackend.helpers.JsonHelper.toJson; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class LocationControllerE2ETest { + + private static final String LOCATION_ENDPOINT = "/api/locations"; + + @Autowired + private MockMvc mockMvc; + + @Test + @WithMockUser + void shouldCreateLocationEndToEnd() throws Exception { + Location location = new Location(); + location.setName("Magazyn E2E"); + + mockMvc.perform(post(LOCATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(location))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.name").value("Magazyn E2E")); + } + + @Test + @WithMockUser + void shouldGetAllLocationsEndToEnd() throws Exception { + mockMvc.perform(get(LOCATION_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceSpecificationTest.java new file mode 100644 index 0000000..0a91324 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/location/LocationServiceSpecificationTest.java @@ -0,0 +1,54 @@ +package pl.milosnicyit.codewarehousebackend.location; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LocationServiceSpecificationTest { + + @Mock + private LocationRepository locationRepository; + + private LocationService locationService; + + @BeforeEach + void setUp() { + locationService = new LocationServiceImpl(locationRepository); + } + + @Test + void shouldDeactivateLocationWhenEmpty() { + // given (empty = true) + Location location = new Location(1L, "Magazyn", true, true); + when(locationRepository.findById(1L)).thenReturn(Optional.of(location)); + + // when + locationService.deleteLocation(1L); + + // then + assertFalse(location.isActive()); + verify(locationRepository).save(location); + } + + @Test + void shouldThrowExceptionWhenDeletingNonEmptyLocation() { + // given (empty = false) + Location location = new Location(1L, "Magazyn", true, false); + when(locationRepository.findById(1L)).thenReturn(Optional.of(location)); + + // when / then + assertThrows(IllegalStateException.class, () -> locationService.deleteLocation(1L)); + } +} \ No newline at end of file