Skip to content

Commit

Permalink
feat: merge develop into main (#97)
Browse files Browse the repository at this point in the history
* 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 <ksl2950@o.cnu.ac.kr>

* 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 <ksl2950@o.cnu.ac.kr>

---------

Co-authored-by: Songyi Kim <52441906+songyi00@users.noreply.github.com>
Co-authored-by: Songyi Kim <ksl2950@o.cnu.ac.kr>
  • Loading branch information
3 people authored Apr 26, 2024
1 parent 6fba9c6 commit eea59fe
Show file tree
Hide file tree
Showing 59 changed files with 1,803 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ out/

**/logs
**/db/data
domain/src/main/generated/nexters/payout/domain/stock/domain/QStock.java
**/src/main/generated/**
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());
}
}
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);
}
}
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
) {
}
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
) { }
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()
);
}
}
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
) {
}
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());
}
}
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
);
}
}
Loading

0 comments on commit eea59fe

Please sign in to comment.