From 5df9a104b1bd0fba1f3a981c2f3fa3a060337842 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:22:28 +0900 Subject: [PATCH 01/18] chore: remove unnecessary import --- .../java/nexters/payout/domain/portfolio/domain/Portfolio.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java index 1bedc80..48325d6 100644 --- a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java +++ b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java @@ -5,9 +5,7 @@ import nexters.payout.domain.BaseEntity; import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.UUID; @Entity From d6d94d7f8727daab690b728392cca5aa699251bb Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:44:39 +0900 Subject: [PATCH 02/18] feat: add portfolio command service --- .../application/PortfolioCommandService.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java b/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java new file mode 100644 index 0000000..b324667 --- /dev/null +++ b/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java @@ -0,0 +1,21 @@ +package nexters.payout.domain.portfolio.application; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import nexters.payout.domain.portfolio.domain.Portfolio; +import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional +public class PortfolioCommandService { + + private final PortfolioRepository portfolioRepository; + + public UUID createPortfolio(Portfolio portfolio) { + return portfolioRepository.save(portfolio).getId(); + } +} From c2653f53ac6fef1d87be0fc37578e71fcd5f3277 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:50:34 +0900 Subject: [PATCH 03/18] feat: add portfolio query service --- .../application/PortfolioQueryService.java | 44 +++++++++++++++++++ .../dto/request/PortfolioRequest.java | 16 +++++++ .../application/dto/request/TickerShare.java | 14 ++++++ .../dto/response/PortfolioResponse.java | 11 +++++ .../payout/core/time/InstantProvider.java | 4 ++ 5 files changed, 89 insertions(+) create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/TickerShare.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/PortfolioResponse.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java new file mode 100644 index 0000000..66d97bd --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -0,0 +1,44 @@ +package nexters.payout.apiserver.portfolio.application; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.core.time.InstantProvider; +import nexters.payout.domain.portfolio.application.PortfolioCommandService; +import nexters.payout.domain.portfolio.domain.Portfolio; +import nexters.payout.domain.portfolio.domain.PortfolioStock; +import nexters.payout.domain.stock.domain.exception.TickerNotFoundException; +import nexters.payout.domain.stock.domain.repository.StockRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +@Slf4j +public class PortfolioQueryService { + + private final PortfolioCommandService portfolioCommandService; + private final StockRepository stockRepository; + + public PortfolioResponse createPortfolio(final PortfolioRequest request) { + + List portfolioStocks = + request.tickerShares() + .stream().map(tickerShare -> new PortfolioStock( + stockRepository.findByTicker(tickerShare.ticker()).orElseThrow( + () -> new TickerNotFoundException(tickerShare.ticker())).getId(), + tickerShare.share())) + .toList(); + + return new PortfolioResponse(portfolioCommandService.createPortfolio( + new Portfolio( + InstantProvider.getExpireAt(), + portfolioStocks + ) + )); + } +} diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java new file mode 100644 index 0000000..798a805 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java @@ -0,0 +1,16 @@ +package nexters.payout.apiserver.portfolio.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; +import nexters.payout.apiserver.dividend.application.dto.request.TickerShare; + +import java.util.List; + +public record PortfolioRequest( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @Size(min = 1) + List tickerShares +) { +} diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/TickerShare.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/TickerShare.java new file mode 100644 index 0000000..dbc28c1 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/TickerShare.java @@ -0,0 +1,14 @@ +package nexters.payout.apiserver.portfolio.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; + +public record TickerShare( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "ticker name") + @NotEmpty + String ticker, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "share") + @Min(value = 1) + Integer share +) { } \ No newline at end of file diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/PortfolioResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/PortfolioResponse.java new file mode 100644 index 0000000..5a21316 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/PortfolioResponse.java @@ -0,0 +1,11 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +public record PortfolioResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + UUID id +) { +} diff --git a/core/src/main/java/nexters/payout/core/time/InstantProvider.java b/core/src/main/java/nexters/payout/core/time/InstantProvider.java index cc775f1..7886f5c 100644 --- a/core/src/main/java/nexters/payout/core/time/InstantProvider.java +++ b/core/src/main/java/nexters/payout/core/time/InstantProvider.java @@ -42,6 +42,10 @@ public static Instant getYesterday() { return getNow().minusDays(1).atStartOfDay(ZoneId.of("UTC")).toInstant(); } + public static Instant getExpireAt() { + return getNow().plusMonths(1).atStartOfDay(ZoneId.of("UTC")).toInstant(); + } + public static Integer getYear(Instant date) { return ZonedDateTime.ofInstant(date, UTC).getYear(); } From 7d7355e5705a50051ef74d4fb29350c0b66d702d Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:50:47 +0900 Subject: [PATCH 04/18] feat: add portfolio controller --- .../presentation/PortfolioController.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java new file mode 100644 index 0000000..b666421 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java @@ -0,0 +1,27 @@ +package nexters.payout.apiserver.portfolio.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import nexters.payout.apiserver.portfolio.application.PortfolioQueryService; +import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/portfolios") +@Slf4j +public class PortfolioController implements PortfolioControllerDocs { + + private final PortfolioQueryService portfolioQueryService; + + @PostMapping + public ResponseEntity createPortfolio(@RequestBody @Valid final PortfolioRequest portfolioRequest) { + return ResponseEntity.ok(portfolioQueryService.createPortfolio(portfolioRequest)); + } +} From 4e70f8feaed7bdbe3e0f7268ffa07fa6883dca9e Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:50:57 +0900 Subject: [PATCH 05/18] docs: add portfolio controller docs --- .../presentation/PortfolioControllerDocs.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java new file mode 100644 index 0000000..1ec6830 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java @@ -0,0 +1,37 @@ +package nexters.payout.apiserver.portfolio.presentation; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.core.exception.ErrorResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + + +public interface PortfolioControllerDocs { + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "SUCCESS"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "NOT FOUND", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "SERVER ERROR", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}) + }) + @Operation(summary = "포트폴리오 생성", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = PortfolioRequest.class), + examples = { + @ExampleObject(name = "PortfolioRequestExample", value = "{\"tickerShares\":[{\"ticker\":\"AAPL\",\"share\":3}]}") + }))) + ResponseEntity createPortfolio(@RequestBody @Valid final PortfolioRequest portfolioRequest); +} From d40be82fed833edf3e4c7f47f868535c380ff932 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:58:39 +0900 Subject: [PATCH 06/18] fix: remove portfolio command service --- .../application/PortfolioQueryService.java | 10 ++++----- .../PortfolioQueryServiceTest.java | 20 ++++++++++++++++++ .../application/PortfolioCommandService.java | 21 ------------------- 3 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java delete mode 100644 domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java index 66d97bd..932951e 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -6,9 +6,9 @@ import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; import nexters.payout.core.time.InstantProvider; -import nexters.payout.domain.portfolio.application.PortfolioCommandService; import nexters.payout.domain.portfolio.domain.Portfolio; import nexters.payout.domain.portfolio.domain.PortfolioStock; +import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; import nexters.payout.domain.stock.domain.exception.TickerNotFoundException; import nexters.payout.domain.stock.domain.repository.StockRepository; import org.springframework.stereotype.Service; @@ -21,8 +21,8 @@ @Slf4j public class PortfolioQueryService { - private final PortfolioCommandService portfolioCommandService; private final StockRepository stockRepository; + private final PortfolioRepository portfolioRepository; public PortfolioResponse createPortfolio(final PortfolioRequest request) { @@ -34,11 +34,11 @@ public PortfolioResponse createPortfolio(final PortfolioRequest request) { tickerShare.share())) .toList(); - return new PortfolioResponse(portfolioCommandService.createPortfolio( + return new PortfolioResponse(portfolioRepository.save( new Portfolio( InstantProvider.getExpireAt(), portfolioStocks - ) - )); + )).getId() + ); } } diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java new file mode 100644 index 0000000..63af0d9 --- /dev/null +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java @@ -0,0 +1,20 @@ +package nexters.payout.apiserver.portfolio.application; + +import nexters.payout.apiserver.dividend.common.GivenFixtureTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class PortfolioQueryServiceTest extends GivenFixtureTest { + + @InjectMocks + private PortfolioQueryService portfolioQueryService; + + @Test + void createPortfolio() { + } +} \ No newline at end of file diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java b/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java deleted file mode 100644 index b324667..0000000 --- a/domain/src/main/java/nexters/payout/domain/portfolio/application/PortfolioCommandService.java +++ /dev/null @@ -1,21 +0,0 @@ -package nexters.payout.domain.portfolio.application; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import nexters.payout.domain.portfolio.domain.Portfolio; -import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; -import org.springframework.stereotype.Service; - -import java.util.UUID; - -@Service -@RequiredArgsConstructor -@Transactional -public class PortfolioCommandService { - - private final PortfolioRepository portfolioRepository; - - public UUID createPortfolio(Portfolio portfolio) { - return portfolioRepository.save(portfolio).getId(); - } -} From 5c063463cc6477eb499b176b9e2772889ff66d6b Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:26:19 +0900 Subject: [PATCH 07/18] test: add test for create portfolio api --- .../PortfolioQueryServiceTest.java | 62 +++++++++++++++++-- .../domain/portfolio/domain/Portfolio.java | 7 +++ .../payout/domain/PortfolioFixture.java | 14 ++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java index 63af0d9..535e0e0 100644 --- a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java @@ -1,20 +1,74 @@ package nexters.payout.apiserver.portfolio.application; -import nexters.payout.apiserver.dividend.common.GivenFixtureTest; +import nexters.payout.apiserver.dividend.application.dto.request.TickerShare; +import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.core.time.InstantProvider; +import nexters.payout.domain.PortfolioFixture; +import nexters.payout.domain.StockFixture; +import nexters.payout.domain.portfolio.domain.PortfolioStock; +import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; +import nexters.payout.domain.stock.domain.Sector; +import nexters.payout.domain.stock.domain.Stock; +import nexters.payout.domain.stock.domain.repository.StockRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static nexters.payout.domain.StockFixture.*; @ExtendWith(MockitoExtension.class) -class PortfolioQueryServiceTest extends GivenFixtureTest { +class PortfolioQueryServiceTest { + + @Mock + private PortfolioRepository portfolioRepository; + + @Mock + private StockRepository stockRepository; @InjectMocks private PortfolioQueryService portfolioQueryService; @Test - void createPortfolio() { + void 포트폴리오를_생성한다() { + // given + Stock appl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 4.0); + Stock tsla = StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL, 2.2); + String expected = "67221662-c2f7-4f35-9447-6a65ca88d5ea"; + + given(stockRepository.findByTicker(AAPL)).willReturn(Optional.of(appl)); + given(stockRepository.findByTicker(TSLA)).willReturn(Optional.of(tsla)); + given(portfolioRepository.save(any())).willReturn(PortfolioFixture.createPortfolio( + UUID.fromString("67221662-c2f7-4f35-9447-6a65ca88d5ea"), + InstantProvider.getExpireAt(), + List.of( + new PortfolioStock(UUID.randomUUID(), 2), + new PortfolioStock(UUID.randomUUID(), 1) + ) + ) + ); + + // when + PortfolioResponse actual = portfolioQueryService.createPortfolio(request()); + + // then + assertThat(actual.id()).isEqualTo(UUID.fromString(expected)); + } + + private PortfolioRequest request() { + return new PortfolioRequest(List.of( + new TickerShare(AAPL, 2), + new TickerShare(TSLA, 1)) + ); } } \ No newline at end of file diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java index 48325d6..1772de3 100644 --- a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java +++ b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java @@ -6,6 +6,7 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; @Entity @@ -21,6 +22,12 @@ public Portfolio() { super(null); } + public Portfolio(final UUID id, final Instant expireAt, List stocks) { + super(id); + this.portfolioStocks = new PortfolioStocks(stocks); + this.expireAt = expireAt; + } + public Portfolio(final Instant expireAt, List stocks) { super(null); this.portfolioStocks = new PortfolioStocks(stocks); diff --git a/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java b/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java index 59ffb6e..4d2eb3d 100644 --- a/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java +++ b/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java @@ -12,7 +12,19 @@ public class PortfolioFixture { public static UUID STOCK_ID = UUID.randomUUID(); + public static Portfolio createPortfolio(UUID id, Instant expireAt, List stocks) { + return new Portfolio( + id, + expireAt, + stocks + ); + } + public static Portfolio createPortfolio(Instant expireAt, List stocks) { - return new Portfolio(expireAt, stocks); + return new Portfolio( + UUID.randomUUID(), + expireAt, + stocks + ); } } From 6d94d0243b9ad31ad81b479160736a91bf5440b5 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:37:03 +0900 Subject: [PATCH 08/18] feat: add monthly/yearly dividend api --- .../application/PortfolioQueryService.java | 85 ++++++++++++++++++- .../dto/response/MonthlyDividendResponse.java | 32 +++++++ .../SingleMonthlyDividendResponse.java | 28 ++++++ .../SingleYearlyDividendResponse.java | 24 ++++++ .../dto/response/YearlyDividendResponse.java | 28 ++++++ .../exception/PortfolioNotFoundException.java | 12 +++ .../exception/StockIdNotFoundException.java | 12 +++ 7 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/MonthlyDividendResponse.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleMonthlyDividendResponse.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleYearlyDividendResponse.java create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/YearlyDividendResponse.java create mode 100644 domain/src/main/java/nexters/payout/domain/portfolio/domain/exception/PortfolioNotFoundException.java create mode 100644 domain/src/main/java/nexters/payout/domain/stock/domain/exception/StockIdNotFoundException.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java index 932951e..d012582 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -1,19 +1,31 @@ package nexters.payout.apiserver.portfolio.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.SingleYearlyDividendResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.SingleMonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; import nexters.payout.core.time.InstantProvider; +import nexters.payout.domain.dividend.domain.Dividend; +import nexters.payout.domain.dividend.domain.repository.DividendRepository; import nexters.payout.domain.portfolio.domain.Portfolio; import nexters.payout.domain.portfolio.domain.PortfolioStock; +import nexters.payout.domain.portfolio.domain.exception.PortfolioNotFoundException; import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; +import nexters.payout.domain.stock.domain.Stock; +import nexters.payout.domain.stock.domain.exception.StockIdNotFoundException; import nexters.payout.domain.stock.domain.exception.TickerNotFoundException; import nexters.payout.domain.stock.domain.repository.StockRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Service @RequiredArgsConstructor @@ -23,6 +35,7 @@ public class PortfolioQueryService { private final StockRepository stockRepository; private final PortfolioRepository portfolioRepository; + private final DividendRepository dividendRepository; public PortfolioResponse createPortfolio(final PortfolioRequest request) { @@ -41,4 +54,74 @@ public PortfolioResponse createPortfolio(final PortfolioRequest request) { )).getId() ); } + + @Transactional(readOnly = true) + public List getMonthlyDividends(final UUID id) { + return InstantProvider.generateNext12Months() + .stream() + .map(yearMonth -> MonthlyDividendResponse.of( + yearMonth.getYear(), + yearMonth.getMonthValue(), + getDividendsOfLastYearAndMonth( + portfolioRepository.findById(id) + .orElseThrow(() -> new PortfolioNotFoundException(id)) + .getPortfolioStocks().getPortfolioStocks(), + yearMonth.getMonthValue()) + ) + ) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public YearlyDividendResponse getYearlyDividends(final UUID id) { + + List dividends = portfolioRepository.findById(id) + .orElseThrow(() -> new PortfolioNotFoundException(id)) + .getPortfolioStocks().getPortfolioStocks() + .stream().map(portfolioStock -> { + Stock stock = stockRepository.findById(portfolioStock.getStockId()) + .orElseThrow(() -> new StockIdNotFoundException(portfolioStock.getStockId())); + return SingleYearlyDividendResponse.of( + stock, portfolioStock.getShares(), getYearlyDividend(stock.getId()) + ); + }) + .filter(response -> response.totalDividend() != 0) + .toList(); + + return YearlyDividendResponse.of(dividends); + } + + private double getYearlyDividend(final UUID stockId) { + return getLastYearDividendsByStockId(stockId) + .stream() + .mapToDouble(Dividend::getDividend) + .sum(); + } + + private List getLastYearDividendsByStockId(final UUID id) { + return dividendRepository.findAllByIdAndYear(id, InstantProvider.getLastYear()); + } + + private List getDividendsOfLastYearAndMonth( + final List portfolioStocks, final int month + ) { + return portfolioStocks + .stream() + .flatMap(portfolioStock -> stockRepository.findById(portfolioStock.getStockId()) + .map(stock -> getMonthlyDividendResponse(month, portfolioStock, stock)) + .orElseThrow(() -> new StockIdNotFoundException(portfolioStock.getStockId()))) + .toList(); + } + + private Stream getMonthlyDividendResponse( + final int month, final PortfolioStock portfolioStock, final Stock stock + ) { + return getLastYearDividendsByStockIdAndMonth(portfolioStock.getStockId(), month) + .stream() + .map(dividend -> SingleMonthlyDividendResponse.of(stock, portfolioStock.getShares(), dividend)); + } + + private List getLastYearDividendsByStockIdAndMonth(final UUID stockId, final int month) { + return dividendRepository.findAllByIdAndYearAndMonth(stockId, InstantProvider.getLastYear(), month); + } } diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/MonthlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/MonthlyDividendResponse.java new file mode 100644 index 0000000..05ab9d4 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/MonthlyDividendResponse.java @@ -0,0 +1,32 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Comparator; +import java.util.List; + +public record MonthlyDividendResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Integer year, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Integer month, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List dividends, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Double totalDividend +) { + public static MonthlyDividendResponse of( + final int year, final int month, final List dividends + ) { + return new MonthlyDividendResponse( + year, + month, + dividends.stream() + .sorted(Comparator.comparingDouble(SingleMonthlyDividendResponse::totalDividend).reversed()) + .toList(), + dividends.stream() + .mapToDouble(SingleMonthlyDividendResponse::totalDividend) + .sum() + ); + } +} diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleMonthlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleMonthlyDividendResponse.java new file mode 100644 index 0000000..1dee85c --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleMonthlyDividendResponse.java @@ -0,0 +1,28 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import nexters.payout.domain.dividend.domain.Dividend; +import nexters.payout.domain.stock.domain.Stock; + +public record SingleMonthlyDividendResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String ticker, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String logoUrl, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Integer share, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Double dividend, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Double totalDividend +) { + public static SingleMonthlyDividendResponse of(Stock stock, int share, Dividend dividend) { + return new SingleMonthlyDividendResponse( + stock.getTicker(), + stock.getLogoUrl(), + share, + dividend.getDividend(), + dividend.getDividend() * share + ); + } +} diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleYearlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleYearlyDividendResponse.java new file mode 100644 index 0000000..6bb7d1b --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SingleYearlyDividendResponse.java @@ -0,0 +1,24 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import nexters.payout.domain.stock.domain.Stock; + +public record SingleYearlyDividendResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String ticker, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String logoUrl, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Integer share, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Double totalDividend +) { + public static SingleYearlyDividendResponse of(Stock stock, int share, double dividend) { + return new SingleYearlyDividendResponse( + stock.getTicker(), + stock.getLogoUrl(), + share, + dividend * share + ); + } +} diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/YearlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/YearlyDividendResponse.java new file mode 100644 index 0000000..d25a10a --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/YearlyDividendResponse.java @@ -0,0 +1,28 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Comparator; +import java.util.List; + +public record YearlyDividendResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List dividends, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Double totalDividend +) { + public static YearlyDividendResponse of(List dividends) { + + dividends = dividends + .stream() + .sorted(Comparator.comparingDouble(SingleYearlyDividendResponse::totalDividend).reversed()) + .toList(); + return new YearlyDividendResponse( + dividends, + dividends + .stream() + .mapToDouble(SingleYearlyDividendResponse::totalDividend) + .sum() + ); + } +} diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/domain/exception/PortfolioNotFoundException.java b/domain/src/main/java/nexters/payout/domain/portfolio/domain/exception/PortfolioNotFoundException.java new file mode 100644 index 0000000..5144225 --- /dev/null +++ b/domain/src/main/java/nexters/payout/domain/portfolio/domain/exception/PortfolioNotFoundException.java @@ -0,0 +1,12 @@ +package nexters.payout.domain.portfolio.domain.exception; + +import nexters.payout.core.exception.error.NotFoundException; + +import java.util.UUID; + +public class PortfolioNotFoundException extends NotFoundException { + + public PortfolioNotFoundException(UUID id) { + super(String.format("not found portfolio [%s]", id)); + } +} diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/exception/StockIdNotFoundException.java b/domain/src/main/java/nexters/payout/domain/stock/domain/exception/StockIdNotFoundException.java new file mode 100644 index 0000000..54ed6da --- /dev/null +++ b/domain/src/main/java/nexters/payout/domain/stock/domain/exception/StockIdNotFoundException.java @@ -0,0 +1,12 @@ +package nexters.payout.domain.stock.domain.exception; + +import nexters.payout.core.exception.error.NotFoundException; + +import java.util.UUID; + +public class StockIdNotFoundException extends NotFoundException { + + public StockIdNotFoundException(UUID id) { + super(String.format("not found stock id [%s]", id)); + } +} From 1710e2f6c1fd6a8d9bf7653c052f23c72850d912 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:37:25 +0900 Subject: [PATCH 09/18] feat: add monthly/yearly dividend api --- .../presentation/PortfolioController.java | 20 +++++++++++++++---- .../infra/DividendRepositoryCustom.java | 2 ++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java index b666421..aadef1d 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java @@ -5,12 +5,14 @@ import lombok.extern.slf4j.Slf4j; import nexters.payout.apiserver.portfolio.application.PortfolioQueryService; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; @RestController @RequiredArgsConstructor @@ -24,4 +26,14 @@ public class PortfolioController implements PortfolioControllerDocs { public ResponseEntity createPortfolio(@RequestBody @Valid final PortfolioRequest portfolioRequest) { return ResponseEntity.ok(portfolioQueryService.createPortfolio(portfolioRequest)); } + + @GetMapping("/{id}/monthly") + public ResponseEntity> getMonthlyDividends(@PathVariable("id") final UUID portfolioId) { + return ResponseEntity.ok(portfolioQueryService.getMonthlyDividends(portfolioId)); + } + + @GetMapping("/{id}/yearly") + public ResponseEntity getYearlyDividends(@PathVariable("id") final UUID portfolioId) { + return ResponseEntity.ok(portfolioQueryService.getYearlyDividends(portfolioId)); + } } diff --git a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java index dbd020d..fd1e12c 100644 --- a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java +++ b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java @@ -12,6 +12,8 @@ public interface DividendRepositoryCustom { Optional findByStockIdAndExDividendDate(UUID stockId, Instant date); List findAllByTickerAndYearAndMonth(String ticker, Integer year, Integer month); + List findAllByIdAndYearAndMonth(UUID id, Integer year, Integer month); List findAllByTickerAndYear(String ticker, Integer year); + List findAllByIdAndYear(UUID id, Integer year); void deleteByYearAndCreatedAt(Integer year, Instant createdAt); } From 5d09011e757eb67f3fba2e72e1c8774960975a02 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:37:41 +0900 Subject: [PATCH 10/18] docs: add swagger docs --- .../presentation/PortfolioControllerDocs.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java index 1ec6830..b7764f2 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java @@ -1,6 +1,7 @@ package nexters.payout.apiserver.portfolio.presentation; 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.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,11 +9,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; import nexters.payout.core.exception.ErrorResponse; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import java.util.List; +import java.util.UUID; + public interface PortfolioControllerDocs { @@ -34,4 +41,34 @@ public interface PortfolioControllerDocs { @ExampleObject(name = "PortfolioRequestExample", value = "{\"tickerShares\":[{\"ticker\":\"AAPL\",\"share\":3}]}") }))) ResponseEntity createPortfolio(@RequestBody @Valid final PortfolioRequest portfolioRequest); + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "SUCCESS"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "NOT FOUND", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "SERVER ERROR", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}) + }) + @Operation(summary = "포트폴리오 기반 월간 배당금 조회") + ResponseEntity> getMonthlyDividends( + @Parameter(description = "portfolio id", example = "bf5ffb6d-ae70-4171-8c86-b27c8ab2efbb", required = true) + @PathVariable("id") final UUID portfolioId + ); + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "SUCCESS"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "NOT FOUND", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "SERVER ERROR", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}) + }) + @Operation(summary = "포트폴리오 기반 연간 배당금 조회") + ResponseEntity getYearlyDividends( + @Parameter(description = "portfolio id", example = "bf5ffb6d-ae70-4171-8c86-b27c8ab2efbb", required = true) + @PathVariable("id") final UUID portfolioId + ); } From 5c4eac669c4dd06a2aa04995407d7234bcec7d38 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:04:43 +0900 Subject: [PATCH 11/18] feat: implement dividend repository custom --- .../infra/DividendRepositoryImpl.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java index cbf7b9e..b0c0757 100644 --- a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java +++ b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java @@ -51,6 +51,18 @@ public List findAllByTickerAndYearAndMonth(String ticker, Integer year .fetch(); } + @Override + public List findAllByIdAndYearAndMonth(UUID id, Integer year, Integer month) { + + return queryFactory + .selectFrom(dividend1) + .innerJoin(stock).on(dividend1.stockId.eq(stock.id)) + .where(dividend1.exDividendDate.year().eq(year) + .and(dividend1.exDividendDate.month().eq(month)) + .and(stock.id.eq(id))) + .fetch(); + } + @Override public List findAllByTickerAndYear(String ticker, Integer year) { @@ -62,6 +74,17 @@ public List findAllByTickerAndYear(String ticker, Integer year) { .fetch(); } + @Override + public List findAllByIdAndYear(UUID id, Integer year) { + + return queryFactory + .selectFrom(dividend1) + .innerJoin(stock).on(dividend1.stockId.eq(stock.id)) + .where(dividend1.exDividendDate.year().eq(year) + .and(stock.id.eq(id))) + .fetch(); + } + @Override public void deleteByYearAndCreatedAt(Integer year, Instant createdAt) { @@ -73,4 +96,6 @@ public void deleteByYearAndCreatedAt(Integer year, Instant createdAt) { .and(dividend1.createdAt.dayOfMonth().eq(InstantProvider.getDayOfMonth(createdAt)))) .execute(); } + + } From d1e9d840f0bdfe2feb4a72aeb5e427afc77cf9ca Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:05:12 +0900 Subject: [PATCH 12/18] test: add portfolio query service test --- .../application/PortfolioQueryService.java | 5 +- .../dto/request/PortfolioRequest.java | 1 - .../presentation/DividendControllerTest.java | 2 +- .../PortfolioQueryServiceTest.java | 107 ++++++++++++++++-- .../portfolio/common/GivenFixtureTest.java | 94 +++++++++++++++ 5 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 api-server/src/test/java/nexters/payout/apiserver/portfolio/common/GivenFixtureTest.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java index d012582..8348b9d 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -42,8 +42,9 @@ public PortfolioResponse createPortfolio(final PortfolioRequest request) { List portfolioStocks = request.tickerShares() .stream().map(tickerShare -> new PortfolioStock( - stockRepository.findByTicker(tickerShare.ticker()).orElseThrow( - () -> new TickerNotFoundException(tickerShare.ticker())).getId(), + stockRepository.findByTicker(tickerShare.ticker()) + .orElseThrow(() -> new TickerNotFoundException(tickerShare.ticker())) + .getId(), tickerShare.share())) .toList(); diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java index 798a805..612a9bb 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java @@ -3,7 +3,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; -import nexters.payout.apiserver.dividend.application.dto.request.TickerShare; import java.util.List; diff --git a/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java b/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java index d86b91f..dd9a50f 100644 --- a/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java +++ b/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java @@ -7,7 +7,7 @@ import nexters.payout.apiserver.dividend.application.dto.request.TickerShare; import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse; -import nexters.payout.apiserver.stock.common.IntegrationTest; +import nexters.payout.apiserver.dividend.common.IntegrationTest; import nexters.payout.core.exception.ErrorResponse; import nexters.payout.core.time.InstantProvider; import nexters.payout.domain.DividendFixture; diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java index 535e0e0..984a220 100644 --- a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java @@ -1,41 +1,46 @@ package nexters.payout.apiserver.portfolio.application; -import nexters.payout.apiserver.dividend.application.dto.request.TickerShare; +import nexters.payout.apiserver.portfolio.application.dto.request.TickerShare; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; +import nexters.payout.apiserver.portfolio.common.GivenFixtureTest; import nexters.payout.core.time.InstantProvider; import nexters.payout.domain.PortfolioFixture; import nexters.payout.domain.StockFixture; +import nexters.payout.domain.portfolio.domain.Portfolio; import nexters.payout.domain.portfolio.domain.PortfolioStock; import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; import nexters.payout.domain.stock.domain.Sector; import nexters.payout.domain.stock.domain.Stock; -import nexters.payout.domain.stock.domain.repository.StockRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; +import static nexters.payout.domain.stock.domain.Sector.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static nexters.payout.domain.StockFixture.*; @ExtendWith(MockitoExtension.class) -class PortfolioQueryServiceTest { +class PortfolioQueryServiceTest extends GivenFixtureTest { @Mock private PortfolioRepository portfolioRepository; - @Mock - private StockRepository stockRepository; - @InjectMocks private PortfolioQueryService portfolioQueryService; @@ -44,10 +49,9 @@ class PortfolioQueryServiceTest { // given Stock appl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 4.0); Stock tsla = StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL, 2.2); - String expected = "67221662-c2f7-4f35-9447-6a65ca88d5ea"; + given(stockRepository.findByTicker(eq(AAPL))).willReturn(Optional.of(appl)); + given(stockRepository.findByTicker(eq(TSLA))).willReturn(Optional.of(tsla)); - given(stockRepository.findByTicker(AAPL)).willReturn(Optional.of(appl)); - given(stockRepository.findByTicker(TSLA)).willReturn(Optional.of(tsla)); given(portfolioRepository.save(any())).willReturn(PortfolioFixture.createPortfolio( UUID.fromString("67221662-c2f7-4f35-9447-6a65ca88d5ea"), InstantProvider.getExpireAt(), @@ -57,6 +61,7 @@ class PortfolioQueryServiceTest { ) ) ); + String expected = "67221662-c2f7-4f35-9447-6a65ca88d5ea"; // when PortfolioResponse actual = portfolioQueryService.createPortfolio(request()); @@ -65,6 +70,90 @@ class PortfolioQueryServiceTest { assertThat(actual.id()).isEqualTo(UUID.fromString(expected)); } + @Test + void 사용자의_월간_배당금_정보를_가져온다() { + // given + UUID id = UUID.fromString("bf5ffb6d-ae70-4171-8c86-b27c8ab2efbb"); + givenPortfolioForMonthlyDividend(id); + double expected = 86.8; + + // when + List actual = portfolioQueryService.getMonthlyDividends(id); + + // then + assertAll( + () -> assertThat(actual.size()).isEqualTo(12), + () -> assertThat(actual + .stream() + .mapToDouble(MonthlyDividendResponse::totalDividend) + .sum()).isEqualTo(expected), + () -> assertThat(actual.get(11).dividends().get(0).totalDividend()).isEqualTo(5.0) + ); + } + + @Test + void 사용자의_연간_배당금_정보를_가져온다() { + // given + UUID id = UUID.fromString("bf5ffb6d-ae70-4171-8c86-b27c8ab2efbb"); + givenPortfolioForYearlyDividend(id); + double totalDividendExpected = 86.8; + double aaplDividendExpected = 60.0; + + // when + YearlyDividendResponse actual = portfolioQueryService.getYearlyDividends(id); + + // then + assertAll( + () -> assertThat(actual.totalDividend()).isEqualTo(totalDividendExpected), + () -> assertThat(actual.dividends() + .stream() + .filter(dividend -> dividend.ticker().equals(AAPL)) + .findFirst().get() + .totalDividend()) + .isEqualTo(aaplDividendExpected) + ); + } + + private void givenPortfolioForMonthlyDividend(UUID id) { + Stock aapl = givenStockAndDividendForMonthly(AAPL, TECHNOLOGY, 2.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + Stock tsla = givenStockAndDividendForMonthly(TSLA, UTILITIES, 4.2, 1, 4, 7, 10); + Stock sbux = givenStockAndDividendForMonthly(SBUX, CONSUMER_CYCLICAL, 5.0, 6, 12); + + List portfolioStocks = new ArrayList<>(); + + portfolioStocks.add(new PortfolioStock(aapl.getId(), 2)); + portfolioStocks.add(new PortfolioStock(tsla.getId(), 1)); + portfolioStocks.add(new PortfolioStock(sbux.getId(), 1)); + + Portfolio portfolio = PortfolioFixture.createPortfolio( + id, + LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), + portfolioStocks + ); + + given(portfolioRepository.findById(eq(id))).willReturn(Optional.of(portfolio)); + } + + private void givenPortfolioForYearlyDividend(UUID id) { + Stock aapl = givenStockAndDividendForYearly(AAPL, TECHNOLOGY, 2.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + Stock tsla = givenStockAndDividendForYearly(TSLA, UTILITIES, 4.2, 1, 4, 7, 10); + Stock sbux = givenStockAndDividendForYearly(SBUX, CONSUMER_CYCLICAL, 5.0, 6, 12); + + List portfolioStocks = new ArrayList<>(); + + portfolioStocks.add(new PortfolioStock(aapl.getId(), 2)); + portfolioStocks.add(new PortfolioStock(tsla.getId(), 1)); + portfolioStocks.add(new PortfolioStock(sbux.getId(), 1)); + + Portfolio portfolio = PortfolioFixture.createPortfolio( + id, + LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), + portfolioStocks + ); + + given(portfolioRepository.findById(eq(id))).willReturn(Optional.of(portfolio)); + } + private PortfolioRequest request() { return new PortfolioRequest(List.of( new TickerShare(AAPL, 2), diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/GivenFixtureTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/GivenFixtureTest.java new file mode 100644 index 0000000..d55b349 --- /dev/null +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/GivenFixtureTest.java @@ -0,0 +1,94 @@ +package nexters.payout.apiserver.portfolio.common; + +import nexters.payout.core.time.InstantProvider; +import nexters.payout.domain.DividendFixture; +import nexters.payout.domain.StockFixture; +import nexters.payout.domain.dividend.domain.Dividend; +import nexters.payout.domain.dividend.domain.repository.DividendRepository; +import nexters.payout.domain.stock.domain.Sector; +import nexters.payout.domain.stock.domain.Stock; +import nexters.payout.domain.stock.domain.repository.StockRepository; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.*; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public abstract class GivenFixtureTest { + + private final Integer JANUARY = 1; + private final Integer DECEMBER = 12; + + @Mock + protected DividendRepository dividendRepository; + + @Mock + protected StockRepository stockRepository; + + public Stock givenStockAndDividendForMonthly(String ticker, Sector sector, double dividend, int... cycle) { + Stock stock = StockFixture.createStock(ticker, sector); + given(stockRepository.findById(eq(stock.getId()))).willReturn(Optional.of(stock)); + + for (int month = JANUARY; month <= DECEMBER; month++) { + if (isContain(cycle, month)) { + // 배당 주기에 해당하는 경우 + given(dividendRepository.findAllByIdAndYearAndMonth( + eq(stock.getId()), + eq(InstantProvider.getLastYear()), + eq(month))) + .willReturn(List.of(DividendFixture.createDividend( + stock.getId(), + dividend, + parseDate(InstantProvider.getLastYear(), month) + ))); + } else { + // 배당 주기에 해당하지 않는 경우 + given(dividendRepository.findAllByIdAndYearAndMonth( + eq(stock.getId()), + eq(InstantProvider.getLastYear()), + eq(month))) + .willReturn(new ArrayList<>()); + } + } + + return stock; + } + + public Stock givenStockAndDividendForYearly(String ticker, Sector sector, double dividend, int... cycle) { + Stock stock = StockFixture.createStock(ticker, sector); + given(stockRepository.findById(eq(stock.getId()))).willReturn(Optional.of(stock)); + + List dividends = new ArrayList<>(); + for (int month : cycle) { + dividends.add(DividendFixture.createDividend( + stock.getId(), + dividend, + parseDate(InstantProvider.getLastYear(), month))); + } + + given(dividendRepository.findAllByIdAndYear( + eq(stock.getId()), + eq(InstantProvider.getLastYear()))) + .willReturn(dividends); + + return stock; + } + + private boolean isContain(int[] cycle, int month) { + return Arrays.stream(cycle).anyMatch(m -> m == month); + } + + private Instant parseDate(int year, int month) { + LocalDate date = LocalDate.of(year, month, 1); + ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.of("UTC")); + return zonedDateTime.toInstant(); + } +} From a9e5567bfc8e3c048822b612a1ebeb06d9ef0ff8 Mon Sep 17 00:00:00 2001 From: Min-Ho CHO <66549638+chominho96@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:05:25 +0900 Subject: [PATCH 13/18] test: add portfolio controller test --- .../dividend/common/IntegrationTest.java | 36 +++ .../portfolio/common/IntegrationTest.java | 41 +++ .../presentation/PortfolioControllerTest.java | 269 ++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 api-server/src/test/java/nexters/payout/apiserver/dividend/common/IntegrationTest.java create mode 100644 api-server/src/test/java/nexters/payout/apiserver/portfolio/common/IntegrationTest.java create mode 100644 api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java diff --git a/api-server/src/test/java/nexters/payout/apiserver/dividend/common/IntegrationTest.java b/api-server/src/test/java/nexters/payout/apiserver/dividend/common/IntegrationTest.java new file mode 100644 index 0000000..f39d385 --- /dev/null +++ b/api-server/src/test/java/nexters/payout/apiserver/dividend/common/IntegrationTest.java @@ -0,0 +1,36 @@ +package nexters.payout.apiserver.dividend.common; + +import io.restassured.RestAssured; +import nexters.payout.domain.dividend.domain.repository.DividendRepository; +import nexters.payout.domain.stock.domain.repository.StockRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public abstract class IntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + public StockRepository stockRepository; + + @Autowired + public DividendRepository dividendRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @AfterEach + void afterEach() { + dividendRepository.deleteAll(); + stockRepository.deleteAll(); + } +} diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/IntegrationTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/IntegrationTest.java new file mode 100644 index 0000000..148c4a5 --- /dev/null +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/common/IntegrationTest.java @@ -0,0 +1,41 @@ +package nexters.payout.apiserver.portfolio.common; + +import io.restassured.RestAssured; +import nexters.payout.domain.dividend.domain.repository.DividendRepository; +import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; +import nexters.payout.domain.stock.domain.repository.StockRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public abstract class IntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + public StockRepository stockRepository; + + @Autowired + public DividendRepository dividendRepository; + + @Autowired + public PortfolioRepository portfolioRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @AfterEach + void afterEach() { + dividendRepository.deleteAll(); + stockRepository.deleteAll(); + portfolioRepository.deleteAll(); + } +} diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java new file mode 100644 index 0000000..5d7d63d --- /dev/null +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java @@ -0,0 +1,269 @@ +package nexters.payout.apiserver.portfolio.presentation; + +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.http.ContentType; +import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; +import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.request.TickerShare; +import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; +import nexters.payout.apiserver.portfolio.common.IntegrationTest; +import nexters.payout.core.exception.ErrorResponse; +import nexters.payout.core.time.InstantProvider; +import nexters.payout.domain.DividendFixture; +import nexters.payout.domain.PortfolioFixture; +import nexters.payout.domain.StockFixture; +import nexters.payout.domain.portfolio.domain.Portfolio; +import nexters.payout.domain.portfolio.domain.PortfolioStock; +import nexters.payout.domain.stock.domain.Sector; +import nexters.payout.domain.stock.domain.Stock; +import org.junit.jupiter.api.Test; + +import java.time.*; +import java.util.ArrayList; +import java.util.List; + +import static nexters.payout.domain.StockFixture.AAPL; +import static nexters.payout.domain.StockFixture.TSLA; +import static org.apache.http.HttpStatus.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class PortfolioControllerTest extends IntegrationTest { + + @Test + void 포트폴리오_생성시_티커를_찾을수_없는경우_404_예외가_발생한다() { + // given + stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL)); + + // when, then + RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .body(request()) + .when().post("api/portfolios") + .then().log().all() + .statusCode(SC_NOT_FOUND) + .extract() + .as(ErrorResponse.class); + } + + @Test + void 포트폴리오_생성시_빈_리스트로_요청한_경우_400_예외가_발생한다() { + // given + PortfolioRequest request = new PortfolioRequest(new ArrayList<>()); + + // when, then + RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("api/portfolios") + .then().log().all() + .statusCode(SC_BAD_REQUEST) + .extract() + .as(ErrorResponse.class); + } + + @Test + void 포트폴리오_생성시_티커가_빈문자열이면_400_예외가_발생한다() { + // given + PortfolioRequest request = new PortfolioRequest(List.of(new TickerShare("", 2))); + + // when, then + RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("api/portfolios") + .then().log().all() + .statusCode(SC_BAD_REQUEST) + .extract() + .as(ErrorResponse.class); + } + + @Test + void 포트폴리오_생성시__종목_소유_개수가_0개인_경우_400_예외가_발생한다() { + // given + stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL)); + PortfolioRequest request = new PortfolioRequest(List.of(new TickerShare(TSLA, 0))); + + // when, then + RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("api/portfolios") + .then().log().all() + .statusCode(SC_BAD_REQUEST) + .extract() + .as(ErrorResponse.class); + } + + @Test + void 월별_배당금_조회시_배당금이_존재하지_않는_경우_정상적으로_조회된다() { + // given + Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL)); + Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY)); + Portfolio portfolio = portfolioRepository.save(PortfolioFixture.createPortfolio( + LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), + List.of(new PortfolioStock(tsla.getId(), 2), new PortfolioStock(aapl.getId(), 1)) + ) + ); + double expected = 0.0; + + // when + List actual = RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .request() + .body(request()) + .when().get(String.format("api/portfolios/%s/monthly", portfolio.getId())) + .then().log().all() + .statusCode(SC_OK) + .extract() + .as(new TypeRef<>() { + }); + + assertAll( + () -> assertThat(actual + .stream() + .mapToDouble(MonthlyDividendResponse::totalDividend) + .sum()) + .isEqualTo(expected), + () -> actual.forEach(res -> assertThat(res.dividends()).isEmpty()) + ); + } + + @Test + void 월별_배당금_조회시_배당금이_존재하는_경우_정상적으로_조회된다() { + // given + Portfolio portfolio = stockAndDividendAndPortfolioGiven(); + double expected = 13.0; + + // when + List actual = RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .request() + .body(request()) + .when().get(String.format("api/portfolios/%s/monthly", portfolio.getId())) + .then().log().all() + .statusCode(SC_OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertAll( + () -> assertThat(actual + .stream() + .mapToDouble(MonthlyDividendResponse::totalDividend) + .sum()) + .isEqualTo(expected), + () -> assertThat(actual).hasSize(12) + ); + } + + @Test + void 연간_배당금_조회시_배당금이_존재하지_않는_경우_정상적으로_조회된다() { + // given + Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL)); + Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY)); + Portfolio portfolio = portfolioRepository.save(PortfolioFixture.createPortfolio( + LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), + List.of(new PortfolioStock(tsla.getId(), 2), new PortfolioStock(aapl.getId(), 1)) + ) + ); + double expected = 0.0; + + // when + YearlyDividendResponse actual = RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .request() + .body(request()) + .when().get(String.format("api/portfolios/%s/yearly", portfolio.getId())) + .then().log().all() + .statusCode(SC_OK) + .extract() + .as(new TypeRef<>() { + }); + + assertAll( + () -> assertThat(actual.totalDividend()).isEqualTo(expected), + () -> assertThat(actual.dividends()).isEmpty() + ); + } + + @Test + void 연간_배당금_조회시_배당금이_존재하는_경우_정상적으로_조회된다() { + // given + Portfolio portfolio = stockAndDividendAndPortfolioGiven(); + double expected = 13.0; + + // when + YearlyDividendResponse actual = RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .request() + .body(request()) + .when().get(String.format("api/portfolios/%s/yearly", portfolio.getId())) + .then().log().all() + .statusCode(SC_OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertAll( + () -> assertThat(actual.totalDividend()).isEqualTo(expected), + () -> assertThat(actual.dividends().size()).isEqualTo(2) + ); + } + + private PortfolioRequest request() { + return new PortfolioRequest(List.of( + new TickerShare(AAPL, 2), + new TickerShare(TSLA, 2) + )); + } + + private Portfolio stockAndDividendAndPortfolioGiven() { + Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY)); + Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL)); + + dividendRepository.save(DividendFixture.createDividend( + aapl.getId(), + 2.5, + parseDate(InstantProvider.getLastYear(), 1))); + dividendRepository.save(DividendFixture.createDividend( + aapl.getId(), + 2.5, + parseDate(InstantProvider.getLastYear(), 6))); + dividendRepository.save(DividendFixture.createDividend( + tsla.getId(), + 3.0, + parseDate(InstantProvider.getLastYear(), 6))); + + return portfolioRepository.save(PortfolioFixture.createPortfolio( + LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), + List.of(new PortfolioStock(aapl.getId(), 2), new PortfolioStock(tsla.getId(), 1)) + ) + ); + } + + private Instant parseDate(int year, int month) { + LocalDate date = LocalDate.of(year, month, 1); + ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.of("UTC")); + return zonedDateTime.toInstant(); + } +} From 41393a8a9f66af3346478556ed9f73e28a46deca Mon Sep 17 00:00:00 2001 From: Songyi Kim Date: Sat, 20 Apr 2024 20:10:48 +0900 Subject: [PATCH 14/18] feat: add sector-ratio service --- .../application/PortfolioQueryService.java | 73 ++++++++++++------- .../dto/response/SectorRatioResponse.java | 37 ++++++++++ .../domain/portfolio/domain/Portfolio.java | 7 +- .../portfolio/domain/PortfolioStocks.java | 2 +- 4 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SectorRatioResponse.java diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java index 8348b9d..5504f1e 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -2,12 +2,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; -import nexters.payout.apiserver.portfolio.application.dto.response.SingleYearlyDividendResponse; -import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; -import nexters.payout.apiserver.portfolio.application.dto.response.SingleMonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; -import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; +import nexters.payout.apiserver.portfolio.application.dto.response.*; +import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse; import nexters.payout.core.time.InstantProvider; import nexters.payout.domain.dividend.domain.Dividend; import nexters.payout.domain.dividend.domain.repository.DividendRepository; @@ -15,14 +12,19 @@ import nexters.payout.domain.portfolio.domain.PortfolioStock; import nexters.payout.domain.portfolio.domain.exception.PortfolioNotFoundException; import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; +import nexters.payout.domain.stock.domain.Sector; import nexters.payout.domain.stock.domain.Stock; import nexters.payout.domain.stock.domain.exception.StockIdNotFoundException; import nexters.payout.domain.stock.domain.exception.TickerNotFoundException; import nexters.payout.domain.stock.domain.repository.StockRepository; +import nexters.payout.domain.stock.domain.service.SectorAnalysisService; +import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo; +import nexters.payout.domain.stock.domain.service.SectorAnalysisService.StockShare; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,26 +38,35 @@ public class PortfolioQueryService { private final StockRepository stockRepository; private final PortfolioRepository portfolioRepository; private final DividendRepository dividendRepository; + private final SectorAnalysisService sectorAnalysisService; public PortfolioResponse createPortfolio(final PortfolioRequest request) { List portfolioStocks = request.tickerShares() - .stream().map(tickerShare -> new PortfolioStock( - stockRepository.findByTicker(tickerShare.ticker()) - .orElseThrow(() -> new TickerNotFoundException(tickerShare.ticker())) - .getId(), - tickerShare.share())) + .stream() + .map(tickerShare -> new PortfolioStock( + getStockByTicker(tickerShare.ticker()).getId(), + tickerShare.share()) + ) .toList(); - return new PortfolioResponse(portfolioRepository.save( - new Portfolio( - InstantProvider.getExpireAt(), - portfolioStocks - )).getId() + return new PortfolioResponse( + portfolioRepository.save(new Portfolio(InstantProvider.getExpireAt(), portfolioStocks)) + .getId() ); } + public List analyzeSectorRatio(final UUID portfolioId) { + List portfolioStocks = getPortfolio(portfolioId).portfolioStocks(); + List stockShares = portfolioStocks.stream() + .map(ps -> new StockShare(getStock(ps.getStockId()), ps.getShares())) + .toList(); + Map sectorInfoMap = sectorAnalysisService.calculateSectorRatios(stockShares); + + return SectorRatioResponse.fromMap(sectorInfoMap); + } + @Transactional(readOnly = true) public List getMonthlyDividends(final UUID id) { return InstantProvider.generateNext12Months() @@ -64,24 +75,36 @@ public List getMonthlyDividends(final UUID id) { yearMonth.getYear(), yearMonth.getMonthValue(), getDividendsOfLastYearAndMonth( - portfolioRepository.findById(id) - .orElseThrow(() -> new PortfolioNotFoundException(id)) - .getPortfolioStocks().getPortfolioStocks(), - yearMonth.getMonthValue()) + getPortfolio(id).portfolioStocks(), + yearMonth.getMonthValue() + ) ) ) .collect(Collectors.toList()); } + private Stock getStockByTicker(String ticker) { + return stockRepository.findByTicker(ticker) + .orElseThrow(() -> new TickerNotFoundException(ticker)); + } + + private Stock getStock(UUID stockId) { + return stockRepository.findById(stockId).orElseThrow(() -> new StockIdNotFoundException(stockId)); + } + + private Portfolio getPortfolio(UUID id) { + return portfolioRepository.findById(id) + .orElseThrow(() -> new PortfolioNotFoundException(id)); + } + @Transactional(readOnly = true) public YearlyDividendResponse getYearlyDividends(final UUID id) { - List dividends = portfolioRepository.findById(id) - .orElseThrow(() -> new PortfolioNotFoundException(id)) - .getPortfolioStocks().getPortfolioStocks() - .stream().map(portfolioStock -> { - Stock stock = stockRepository.findById(portfolioStock.getStockId()) - .orElseThrow(() -> new StockIdNotFoundException(portfolioStock.getStockId())); + List dividends = getPortfolio(id) + .portfolioStocks() + .stream() + .map(portfolioStock -> { + Stock stock = getStock(portfolioStock.getStockId()); return SingleYearlyDividendResponse.of( stock, portfolioStock.getShares(), getYearlyDividend(stock.getId()) ); diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SectorRatioResponse.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SectorRatioResponse.java new file mode 100644 index 0000000..5ba5a36 --- /dev/null +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/dto/response/SectorRatioResponse.java @@ -0,0 +1,37 @@ +package nexters.payout.apiserver.portfolio.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import nexters.payout.apiserver.stock.application.dto.response.StockShareResponse; +import nexters.payout.domain.stock.domain.Sector; +import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public record SectorRatioResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector name") + String sectorName, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector value") + String sectorValue, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector ratio") + Double sectorRatio, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List stockShares +) { + public static List fromMap(final Map sectorRatioMap) { + return sectorRatioMap.entrySet() + .stream() + .map(entry -> new SectorRatioResponse( + entry.getKey().getName(), + entry.getKey().name(), + entry.getValue().ratio(), + entry.getValue() + .stockShares() + .stream() + .map(StockShareResponse::from) + .collect(Collectors.toList())) + ) + .collect(Collectors.toList()); + } +} diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java index 1772de3..da8501f 100644 --- a/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java +++ b/domain/src/main/java/nexters/payout/domain/portfolio/domain/Portfolio.java @@ -1,16 +1,15 @@ package nexters.payout.domain.portfolio.domain; import jakarta.persistence.*; -import lombok.Getter; import nexters.payout.domain.BaseEntity; import java.time.Instant; +import java.util.Collections; import java.util.List; import java.util.UUID; @Entity -@Getter public class Portfolio extends BaseEntity { @Embedded @@ -34,6 +33,10 @@ public Portfolio(final Instant expireAt, List stocks) { this.expireAt = expireAt; } + public List portfolioStocks() { + return Collections.unmodifiableList(portfolioStocks.stockShares()); + } + public boolean isExpired() { return expireAt.isAfter(Instant.now()); } diff --git a/domain/src/main/java/nexters/payout/domain/portfolio/domain/PortfolioStocks.java b/domain/src/main/java/nexters/payout/domain/portfolio/domain/PortfolioStocks.java index 799ef58..bfe3e0b 100644 --- a/domain/src/main/java/nexters/payout/domain/portfolio/domain/PortfolioStocks.java +++ b/domain/src/main/java/nexters/payout/domain/portfolio/domain/PortfolioStocks.java @@ -25,7 +25,7 @@ public PortfolioStocks(List stocks) { portfolioStocks = stocks; } - public List getPortfolioStocks() { + public List stockShares() { return Collections.unmodifiableList(portfolioStocks); } } From fd284404daadfc984ff9de0703dc91a462850ce1 Mon Sep 17 00:00:00 2001 From: Songyi Kim Date: Sat, 20 Apr 2024 20:11:02 +0900 Subject: [PATCH 15/18] feat: update portfolio controller --- .../portfolio/presentation/PortfolioController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java index aadef1d..ccde91d 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java @@ -8,6 +8,8 @@ import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; +import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest; +import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -36,4 +38,9 @@ public ResponseEntity> getMonthlyDividends(@PathVa public ResponseEntity getYearlyDividends(@PathVariable("id") final UUID portfolioId) { return ResponseEntity.ok(portfolioQueryService.getYearlyDividends(portfolioId)); } + + @GetMapping("/{id}/sector-ratio") + public ResponseEntity> getSectorRatios(@PathVariable("id") final UUID portfolioId) { + return ResponseEntity.ok(portfolioQueryService.analyzeSectorRatio(portfolioId)); + } } From 2527f261aadd629e95982e5f8dab0ca308112177 Mon Sep 17 00:00:00 2001 From: Songyi Kim Date: Sat, 20 Apr 2024 20:11:14 +0900 Subject: [PATCH 16/18] test: add test code --- .../presentation/PortfolioControllerTest.java | 41 +++++++++++++++++++ .../payout/domain/PortfolioFixture.java | 16 +++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java index 5d7d63d..6231ff0 100644 --- a/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerTest.java @@ -6,6 +6,7 @@ import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; import nexters.payout.apiserver.portfolio.application.dto.request.TickerShare; +import nexters.payout.apiserver.portfolio.application.dto.response.SectorRatioResponse; import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; import nexters.payout.apiserver.portfolio.common.IntegrationTest; import nexters.payout.core.exception.ErrorResponse; @@ -17,11 +18,15 @@ import nexters.payout.domain.portfolio.domain.PortfolioStock; import nexters.payout.domain.stock.domain.Sector; import nexters.payout.domain.stock.domain.Stock; +import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; import java.time.*; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; import static nexters.payout.domain.StockFixture.AAPL; import static nexters.payout.domain.StockFixture.TSLA; @@ -104,6 +109,42 @@ public class PortfolioControllerTest extends IntegrationTest { .as(ErrorResponse.class); } + @Test + void 사용자의_섹터_비중을_분석한다() { + // given + Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL, 10.0)); + Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 20.0)); + Portfolio portfolio = portfolioRepository.save(PortfolioFixture.createPortfolio( + List.of(new PortfolioStock(tsla.getId(), 1), new PortfolioStock(aapl.getId(), 1)) + ) + ); + + // when + List actual = RestAssured + .given() + .log().all() + .contentType(ContentType.JSON) + .body(request()) + .when().get(String.format("api/portfolios/%s/sector-ratio", portfolio.getId())) + .then().log().all() + .statusCode(SC_OK) + .extract() + .as(new TypeRef<>() { + }); + + List sorted = actual.stream() + .sorted(Comparator.comparing(SectorRatioResponse::sectorRatio)) + .toList(); + // then + assertAll( + () -> assertThat(sorted).hasSize(2), + () -> assertThat(sorted.get(0).sectorRatio()).isCloseTo(0.33, Offset.offset(0.01)), + () -> assertThat(sorted.get(0).sectorName()).isEqualTo(Sector.CONSUMER_CYCLICAL.getName()), + () -> assertThat(sorted.get(1).sectorRatio()).isCloseTo(0.66, Offset.offset(0.01)), + () -> assertThat(sorted.get(1).sectorName()).isEqualTo(Sector.TECHNOLOGY.getName()) + ); + } + @Test void 월별_배당금_조회시_배당금이_존재하지_않는_경우_정상적으로_조회된다() { // given diff --git a/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java b/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java index 4d2eb3d..542b857 100644 --- a/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java +++ b/domain/src/testFixtures/java/nexters/payout/domain/PortfolioFixture.java @@ -13,18 +13,14 @@ public class PortfolioFixture { public static UUID STOCK_ID = UUID.randomUUID(); public static Portfolio createPortfolio(UUID id, Instant expireAt, List stocks) { - return new Portfolio( - id, - expireAt, - stocks - ); + return new Portfolio(id, expireAt, stocks); } public static Portfolio createPortfolio(Instant expireAt, List stocks) { - return new Portfolio( - UUID.randomUUID(), - expireAt, - stocks - ); + return new Portfolio(UUID.randomUUID(), expireAt, stocks); + } + + public static Portfolio createPortfolio(List stocks) { + return new Portfolio(UUID.randomUUID(), Instant.now(), stocks); } } From 425bc77e0b1ed347ccab103e1f0d7a6c32d4cf92 Mon Sep 17 00:00:00 2001 From: Songyi Kim Date: Sat, 20 Apr 2024 20:32:10 +0900 Subject: [PATCH 17/18] test: add service test code --- .../application/PortfolioQueryService.java | 3 +- .../PortfolioQueryServiceTest.java | 48 ++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java index 5504f1e..9da807a 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java @@ -59,7 +59,8 @@ public PortfolioResponse createPortfolio(final PortfolioRequest request) { public List analyzeSectorRatio(final UUID portfolioId) { List portfolioStocks = getPortfolio(portfolioId).portfolioStocks(); - List stockShares = portfolioStocks.stream() + List stockShares = portfolioStocks + .stream() .map(ps -> new StockShare(getStock(ps.getStockId()), ps.getShares())) .toList(); Map sectorInfoMap = sectorAnalysisService.calculateSectorRatios(stockShares); diff --git a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java index 984a220..cc9c8bb 100644 --- a/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java +++ b/api-server/src/test/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryServiceTest.java @@ -1,23 +1,27 @@ package nexters.payout.apiserver.portfolio.application; -import nexters.payout.apiserver.portfolio.application.dto.request.TickerShare; import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest; +import nexters.payout.apiserver.portfolio.application.dto.request.TickerShare; import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; import nexters.payout.apiserver.portfolio.common.GivenFixtureTest; +import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse; +import nexters.payout.apiserver.stock.application.dto.response.StockResponse; +import nexters.payout.apiserver.stock.application.dto.response.StockShareResponse; import nexters.payout.core.time.InstantProvider; -import nexters.payout.domain.PortfolioFixture; import nexters.payout.domain.StockFixture; import nexters.payout.domain.portfolio.domain.Portfolio; import nexters.payout.domain.portfolio.domain.PortfolioStock; import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository; import nexters.payout.domain.stock.domain.Sector; import nexters.payout.domain.stock.domain.Stock; +import nexters.payout.domain.stock.domain.service.SectorAnalysisService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; @@ -27,13 +31,14 @@ import java.util.Optional; import java.util.UUID; +import static nexters.payout.domain.PortfolioFixture.createPortfolio; +import static nexters.payout.domain.StockFixture.*; import static nexters.payout.domain.stock.domain.Sector.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static nexters.payout.domain.StockFixture.*; @ExtendWith(MockitoExtension.class) class PortfolioQueryServiceTest extends GivenFixtureTest { @@ -44,6 +49,9 @@ class PortfolioQueryServiceTest extends GivenFixtureTest { @InjectMocks private PortfolioQueryService portfolioQueryService; + @Spy + private SectorAnalysisService sectorAnalysisService; + @Test void 포트폴리오를_생성한다() { // given @@ -52,7 +60,7 @@ class PortfolioQueryServiceTest extends GivenFixtureTest { given(stockRepository.findByTicker(eq(AAPL))).willReturn(Optional.of(appl)); given(stockRepository.findByTicker(eq(TSLA))).willReturn(Optional.of(tsla)); - given(portfolioRepository.save(any())).willReturn(PortfolioFixture.createPortfolio( + given(portfolioRepository.save(any())).willReturn(createPortfolio( UUID.fromString("67221662-c2f7-4f35-9447-6a65ca88d5ea"), InstantProvider.getExpireAt(), List.of( @@ -70,6 +78,34 @@ class PortfolioQueryServiceTest extends GivenFixtureTest { assertThat(actual.id()).isEqualTo(UUID.fromString(expected)); } + @Test + void 섹터_정보를_정상적으로_반환한다() { + // given + Stock aapl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 2.0); + UUID portfolioId = UUID.randomUUID(); + given(portfolioRepository.findById(portfolioId)).willReturn(Optional.of( + createPortfolio( + List.of(new PortfolioStock(aapl.getId(), 2)) + )) + ); + given(stockRepository.findById(any())).willReturn(Optional.of(aapl)); + + List expected = List.of( + new SectorRatioResponse( + Sector.TECHNOLOGY.getName(), + Sector.TECHNOLOGY.name(), + 1.0, List.of(new StockShareResponse(StockResponse.from(aapl), 2)) + ) + ); + + // when + List actual = portfolioQueryService.analyzeSectorRatio(portfolioId); + + // then + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test void 사용자의_월간_배당금_정보를_가져온다() { // given @@ -125,7 +161,7 @@ private void givenPortfolioForMonthlyDividend(UUID id) { portfolioStocks.add(new PortfolioStock(tsla.getId(), 1)); portfolioStocks.add(new PortfolioStock(sbux.getId(), 1)); - Portfolio portfolio = PortfolioFixture.createPortfolio( + Portfolio portfolio = createPortfolio( id, LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), portfolioStocks @@ -145,7 +181,7 @@ private void givenPortfolioForYearlyDividend(UUID id) { portfolioStocks.add(new PortfolioStock(tsla.getId(), 1)); portfolioStocks.add(new PortfolioStock(sbux.getId(), 1)); - Portfolio portfolio = PortfolioFixture.createPortfolio( + Portfolio portfolio = createPortfolio( id, LocalDate.now().plusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC), portfolioStocks From e522da372ee282fcfb0457e21bffd2f09a24812a Mon Sep 17 00:00:00 2001 From: Songyi Kim Date: Sun, 21 Apr 2024 13:09:06 +0900 Subject: [PATCH 18/18] feat: update swagger docs --- .../presentation/PortfolioControllerDocs.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java index b7764f2..c31ccd7 100644 --- a/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java +++ b/api-server/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioControllerDocs.java @@ -12,6 +12,7 @@ import nexters.payout.apiserver.portfolio.application.dto.response.MonthlyDividendResponse; import nexters.payout.apiserver.portfolio.application.dto.response.PortfolioResponse; import nexters.payout.apiserver.portfolio.application.dto.response.YearlyDividendResponse; +import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse; import nexters.payout.core.exception.ErrorResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -71,4 +72,16 @@ ResponseEntity getYearlyDividends( @Parameter(description = "portfolio id", example = "bf5ffb6d-ae70-4171-8c86-b27c8ab2efbb", required = true) @PathVariable("id") final UUID portfolioId ); + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "SUCCESS"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "NOT FOUND", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "SERVER ERROR", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}) + }) + @Operation(summary = "섹터 비중 분석") + ResponseEntity> getSectorRatios(@PathVariable("id") final UUID portfolioId); }