diff --git a/.gitignore b/.gitignore
index 9154f4c..7ed0d6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,26 +1,32 @@
-# ---> Java
-# Compiled class file
-*.class
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
-# Log file
-*.log
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
-# BlueJ files
-*.ctxt
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-replay_pid*
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+### VS Code ###
+.vscode/
diff --git a/README.md b/README.md
index 91bc1cd..cb70863 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,33 @@
-# web-price
+Prices web service (web-price)
+===================================
+Web service for obtaining the price of a product given a date, the product id and the brand id.
+
+## Requirements
+
+- Java 21
+- Maven
+
+## Development
+
+1. Local installation of the project
+ ```
+ mvn clean install
+ ```
+2. Run the app from the project root with:
+ ```
+ mvn clean spring-boot:run -pl boot -Dspring-boot.run.profiles=local
+ ```
+ or navigating to the `boot` directory of the project:
+ ```
+ mvn clean spring-boot:run -Dspring-boot.run.profiles=local
+ ```
+3. Service should be ready to receive requests at http://localhost:8080/price with OpenAPI docs available at http://localhost:8080/swagger-ui/index.html
+
+## Running Tests
+
+To run all tests in the project:
+
+```bash
+mvn test
+```
diff --git a/application/pom.xml b/application/pom.xml
new file mode 100644
index 0000000..6487922
--- /dev/null
+++ b/application/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ application
+ jar
+ application
+
+
+ com.techivw
+ web-price
+ 1.0-RELEASE
+
+
+
+
+
+ com.techivw
+ domain
+ 1.0-RELEASE
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/java/com/techivw/webprice/application/exceptions/NotFoundException.java b/application/src/main/java/com/techivw/webprice/application/exceptions/NotFoundException.java
new file mode 100644
index 0000000..d3b2145
--- /dev/null
+++ b/application/src/main/java/com/techivw/webprice/application/exceptions/NotFoundException.java
@@ -0,0 +1,8 @@
+package com.techivw.webprice.application.exceptions;
+
+public class NotFoundException extends RuntimeException {
+
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/application/src/main/java/com/techivw/webprice/application/ports/in/PriceServicePort.java b/application/src/main/java/com/techivw/webprice/application/ports/in/PriceServicePort.java
new file mode 100644
index 0000000..1a37997
--- /dev/null
+++ b/application/src/main/java/com/techivw/webprice/application/ports/in/PriceServicePort.java
@@ -0,0 +1,11 @@
+package com.techivw.webprice.application.ports.in;
+
+import com.techivw.webprice.domain.Price;
+
+import java.time.LocalDateTime;
+
+public interface PriceServicePort {
+
+ Price getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId);
+
+}
diff --git a/application/src/main/java/com/techivw/webprice/application/ports/out/PriceRepositoryPort.java b/application/src/main/java/com/techivw/webprice/application/ports/out/PriceRepositoryPort.java
new file mode 100644
index 0000000..b785a12
--- /dev/null
+++ b/application/src/main/java/com/techivw/webprice/application/ports/out/PriceRepositoryPort.java
@@ -0,0 +1,12 @@
+package com.techivw.webprice.application.ports.out;
+
+import com.techivw.webprice.domain.Price;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+public interface PriceRepositoryPort {
+
+ Optional findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId);
+
+}
diff --git a/application/src/main/java/com/techivw/webprice/application/services/PriceService.java b/application/src/main/java/com/techivw/webprice/application/services/PriceService.java
new file mode 100644
index 0000000..eee1d41
--- /dev/null
+++ b/application/src/main/java/com/techivw/webprice/application/services/PriceService.java
@@ -0,0 +1,25 @@
+package com.techivw.webprice.application.services;
+
+import com.techivw.webprice.application.exceptions.NotFoundException;
+import com.techivw.webprice.application.ports.in.PriceServicePort;
+import com.techivw.webprice.application.ports.out.PriceRepositoryPort;
+import com.techivw.webprice.domain.Price;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+@AllArgsConstructor
+public class PriceService implements PriceServicePort {
+
+ PriceRepositoryPort priceRepositoryPort;
+
+ @Override
+ public Price getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId) {
+ return priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(dateTime, productId, brandId)
+ .orElseThrow(() ->
+ new NotFoundException(String.format(
+ "Price for product %d of brand %d not found for date %s", productId, brandId, dateTime)));
+ }
+}
diff --git a/application/src/test/java/com/techivw/webprice/application/services/PriceServiceTest.java b/application/src/test/java/com/techivw/webprice/application/services/PriceServiceTest.java
new file mode 100644
index 0000000..2a61a80
--- /dev/null
+++ b/application/src/test/java/com/techivw/webprice/application/services/PriceServiceTest.java
@@ -0,0 +1,77 @@
+package com.techivw.webprice.application.services;
+
+import com.techivw.webprice.application.exceptions.NotFoundException;
+import com.techivw.webprice.application.ports.in.PriceServicePort;
+import com.techivw.webprice.application.ports.out.PriceRepositoryPort;
+import com.techivw.webprice.domain.Price;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+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.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(SpringExtension.class)
+class PriceServiceTest {
+
+ @Mock
+ private PriceRepositoryPort priceRepositoryPort;
+
+ private PriceServicePort priceService;
+
+ private final LocalDateTime TEST_DATE = LocalDateTime.parse("2023-01-01T12:00:00");
+ private final Long TEST_PRODUCT_ID = 35455L;
+ private final Long TEST_BRAND_ID = 1L;
+
+ @BeforeEach
+ void setUp() {
+ priceService = new PriceService(priceRepositoryPort);
+ }
+
+ @Test
+ void shouldReturnPriceWhenPriceExists() {
+ Price expectedPrice = createPrice();
+ when(priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(
+ eq(TEST_DATE), eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
+ .thenReturn(Optional.of(expectedPrice));
+
+ Price result = priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(
+ TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID);
+
+ assertNotNull(result);
+ assertEquals(expectedPrice.getProductId(), result.getProductId());
+ assertEquals(expectedPrice.getPrice(), result.getPrice());
+ }
+
+ @Test
+ void shouldThrowNotFoundExceptionWhenPriceDoesNotExist() {
+ when(priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(
+ any(LocalDateTime.class), anyLong(), anyLong()))
+ .thenReturn(Optional.empty());
+
+ assertThrows(NotFoundException.class, () ->
+ priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(
+ TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID));
+ }
+
+ private Price createPrice() {
+ return Price.builder()
+ .brandId(TEST_BRAND_ID)
+ .productId(TEST_PRODUCT_ID)
+ .startDate(TEST_DATE.minusDays(1))
+ .endDate(TEST_DATE.plusDays(1))
+ .priceList(1L)
+ .price(BigDecimal.valueOf(35.50))
+ .priority(1)
+ .currency("EUR")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/boot/pom.xml b/boot/pom.xml
new file mode 100644
index 0000000..a27a6b9
--- /dev/null
+++ b/boot/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+
+ com.techivw
+ web-price
+ 1.0-RELEASE
+
+
+
+ boot
+ jar
+ boot
+
+
+
+
+ com.techivw
+ domain
+ 1.0-RELEASE
+
+
+ com.techivw
+ application
+ 1.0-RELEASE
+
+
+ com.techivw
+ rest-api
+ 1.0-RELEASE
+
+
+ com.techivw
+ sql-repository
+ 1.0-RELEASE
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/boot/src/main/java/com/techivw/webprice/Application.java b/boot/src/main/java/com/techivw/webprice/Application.java
new file mode 100644
index 0000000..f8e7b60
--- /dev/null
+++ b/boot/src/main/java/com/techivw/webprice/Application.java
@@ -0,0 +1,13 @@
+package com.techivw.webprice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/boot/src/main/resources/application.properties b/boot/src/main/resources/application.properties
new file mode 100644
index 0000000..1da086d
--- /dev/null
+++ b/boot/src/main/resources/application.properties
@@ -0,0 +1,17 @@
+spring.application.name=webprice
+
+# Database Configuration
+spring.datasource.url=jdbc:h2:mem:webprice
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=admin
+spring.datasource.password=admin
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+
+# H2 console
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2-console
+
+# Flyway
+spring.flyway.enabled=true
+spring.flyway.locations=filesystem:infrastructure/out/sql-repository/sql/migration
+spring.flyway.baseline-on-migrate=true
\ No newline at end of file
diff --git a/domain/pom.xml b/domain/pom.xml
new file mode 100644
index 0000000..ab35c2d
--- /dev/null
+++ b/domain/pom.xml
@@ -0,0 +1,16 @@
+
+
+ 4.0.0
+
+ domain
+ jar
+ domain
+
+
+ com.techivw
+ web-price
+ 1.0-RELEASE
+
+
diff --git a/domain/src/main/java/com/techivw/webprice/domain/Price.java b/domain/src/main/java/com/techivw/webprice/domain/Price.java
new file mode 100644
index 0000000..fc8ba9a
--- /dev/null
+++ b/domain/src/main/java/com/techivw/webprice/domain/Price.java
@@ -0,0 +1,23 @@
+package com.techivw.webprice.domain;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+public class Price {
+
+ private Long id;
+ private Long brandId;
+ private LocalDateTime startDate;
+ private LocalDateTime endDate;
+ private Long priceList;
+ private Long productId;
+ private Integer priority;
+ private BigDecimal price;
+ private String currency;
+
+}
diff --git a/infrastructure/in/rest-api/pom.xml b/infrastructure/in/rest-api/pom.xml
new file mode 100644
index 0000000..5826199
--- /dev/null
+++ b/infrastructure/in/rest-api/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+
+ com.techivw
+ web-price
+ 1.0-RELEASE
+ ../../../pom.xml
+
+
+ rest-api
+ jar
+ rest-api
+
+
+
+ com.techivw
+ domain
+ 1.0-RELEASE
+
+
+ com.techivw
+ application
+ 1.0-RELEASE
+
+
+
+
diff --git a/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/adapters/PriceControllerAdapter.java b/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/adapters/PriceControllerAdapter.java
new file mode 100644
index 0000000..f790640
--- /dev/null
+++ b/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/adapters/PriceControllerAdapter.java
@@ -0,0 +1,52 @@
+package com.techivw.webprice.infrastructure.in.controllers.adapters;
+
+import com.techivw.webprice.application.ports.in.PriceServicePort;
+import com.techivw.webprice.domain.Price;
+import com.techivw.webprice.infrastructure.in.controllers.model.PriceResponse;
+import com.techivw.webprice.infrastructure.in.controllers.model.mappers.PriceResponseMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import lombok.AllArgsConstructor;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+
+@RestController
+@AllArgsConstructor
+@RequestMapping("/price")
+public class PriceControllerAdapter {
+
+ private PriceServicePort priceServicePort;
+
+ @GetMapping
+ @Operation(
+ summary = "Get product price",
+ description = "Returns the current price for a specific product from a particular brand at a given date and time"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Price information found",
+ content = @Content(mediaType = "application/json", schema = @Schema(implementation = PriceResponse.class))
+ )
+ @ApiResponse(responseCode = "400", description = "Invalid request parameters")
+ @ApiResponse(responseCode = "404", description = "Price information not found for the given parameters")
+ public ResponseEntity getPrice(
+ @Parameter(description = "Date and time for price lookup (ISO format)", example = "2025-04-30T12:00:00")
+ @RequestParam(name = "dateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateTime,
+ @Parameter(description = "Product ID", example = "35455")
+ @RequestParam(name = "productId") Long productId,
+ @Parameter(description = "Brand ID", example = "1")
+ @RequestParam(name = "brandId") Long brandId) {
+
+ Price price = priceServicePort.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(dateTime, productId, brandId);
+ return ResponseEntity.ok(PriceResponseMapper.fromPrice(price));
+ }
+}
diff --git a/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/handlers/AdapterExceptionHandler.java b/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/handlers/AdapterExceptionHandler.java
new file mode 100644
index 0000000..50b1daa
--- /dev/null
+++ b/infrastructure/in/rest-api/src/main/java/com/techivw/webprice/infrastructure/in/controllers/handlers/AdapterExceptionHandler.java
@@ -0,0 +1,27 @@
+package com.techivw.webprice.infrastructure.in.controllers.handlers;
+
+import com.techivw.webprice.application.exceptions.NotFoundException;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+
+@RestControllerAdvice
+public class AdapterExceptionHandler {
+
+ @ExceptionHandler(NotFoundException.class)
+ public ResponseEntity