-
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.
* refactor: change dependency direction (#87) * chore: add auto generated qclass to .gitignore * fix: failed test by current time * refactor: apply dip for package dependency * chore: update interface name * test: modify test data * fix: test code * feat: add portfolio, portfolio stock entity (#89) * fix: fix test date time issue * feat: add portfolio, portfolio stock entity * feat: add portfolio, portfolio stock repository * refactor: refactor package structure * test: add test fixture * setting: add db configuration * fix: add @entity annotation * fix: fix portfolio stock to element collection * fix: remove portfolio id from PoltfolioStock * setting: fix db configuration * refactor: delete create method from portfolio entity * feat: implement portfolio batch service (#92) * refactor: divide client package * feat: implement portfolio batch service * test: add test code * test: update test code * chore: update delete query * refactor: refactor portfolio stock domain * feat: implement portfolio api (#93) * 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]> * feat: implement read portfolio event (#95) * feat:wip add portfolio event * feat: add hits to portfolio * feat:wip add portfolio event * feat: add increment hits consumer * feat: add read portfolio event * feat: implement lock for portfolio hits (concurrency) * feat: set version initial value * feat: set version initial value * test: add read portfolio test * test: fix latch * test: fix concurrency test * test: remove event test from portfolio query service test * chore: add event log * chore: fix order of log --------- Co-authored-by: Songyi Kim <[email protected]> --------- Co-authored-by: Songyi Kim <[email protected]> Co-authored-by: Songyi Kim <[email protected]>
- Loading branch information
1 parent
6fba9c6
commit eea59fe
Showing
59 changed files
with
1,803 additions
and
191 deletions.
There are no files selected for viewing
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
86 changes: 86 additions & 0 deletions
86
...ain/java/nexters/payout/apiserver/dividend/application/StockDividendQueryServiceImpl.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,86 @@ | ||
package nexters.payout.apiserver.dividend.application; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import nexters.payout.apiserver.stock.application.StockDividendQueryService; | ||
import nexters.payout.apiserver.stock.application.dto.response.DividendResponse; | ||
import nexters.payout.apiserver.stock.application.dto.response.StockDetailResponse; | ||
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.stock.domain.Stock; | ||
import nexters.payout.domain.stock.domain.exception.TickerNotFoundException; | ||
import nexters.payout.domain.stock.domain.repository.StockRepository; | ||
import nexters.payout.domain.stock.domain.service.StockDividendAnalysisService; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.time.Month; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional(readOnly = true) | ||
public class StockDividendQueryServiceImpl implements StockDividendQueryService { | ||
|
||
private final StockDividendAnalysisService dividendAnalysisService; | ||
private final StockRepository stockRepository; | ||
private final DividendRepository dividendRepository; | ||
|
||
public StockDetailResponse getStockByTicker(final String ticker) { | ||
Stock stock = getStock(ticker); | ||
|
||
List<Dividend> lastYearDividends = getLastYearDividends(stock); | ||
List<Dividend> thisYearDividends = getThisYearDividends(stock); | ||
|
||
if (lastYearDividends.isEmpty() && thisYearDividends.isEmpty()) { | ||
return StockDetailResponse.of(stock, DividendResponse.noDividend()); | ||
} | ||
|
||
List<Month> dividendMonths = dividendAnalysisService.calculateDividendMonths(stock, lastYearDividends); | ||
Double dividendYield = dividendAnalysisService.calculateDividendYield(stock, lastYearDividends); | ||
Double dividendPerShare = dividendAnalysisService.calculateAverageDividend( | ||
combinedDividends(lastYearDividends, thisYearDividends) | ||
); | ||
|
||
return dividendAnalysisService.findUpcomingDividend(lastYearDividends, thisYearDividends) | ||
.map(upcomingDividend -> StockDetailResponse.of( | ||
stock, | ||
DividendResponse.fullDividendInfo(upcomingDividend, dividendYield, dividendMonths) | ||
)) | ||
.orElse(StockDetailResponse.of( | ||
stock, | ||
DividendResponse.withoutDividendDates(dividendPerShare, dividendYield, dividendMonths) | ||
)); | ||
} | ||
|
||
private List<Dividend> combinedDividends(final List<Dividend> lastYearDividends, final List<Dividend> thisYearDividends) { | ||
return Stream.of(lastYearDividends, thisYearDividends) | ||
.flatMap(List::stream) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private Stock getStock(final String ticker) { | ||
return stockRepository.findByTicker(ticker) | ||
.orElseThrow(() -> new TickerNotFoundException(ticker)); | ||
} | ||
|
||
private List<Dividend> getLastYearDividends(final Stock stock) { | ||
int lastYear = InstantProvider.getLastYear(); | ||
|
||
return dividendRepository.findAllByStockId(stock.getId()) | ||
.stream() | ||
.filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == lastYear) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private List<Dividend> getThisYearDividends(final Stock stock) { | ||
int thisYear = InstantProvider.getThisYear(); | ||
|
||
return dividendRepository.findAllByStockId(stock.getId()) | ||
.stream() | ||
.filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == thisYear) | ||
.collect(Collectors.toList()); | ||
} | ||
} |
149 changes: 149 additions & 0 deletions
149
...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,149 @@ | ||
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(it -> new PortfolioStock(getStockByTicker(it.ticker()).getId(), it.share())) | ||
.toList(); | ||
|
||
return new PortfolioResponse( | ||
portfolioRepository.save(new Portfolio(InstantProvider.getExpireAt(), portfolioStocks)) | ||
.getId() | ||
); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
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 | ||
); | ||
} | ||
} |
Oops, something went wrong.