refactor: move price business logic from repository to service

This commit is contained in:
bedroomghost 2025-04-21 19:41:53 +02:00
parent 1382675860
commit d133e9eeea
7 changed files with 85 additions and 59 deletions

View File

@ -2,11 +2,9 @@ package com.techivw.webprice.application.ports.out;
import com.techivw.webprice.domain.Price; import com.techivw.webprice.domain.Price;
import java.time.LocalDateTime; import java.util.List;
import java.util.Optional;
public interface PriceRepositoryPort { public interface PriceRepositoryPort {
Optional<Price> findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId); List<Price> findPricesByProductIdAndBrandId(Long productId, Long brandId);
} }

View File

@ -8,6 +8,9 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@ -17,8 +20,14 @@ public class PriceService implements PriceServicePort {
@Override @Override
public Price getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId) { public Price getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId) {
return priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(dateTime, productId, brandId)
.orElseThrow(() -> List<Price> prices = priceRepositoryPort.findPricesByProductIdAndBrandId(productId, brandId);
Optional<Price> maxPriorityPrice = prices.stream()
.filter(price -> !dateTime.isBefore(price.getStartDate()) && !dateTime.isAfter(price.getEndDate()))
.max(Comparator.comparing(Price::getPriority));
return maxPriorityPrice.orElseThrow(() ->
new NotFoundException(String.format( new NotFoundException(String.format(
"Price for product %d of brand %d not found for date %s", productId, brandId, dateTime))); "Price for product %d of brand %d not found for date %s", productId, brandId, dateTime)));
} }

View File

@ -7,7 +7,9 @@ import com.techivw.webprice.domain.Price;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Optional; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -38,31 +40,55 @@ class PriceServiceTest {
@Test @Test
void shouldReturnPriceWhenPriceExists() { void shouldReturnPriceWhenPriceExists() {
Price expectedPrice = createPrice(); Price lowPriorityPrice = createPrice(0);
when(priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId( Price highPriorityPrice = createPrice(2);
eq(TEST_DATE), eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID))) List<Price> prices = Arrays.asList(lowPriorityPrice, highPriorityPrice);
.thenReturn(Optional.of(expectedPrice));
when(priceRepositoryPort.findPricesByProductIdAndBrandId(
eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
.thenReturn(prices);
Price result = priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId( Price result = priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(
TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID); TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID);
assertNotNull(result); assertNotNull(result);
assertEquals(expectedPrice.getProductId(), result.getProductId()); assertEquals(highPriorityPrice, result);
assertEquals(expectedPrice.getPrice(), result.getPrice());
} }
@Test @Test
void shouldThrowNotFoundExceptionWhenPriceDoesNotExist() { void shouldThrowNotFoundExceptionWhenPriceListIsEmpty() {
when(priceRepositoryPort.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId( when(priceRepositoryPort.findPricesByProductIdAndBrandId(
any(LocalDateTime.class), anyLong(), anyLong())) eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
.thenReturn(Optional.empty()); .thenReturn(Collections.emptyList());
assertThrows(NotFoundException.class, () -> assertThrows(NotFoundException.class, () ->
priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId( priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(
TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID)); TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID));
} }
private Price createPrice() { @Test
void shouldThrowNotFoundExceptionWhenNoPriceInDateRange() {
Price price = Price.builder()
.brandId(TEST_BRAND_ID)
.productId(TEST_PRODUCT_ID)
.startDate(TEST_DATE.plusDays(1))
.endDate(TEST_DATE.plusDays(2))
.priceList(1L)
.price(BigDecimal.valueOf(35.50))
.priority(1)
.currency("EUR")
.build();
when(priceRepositoryPort.findPricesByProductIdAndBrandId(
eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
.thenReturn(Collections.singletonList(price));
assertThrows(NotFoundException.class, () ->
priceService.getPriceWithHighestPriorityByDateTimeAndProductIdAndBrandId(
TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID));
}
private Price createPrice(int priority) {
return Price.builder() return Price.builder()
.brandId(TEST_BRAND_ID) .brandId(TEST_BRAND_ID)
.productId(TEST_PRODUCT_ID) .productId(TEST_PRODUCT_ID)
@ -70,7 +96,7 @@ class PriceServiceTest {
.endDate(TEST_DATE.plusDays(1)) .endDate(TEST_DATE.plusDays(1))
.priceList(1L) .priceList(1L)
.price(BigDecimal.valueOf(35.50)) .price(BigDecimal.valueOf(35.50))
.priority(1) .priority(priority)
.currency("EUR") .currency("EUR")
.build(); .build();
} }

View File

@ -2,19 +2,12 @@ package com.techivw.webprice.infrastructure.repositories;
import com.techivw.webprice.infrastructure.repositories.model.PriceEntity; import com.techivw.webprice.infrastructure.repositories.model.PriceEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDateTime; import java.util.List;
import java.util.Optional;
@Repository @Repository
public interface PriceEntityJPARepository extends JpaRepository<PriceEntity, Long> { public interface PriceEntityJPARepository extends JpaRepository<PriceEntity, Long> {
@Query("SELECT p FROM PriceEntity p WHERE " + List<PriceEntity> findPricesByProductIdAndBrandId(Long productId, Long brandId);
"p.productId = :productId " +
"AND p.brandId = :brandId " +
"AND :dateTime BETWEEN p.startDate AND p.endDate " +
"ORDER BY p.priority DESC LIMIT 1")
Optional<PriceEntity> findPriceByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId);
} }

View File

@ -8,8 +8,7 @@ import com.techivw.webprice.infrastructure.repositories.model.PriceEntity;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.util.List;
import java.util.Optional;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@ -18,11 +17,10 @@ public class PriceRepositoryAdapter implements PriceRepositoryPort {
private final PriceEntityJPARepository repository; private final PriceEntityJPARepository repository;
@Override @Override
public Optional<Price> findHighestPriorityPriceByDateTimeAndProductIdAndBrandId(LocalDateTime dateTime, Long productId, Long brandId) { public List<Price> findPricesByProductIdAndBrandId(Long productId, Long brandId) {
Optional<PriceEntity> priceEntity = List<PriceEntity> priceEntityList = repository.findPricesByProductIdAndBrandId(productId, brandId);
repository.findPriceByDateTimeAndProductIdAndBrandId(dateTime, productId, brandId);
return PriceEntityMapper.toOptionalPrice(priceEntity); return PriceEntityMapper.toPriceList(priceEntityList);
} }
} }

View File

@ -3,7 +3,7 @@ package com.techivw.webprice.infrastructure.repositories.mappers;
import com.techivw.webprice.domain.Price; import com.techivw.webprice.domain.Price;
import com.techivw.webprice.infrastructure.repositories.model.PriceEntity; import com.techivw.webprice.infrastructure.repositories.model.PriceEntity;
import java.util.Optional; import java.util.List;
public class PriceEntityMapper { public class PriceEntityMapper {
@ -21,8 +21,8 @@ public class PriceEntityMapper {
.build(); .build();
} }
public static Optional<Price> toOptionalPrice(Optional<PriceEntity> priceEntity) { public static List<Price> toPriceList(List<PriceEntity> priceEntityList) {
return priceEntity.map(PriceEntityMapper::toPrice); return priceEntityList.stream().map(PriceEntityMapper::toPrice).toList();
} }
} }

View File

@ -11,7 +11,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Optional; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
@ -35,35 +37,35 @@ class PriceRepositoryAdapterTest {
} }
@Test @Test
void shouldReturnPriceWhenPriceEntityExists() { void shouldReturnPricesWhenPriceEntitiesExist() {
PriceEntity priceEntity = createSamplePriceEntity(); List<PriceEntity> priceEntities = Arrays.asList(
when(repository.findPriceByDateTimeAndProductIdAndBrandId( createPriceEntity(1),
eq(TEST_DATE), eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID))) createPriceEntity(2)
.thenReturn(Optional.of(priceEntity)); );
Optional<Price> result = adapter.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId( when(repository.findPricesByProductIdAndBrandId(
TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID); eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
.thenReturn(priceEntities);
assertTrue(result.isPresent()); List<Price> result = adapter.findPricesByProductIdAndBrandId(
Price price = result.get(); TEST_PRODUCT_ID, TEST_BRAND_ID);
assertEquals(TEST_PRODUCT_ID, price.getProductId());
assertEquals(TEST_BRAND_ID, price.getBrandId()); assertEquals(2, result.size());
assertEquals(BigDecimal.valueOf(35.50), price.getPrice());
} }
@Test @Test
void shouldReturnEmptyOptionalWhenPriceEntityDoesNotExist() { void shouldReturnEmptyListWhenNoPriceEntitiesExist() {
when(repository.findPriceByDateTimeAndProductIdAndBrandId( when(repository.findPricesByProductIdAndBrandId(
any(LocalDateTime.class), anyLong(), anyLong())) eq(TEST_PRODUCT_ID), eq(TEST_BRAND_ID)))
.thenReturn(Optional.empty()); .thenReturn(Collections.emptyList());
Optional<Price> result = adapter.findHighestPriorityPriceByDateTimeAndProductIdAndBrandId( List<Price> result = adapter.findPricesByProductIdAndBrandId(
TEST_DATE, TEST_PRODUCT_ID, TEST_BRAND_ID); TEST_PRODUCT_ID, TEST_BRAND_ID);
assertFalse(result.isPresent()); assertTrue(result.isEmpty());
} }
private PriceEntity createSamplePriceEntity() { private PriceEntity createPriceEntity(int priority) {
PriceEntity entity = new PriceEntity(); PriceEntity entity = new PriceEntity();
entity.setBrandId(TEST_BRAND_ID); entity.setBrandId(TEST_BRAND_ID);
entity.setProductId(TEST_PRODUCT_ID); entity.setProductId(TEST_PRODUCT_ID);
@ -71,7 +73,7 @@ class PriceRepositoryAdapterTest {
entity.setEndDate(TEST_DATE.plusDays(1)); entity.setEndDate(TEST_DATE.plusDays(1));
entity.setPriceList(1L); entity.setPriceList(1L);
entity.setPrice(BigDecimal.valueOf(35.50)); entity.setPrice(BigDecimal.valueOf(35.50));
entity.setPriority(1); entity.setPriority(priority);
entity.setCurrency("EUR"); entity.setCurrency("EUR");
return entity; return entity;
} }