-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: remove unnecessary import * feat: add portfolio command service * feat: add portfolio query service * feat: add portfolio controller * docs: add portfolio controller docs * fix: remove portfolio command service * test: add test for create portfolio api * feat: add monthly/yearly dividend api * feat: add monthly/yearly dividend api * docs: add swagger docs * feat: implement dividend repository custom * test: add portfolio query service test * test: add portfolio controller test * feat: add sector-ratio service * feat: update portfolio controller * test: add test code * test: add service test code * feat: update swagger docs --------- Co-authored-by: Songyi Kim <[email protected]>
- Loading branch information
1 parent
ab10b87
commit 88f0363
Showing
25 changed files
with
1,231 additions
and
6 deletions.
There are no files selected for viewing
152 changes: 152 additions & 0 deletions
152
...r/src/main/java/nexters/payout/apiserver/portfolio/application/PortfolioQueryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package nexters.payout.apiserver.portfolio.application; | ||
|
||
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.*; | ||
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; | ||
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.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; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional | ||
@Slf4j | ||
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<PortfolioStock> portfolioStocks = | ||
request.tickerShares() | ||
.stream() | ||
.map(tickerShare -> new PortfolioStock( | ||
getStockByTicker(tickerShare.ticker()).getId(), | ||
tickerShare.share()) | ||
) | ||
.toList(); | ||
|
||
return new PortfolioResponse( | ||
portfolioRepository.save(new Portfolio(InstantProvider.getExpireAt(), portfolioStocks)) | ||
.getId() | ||
); | ||
} | ||
|
||
public List<SectorRatioResponse> analyzeSectorRatio(final UUID portfolioId) { | ||
List<PortfolioStock> portfolioStocks = getPortfolio(portfolioId).portfolioStocks(); | ||
List<StockShare> stockShares = portfolioStocks | ||
.stream() | ||
.map(ps -> new StockShare(getStock(ps.getStockId()), ps.getShares())) | ||
.toList(); | ||
Map<Sector, SectorInfo> sectorInfoMap = sectorAnalysisService.calculateSectorRatios(stockShares); | ||
|
||
return SectorRatioResponse.fromMap(sectorInfoMap); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
public List<MonthlyDividendResponse> getMonthlyDividends(final UUID id) { | ||
return InstantProvider.generateNext12Months() | ||
.stream() | ||
.map(yearMonth -> MonthlyDividendResponse.of( | ||
yearMonth.getYear(), | ||
yearMonth.getMonthValue(), | ||
getDividendsOfLastYearAndMonth( | ||
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<SingleYearlyDividendResponse> dividends = getPortfolio(id) | ||
.portfolioStocks() | ||
.stream() | ||
.map(portfolioStock -> { | ||
Stock stock = getStock(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<Dividend> getLastYearDividendsByStockId(final UUID id) { | ||
return dividendRepository.findAllByIdAndYear(id, InstantProvider.getLastYear()); | ||
} | ||
|
||
private List<SingleMonthlyDividendResponse> getDividendsOfLastYearAndMonth( | ||
final List<PortfolioStock> 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<SingleMonthlyDividendResponse> 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<Dividend> getLastYearDividendsByStockIdAndMonth(final UUID stockId, final int month) { | ||
return dividendRepository.findAllByIdAndYearAndMonth(stockId, InstantProvider.getLastYear(), month); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...ain/java/nexters/payout/apiserver/portfolio/application/dto/request/PortfolioRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
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 java.util.List; | ||
|
||
public record PortfolioRequest( | ||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) | ||
@Valid | ||
@Size(min = 1) | ||
List<TickerShare> tickerShares | ||
) { | ||
} |
14 changes: 14 additions & 0 deletions
14
...src/main/java/nexters/payout/apiserver/portfolio/application/dto/request/TickerShare.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) { } |
32 changes: 32 additions & 0 deletions
32
.../nexters/payout/apiserver/portfolio/application/dto/response/MonthlyDividendResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SingleMonthlyDividendResponse> dividends, | ||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) | ||
Double totalDividend | ||
) { | ||
public static MonthlyDividendResponse of( | ||
final int year, final int month, final List<SingleMonthlyDividendResponse> dividends | ||
) { | ||
return new MonthlyDividendResponse( | ||
year, | ||
month, | ||
dividends.stream() | ||
.sorted(Comparator.comparingDouble(SingleMonthlyDividendResponse::totalDividend).reversed()) | ||
.toList(), | ||
dividends.stream() | ||
.mapToDouble(SingleMonthlyDividendResponse::totalDividend) | ||
.sum() | ||
); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
...n/java/nexters/payout/apiserver/portfolio/application/dto/response/PortfolioResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) { | ||
} |
37 changes: 37 additions & 0 deletions
37
...java/nexters/payout/apiserver/portfolio/application/dto/response/SectorRatioResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<StockShareResponse> stockShares | ||
) { | ||
public static List<SectorRatioResponse> fromMap(final Map<Sector, SectorInfo> 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()); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...rs/payout/apiserver/portfolio/application/dto/response/SingleMonthlyDividendResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...ers/payout/apiserver/portfolio/application/dto/response/SingleYearlyDividendResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...a/nexters/payout/apiserver/portfolio/application/dto/response/YearlyDividendResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SingleYearlyDividendResponse> dividends, | ||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) | ||
Double totalDividend | ||
) { | ||
public static YearlyDividendResponse of(List<SingleYearlyDividendResponse> dividends) { | ||
|
||
dividends = dividends | ||
.stream() | ||
.sorted(Comparator.comparingDouble(SingleYearlyDividendResponse::totalDividend).reversed()) | ||
.toList(); | ||
return new YearlyDividendResponse( | ||
dividends, | ||
dividends | ||
.stream() | ||
.mapToDouble(SingleYearlyDividendResponse::totalDividend) | ||
.sum() | ||
); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
...er/src/main/java/nexters/payout/apiserver/portfolio/presentation/PortfolioController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
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.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.*; | ||
|
||
import java.util.List; | ||
import java.util.UUID; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/portfolios") | ||
@Slf4j | ||
public class PortfolioController implements PortfolioControllerDocs { | ||
|
||
private final PortfolioQueryService portfolioQueryService; | ||
|
||
@PostMapping | ||
public ResponseEntity<PortfolioResponse> createPortfolio(@RequestBody @Valid final PortfolioRequest portfolioRequest) { | ||
return ResponseEntity.ok(portfolioQueryService.createPortfolio(portfolioRequest)); | ||
} | ||
|
||
@GetMapping("/{id}/monthly") | ||
public ResponseEntity<List<MonthlyDividendResponse>> getMonthlyDividends(@PathVariable("id") final UUID portfolioId) { | ||
return ResponseEntity.ok(portfolioQueryService.getMonthlyDividends(portfolioId)); | ||
} | ||
|
||
@GetMapping("/{id}/yearly") | ||
public ResponseEntity<YearlyDividendResponse> getYearlyDividends(@PathVariable("id") final UUID portfolioId) { | ||
return ResponseEntity.ok(portfolioQueryService.getYearlyDividends(portfolioId)); | ||
} | ||
|
||
@GetMapping("/{id}/sector-ratio") | ||
public ResponseEntity<List<SectorRatioResponse>> getSectorRatios(@PathVariable("id") final UUID portfolioId) { | ||
return ResponseEntity.ok(portfolioQueryService.analyzeSectorRatio(portfolioId)); | ||
} | ||
} |
Oops, something went wrong.