diff --git a/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java b/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java new file mode 100644 index 0000000..175666a --- /dev/null +++ b/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java @@ -0,0 +1,31 @@ +package io.autoinvestor.application; + +import io.autoinvestor.domain.Asset; +import io.autoinvestor.domain.AssetRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + + +@Service +public class GetAllAssetsCommandHandler { + + private final AssetRepository repository; + + public GetAllAssetsCommandHandler(AssetRepository repository) { + this.repository = repository; + } + + public List handle() { + List assets = this.repository.findAll(); + return assets.stream() + .map(asset -> new GetAssetResponse( + asset.id(), + asset.mic(), + asset.ticker(), + asset.name() + )) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/io/autoinvestor/application/GetAssetCommand.java b/src/main/java/io/autoinvestor/application/GetAssetCommand.java new file mode 100644 index 0000000..b8df093 --- /dev/null +++ b/src/main/java/io/autoinvestor/application/GetAssetCommand.java @@ -0,0 +1,6 @@ +package io.autoinvestor.application; + +public record GetAssetCommand( + String assetId +) {} + diff --git a/src/main/java/io/autoinvestor/application/GetAssetCommandHandler.java b/src/main/java/io/autoinvestor/application/GetAssetCommandHandler.java new file mode 100644 index 0000000..1559d41 --- /dev/null +++ b/src/main/java/io/autoinvestor/application/GetAssetCommandHandler.java @@ -0,0 +1,30 @@ +package io.autoinvestor.application; + +import io.autoinvestor.domain.Asset; +import io.autoinvestor.domain.AssetId; +import io.autoinvestor.domain.AssetRepository; +import org.springframework.stereotype.Service; + + +@Service +public class GetAssetCommandHandler { + + private final AssetRepository repository; + + public GetAssetCommandHandler(AssetRepository repository) { + this.repository = repository; + } + + public GetAssetResponse handle(GetAssetCommand command) { + Asset asset = repository.findById(AssetId.of(command.assetId())) + .orElseThrow(() -> new AssetNotFoundException( + "Asset not found with id: " + command.assetId())); + + return new GetAssetResponse( + asset.id(), + asset.mic(), + asset.ticker(), + asset.name() + ); + } +} diff --git a/src/main/java/io/autoinvestor/application/GetAssetResponse.java b/src/main/java/io/autoinvestor/application/GetAssetResponse.java new file mode 100644 index 0000000..a07d7dc --- /dev/null +++ b/src/main/java/io/autoinvestor/application/GetAssetResponse.java @@ -0,0 +1,8 @@ +package io.autoinvestor.application; + +public record GetAssetResponse( + String assetId, + String mic, + String ticker, + String name +) {} diff --git a/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java b/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java index 77e7ef1..7bc3e3b 100644 --- a/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java @@ -18,7 +18,7 @@ public RegisterAssetCommandHandler(AssetRepository repository, this.eventPublisher = eventPublisher; } - public void handle(RegisterAssetCommand command) { + public RegisterAssetResponse handle(RegisterAssetCommand command) { if (this.repository.exists(command.mic(), command.ticker())) { throw new AssetAlreadyExists("Duplicated asset for this mic: " + command.mic() + " and ticker: " + command.ticker()); } @@ -27,5 +27,12 @@ public void handle(RegisterAssetCommand command) { this.repository.save(asset); this.eventPublisher.publish(asset.releaseEvents()); + + return new RegisterAssetResponse( + asset.id(), + asset.mic(), + asset.ticker(), + asset.name() + ); } } diff --git a/src/main/java/io/autoinvestor/application/RegisterAssetResponse.java b/src/main/java/io/autoinvestor/application/RegisterAssetResponse.java new file mode 100644 index 0000000..d31f7d5 --- /dev/null +++ b/src/main/java/io/autoinvestor/application/RegisterAssetResponse.java @@ -0,0 +1,8 @@ +package io.autoinvestor.application; + +public record RegisterAssetResponse( + String assetId, + String mic, + String ticker, + String name +) {} diff --git a/src/main/java/io/autoinvestor/domain/Asset.java b/src/main/java/io/autoinvestor/domain/Asset.java index e2d1ce0..ced0a63 100644 --- a/src/main/java/io/autoinvestor/domain/Asset.java +++ b/src/main/java/io/autoinvestor/domain/Asset.java @@ -41,6 +41,10 @@ public String name() { return name.value(); } + public String id() { + return id.value(); + } + public AssetId getId() { return id; } diff --git a/src/main/java/io/autoinvestor/domain/AssetRepository.java b/src/main/java/io/autoinvestor/domain/AssetRepository.java index 1532ace..dcf117c 100644 --- a/src/main/java/io/autoinvestor/domain/AssetRepository.java +++ b/src/main/java/io/autoinvestor/domain/AssetRepository.java @@ -1,10 +1,12 @@ package io.autoinvestor.domain; +import java.util.List; import java.util.Optional; public interface AssetRepository { void save(Asset asset); boolean exists(String mic, String ticker); Optional findById(AssetId assetId); + List findAll(); } diff --git a/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java b/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java index 5e54fcf..ec0235b 100644 --- a/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java +++ b/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java @@ -2,6 +2,8 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetPriceFetcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import yahoofinance.YahooFinance; import yahoofinance.Stock; @@ -15,12 +17,20 @@ import java.util.List; import java.util.TimeZone; - @Component public class YFinanceAssetPriceFetcher implements AssetPriceFetcher { + private static final Logger logger = LoggerFactory.getLogger(YFinanceAssetPriceFetcher.class); private static final int DAYS_LOOKBACK_BUFFER = 7; + static { + System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + System.setProperty( + "yahoofinance.baseurl.quotesquery1v7", + "https://query1.finance.yahoo.com/v6/finance/quote" + ); + } + @Override public float priceOn(Asset asset, Date date) { Calendar target = Calendar.getInstance(TimeZone.getTimeZone("UTC")); @@ -34,25 +44,33 @@ public float priceOn(Asset asset, Date date) { try { Stock stock = YahooFinance.get(asset.ticker(), from, to, Interval.DAILY); - List history = stock.getHistory(); + if (stock == null) { + throw new PriceNotAvailableException("No data returned for " + asset); + } + List history = stock.getHistory(); HistoricalQuote bar = history.stream() .filter(h -> h.getDate() != null) .sorted((a, b) -> b.getDate().compareTo(a.getDate())) .filter(h -> !h.getDate().after(target)) .findFirst() .orElseThrow(() -> new PriceNotAvailableException( - "No historical bar found for %s on/Before %s".formatted(asset, date))); + String.format("No historical bar found for %s on or before %s", asset, date) + )); BigDecimal close = bar.getClose() != null ? bar.getClose() : bar.getAdjClose(); if (close == null) { throw new PriceNotAvailableException( - "Close price missing for %s on %s".formatted(asset, date)); + String.format("Close price missing for %s on %s", asset, date) + ); } + return close.floatValue(); } catch (IOException ex) { + logger.error("Error fetching price for {} on {}: {}", asset, date, ex.getMessage(), ex); throw new PriceFetchFailedException( - "Unable to fetch price for %s from Yahoo Finance".formatted(asset)); + String.format("Unable to fetch price for %s from Yahoo Finance", asset) + ); } } } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java index 0e65e66..8856425 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java @@ -6,9 +6,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; @Repository @Profile("local") @@ -31,6 +29,11 @@ public Optional findById(AssetId assetId) { return Optional.ofNullable(assetStore.get(assetId.value())); } + @Override + public List findAll() { + return new ArrayList<>(assetStore.values()); + } + public void clear() { assetStore.clear(); } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java index 88ec2e1..01ebd33 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java @@ -9,6 +9,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Repository; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; @@ -42,4 +44,15 @@ public Optional findById(AssetId assetId) { return Optional.ofNullable(template.findById(assetId.value(), AssetDocument.class)) .map(mapper::toDomain); } + + @Override + public List findAll() { + var q = new Query(); + var assetDocuments = template.find(q, AssetDocument.class); + List assets = new ArrayList<>(); + for (AssetDocument assetDocument : assetDocuments) { + assets.add(mapper.toDomain(assetDocument)); + } + return assets; + } } diff --git a/src/main/java/io/autoinvestor/ui/AssetDTO.java b/src/main/java/io/autoinvestor/ui/AssetDTO.java new file mode 100644 index 0000000..da84225 --- /dev/null +++ b/src/main/java/io/autoinvestor/ui/AssetDTO.java @@ -0,0 +1,11 @@ +package io.autoinvestor.ui; + +import com.fasterxml.jackson.annotation.JsonFormat; + +public record AssetDTO( + @JsonFormat(shape = JsonFormat.Shape.STRING) + String assetId, + String mic, + String ticker, + String name +) {} diff --git a/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java b/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java new file mode 100644 index 0000000..d181a87 --- /dev/null +++ b/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java @@ -0,0 +1,29 @@ +package io.autoinvestor.ui; + +import io.autoinvestor.application.GetAllAssetsCommandHandler; +import io.autoinvestor.application.GetAssetResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/assets") +public class GetAllAssetsController { + + private final GetAllAssetsCommandHandler handler; + + public GetAllAssetsController(GetAllAssetsCommandHandler handler) { + this.handler = handler; + } + + @GetMapping + public ResponseEntity> getAllAssets() { + List assets = handler.handle(); + List dtos = assets.stream() + .map(a -> new AssetDTO(a.assetId(), a.mic(), a.ticker(), a.name())) + .collect(Collectors.toList()); + return ResponseEntity.ok(dtos); + } +} diff --git a/src/main/java/io/autoinvestor/ui/GetAssetController.java b/src/main/java/io/autoinvestor/ui/GetAssetController.java new file mode 100644 index 0000000..e1931b8 --- /dev/null +++ b/src/main/java/io/autoinvestor/ui/GetAssetController.java @@ -0,0 +1,30 @@ +package io.autoinvestor.ui; + +import io.autoinvestor.application.GetAssetCommand; +import io.autoinvestor.application.GetAssetCommandHandler; +import io.autoinvestor.application.GetAssetResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/assets") +public class GetAssetController { + + private final GetAssetCommandHandler handler; + + public GetAssetController(GetAssetCommandHandler handler) { + this.handler = handler; + } + + @GetMapping("/{assetId}") + public ResponseEntity getAsset(@PathVariable String assetId) { + GetAssetResponse response = handler.handle(new GetAssetCommand(assetId)); + AssetDTO dto = new AssetDTO( + response.assetId(), + response.mic(), + response.ticker(), + response.name() + ); + return ResponseEntity.ok(dto); + } +} diff --git a/src/main/java/io/autoinvestor/ui/RegisterAssetController.java b/src/main/java/io/autoinvestor/ui/RegisterAssetController.java index 06ea3a6..6ea98ba 100644 --- a/src/main/java/io/autoinvestor/ui/RegisterAssetController.java +++ b/src/main/java/io/autoinvestor/ui/RegisterAssetController.java @@ -2,6 +2,7 @@ import io.autoinvestor.application.RegisterAssetCommand; import io.autoinvestor.application.RegisterAssetCommandHandler; +import io.autoinvestor.application.RegisterAssetResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -10,7 +11,7 @@ @RestController -@RequestMapping("/register") +@RequestMapping("/assets") public class RegisterAssetController { private final RegisterAssetCommandHandler commandHandler; @@ -20,10 +21,16 @@ public RegisterAssetController(RegisterAssetCommandHandler commandHandler) { } @PostMapping - public ResponseEntity handle(@RequestBody RegisterAssetDTO dto) { - this.commandHandler.handle(new RegisterAssetCommand( - dto.mic(), dto.ticker(), dto.name() + public ResponseEntity handle(@RequestBody RegisterAssetDTO queryDto) { + RegisterAssetResponse response = this.commandHandler.handle(new RegisterAssetCommand( + queryDto.mic(), queryDto.ticker(), queryDto.name() )); - return ResponseEntity.ok().build(); + AssetDTO dto = new AssetDTO( + response.assetId(), + response.mic(), + response.ticker(), + response.name() + ); + return ResponseEntity.ok(dto); } }