Skip to content

Commit eea59fe

Browse files
chominho96songyi00
andauthored
feat: merge develop into main (#97)
* 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]>
1 parent 6fba9c6 commit eea59fe

File tree

59 files changed

+1803
-191
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1803
-191
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ out/
3939

4040
**/logs
4141
**/db/data
42-
domain/src/main/generated/nexters/payout/domain/stock/domain/QStock.java
42+
**/src/main/generated/**
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package nexters.payout.apiserver.dividend.application;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import nexters.payout.apiserver.stock.application.StockDividendQueryService;
5+
import nexters.payout.apiserver.stock.application.dto.response.DividendResponse;
6+
import nexters.payout.apiserver.stock.application.dto.response.StockDetailResponse;
7+
import nexters.payout.core.time.InstantProvider;
8+
import nexters.payout.domain.dividend.domain.Dividend;
9+
import nexters.payout.domain.dividend.domain.repository.DividendRepository;
10+
import nexters.payout.domain.stock.domain.Stock;
11+
import nexters.payout.domain.stock.domain.exception.TickerNotFoundException;
12+
import nexters.payout.domain.stock.domain.repository.StockRepository;
13+
import nexters.payout.domain.stock.domain.service.StockDividendAnalysisService;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
import java.time.Month;
18+
import java.util.List;
19+
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
@Transactional(readOnly = true)
25+
public class StockDividendQueryServiceImpl implements StockDividendQueryService {
26+
27+
private final StockDividendAnalysisService dividendAnalysisService;
28+
private final StockRepository stockRepository;
29+
private final DividendRepository dividendRepository;
30+
31+
public StockDetailResponse getStockByTicker(final String ticker) {
32+
Stock stock = getStock(ticker);
33+
34+
List<Dividend> lastYearDividends = getLastYearDividends(stock);
35+
List<Dividend> thisYearDividends = getThisYearDividends(stock);
36+
37+
if (lastYearDividends.isEmpty() && thisYearDividends.isEmpty()) {
38+
return StockDetailResponse.of(stock, DividendResponse.noDividend());
39+
}
40+
41+
List<Month> dividendMonths = dividendAnalysisService.calculateDividendMonths(stock, lastYearDividends);
42+
Double dividendYield = dividendAnalysisService.calculateDividendYield(stock, lastYearDividends);
43+
Double dividendPerShare = dividendAnalysisService.calculateAverageDividend(
44+
combinedDividends(lastYearDividends, thisYearDividends)
45+
);
46+
47+
return dividendAnalysisService.findUpcomingDividend(lastYearDividends, thisYearDividends)
48+
.map(upcomingDividend -> StockDetailResponse.of(
49+
stock,
50+
DividendResponse.fullDividendInfo(upcomingDividend, dividendYield, dividendMonths)
51+
))
52+
.orElse(StockDetailResponse.of(
53+
stock,
54+
DividendResponse.withoutDividendDates(dividendPerShare, dividendYield, dividendMonths)
55+
));
56+
}
57+
58+
private List<Dividend> combinedDividends(final List<Dividend> lastYearDividends, final List<Dividend> thisYearDividends) {
59+
return Stream.of(lastYearDividends, thisYearDividends)
60+
.flatMap(List::stream)
61+
.collect(Collectors.toList());
62+
}
63+
64+
private Stock getStock(final String ticker) {
65+
return stockRepository.findByTicker(ticker)
66+
.orElseThrow(() -> new TickerNotFoundException(ticker));
67+
}
68+
69+
private List<Dividend> getLastYearDividends(final Stock stock) {
70+
int lastYear = InstantProvider.getLastYear();
71+
72+
return dividendRepository.findAllByStockId(stock.getId())
73+
.stream()
74+
.filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == lastYear)
75+
.collect(Collectors.toList());
76+
}
77+
78+
private List<Dividend> getThisYearDividends(final Stock stock) {
79+
int thisYear = InstantProvider.getThisYear();
80+
81+
return dividendRepository.findAllByStockId(stock.getId())
82+
.stream()
83+
.filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == thisYear)
84+
.collect(Collectors.toList());
85+
}
86+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package nexters.payout.apiserver.portfolio.application;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import nexters.payout.apiserver.portfolio.application.dto.request.PortfolioRequest;
6+
import nexters.payout.apiserver.portfolio.application.dto.response.*;
7+
import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse;
8+
import nexters.payout.core.time.InstantProvider;
9+
import nexters.payout.domain.dividend.domain.Dividend;
10+
import nexters.payout.domain.dividend.domain.repository.DividendRepository;
11+
import nexters.payout.domain.portfolio.domain.Portfolio;
12+
import nexters.payout.domain.portfolio.domain.PortfolioStock;
13+
import nexters.payout.domain.portfolio.domain.exception.PortfolioNotFoundException;
14+
import nexters.payout.domain.portfolio.domain.repository.PortfolioRepository;
15+
import nexters.payout.domain.stock.domain.Sector;
16+
import nexters.payout.domain.stock.domain.Stock;
17+
import nexters.payout.domain.stock.domain.exception.StockIdNotFoundException;
18+
import nexters.payout.domain.stock.domain.exception.TickerNotFoundException;
19+
import nexters.payout.domain.stock.domain.repository.StockRepository;
20+
import nexters.payout.domain.stock.domain.service.SectorAnalysisService;
21+
import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo;
22+
import nexters.payout.domain.stock.domain.service.SectorAnalysisService.StockShare;
23+
import org.springframework.stereotype.Service;
24+
import org.springframework.transaction.annotation.Transactional;
25+
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.UUID;
29+
import java.util.stream.Collectors;
30+
import java.util.stream.Stream;
31+
32+
@Service
33+
@RequiredArgsConstructor
34+
@Transactional
35+
@Slf4j
36+
public class PortfolioQueryService {
37+
38+
private final StockRepository stockRepository;
39+
private final PortfolioRepository portfolioRepository;
40+
private final DividendRepository dividendRepository;
41+
private final SectorAnalysisService sectorAnalysisService;
42+
43+
public PortfolioResponse createPortfolio(final PortfolioRequest request) {
44+
45+
List<PortfolioStock> portfolioStocks =
46+
request.tickerShares()
47+
.stream()
48+
.map(it -> new PortfolioStock(getStockByTicker(it.ticker()).getId(), it.share()))
49+
.toList();
50+
51+
return new PortfolioResponse(
52+
portfolioRepository.save(new Portfolio(InstantProvider.getExpireAt(), portfolioStocks))
53+
.getId()
54+
);
55+
}
56+
57+
@Transactional(readOnly = true)
58+
public List<SectorRatioResponse> analyzeSectorRatio(final UUID portfolioId) {
59+
List<PortfolioStock> portfolioStocks = getPortfolio(portfolioId).portfolioStocks();
60+
List<StockShare> stockShares = portfolioStocks
61+
.stream()
62+
.map(ps -> new StockShare(getStock(ps.getStockId()), ps.getShares()))
63+
.toList();
64+
Map<Sector, SectorInfo> sectorInfoMap = sectorAnalysisService.calculateSectorRatios(stockShares);
65+
return SectorRatioResponse.fromMap(sectorInfoMap);
66+
}
67+
68+
@Transactional(readOnly = true)
69+
public List<MonthlyDividendResponse> getMonthlyDividends(final UUID id) {
70+
return InstantProvider.generateNext12Months()
71+
.stream()
72+
.map(yearMonth -> MonthlyDividendResponse.of(
73+
yearMonth.getYear(),
74+
yearMonth.getMonthValue(),
75+
getDividendsOfLastYearAndMonth(
76+
getPortfolio(id).portfolioStocks(),
77+
yearMonth.getMonthValue()
78+
)
79+
)
80+
)
81+
.collect(Collectors.toList());
82+
}
83+
84+
private Stock getStockByTicker(String ticker) {
85+
return stockRepository.findByTicker(ticker)
86+
.orElseThrow(() -> new TickerNotFoundException(ticker));
87+
}
88+
89+
private Stock getStock(UUID stockId) {
90+
return stockRepository.findById(stockId).orElseThrow(() -> new StockIdNotFoundException(stockId));
91+
}
92+
93+
private Portfolio getPortfolio(UUID id) {
94+
return portfolioRepository.findById(id)
95+
.orElseThrow(() -> new PortfolioNotFoundException(id));
96+
}
97+
98+
@Transactional(readOnly = true)
99+
public YearlyDividendResponse getYearlyDividends(final UUID id) {
100+
101+
List<SingleYearlyDividendResponse> dividends = getPortfolio(id)
102+
.portfolioStocks()
103+
.stream()
104+
.map(portfolioStock -> {
105+
Stock stock = getStock(portfolioStock.getStockId());
106+
return SingleYearlyDividendResponse.of(
107+
stock, portfolioStock.getShares(), getYearlyDividend(stock.getId())
108+
);
109+
})
110+
.filter(response -> response.totalDividend() != 0)
111+
.toList();
112+
113+
return YearlyDividendResponse.of(dividends);
114+
}
115+
116+
private double getYearlyDividend(final UUID stockId) {
117+
return getLastYearDividendsByStockId(stockId)
118+
.stream()
119+
.mapToDouble(Dividend::getDividend)
120+
.sum();
121+
}
122+
123+
private List<Dividend> getLastYearDividendsByStockId(final UUID id) {
124+
return dividendRepository.findAllByIdAndYear(id, InstantProvider.getLastYear());
125+
}
126+
127+
private List<SingleMonthlyDividendResponse> getDividendsOfLastYearAndMonth(
128+
final List<PortfolioStock> portfolioStocks, final int month
129+
) {
130+
return portfolioStocks
131+
.stream()
132+
.flatMap(portfolioStock -> stockRepository.findById(portfolioStock.getStockId())
133+
.map(stock -> getMonthlyDividendResponse(month, portfolioStock, stock))
134+
.orElseThrow(() -> new StockIdNotFoundException(portfolioStock.getStockId())))
135+
.toList();
136+
}
137+
138+
private Stream<SingleMonthlyDividendResponse> getMonthlyDividendResponse(
139+
final int month, final PortfolioStock portfolioStock, final Stock stock
140+
) {
141+
return getLastYearDividendsByStockIdAndMonth(portfolioStock.getStockId(), month)
142+
.stream()
143+
.map(dividend -> SingleMonthlyDividendResponse.of(stock, portfolioStock.getShares(), dividend));
144+
}
145+
146+
private List<Dividend> getLastYearDividendsByStockIdAndMonth(final UUID stockId, final int month) {
147+
return dividendRepository.findAllByIdAndYearAndMonth(stockId, InstantProvider.getLastYear(), month);
148+
}
149+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.Valid;
5+
import jakarta.validation.constraints.Size;
6+
7+
import java.util.List;
8+
9+
public record PortfolioRequest(
10+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
11+
@Valid
12+
@Size(min = 1)
13+
List<TickerShare> tickerShares
14+
) {
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Min;
5+
import jakarta.validation.constraints.NotEmpty;
6+
7+
public record TickerShare(
8+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "ticker name")
9+
@NotEmpty
10+
String ticker,
11+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "share")
12+
@Min(value = 1)
13+
Integer share
14+
) { }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
import java.util.Comparator;
6+
import java.util.List;
7+
8+
public record MonthlyDividendResponse(
9+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
10+
Integer year,
11+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
12+
Integer month,
13+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
14+
List<SingleMonthlyDividendResponse> dividends,
15+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
16+
Double totalDividend
17+
) {
18+
public static MonthlyDividendResponse of(
19+
final int year, final int month, final List<SingleMonthlyDividendResponse> dividends
20+
) {
21+
return new MonthlyDividendResponse(
22+
year,
23+
month,
24+
dividends.stream()
25+
.sorted(Comparator.comparingDouble(SingleMonthlyDividendResponse::totalDividend).reversed())
26+
.toList(),
27+
dividends.stream()
28+
.mapToDouble(SingleMonthlyDividendResponse::totalDividend)
29+
.sum()
30+
);
31+
}
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
import java.util.UUID;
6+
7+
public record PortfolioResponse(
8+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
9+
UUID id
10+
) {
11+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import nexters.payout.apiserver.stock.application.dto.response.StockShareResponse;
5+
import nexters.payout.domain.stock.domain.Sector;
6+
import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.stream.Collectors;
11+
12+
public record SectorRatioResponse(
13+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector name")
14+
String sectorName,
15+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector value")
16+
String sectorValue,
17+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector ratio")
18+
Double sectorRatio,
19+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
20+
List<StockShareResponse> stockShares
21+
) {
22+
public static List<SectorRatioResponse> fromMap(final Map<Sector, SectorInfo> sectorRatioMap) {
23+
return sectorRatioMap.entrySet()
24+
.stream()
25+
.map(entry -> new SectorRatioResponse(
26+
entry.getKey().getName(),
27+
entry.getKey().name(),
28+
entry.getValue().ratio(),
29+
entry.getValue()
30+
.stockShares()
31+
.stream()
32+
.map(StockShareResponse::from)
33+
.collect(Collectors.toList()))
34+
)
35+
.collect(Collectors.toList());
36+
}
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package nexters.payout.apiserver.portfolio.application.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import nexters.payout.domain.dividend.domain.Dividend;
5+
import nexters.payout.domain.stock.domain.Stock;
6+
7+
public record SingleMonthlyDividendResponse(
8+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
9+
String ticker,
10+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
11+
String logoUrl,
12+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
13+
Integer share,
14+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
15+
Double dividend,
16+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
17+
Double totalDividend
18+
) {
19+
public static SingleMonthlyDividendResponse of(Stock stock, int share, Dividend dividend) {
20+
return new SingleMonthlyDividendResponse(
21+
stock.getTicker(),
22+
stock.getLogoUrl(),
23+
share,
24+
dividend.getDividend(),
25+
dividend.getDividend() * share
26+
);
27+
}
28+
}

0 commit comments

Comments
 (0)