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.exception.UnauthorizedException; import com.danielbohry.stocks.repository.portfolio.PortfolioEntity; import com.danielbohry.stocks.repository.portfolio.PortfolioEntity.PortfolioStock; import com.danielbohry.stocks.repository.portfolio.PortfolioRepository; import com.danielbohry.stocks.service.ExchangeService.ExchangeRateResponse; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import static com.danielbohry.stocks.domain.Portfolio.convert; 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; private final PortfolioEncryptService encryptService; public List getAllIds() { return repository.findAllPortfolioIds(); } public List getByUser(String userId, String currency) { return repository.findAllByUser(userId).stream() .map(entity -> get(entity.getId(), currency)) .toList(); } public Portfolio get(String id, String currency) { PortfolioEntity entity = repository.findById(id) .orElseThrow(() -> new NotFoundException("No portfolio found with id: " + id)); String encrypted = entity.getEncryptedStocks(); if (encrypted != null) { entity.setStocks(encryptService.decryptStocks(encrypted)); } Portfolio portfolio = convert(entity); 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(); PortfolioEntity toSave = PortfolioEntity.builder() .id(id) .stocks(emptyList()) .user(UserContextHolder.get().getId()) .createdAt(now()) .updatedAt(now()) .build(); return convert(repository.save(toSave)); } public Portfolio update(String id, List stocks) { log.info("Updating portfolio [{}]", id); PortfolioEntity toUpdate = repository.findById(id).orElseThrow(() -> new NotFoundException("Failed to update portfolio with id: " + id)); if (!Objects.equals(toUpdate.getUser(), UserContextHolder.get().getId()) || UserContextHolder.isAdmin()) { throw new UnauthorizedException("You do not have permission to update portfolio"); } validate(stocks); toUpdate.setUpdatedAt(now()); toUpdate.setEncryptedStocks(encryptService.encryptStocks(stocks.stream() .map(stock -> new PortfolioStock(stock.getCode(), stock.getQuantity())) .toList())); toUpdate.setStocks(null); PortfolioEntity updated = repository.save(toUpdate); List decryptStocks = encryptService.decryptStocks(updated.getEncryptedStocks()); updated.setStocks(decryptStocks); return convert(updated); } public void delete(String id) { log.info("Deleting portfolio [{}]", id); repository.deleteById(id); } public void cleanup() { repository.findAll().forEach(portfolio -> { if ((ObjectUtils.isEmpty(portfolio.getEncryptedStocks()))) { 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"); } } }