diff --git a/pom.xml b/pom.xml
index 543c1aa50..dfea121e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,19 +16,19 @@
org.junit.jupiter
junit-jupiter
- 5.11.4
+ 6.0.1
test
org.assertj
assertj-core
- 3.27.3
+ 3.27.7
test
org.mockito
mockito-junit-jupiter
- 5.15.2
+ 5.21.0
test
@@ -37,17 +37,17 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.13.0
+ 3.14.1
org.apache.maven.plugins
maven-surefire-plugin
- 3.5.2
+ 3.5.4
org.apache.maven.plugins
maven-failsafe-plugin
- 3.5.2
+ 3.5.4
@@ -60,7 +60,7 @@
org.jacoco
jacoco-maven-plugin
- 0.8.12
+ 0.8.14
true
diff --git a/src/main/java/com/example/shop/DiscountService.java b/src/main/java/com/example/shop/DiscountService.java
new file mode 100644
index 000000000..04733e75d
--- /dev/null
+++ b/src/main/java/com/example/shop/DiscountService.java
@@ -0,0 +1,7 @@
+package com.example.shop;
+
+import java.math.BigDecimal;
+
+public interface DiscountService {
+ BigDecimal getDiscountMultiplier(String itemName);
+}
diff --git a/src/main/java/com/example/shop/Item.java b/src/main/java/com/example/shop/Item.java
new file mode 100644
index 000000000..1d16a81e4
--- /dev/null
+++ b/src/main/java/com/example/shop/Item.java
@@ -0,0 +1,45 @@
+package com.example.shop;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+public class Item {
+
+ private final String name;
+ private final BigDecimal price;
+ private int quantity;
+ private final BigDecimal discountPercentage;
+ public Item(String name, BigDecimal price, int quantity) {
+ this.name = name;
+ this.price = price;
+ this.quantity = quantity;
+ this.discountPercentage = BigDecimal.ZERO;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Item item)) return false;
+ return quantity == item.quantity && Objects.equals(name, item.name) && Objects.equals(price, item.price) && Objects.equals(discountPercentage, item.discountPercentage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, price, quantity, discountPercentage);
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java
new file mode 100644
index 000000000..98bffa244
--- /dev/null
+++ b/src/main/java/com/example/shop/ShoppingCart.java
@@ -0,0 +1,51 @@
+package com.example.shop;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShoppingCart {
+ private final List- items = new ArrayList<>();
+ private final DiscountService discountService;
+
+ ShoppingCart(DiscountService discountService) {
+ this.discountService = discountService;
+ }
+ ShoppingCart() {
+ this.discountService = itemName -> BigDecimal.ONE;
+ }
+
+ public void addItem(Item newItem) {
+ items.stream()
+ .filter(item -> item.getName().equals(newItem.getName()))
+ .findFirst()
+ .ifPresentOrElse(
+ existing -> existing.setQuantity(existing.getQuantity() + newItem.getQuantity()),
+ () -> items.add(newItem)
+ );
+ }
+
+ public List
- getItems() {
+ return items;
+ }
+
+ public BigDecimal getTotalPrice() {
+ BigDecimal totalPrice = BigDecimal.ZERO;
+ for(Item item : items) {
+ BigDecimal multiplier = discountService.getDiscountMultiplier(item.getName());
+ if (multiplier == null) {
+ multiplier = BigDecimal.ONE;
+ }
+ BigDecimal itemTotal = item.getPrice()
+ .multiply(BigDecimal.valueOf(item.getQuantity()))
+ .multiply(multiplier);
+
+ totalPrice = totalPrice.add(itemTotal);
+ }
+ return totalPrice;
+ }
+
+ public void removeItem(Item item) {
+ items.remove(item);
+ }
+}
diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java
new file mode 100644
index 000000000..ebea7b560
--- /dev/null
+++ b/src/test/java/com/example/BookingSystemTest.java
@@ -0,0 +1,247 @@
+package com.example;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class BookingSystemTest {
+
+ @Mock
+ NotificationService notificationService;
+ @Mock
+ RoomRepository roomRepository;
+ @Mock
+ TimeProvider timeProvider;
+ @InjectMocks
+ BookingSystem bookingSystem;
+
+
+
+ @Test
+ void whenMakingCorrectBookingBookRoomReturnTrue() {
+
+ String roomId = "5";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+
+ Room room = new Room(roomId, "Room Five");
+
+ when(roomRepository.findById(roomId)).thenReturn(Optional.of(room));
+
+ boolean result = bookingSystem.bookRoom(roomId, startTime, endTime);
+
+ assertThat(result).isTrue();
+
+ Mockito.verify(roomRepository).save(room);
+
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ ", 2026-02-02T10:00, 2026-02-02T12:00, 'Bokning kräver giltiga start- och sluttider samt rum-id'", // roomId is null
+ "5, , 2026-02-02T12:00, 'Bokning kräver giltiga start- och sluttider samt rum-id'", // startTime is null
+ "5, 2026-02-02T10:00, , 'Bokning kräver giltiga start- och sluttider samt rum-id'", // endTime is null
+ "5, 2025-02-02T10:00, 2026-02-02T12:00, 'Kan inte boka tid i dåtid'", // startTime is in the past
+ "5, 2026-02-02T10:00, 2026-01-02T12:00, 'Sluttid måste vara efter starttid'" // endTime is before starTime
+ })
+ void shouldThrowExceptionForInvalidInputInBookRoom(String roomId, LocalDateTime startTime, LocalDateTime endTime, String expectedMessage) {
+ if (roomId != null && startTime != null && endTime != null) {
+ LocalDateTime testNow = LocalDateTime.of(2026, 1, 1, 0, 0);
+ when(timeProvider.getCurrentTime()).thenReturn(testNow);
+ }
+ assertThatThrownBy(() -> bookingSystem.bookRoom(roomId, startTime, endTime))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(expectedMessage);
+ }
+
+ @Test
+ void whenRoomDoesNotExistBookRoomReturnException() {
+ String roomId = "5";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+
+ when(roomRepository.findById(roomId)).thenReturn(Optional.empty());
+
+ assertThatThrownBy(()-> bookingSystem.bookRoom(roomId, startTime, endTime))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Rummet existerar inte");
+ }
+
+ @Test
+ void whenARoomIsOccupiedBookRoomReturnFalse() {
+
+ String roomId = "5";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+
+ Room room = new Room(roomId, "Room Five");
+ Booking existingBooking = new Booking("existing-id", roomId, startTime, endTime);
+ room.addBooking(existingBooking);
+
+ when(roomRepository.findById(roomId)).thenReturn(Optional.of(room));
+
+ boolean result = bookingSystem.bookRoom(roomId, startTime, endTime);
+
+ assertThat(result).isFalse();
+
+ verify(roomRepository, never()).save(any());
+ }
+
+ @Test
+ void shouldReturnTrueEvenWhenNotificationFails() throws NotificationException {
+ String roomId = "5";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+
+ Room room = new Room(roomId, "Room Five");
+ when(roomRepository.findById(roomId)).thenReturn(Optional.of(room));
+
+ doThrow(NotificationException.class)
+ .when(notificationService).sendBookingConfirmation(any());
+
+ boolean result = bookingSystem.bookRoom(roomId, startTime, endTime);
+ assertThat(result).isTrue();
+
+ verify(roomRepository).save(room);
+ Mockito.verify(notificationService).sendBookingConfirmation(any());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "2026-02-02T10:00, ,'Måste ange både start- och sluttid'",
+ " , 2026-02-02T10:00,'Måste ange både start- och sluttid'",
+ "2026-02-02T10:00,2026-01-05T10:00,'Sluttid måste vara efter starttid'"
+ })
+ void shouldThrowExceptionForInvalidInputInGetAvailableRooms(LocalDateTime startTime, LocalDateTime endTime, String expectedMessage) {
+
+ assertThatThrownBy(() -> bookingSystem.getAvailableRooms(startTime, endTime))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(expectedMessage);
+ }
+
+ @Test
+ void whenGivenOccupiedAndAvailableRoomGetAvailableRoomsShouldReturnAvailableRooms() {
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ Room roomA = new Room("5", "Room Five");
+ Room roomB = new Room("6", "Room Six");
+ Room roomC = new Room("7", "Room Seven");
+ Booking booking = new Booking("b1","6", startTime, endTime);
+ roomB.addBooking(booking);
+
+ when(roomRepository.findAll()).thenReturn(List.of(roomA, roomB, roomC));
+
+ List result = bookingSystem.getAvailableRooms(startTime, endTime);
+
+ assertThat(result)
+ .hasSize(2)
+ .containsExactly(roomA, roomC)
+ .doesNotContain(roomB);
+ }
+
+
+ @Test
+ void whenNullBookingIdCancelBookingShouldReturnException(){
+
+ String bookingId = null;
+
+ assertThatThrownBy(() -> bookingSystem.cancelBooking(bookingId))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Boknings-id kan inte vara null");
+ }
+
+ @Test
+ void whenCancelingAnExistingBookingCancelBookingShouldReturnTrue() throws NotificationException {
+ String roomId = "5";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 0, 0);
+ LocalDateTime startTime = now.plusDays(1);
+ LocalDateTime endTime = startTime.plusDays(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+ Room room = new Room(roomId, "Room Five");
+ Room room2 = new Room("6", "Room Six");
+
+ when(roomRepository.findAll()).thenReturn(List.of(room, room2));
+
+ Booking fakeBooking = new Booking("fakeBookingId", roomId, startTime, endTime);
+ Booking fakeBooking2 = new Booking("fakeBookingId2", "6", startTime, endTime);
+ room.addBooking(fakeBooking);
+ room2.addBooking(fakeBooking2);
+
+ assertThat(bookingSystem.cancelBooking(fakeBooking.getId())).isTrue();
+ assertThat(room.hasBooking("fakeBookingId")).isFalse();
+
+ verify(roomRepository).save(room);
+ verify(notificationService).sendCancellationConfirmation(fakeBooking);
+
+ }
+
+ @Test
+ void cancelBookingShouldReturnFalseWhenBookingNotFound(){
+
+ Room room = new Room("5", "Room Five");
+ Room room2 = new Room("6", "Room Six");
+
+ String bookingId = "fakeBookingId";
+
+ when(roomRepository.findAll()).thenReturn(List.of(room, room2));
+
+ boolean result = bookingSystem.cancelBooking(bookingId);
+
+ assertThat(result).isFalse();
+
+ verify(roomRepository, never()).save(any());
+ }
+
+ @Test
+ void cancelBookingShouldThrowExceptionWhenBookinghasAlreadyStarted() {
+ String bookingId = "past-booking";
+ LocalDateTime now = LocalDateTime.of(2026, 1, 1, 10, 0);
+ LocalDateTime startTime = now.minusHours(1);
+ LocalDateTime endTime = startTime.plusHours(3);
+
+ when(timeProvider.getCurrentTime()).thenReturn(now);
+
+ Room room = new Room("5", "Room Five");
+ Booking pastBooking = new Booking(bookingId,"5", startTime, endTime);
+ room.addBooking(pastBooking);
+
+ when(roomRepository.findAll()).thenReturn(List.of(room));
+
+ assertThatThrownBy(() -> bookingSystem.cancelBooking(bookingId))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Kan inte avboka påbörjad eller avslutad bokning");
+
+ assertThat(room.hasBooking(bookingId)).isTrue();
+
+ }
+}
diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java
new file mode 100644
index 000000000..3368e92e5
--- /dev/null
+++ b/src/test/java/com/example/shop/ShoppingCartTest.java
@@ -0,0 +1,107 @@
+package com.example.shop;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(MockitoExtension.class)
+class ShoppingCartTest {
+
+ @Mock
+ DiscountService discountService;
+
+ @Test
+ void shouldReturnSizeOneWhenOneItemIsAdded() {
+ ShoppingCart cart = new ShoppingCart();
+ Item item = new Item("Football", new BigDecimal("150.0"), 1);
+ cart.addItem(item);
+ assertEquals(1, cart.getItems().size());
+ }
+
+ @Test
+ void shouldCalculateTotalPriceForMultipleItemsOfOneQuantity() {
+ ShoppingCart cart = new ShoppingCart();
+ cart.addItem(new Item("Football", new BigDecimal("150.0"), 1));
+ cart.addItem(new Item("Socks", new BigDecimal("50.0"), 1));
+
+ assertEquals(new BigDecimal("200.0"), cart.getTotalPrice());
+ }
+
+ @Test
+ void shouldRemoveItemAndUpdateTotalWhenItemIsRemoved() {
+ ShoppingCart cart = new ShoppingCart();
+ Item football = new Item("Football", new BigDecimal("150.0"), 1);
+ Item socks = new Item("Socks", new BigDecimal("50.0"), 1);
+
+ cart.addItem(football);
+ cart.addItem(socks);
+
+ cart.removeItem(football);
+
+ assertAll(
+ () -> assertEquals(1, cart.getItems().size(), "Number of items is incorrect"),
+ () -> assertEquals(0, new BigDecimal("50.0")
+ .compareTo(cart.getTotalPrice()), "Total price is incorrect")
+ );
+
+ }
+
+ @Test
+ void shouldIncreaseQuantityWhenAddingSameItem() {
+ ShoppingCart cart = new ShoppingCart();
+ Item football = new Item("Football", new BigDecimal("150.0"), 1);
+
+ cart.addItem(football);
+ cart.addItem(football);
+
+ assertEquals(1, cart.getItems().size(), "Should not add new row of the same item");
+ assertEquals(2, cart.getItems().getFirst().getQuantity(), "Quantity should be two");
+ assertEquals(0, new BigDecimal("300.0").compareTo(cart.getTotalPrice()), "Price should reflect total quantity");
+
+ }
+
+ @Test
+ void shouldApplyDiscountToPrice() {
+ DiscountService discountServiceMock = Mockito.mock(DiscountService.class);
+
+ Mockito.when(discountServiceMock.getDiscountMultiplier("Handball")).thenReturn(new BigDecimal("0.9"));
+
+ ShoppingCart cart = new ShoppingCart(discountServiceMock);
+ cart.addItem(new Item("Handball", new BigDecimal("100.0"), 1));
+
+ assertEquals(0, new BigDecimal("90").compareTo(cart.getTotalPrice()), "Discount wasn't applied correctly");
+ }
+
+ @Test
+ void shouldReturnZeroPriceWhenCartIsEmpty() {
+ ShoppingCart cart = new ShoppingCart();
+ assertEquals(0, BigDecimal.ZERO.compareTo(cart.getTotalPrice()));
+ }
+
+ @Test
+ void shouldReturnZeroPriceWhenOnlyItemIsRemoved() {
+ ShoppingCart cart = new ShoppingCart();
+ Item ball = new Item("Ball", new BigDecimal("100.0"), 1);
+ cart.addItem(ball);
+ cart.removeItem(ball);
+
+ assertEquals(0, cart.getItems().size());
+ assertEquals(0, BigDecimal.ZERO.compareTo(cart.getTotalPrice()));
+ }
+ @Test
+ void shouldHandleNullDiscountByUsingOriginalPrice() {
+ DiscountService discountServiceMock = Mockito.mock(DiscountService.class);
+
+ ShoppingCart cart = new ShoppingCart(discountServiceMock);
+ cart.addItem(new Item("Socks", new BigDecimal("50.0"), 1));
+
+ assertEquals(0, new BigDecimal("50.0").compareTo(cart.getTotalPrice()));
+ }
+}