From 4ff3f235de5f06f36cb1a2a0a4cffaa082ac7b88 Mon Sep 17 00:00:00 2001 From: YongHwan Kim Date: Thu, 29 Aug 2024 15:34:28 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=08=EB=B0=B0=EB=8B=B9=EA=B8=88=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=8F=AC=ED=8A=B8=ED=8F=B4?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Portfolio의 LocalDate 모킹 * feat: StockDividend 필드에 isDeleted 컬럼 추가 --- .../dividend/domain/entity/StockDividend.java | 8 +++----- .../repository/StockDividendRepository.java | 15 +++++++++++---- .../portfolio/domain/entity/Portfolio.java | 19 +++++++++++++++---- .../stock/repository/StockRepository.java | 3 +++ .../service/StockAndDividendManager.java | 2 +- .../fineants/AbstractContainerBaseTest.java | 17 +++++++++++++++++ .../service/PortfolioHoldingServiceTest.java | 16 ++++++++++++---- 7 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/main/java/codesquad/fineants/domain/dividend/domain/entity/StockDividend.java b/src/main/java/codesquad/fineants/domain/dividend/domain/entity/StockDividend.java index ecfa6596..03d2c888 100644 --- a/src/main/java/codesquad/fineants/domain/dividend/domain/entity/StockDividend.java +++ b/src/main/java/codesquad/fineants/domain/dividend/domain/entity/StockDividend.java @@ -16,7 +16,6 @@ import codesquad.fineants.domain.dividend.domain.reader.HolidayFileReader; import codesquad.fineants.domain.kis.repository.HolidayRepository; import codesquad.fineants.domain.stock.domain.entity.Stock; -import codesquad.fineants.infra.s3.dto.Dividend; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; @@ -56,6 +55,8 @@ public class StockDividend extends BaseEntity { private LocalDate exDividendDate; @Column(name = "payment_date") private LocalDate paymentDate; + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ticker_symbol") private Stock stock; @@ -70,6 +71,7 @@ private StockDividend(Long id, Money dividend, LocalDate recordDate, LocalDate e this.recordDate = recordDate; this.exDividendDate = exDividendDate; this.paymentDate = paymentDate; + this.isDeleted = false; this.stock = stock; } @@ -180,10 +182,6 @@ public boolean hasInRange(LocalDate from, LocalDate to) { return recordDate.isAfter(from) && recordDate.isBefore(to); } - public Dividend toDividend() { - return Dividend.create(recordDate, paymentDate, stock.getTickerSymbol(), stock.getCompanyName(), dividend); - } - public boolean equalPaymentDate(LocalDate paymentDate) { if (this.paymentDate == null || paymentDate == null) { return false; diff --git a/src/main/java/codesquad/fineants/domain/dividend/repository/StockDividendRepository.java b/src/main/java/codesquad/fineants/domain/dividend/repository/StockDividendRepository.java index c1cadac5..9aa338f0 100644 --- a/src/main/java/codesquad/fineants/domain/dividend/repository/StockDividendRepository.java +++ b/src/main/java/codesquad/fineants/domain/dividend/repository/StockDividendRepository.java @@ -14,17 +14,24 @@ public interface StockDividendRepository extends JpaRepository { - @Query("select sd from StockDividend sd join fetch sd.stock s order by s.tickerSymbol, sd.recordDate") + @Query("select sd from StockDividend sd join fetch sd.stock s " + + "where sd.isDeleted = false " + + "order by s.tickerSymbol, sd.recordDate") List findAllStockDividends(); - @Query("select sd from StockDividend sd join fetch sd.stock s where s.tickerSymbol = :tickerSymbol order by s.tickerSymbol, sd.recordDate") + @Query("select sd from StockDividend sd join fetch sd.stock s " + + "where s.tickerSymbol = :tickerSymbol and sd.isDeleted = false " + + "order by s.tickerSymbol, sd.recordDate") List findStockDividendsByTickerSymbol(@Param("tickerSymbol") String tickerSymbol); - @Query("select sd from StockDividend sd join fetch sd.stock s where s.tickerSymbol = :tickerSymbol and sd.recordDate = :recordDate order by s.tickerSymbol, sd.recordDate") + @Query("select sd from StockDividend sd join fetch sd.stock s " + + "where s.tickerSymbol = :tickerSymbol and sd.recordDate = :recordDate and sd.isDeleted = false " + + "order by s.tickerSymbol, sd.recordDate") Optional findByTickerSymbolAndRecordDate(@Param("tickerSymbol") String tickerSymbol, @Param("recordDate") LocalDate recordDate); @Modifying - @Query("delete from StockDividend sd where sd.stock.tickerSymbol in :tickerSymbols") + @Query("update StockDividend sd set sd.isDeleted = true " + + "where sd.stock.tickerSymbol in :tickerSymbols") int deleteByTickerSymbols(@Param("tickerSymbols") Set tickerSymbols); } diff --git a/src/main/java/codesquad/fineants/domain/portfolio/domain/entity/Portfolio.java b/src/main/java/codesquad/fineants/domain/portfolio/domain/entity/Portfolio.java index ed4988a6..100fa6d4 100644 --- a/src/main/java/codesquad/fineants/domain/portfolio/domain/entity/Portfolio.java +++ b/src/main/java/codesquad/fineants/domain/portfolio/domain/entity/Portfolio.java @@ -32,6 +32,8 @@ import codesquad.fineants.domain.notification.domain.entity.type.NotificationType; import codesquad.fineants.domain.notification.repository.NotificationSentRepository; import codesquad.fineants.domain.notificationpreference.domain.entity.NotificationPreference; +import codesquad.fineants.global.common.time.DefaultLocalDateTimeService; +import codesquad.fineants.global.common.time.LocalDateTimeService; import codesquad.fineants.global.errors.errorcode.PortfolioErrorCode; import codesquad.fineants.global.errors.exception.BadRequestException; import codesquad.fineants.global.errors.exception.FineAntsException; @@ -50,6 +52,7 @@ import jakarta.persistence.NamedSubgraph; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; @@ -101,6 +104,9 @@ public class Portfolio extends BaseEntity implements Notifiable { @OneToMany(mappedBy = "portfolio") private final List portfolioHoldings = new ArrayList<>(); + @Transient + private LocalDateTimeService localDateTimeService = new DefaultLocalDateTimeService(); + private Portfolio(Long id, String name, String securitiesFirm, Money budget, Money targetGain, Money maximumLoss, Boolean targetGainIsActive, Boolean maximumLossIsActive, Member member) { validateBudget(budget, targetGain, maximumLoss); @@ -252,7 +258,8 @@ public Expression calculateBalance() { // 총 연간 배당금 = 각 종목들의 연배당금의 합계 public Expression calculateAnnualDividend() { return portfolioHoldings.stream() - .map(portfolioHolding -> portfolioHolding.createMonthlyDividendMap(LocalDate.now())) + .map(portfolioHolding -> portfolioHolding.createMonthlyDividendMap( + localDateTimeService.getLocalDateWithNow())) .map(map -> map.values().stream() .reduce(Money.zero(), Expression::plus)) .reduce(Money.zero(), Expression::plus); @@ -360,7 +367,7 @@ public boolean reachedMaximumLoss() { public List createPieChart() { List stocks = portfolioHoldings.stream() .map(portfolioHolding -> portfolioHolding.createPieChartItem(calculateWeightBy(portfolioHolding))) - .collect(Collectors.toList()); + .toList(); Bank bank = Bank.getInstance(); Percentage weight = calculateCashWeight().toPercentage(bank, Currency.KRW); PortfolioPieChartItem cash = PortfolioPieChartItem.cash(weight, bank.toWon(calculateBalance())); @@ -406,14 +413,14 @@ public List createDividendChart(LocalDate currentLoc Currency to = Currency.KRW; return totalDividendMap.entrySet().stream() .map(entry -> PortfolioDividendChartItem.create(entry.getKey(), entry.getValue().reduce(bank, to))) - .collect(Collectors.toList()); + .toList(); } public List createSectorChart() { return calculateSectorCurrentValuationMap().entrySet().stream() .map(mappingSectorChartItem()) .sorted(PortfolioSectorChartItem::compareTo) - .collect(Collectors.toList()); + .toList(); } private Map> calculateSectorCurrentValuationMap() { @@ -533,4 +540,8 @@ public NotifyMessage getTargetPriceMessage(String token) { public List getPortfolioHoldings() { return Collections.unmodifiableList(portfolioHoldings); } + + public void setLocalDateTimeService(LocalDateTimeService localDateTimeService) { + this.localDateTimeService = localDateTimeService; + } } diff --git a/src/main/java/codesquad/fineants/domain/stock/repository/StockRepository.java b/src/main/java/codesquad/fineants/domain/stock/repository/StockRepository.java index 4ec1bb3f..feb592a2 100644 --- a/src/main/java/codesquad/fineants/domain/stock/repository/StockRepository.java +++ b/src/main/java/codesquad/fineants/domain/stock/repository/StockRepository.java @@ -13,6 +13,9 @@ public interface StockRepository extends JpaRepository { + @Query("select s from Stock s where s.isDeleted = false") + List findAllStocks(); + Optional findByTickerSymbol(String tickerSymbol); @Query("select s from Stock s where s.tickerSymbol in :tickerSymbols") diff --git a/src/main/java/codesquad/fineants/domain/stock/service/StockAndDividendManager.java b/src/main/java/codesquad/fineants/domain/stock/service/StockAndDividendManager.java index 27cf2221..5784679b 100644 --- a/src/main/java/codesquad/fineants/domain/stock/service/StockAndDividendManager.java +++ b/src/main/java/codesquad/fineants/domain/stock/service/StockAndDividendManager.java @@ -168,7 +168,7 @@ private Mono>> fetchPartitionedStocksForDelisted() { @NotNull private Set findAllTickerSymbols() { - return stockRepository.findAll() + return stockRepository.findAllStocks() .stream() .map(Stock::getTickerSymbol) .collect(Collectors.toUnmodifiableSet()); diff --git a/src/test/java/codesquad/fineants/AbstractContainerBaseTest.java b/src/test/java/codesquad/fineants/AbstractContainerBaseTest.java index bc3097c2..c20fc987 100644 --- a/src/test/java/codesquad/fineants/AbstractContainerBaseTest.java +++ b/src/test/java/codesquad/fineants/AbstractContainerBaseTest.java @@ -337,6 +337,23 @@ protected List createStockDividendWith(Stock stock) { ); } + protected List createStockDividendThisYearWith(Stock stock) { + return List.of( + createStockDividend( + LocalDate.of(2024, 3, 31), LocalDate.of(2024, 3, 29), + LocalDate.of(2024, 5, 17), + stock), + createStockDividend( + LocalDate.of(2024, 6, 30), LocalDate.of(2024, 6, 28), + LocalDate.of(2024, 8, 16), + stock), + createStockDividend( + LocalDate.of(2024, 9, 30), LocalDate.of(2024, 9, 27), + LocalDate.of(2024, 11, 20), + stock) + ); + } + protected Cookie[] createTokenCookies() { TokenFactory tokenFactory = new TokenFactory(); Token token = Token.create("accessToken", "refreshToken"); diff --git a/src/test/java/codesquad/fineants/domain/holding/service/PortfolioHoldingServiceTest.java b/src/test/java/codesquad/fineants/domain/holding/service/PortfolioHoldingServiceTest.java index 3e53a487..92bf92c1 100644 --- a/src/test/java/codesquad/fineants/domain/holding/service/PortfolioHoldingServiceTest.java +++ b/src/test/java/codesquad/fineants/domain/holding/service/PortfolioHoldingServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; import java.time.LocalDate; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.security.core.context.SecurityContextHolder; import codesquad.fineants.AbstractContainerBaseTest; @@ -49,6 +51,7 @@ import codesquad.fineants.domain.purchasehistory.repository.PurchaseHistoryRepository; import codesquad.fineants.domain.stock.domain.entity.Stock; import codesquad.fineants.domain.stock.repository.StockRepository; +import codesquad.fineants.global.common.time.LocalDateTimeService; import codesquad.fineants.global.errors.errorcode.MemberErrorCode; import codesquad.fineants.global.errors.errorcode.PortfolioHoldingErrorCode; import codesquad.fineants.global.errors.exception.FineAntsException; @@ -86,6 +89,9 @@ class PortfolioHoldingServiceTest extends AbstractContainerBaseTest { @Autowired private ClosingPriceRepository closingPriceRepository; + @SpyBean + private LocalDateTimeService localDateTimeService; + @MockBean private PortfolioHoldingEventPublisher publisher; @@ -95,8 +101,10 @@ void readMyPortfolioStocks() { // given Member member = memberRepository.save(createMember()); Portfolio portfolio = portfolioRepository.save(createPortfolio(member)); + portfolio.setLocalDateTimeService(localDateTimeService); + given(localDateTimeService.getLocalDateWithNow()).willReturn(LocalDate.of(2024, 1, 1)); Stock stock = stockRepository.save(createSamsungStock()); - stockDividendRepository.saveAll(createStockDividendWith(stock)); + stockDividendRepository.saveAll(createStockDividendThisYearWith(stock)); PortfolioHolding portfolioHolding = portFolioHoldingRepository.save(createPortfolioHolding(portfolio, stock)); LocalDateTime purchaseDate = LocalDateTime.of(2023, 9, 26, 9, 30, 0); @@ -133,7 +141,7 @@ void readMyPortfolioStocks() { Percentage dailyGainRate = RateDivision.of(dailyGain, totalInvestmentAmount) .toPercentage(Bank.getInstance(), Currency.KRW); - Expression totalAnnualDividend = Money.won(361 * 3 * 4); + Money totalAnnualDividend = Money.won(361 * 3 * 3); Expression currentValuation = Money.won(180000); Percentage annualDividendYield = RateDivision.of(totalAnnualDividend, currentValuation) .toPercentage(Bank.getInstance(), Currency.KRW); @@ -152,7 +160,7 @@ void readMyPortfolioStocks() { () -> assertThat(details.getDailyGain()).isEqualByComparingTo(Money.won(30000L)), () -> assertThat(details.getDailyGainRate()).isEqualByComparingTo(dailyGainRate), () -> assertThat(details.getBalance()).isEqualByComparingTo(Money.won(850000L)), - () -> assertThat(details.getAnnualDividend()).isEqualByComparingTo(Money.won(4332L)), + () -> assertThat(details.getAnnualDividend()).isEqualByComparingTo(totalAnnualDividend), () -> assertThat(details.getAnnualDividendYield()).isEqualByComparingTo(annualDividendYield), () -> assertThat(details.getProvisionalLossBalance()).isEqualByComparingTo(Money.won(0L)), () -> assertThat(details.getTargetGainNotify()).isTrue(), @@ -183,7 +191,7 @@ void readMyPortfolioStocks() { Percentage.from(0.2), Money.won(30000), Percentage.from(0.2), - Money.won(4332) + Money.won(3249) ) ), () -> assertThat(response.getPortfolioHoldings())