package com.danielbohry.stocks.service; import com.danielbohry.stocks.context.UserContextHolder; import com.danielbohry.stocks.domain.Portfolio; import com.danielbohry.stocks.domain.Quote; import com.danielbohry.stocks.domain.Stock; import com.danielbohry.stocks.exception.BadRequestException; import com.danielbohry.stocks.exception.NotFoundException; import com.danielbohry.stocks.repository.PortfolioRepository; import com.danielbohry.stocks.service.ExchangeService.ExchangeRateResponse; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import static java.time.LocalDateTime.now; import static java.util.Collections.emptyList; @Slf4j @Service @AllArgsConstructor public class PortfolioService { private final PortfolioRepository repository; private final StockService stockService; private final ExchangeService exchangeService; public List getAll() { return repository.findAll(); } public List getAllIds() { return repository.findAllPortfolioIds(); } @Cacheable(value = "portfolio", key = "#username + '_' + #currency") public List getByUser(String username, String currency) { List portfolios = repository.findAllByUsername(username); return portfolios.stream() .map(portfolio -> get(portfolio.getId(), currency)) .toList(); } public Portfolio get(String id, String currency) { Portfolio portfolio = repository.findById(id) .orElseThrow(() -> new NotFoundException("No portfolio found with id: " + id)); ExchangeRateResponse exchangeRate = exchangeService.getCurrentRate(currency); Map rates = exchangeRate.getConversionRates(); BigDecimal targetRate = rates.getOrDefault(currency, BigDecimal.ONE); log.info("Getting portfolio [{}] and converting to [{}]", id, currency); List updatedStocks = portfolio.getStocks().stream() .peek(stock -> { Quote quote = stockService.getStockQuote(stock.getCode()); stock.setName(quote.getName()); BigDecimal quoteRate = rates.getOrDefault(quote.getCurrency(), BigDecimal.ONE); BigDecimal convertedPrice = quote.getPrice() .divide(quoteRate, 2, RoundingMode.HALF_UP) .multiply(targetRate); stock.setPrice(convertedPrice); stock.setTotal(convertedPrice.multiply(new BigDecimal(stock.getQuantity()))); }).toList(); portfolio.setStocks(updatedStocks); return portfolio; } public Portfolio create() { String id = UUID.randomUUID().toString(); Portfolio toSave = Portfolio.builder() .id(id) .stocks(emptyList()) .username(UserContextHolder.get().getUsername()) .createdAt(now()) .updatedAt(now()) .build(); return repository.save(toSave); } public Portfolio update(String id, List stocks) { log.info("Updating portfolio [{}]", id); Optional portfolio = repository.findById(id); if (portfolio.isEmpty()) { throw new NotFoundException("Failed to update portfolio with id: " + id); } validate(stocks); Portfolio toUpdate = portfolio.get(); toUpdate.setUpdatedAt(now()); toUpdate.setStocks(stocks); return repository.save(toUpdate); } public void delete(String id) { log.info("Deleting portfolio [{}]", id); repository.deleteById(id); } public void cleanup() { repository.findAllByEmptyStocks().forEach(portfolio -> { if (portfolio.getStocks().isEmpty()) { log.info("Removing empty portfolio [{}]", portfolio.getId()); repository.deleteById(portfolio.getId()); } }); } private void validate(List stocks) { boolean anyInvalid = stocks.stream() .anyMatch(stock -> !stockService.isValid(stock.getCode())); if (anyInvalid) { throw new BadRequestException("Invalid stock found"); } } }