diff --git a/.github/workflows/pull-request-pipeline.yml b/.github/workflows/pull-request-pipeline.yml index 11f9959..b506932 100644 --- a/.github/workflows/pull-request-pipeline.yml +++ b/.github/workflows/pull-request-pipeline.yml @@ -21,4 +21,7 @@ jobs: java-version: 21 - name: Build & Test - run: ./gradlew clean build \ No newline at end of file + run: ./gradlew clean build + + - name: Spotless Check + run: ./gradlew spotlessCheck \ No newline at end of file diff --git a/HELP.md b/HELP.md index f876abc..bdbf568 100644 --- a/HELP.md +++ b/HELP.md @@ -11,4 +11,3 @@ For further reference, please consider the following sections: These additional references should also help you: * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) - diff --git a/build.gradle b/build.gradle index 3d8bc86..4d4d488 100644 --- a/build.gradle +++ b/build.gradle @@ -1,44 +1,63 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.3' - id 'io.spring.dependency-management' version '1.1.7' - id 'io.freefair.lombok' version '8.13.1' + id 'java' + id 'org.springframework.boot' version '3.4.3' + id 'io.spring.dependency-management' version '1.1.7' + id 'io.freefair.lombok' version '8.13.1' + id 'com.diffplug.spotless' version '7.0.4' } group = 'io.autoinvestor' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - - implementation 'com.google.cloud:google-cloud-pubsub:1.123.0' - implementation "com.google.cloud:spring-cloud-gcp-starter-pubsub:6.1.1" - implementation 'com.fasterxml.jackson.core:jackson-databind' - implementation 'org.springframework.integration:spring-integration-core' - implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' - implementation 'com.yahoofinance-api:YahooFinanceAPI:3.17.0' - - testImplementation 'org.springframework.boot:spring-boot-testcontainers' - testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.testcontainers:gcloud' + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation 'com.google.cloud:google-cloud-pubsub:1.123.0' + implementation "com.google.cloud:spring-cloud-gcp-starter-pubsub:6.1.1" + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.springframework.integration:spring-integration-core' + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + implementation 'com.yahoofinance-api:YahooFinanceAPI:3.17.0' + + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:gcloud' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } bootBuildImage { - publish = false + publish = false +} + +spotless { + java { + googleJavaFormat('1.22.0') + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + importOrder '', 'java', 'javax', 'org', 'com' + target 'src/**/*.java' + } + + format 'misc', { + target '*.gradle', '*.md', '.gitignore' + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } } diff --git a/src/main/java/io/autoinvestor/CoreApplication.java b/src/main/java/io/autoinvestor/CoreApplication.java index 75664d9..bce7555 100644 --- a/src/main/java/io/autoinvestor/CoreApplication.java +++ b/src/main/java/io/autoinvestor/CoreApplication.java @@ -6,7 +6,7 @@ @SpringBootApplication public class CoreApplication { - public static void main(String[] args) { - SpringApplication.run(CoreApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(CoreApplication.class, args); + } } diff --git a/src/main/java/io/autoinvestor/application/AssetAlreadyExists.java b/src/main/java/io/autoinvestor/application/AssetAlreadyExists.java index efc59a8..fbbab3b 100644 --- a/src/main/java/io/autoinvestor/application/AssetAlreadyExists.java +++ b/src/main/java/io/autoinvestor/application/AssetAlreadyExists.java @@ -3,7 +3,7 @@ import io.autoinvestor.exceptions.DuplicatedException; public class AssetAlreadyExists extends DuplicatedException { - public AssetAlreadyExists(String message) { - super(message); - } + public AssetAlreadyExists(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/application/AssetNotFoundException.java b/src/main/java/io/autoinvestor/application/AssetNotFoundException.java index 2f52f50..5a1ad4d 100644 --- a/src/main/java/io/autoinvestor/application/AssetNotFoundException.java +++ b/src/main/java/io/autoinvestor/application/AssetNotFoundException.java @@ -3,7 +3,7 @@ import io.autoinvestor.exceptions.BadRequestException; public class AssetNotFoundException extends BadRequestException { - public AssetNotFoundException(String message) { - super(message); - } + public AssetNotFoundException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java b/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java index 175666a..bdc467e 100644 --- a/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/GetAllAssetsCommandHandler.java @@ -2,30 +2,25 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetRepository; -import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; +import org.springframework.stereotype.Service; @Service public class GetAllAssetsCommandHandler { - private final AssetRepository repository; + private final AssetRepository repository; - public GetAllAssetsCommandHandler(AssetRepository repository) { - this.repository = 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()); - } + 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 index b8df093..53e68a5 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetCommand.java +++ b/src/main/java/io/autoinvestor/application/GetAssetCommand.java @@ -1,6 +1,3 @@ package io.autoinvestor.application; -public record GetAssetCommand( - String assetId -) {} - +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 index 242bf0c..0f2e228 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/GetAssetCommandHandler.java @@ -3,32 +3,28 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetId; import io.autoinvestor.domain.AssetRepository; -import org.springframework.stereotype.Service; import java.util.Optional; +import org.springframework.stereotype.Service; @Service public class GetAssetCommandHandler { - private final AssetRepository repository; + private final AssetRepository repository; - public GetAssetCommandHandler(AssetRepository repository) { - this.repository = repository; - } + public GetAssetCommandHandler(AssetRepository repository) { + this.repository = repository; + } - public GetAssetResponse handle(GetAssetCommand command) { - Optional asset = repository.findById(AssetId.of(command.assetId())); + public GetAssetResponse handle(GetAssetCommand command) { + Optional asset = repository.findById(AssetId.of(command.assetId())); - if (asset.isEmpty()) { - throw new AssetNotFoundException("Asset not found with ID: " + command.assetId()); - } - - return new GetAssetResponse( - asset.get().id(), - asset.get().mic(), - asset.get().ticker(), - asset.get().name() - ); + if (asset.isEmpty()) { + throw new AssetNotFoundException("Asset not found with ID: " + command.assetId()); } + + return new GetAssetResponse( + asset.get().id(), asset.get().mic(), asset.get().ticker(), asset.get().name()); + } } diff --git a/src/main/java/io/autoinvestor/application/GetAssetPriceCommand.java b/src/main/java/io/autoinvestor/application/GetAssetPriceCommand.java index f39f3f7..51f4887 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetPriceCommand.java +++ b/src/main/java/io/autoinvestor/application/GetAssetPriceCommand.java @@ -2,4 +2,4 @@ import java.util.Date; -public record GetAssetPriceCommand(String assetId, Date date) { } +public record GetAssetPriceCommand(String assetId, Date date) {} diff --git a/src/main/java/io/autoinvestor/application/GetAssetPriceCommandHandler.java b/src/main/java/io/autoinvestor/application/GetAssetPriceCommandHandler.java index e09d0fd..dd0bf5c 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetPriceCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/GetAssetPriceCommandHandler.java @@ -4,30 +4,30 @@ import io.autoinvestor.domain.AssetId; import io.autoinvestor.domain.AssetPriceFetcher; import io.autoinvestor.domain.AssetRepository; -import org.springframework.stereotype.Service; import java.util.Optional; +import org.springframework.stereotype.Service; @Service public class GetAssetPriceCommandHandler { - private final AssetPriceFetcher fetcher; - private final AssetRepository repository; + private final AssetPriceFetcher fetcher; + private final AssetRepository repository; - public GetAssetPriceCommandHandler(AssetRepository repository, AssetPriceFetcher fetcher) { - this.repository = repository; - this.fetcher = fetcher; - } + public GetAssetPriceCommandHandler(AssetRepository repository, AssetPriceFetcher fetcher) { + this.repository = repository; + this.fetcher = fetcher; + } - public GetAssetPriceResponse handle(GetAssetPriceCommand command) { - Optional asset = repository.findById(AssetId.of(command.assetId())); + public GetAssetPriceResponse handle(GetAssetPriceCommand command) { + Optional asset = repository.findById(AssetId.of(command.assetId())); - if (asset.isEmpty()) { - throw new AssetNotFoundException("Asset not found with ID: " + command.assetId()); - } - - float price = fetcher.priceOn(asset.get(), command.date()); - return new GetAssetPriceResponse(price, command.date()); + if (asset.isEmpty()) { + throw new AssetNotFoundException("Asset not found with ID: " + command.assetId()); } + + float price = fetcher.priceOn(asset.get(), command.date()); + return new GetAssetPriceResponse(price, command.date()); + } } diff --git a/src/main/java/io/autoinvestor/application/GetAssetPriceResponse.java b/src/main/java/io/autoinvestor/application/GetAssetPriceResponse.java index e1ecbc7..5dac4b3 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetPriceResponse.java +++ b/src/main/java/io/autoinvestor/application/GetAssetPriceResponse.java @@ -2,5 +2,4 @@ import java.util.Date; - -public record GetAssetPriceResponse(float price, Date date) { } +public record GetAssetPriceResponse(float price, Date date) {} diff --git a/src/main/java/io/autoinvestor/application/GetAssetResponse.java b/src/main/java/io/autoinvestor/application/GetAssetResponse.java index a07d7dc..c126b53 100644 --- a/src/main/java/io/autoinvestor/application/GetAssetResponse.java +++ b/src/main/java/io/autoinvestor/application/GetAssetResponse.java @@ -1,8 +1,3 @@ package io.autoinvestor.application; -public record GetAssetResponse( - String assetId, - String mic, - String ticker, - String name -) {} +public record GetAssetResponse(String assetId, String mic, String ticker, String name) {} diff --git a/src/main/java/io/autoinvestor/application/RegisterAssetCommand.java b/src/main/java/io/autoinvestor/application/RegisterAssetCommand.java index 2cfbda7..cfa112f 100644 --- a/src/main/java/io/autoinvestor/application/RegisterAssetCommand.java +++ b/src/main/java/io/autoinvestor/application/RegisterAssetCommand.java @@ -1,4 +1,3 @@ package io.autoinvestor.application; - -public record RegisterAssetCommand(String mic, String ticker, String name) { } +public record RegisterAssetCommand(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 7bc3e3b..07a09e0 100644 --- a/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/RegisterAssetCommandHandler.java @@ -3,36 +3,31 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetRepository; import io.autoinvestor.domain.EventPublisher; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Service; @Service public class RegisterAssetCommandHandler { - private final AssetRepository repository; - private final EventPublisher eventPublisher; + private final AssetRepository repository; + private final EventPublisher eventPublisher; - public RegisterAssetCommandHandler(AssetRepository repository, - EventPublisher eventPublisher) { - this.repository = repository; - this.eventPublisher = eventPublisher; - } + public RegisterAssetCommandHandler(AssetRepository repository, EventPublisher eventPublisher) { + this.repository = repository; + this.eventPublisher = eventPublisher; + } - 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()); - } + 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()); + } - Asset asset = Asset.create(command.mic(), command.ticker(), command.name()); + Asset asset = Asset.create(command.mic(), command.ticker(), command.name()); - this.repository.save(asset); - this.eventPublisher.publish(asset.releaseEvents()); + this.repository.save(asset); + this.eventPublisher.publish(asset.releaseEvents()); - return new RegisterAssetResponse( - asset.id(), - asset.mic(), - asset.ticker(), - asset.name() - ); - } + 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 index d31f7d5..9cbd1b8 100644 --- a/src/main/java/io/autoinvestor/application/RegisterAssetResponse.java +++ b/src/main/java/io/autoinvestor/application/RegisterAssetResponse.java @@ -1,8 +1,3 @@ package io.autoinvestor.application; -public record RegisterAssetResponse( - String assetId, - String mic, - String ticker, - String name -) {} +public record RegisterAssetResponse(String assetId, String mic, String ticker, String name) {} diff --git a/src/main/java/io/autoinvestor/domain/AggregateRoot.java b/src/main/java/io/autoinvestor/domain/AggregateRoot.java index 49ff196..ba5830f 100644 --- a/src/main/java/io/autoinvestor/domain/AggregateRoot.java +++ b/src/main/java/io/autoinvestor/domain/AggregateRoot.java @@ -4,19 +4,19 @@ import java.util.List; public class AggregateRoot { - private final List> events; + private final List> events; - public AggregateRoot() { - this.events = new ArrayList<>(); - } + public AggregateRoot() { + this.events = new ArrayList<>(); + } - protected void recordEvent(Event event) { - this.events.add(event); - } + protected void recordEvent(Event event) { + this.events.add(event); + } - public List> releaseEvents() { - List> events = new ArrayList<>(this.events); - this.events.clear(); - return events; - } + public List> releaseEvents() { + List> events = new ArrayList<>(this.events); + this.events.clear(); + return events; + } } diff --git a/src/main/java/io/autoinvestor/domain/Asset.java b/src/main/java/io/autoinvestor/domain/Asset.java index c676f21..d07e1e1 100644 --- a/src/main/java/io/autoinvestor/domain/Asset.java +++ b/src/main/java/io/autoinvestor/domain/Asset.java @@ -3,79 +3,88 @@ import java.util.Date; public class Asset extends AggregateRoot { - private final AssetId id; - private final Mic mic; - private final Ticker ticker; - private final CompanyName name; - private final Date createdAt; - private final Date updatedAt; - - private Asset(AssetId id, Mic mic, Ticker ticker, CompanyName name, Date createdAt, Date updatedAt) { - this.id = id; - this.mic = mic; - this.ticker = ticker; - this.name = name; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - private Asset(Mic mic, Ticker ticker, CompanyName name, Date createdAt, Date updatedAt) { - this.id = AssetId.generate(); - this.mic = mic; - this.ticker = ticker; - this.name = name; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - - this.recordEvent(AssetWasRegisteredEvent.from(this)); - } - - public static Asset create(String mic, String ticker, String name) { - return new Asset(Mic.from(mic), Ticker.from(ticker), CompanyName.from(name), new Date(), new Date()); - } - - public static Asset from(String assetId, String mic, String ticker, String name, Date createdAt, Date updatedAt) { - return new Asset(AssetId.of(assetId), Mic.from(mic), Ticker.from(ticker), CompanyName.from(name), createdAt, updatedAt); - } - - public String mic() { - return mic.value(); - } - - public String ticker() { - return ticker.value(); - } - - public String name() { - return name.value(); - } - - public String id() { - return id.value(); - } - - public AssetId getId() { - return id; - } - - public Date getCreatedAt() { - return createdAt; - } - - public Date getUpdatedAt() { - return updatedAt; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - Asset that = (Asset) obj; - return mic.equals(that.mic) && ticker.equals(that.ticker); - } - - @Override - public String toString() { - return mic + ":" + ticker; - } + private final AssetId id; + private final Mic mic; + private final Ticker ticker; + private final CompanyName name; + private final Date createdAt; + private final Date updatedAt; + + private Asset( + AssetId id, Mic mic, Ticker ticker, CompanyName name, Date createdAt, Date updatedAt) { + this.id = id; + this.mic = mic; + this.ticker = ticker; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private Asset(Mic mic, Ticker ticker, CompanyName name, Date createdAt, Date updatedAt) { + this.id = AssetId.generate(); + this.mic = mic; + this.ticker = ticker; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + + this.recordEvent(AssetWasRegisteredEvent.from(this)); + } + + public static Asset create(String mic, String ticker, String name) { + return new Asset( + Mic.from(mic), Ticker.from(ticker), CompanyName.from(name), new Date(), new Date()); + } + + public static Asset from( + String assetId, String mic, String ticker, String name, Date createdAt, Date updatedAt) { + return new Asset( + AssetId.of(assetId), + Mic.from(mic), + Ticker.from(ticker), + CompanyName.from(name), + createdAt, + updatedAt); + } + + public String mic() { + return mic.value(); + } + + public String ticker() { + return ticker.value(); + } + + public String name() { + return name.value(); + } + + public String id() { + return id.value(); + } + + public AssetId getId() { + return id; + } + + public Date getCreatedAt() { + return createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Asset that = (Asset) obj; + return mic.equals(that.mic) && ticker.equals(that.ticker); + } + + @Override + public String toString() { + return mic + ":" + ticker; + } } diff --git a/src/main/java/io/autoinvestor/domain/AssetId.java b/src/main/java/io/autoinvestor/domain/AssetId.java index c2fbaed..2026644 100644 --- a/src/main/java/io/autoinvestor/domain/AssetId.java +++ b/src/main/java/io/autoinvestor/domain/AssetId.java @@ -1,15 +1,15 @@ package io.autoinvestor.domain; public class AssetId extends Id { - AssetId(String id) { - super(id); - } + AssetId(String id) { + super(id); + } - public static AssetId generate() { - return new AssetId(generateId()); - } + public static AssetId generate() { + return new AssetId(generateId()); + } - public static AssetId of(String id) { - return new AssetId(id); - } + public static AssetId of(String id) { + return new AssetId(id); + } } diff --git a/src/main/java/io/autoinvestor/domain/AssetPriceFetcher.java b/src/main/java/io/autoinvestor/domain/AssetPriceFetcher.java index b94de53..e1f3280 100644 --- a/src/main/java/io/autoinvestor/domain/AssetPriceFetcher.java +++ b/src/main/java/io/autoinvestor/domain/AssetPriceFetcher.java @@ -3,5 +3,5 @@ import java.util.Date; public interface AssetPriceFetcher { - float priceOn(Asset asset, Date date); + float priceOn(Asset asset, Date date); } diff --git a/src/main/java/io/autoinvestor/domain/AssetRepository.java b/src/main/java/io/autoinvestor/domain/AssetRepository.java index dcf117c..f1a2f97 100644 --- a/src/main/java/io/autoinvestor/domain/AssetRepository.java +++ b/src/main/java/io/autoinvestor/domain/AssetRepository.java @@ -1,12 +1,14 @@ 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(); + void save(Asset asset); + + boolean exists(String mic, String ticker); + + Optional findById(AssetId assetId); + + List findAll(); } diff --git a/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEvent.java b/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEvent.java index 99309b4..65c9359 100644 --- a/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEvent.java +++ b/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEvent.java @@ -2,16 +2,13 @@ public class AssetWasRegisteredEvent extends Event { - private AssetWasRegisteredEvent(Id aggregateId, AssetWasRegisteredEventPayload payload) { - super(aggregateId, "ASSET_CREATED", payload); - } + private AssetWasRegisteredEvent(Id aggregateId, AssetWasRegisteredEventPayload payload) { + super(aggregateId, "ASSET_CREATED", payload); + } - public static AssetWasRegisteredEvent from(Asset asset) { - AssetWasRegisteredEventPayload payload = new AssetWasRegisteredEventPayload( - asset.mic(), - asset.ticker(), - asset.name() - ); - return new AssetWasRegisteredEvent(asset.getId(), payload); - } + public static AssetWasRegisteredEvent from(Asset asset) { + AssetWasRegisteredEventPayload payload = + new AssetWasRegisteredEventPayload(asset.mic(), asset.ticker(), asset.name()); + return new AssetWasRegisteredEvent(asset.getId(), payload); + } } diff --git a/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEventPayload.java b/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEventPayload.java index e2a4f1e..cc64780 100644 --- a/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEventPayload.java +++ b/src/main/java/io/autoinvestor/domain/AssetWasRegisteredEventPayload.java @@ -2,9 +2,10 @@ import java.util.Map; -public record AssetWasRegisteredEventPayload(String mic, String ticker, String name) implements EventPayload { - @Override - public Map asMap() { - return Map.of("mic", mic, "ticker", ticker, "name", name); - } +public record AssetWasRegisteredEventPayload(String mic, String ticker, String name) + implements EventPayload { + @Override + public Map asMap() { + return Map.of("mic", mic, "ticker", ticker, "name", name); + } } diff --git a/src/main/java/io/autoinvestor/domain/CompanyName.java b/src/main/java/io/autoinvestor/domain/CompanyName.java index 6c6be54..6c94833 100644 --- a/src/main/java/io/autoinvestor/domain/CompanyName.java +++ b/src/main/java/io/autoinvestor/domain/CompanyName.java @@ -1,22 +1,22 @@ package io.autoinvestor.domain; public class CompanyName { - private final String value; + private final String value; - private CompanyName(String value) { - this.value = value; - } + private CompanyName(String value) { + this.value = value; + } - public static CompanyName from(String value) { - return new CompanyName(value); - } + public static CompanyName from(String value) { + return new CompanyName(value); + } - public String value() { - return value; - } + public String value() { + return value; + } - @Override - public String toString() { - return value; - } + @Override + public String toString() { + return value; + } } diff --git a/src/main/java/io/autoinvestor/domain/Event.java b/src/main/java/io/autoinvestor/domain/Event.java index a5e455f..7b8d335 100644 --- a/src/main/java/io/autoinvestor/domain/Event.java +++ b/src/main/java/io/autoinvestor/domain/Event.java @@ -3,47 +3,47 @@ import java.util.Date; public abstract class Event

{ - private final EventId id; - private final Id aggregateId; - private final String type; - private final P payload; - private final Date occurredAt; - private final int version; - - protected Event(Id aggregateId, String type, P payload) { - this(aggregateId, type, payload, 1); - } - - protected Event(Id aggregateId, String type, P payload, int version) { - this.id = EventId.generate(); - this.aggregateId = aggregateId; - this.type = type; - this.payload = payload; - this.occurredAt = new Date(); - this.version = version; - } - - public EventId getId() { - return id; - } - - public Id getAggregateId() { - return aggregateId; - } - - public String getType() { - return type; - } - - public P getPayload() { - return payload; - } - - public Date getOccurredAt() { - return occurredAt; - } - - public int getVersion() { - return version; - } + private final EventId id; + private final Id aggregateId; + private final String type; + private final P payload; + private final Date occurredAt; + private final int version; + + protected Event(Id aggregateId, String type, P payload) { + this(aggregateId, type, payload, 1); + } + + protected Event(Id aggregateId, String type, P payload, int version) { + this.id = EventId.generate(); + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = new Date(); + this.version = version; + } + + public EventId getId() { + return id; + } + + public Id getAggregateId() { + return aggregateId; + } + + public String getType() { + return type; + } + + public P getPayload() { + return payload; + } + + public Date getOccurredAt() { + return occurredAt; + } + + public int getVersion() { + return version; + } } diff --git a/src/main/java/io/autoinvestor/domain/EventId.java b/src/main/java/io/autoinvestor/domain/EventId.java index 8601083..1829578 100644 --- a/src/main/java/io/autoinvestor/domain/EventId.java +++ b/src/main/java/io/autoinvestor/domain/EventId.java @@ -1,11 +1,11 @@ package io.autoinvestor.domain; public class EventId extends Id { - EventId(String id) { - super(id); - } + EventId(String id) { + super(id); + } - public static EventId generate() { - return new EventId(generateId()); - } + public static EventId generate() { + return new EventId(generateId()); + } } diff --git a/src/main/java/io/autoinvestor/domain/EventPayload.java b/src/main/java/io/autoinvestor/domain/EventPayload.java index 7e94cd3..b39ed73 100644 --- a/src/main/java/io/autoinvestor/domain/EventPayload.java +++ b/src/main/java/io/autoinvestor/domain/EventPayload.java @@ -3,5 +3,5 @@ import java.util.Map; public interface EventPayload { - Map asMap(); -} \ No newline at end of file + Map asMap(); +} diff --git a/src/main/java/io/autoinvestor/domain/EventPublisher.java b/src/main/java/io/autoinvestor/domain/EventPublisher.java index 799870e..717fb9f 100644 --- a/src/main/java/io/autoinvestor/domain/EventPublisher.java +++ b/src/main/java/io/autoinvestor/domain/EventPublisher.java @@ -3,5 +3,5 @@ import java.util.List; public interface EventPublisher { - void publish(List> events); + void publish(List> events); } diff --git a/src/main/java/io/autoinvestor/domain/Id.java b/src/main/java/io/autoinvestor/domain/Id.java index 0ce2592..55b11a3 100644 --- a/src/main/java/io/autoinvestor/domain/Id.java +++ b/src/main/java/io/autoinvestor/domain/Id.java @@ -4,30 +4,29 @@ import java.util.UUID; public abstract class Id { - private final String id; - - public Id(String id) { - this.id = id; - } - - public String value() { - return id; - } - - protected static String generateId() { - return UUID.randomUUID().toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Id that)) return false; - return Objects.equals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } + private final String id; + + public Id(String id) { + this.id = id; + } + + public String value() { + return id; + } + + protected static String generateId() { + return UUID.randomUUID().toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Id that)) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } - diff --git a/src/main/java/io/autoinvestor/domain/Mic.java b/src/main/java/io/autoinvestor/domain/Mic.java index 794f517..04b7f6a 100644 --- a/src/main/java/io/autoinvestor/domain/Mic.java +++ b/src/main/java/io/autoinvestor/domain/Mic.java @@ -1,24 +1,22 @@ package io.autoinvestor.domain; public class Mic { - private final String value; + private final String value; - private Mic(String value) { - this.value = value; - } + private Mic(String value) { + this.value = value; + } - public static Mic from(String mic) { - return new Mic(mic); - } + public static Mic from(String mic) { + return new Mic(mic); + } - public String value() { - return value; - } + public String value() { + return value; + } - @Override - public String toString() { - return "Mic{" + - "mic='" + value + '\'' + - '}'; - } + @Override + public String toString() { + return "Mic{" + "mic='" + value + '\'' + '}'; + } } diff --git a/src/main/java/io/autoinvestor/domain/Ticker.java b/src/main/java/io/autoinvestor/domain/Ticker.java index 00399ce..5bb9a0c 100644 --- a/src/main/java/io/autoinvestor/domain/Ticker.java +++ b/src/main/java/io/autoinvestor/domain/Ticker.java @@ -1,24 +1,22 @@ package io.autoinvestor.domain; public class Ticker { - private final String value; + private final String value; - private Ticker(String value) { - this.value = value; - } + private Ticker(String value) { + this.value = value; + } - public static Ticker from(String ticker) { - return new Ticker(ticker); - } + public static Ticker from(String ticker) { + return new Ticker(ticker); + } - public String value() { - return value; - } + public String value() { + return value; + } - @Override - public String toString() { - return "Ticker{" + - "ticker='" + value + '\'' + - '}'; - } -} \ No newline at end of file + @Override + public String toString() { + return "Ticker{" + "ticker='" + value + '\'' + '}'; + } +} diff --git a/src/main/java/io/autoinvestor/exceptions/BadRequestException.java b/src/main/java/io/autoinvestor/exceptions/BadRequestException.java index 6e67872..65f763c 100644 --- a/src/main/java/io/autoinvestor/exceptions/BadRequestException.java +++ b/src/main/java/io/autoinvestor/exceptions/BadRequestException.java @@ -1,7 +1,7 @@ package io.autoinvestor.exceptions; public class BadRequestException extends RuntimeException { - public BadRequestException(String message) { - super(message); - } + public BadRequestException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/exceptions/DuplicatedException.java b/src/main/java/io/autoinvestor/exceptions/DuplicatedException.java index 1ddfe2b..8329bec 100644 --- a/src/main/java/io/autoinvestor/exceptions/DuplicatedException.java +++ b/src/main/java/io/autoinvestor/exceptions/DuplicatedException.java @@ -1,7 +1,7 @@ package io.autoinvestor.exceptions; public class DuplicatedException extends RuntimeException { - public DuplicatedException(String message) { - super(message); - } + public DuplicatedException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java index 01e514b..c7f94f3 100644 --- a/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java @@ -1,9 +1,5 @@ package io.autoinvestor.infrastructure.event_publishers; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.protobuf.ByteString; -import com.google.pubsub.v1.PubsubMessage; import io.autoinvestor.domain.Event; import io.autoinvestor.exceptions.InternalErrorException; @@ -11,33 +7,34 @@ import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.PubsubMessage; final class EventMessageMapper { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - EventMessageMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } + EventMessageMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + PubsubMessage toMessage(Event event) { + try { + Map envelope = new HashMap<>(); + envelope.put("payload", event.getPayload().asMap()); + envelope.put("eventId", event.getId().toString()); + envelope.put("type", event.getType()); + envelope.put("aggregateId", event.getAggregateId().value()); + envelope.put("occurredAt", Instant.ofEpochMilli(event.getOccurredAt().getTime()).toString()); + envelope.put("version", event.getVersion()); + + String json = objectMapper.writeValueAsString(envelope); + return PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8(json)).build(); - PubsubMessage toMessage(Event event) { - try { - Map envelope = new HashMap<>(); - envelope.put("payload", event.getPayload().asMap()); - envelope.put("eventId", event.getId().toString()); - envelope.put("type", event.getType()); - envelope.put("aggregateId", event.getAggregateId().value()); - envelope.put("occurredAt", - Instant.ofEpochMilli(event.getOccurredAt().getTime()).toString()); - envelope.put("version", event.getVersion()); - - String json = objectMapper.writeValueAsString(envelope); - return PubsubMessage.newBuilder() - .setData(ByteString.copyFromUtf8(json)) - .build(); - - } catch (JsonProcessingException ex) { - throw new InternalErrorException("Failed to serialise domain event"); - } + } catch (JsonProcessingException ex) { + throw new InternalErrorException("Failed to serialise domain event"); } + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/GcpPubSubEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/GcpPubSubEventPublisher.java index 691c04f..ed924a9 100644 --- a/src/main/java/io/autoinvestor/infrastructure/event_publishers/GcpPubSubEventPublisher.java +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/GcpPubSubEventPublisher.java @@ -1,45 +1,45 @@ package io.autoinvestor.infrastructure.event_publishers; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.pubsub.v1.Publisher; -import com.google.pubsub.v1.ProjectTopicName; import io.autoinvestor.domain.Event; import io.autoinvestor.domain.EventPublisher; import jakarta.annotation.PreDestroy; + +import java.util.List; +import java.util.concurrent.TimeUnit; + import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import java.util.List; -import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.pubsub.v1.Publisher; +import com.google.pubsub.v1.ProjectTopicName; @Component @Profile("prod") public class GcpPubSubEventPublisher implements EventPublisher { - private final Publisher publisher; - private final EventMessageMapper mapper; - - public GcpPubSubEventPublisher( - @Value("${GCP_PROJECT}") String projectId, - @Value("${PUBSUB_TOPIC}") String topic, - ObjectMapper objectMapper - ) throws Exception { - this.mapper = new EventMessageMapper(objectMapper); - ProjectTopicName topicName = ProjectTopicName.of(projectId, topic); - this.publisher = Publisher.newBuilder(topicName).build(); - } - - @Override - public void publish(List> events) { - events.stream() - .map(mapper::toMessage) - .forEach(publisher::publish); - } - - @PreDestroy - public void shutdown() throws Exception { - publisher.shutdown(); - publisher.awaitTermination(1, TimeUnit.MINUTES); - } + private final Publisher publisher; + private final EventMessageMapper mapper; + + public GcpPubSubEventPublisher( + @Value("${GCP_PROJECT}") String projectId, + @Value("${PUBSUB_TOPIC}") String topic, + ObjectMapper objectMapper) + throws Exception { + this.mapper = new EventMessageMapper(objectMapper); + ProjectTopicName topicName = ProjectTopicName.of(projectId, topic); + this.publisher = Publisher.newBuilder(topicName).build(); + } + + @Override + public void publish(List> events) { + events.stream().map(mapper::toMessage).forEach(publisher::publish); + } + + @PreDestroy + public void shutdown() throws Exception { + publisher.shutdown(); + publisher.awaitTermination(1, TimeUnit.MINUTES); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java index bb78526..907cf38 100644 --- a/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java @@ -2,33 +2,35 @@ import io.autoinvestor.domain.Event; import io.autoinvestor.domain.EventPublisher; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + @Component @Profile("local") public class InMemoryEventPublisher implements EventPublisher { - private final ApplicationEventPublisher eventPublisher; - private final List> publishedEvents = new ArrayList<>(); - - public InMemoryEventPublisher(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - @Override - public void publish(List> events) { - this.publishedEvents.addAll(events); - events.forEach(this.eventPublisher::publishEvent); - } - - public boolean hasPublishedEvent(String type, String aggregateId) { - return publishedEvents.stream() - .anyMatch(event -> event.getType().equals(type) - && event.getAggregateId().value().equals(aggregateId)); - } -} \ No newline at end of file + private final ApplicationEventPublisher eventPublisher; + private final List> publishedEvents = new ArrayList<>(); + + public InMemoryEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public void publish(List> events) { + this.publishedEvents.addAll(events); + events.forEach(this.eventPublisher::publishEvent); + } + + public boolean hasPublishedEvent(String type, String aggregateId) { + return publishedEvents.stream() + .anyMatch( + event -> + event.getType().equals(type) && event.getAggregateId().value().equals(aggregateId)); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceFetchFailedException.java b/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceFetchFailedException.java index 3acd3ed..1bf5b57 100644 --- a/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceFetchFailedException.java +++ b/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceFetchFailedException.java @@ -1,7 +1,7 @@ package io.autoinvestor.infrastructure.fetchers; public class PriceFetchFailedException extends RuntimeException { - public PriceFetchFailedException(String message) { - super(message); - } + public PriceFetchFailedException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceNotAvailableException.java b/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceNotAvailableException.java index ca82c0e..2210f0b 100644 --- a/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceNotAvailableException.java +++ b/src/main/java/io/autoinvestor/infrastructure/fetchers/PriceNotAvailableException.java @@ -1,7 +1,7 @@ package io.autoinvestor.infrastructure.fetchers; public class PriceNotAvailableException extends RuntimeException { - public PriceNotAvailableException(String message) { - super(message); - } + public PriceNotAvailableException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java b/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java index a5899a5..e410ae4 100644 --- a/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java +++ b/src/main/java/io/autoinvestor/infrastructure/fetchers/YFinanceAssetPriceFetcher.java @@ -1,12 +1,7 @@ package io.autoinvestor.infrastructure.fetchers; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetPriceFetcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import java.io.IOException; import java.net.HttpURLConnection; @@ -19,98 +14,102 @@ import java.time.Duration; import java.util.Date; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; @Component public class YFinanceAssetPriceFetcher implements AssetPriceFetcher { - private static final Logger logger = - LoggerFactory.getLogger(YFinanceAssetPriceFetcher.class); - - /** ± days around the target date that we request in one call */ - private static final int DAYS_LOOKBACK_BUFFER = 7; - private static final long SECONDS_PER_DAY = 86_400L; - - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); - - private final ObjectMapper mapper = new ObjectMapper(); - - @Override - public float priceOn(Asset asset, Date date) { - // --- 1. Build period1 / period2 (Unix seconds) ----------------------- - long targetEpoch = date.toInstant().getEpochSecond(); - long period1 = targetEpoch - DAYS_LOOKBACK_BUFFER * SECONDS_PER_DAY; - long period2 = targetEpoch + DAYS_LOOKBACK_BUFFER * SECONDS_PER_DAY; - - // --- 2. Build URL ---------------------------------------------------- - String encoded = URLEncoder.encode(asset.ticker(), StandardCharsets.UTF_8); - String url = String.format( - "https://query2.finance.yahoo.com/v8/finance/chart/%s" - + "?period1=%d&period2=%d&interval=1d&events=history", - encoded, period1, period2); - - try { - // --- 3. Execute HTTP request ------------------------------------ - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("User-Agent", "Mozilla/5.0") // helps avoid 429s - .timeout(Duration.ofSeconds(10)) - .GET() - .build(); - - HttpResponse response = httpClient.send( - request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != HttpURLConnection.HTTP_OK) { - throw new PriceFetchFailedException( - String.format("HTTP %d for %s (%s)", - response.statusCode(), asset, url)); - } - - // --- 4. Parse JSON ------------------------------------------------ - JsonNode root = mapper.readTree(response.body()); - JsonNode result = root.at("/chart/result/0"); - if (result.isMissingNode()) { - throw new PriceNotAvailableException("Empty result for " + asset); - } - - JsonNode timestamps = result.path("timestamp"); - JsonNode closes = result.at("/indicators/quote/0/close"); - - if (!timestamps.isArray() || !closes.isArray() - || timestamps.size() != closes.size()) { - throw new PriceNotAvailableException("Malformed data for " + asset); - } - - // --- 5. Walk arrays to find the latest bar ≤ target date --------- - float chosen = Float.NaN; - for (int i = 0; i < timestamps.size(); i++) { - long ts = timestamps.get(i).asLong() * 1000; // to millis - if (ts > date.getTime()) break; // past target - - if (!closes.get(i).isNull()) { - chosen = closes.get(i).floatValue(); - } - } - - if (Float.isNaN(chosen)) { - // Fallback to meta.regularMarketPrice if available - JsonNode fallback = result.at("/meta/regularMarketPrice"); - if (!fallback.isMissingNode() && !fallback.isNull()) { - chosen = (float) fallback.asDouble(); - } else { - throw new PriceNotAvailableException( - "No bar ≤ target date for " + asset); - } - } - return chosen; - - } catch (IOException | InterruptedException ex) { - logger.error("Error fetching price for {} on {}:", asset, date, ex); - Thread.currentThread().interrupt(); - throw new PriceFetchFailedException( - String.format("Unable to fetch price for %s (%s)", asset, ex)); + private static final Logger logger = LoggerFactory.getLogger(YFinanceAssetPriceFetcher.class); + + /** ± days around the target date that we request in one call */ + private static final int DAYS_LOOKBACK_BUFFER = 7; + + private static final long SECONDS_PER_DAY = 86_400L; + + private final HttpClient httpClient = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public float priceOn(Asset asset, Date date) { + // --- 1. Build period1 / period2 (Unix seconds) ----------------------- + long targetEpoch = date.toInstant().getEpochSecond(); + long period1 = targetEpoch - DAYS_LOOKBACK_BUFFER * SECONDS_PER_DAY; + long period2 = targetEpoch + DAYS_LOOKBACK_BUFFER * SECONDS_PER_DAY; + + // --- 2. Build URL ---------------------------------------------------- + String encoded = URLEncoder.encode(asset.ticker(), StandardCharsets.UTF_8); + String url = + String.format( + "https://query2.finance.yahoo.com/v8/finance/chart/%s" + + "?period1=%d&period2=%d&interval=1d&events=history", + encoded, period1, period2); + + try { + // --- 3. Execute HTTP request ------------------------------------ + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("User-Agent", "Mozilla/5.0") // helps avoid 429s + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + throw new PriceFetchFailedException( + String.format("HTTP %d for %s (%s)", response.statusCode(), asset, url)); + } + + // --- 4. Parse JSON ------------------------------------------------ + JsonNode root = mapper.readTree(response.body()); + JsonNode result = root.at("/chart/result/0"); + if (result.isMissingNode()) { + throw new PriceNotAvailableException("Empty result for " + asset); + } + + JsonNode timestamps = result.path("timestamp"); + JsonNode closes = result.at("/indicators/quote/0/close"); + + if (!timestamps.isArray() || !closes.isArray() || timestamps.size() != closes.size()) { + throw new PriceNotAvailableException("Malformed data for " + asset); + } + + // --- 5. Walk arrays to find the latest bar ≤ target date --------- + float chosen = Float.NaN; + for (int i = 0; i < timestamps.size(); i++) { + long ts = timestamps.get(i).asLong() * 1000; // to millis + if (ts > date.getTime()) break; // past target + + if (!closes.get(i).isNull()) { + chosen = closes.get(i).floatValue(); + } + } + + if (Float.isNaN(chosen)) { + // Fallback to meta.regularMarketPrice if available + JsonNode fallback = result.at("/meta/regularMarketPrice"); + if (!fallback.isMissingNode() && !fallback.isNull()) { + chosen = (float) fallback.asDouble(); + } else { + throw new PriceNotAvailableException("No bar ≤ target date for " + asset); } + } + return chosen; + + } catch (IOException | InterruptedException ex) { + logger.error("Error fetching price for {} on {}:", asset, date, ex); + Thread.currentThread().interrupt(); + throw new PriceFetchFailedException( + String.format("Unable to fetch price for %s (%s)", asset, ex)); } + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/AssetDocument.java b/src/main/java/io/autoinvestor/infrastructure/repositories/AssetDocument.java index 03eab3c..c059d1a 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/AssetDocument.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/AssetDocument.java @@ -1,16 +1,16 @@ package io.autoinvestor.infrastructure.repositories; +import java.time.Instant; + import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; -import java.time.Instant; - @Document(collection = "assets") public record AssetDocument( - @Id String assetId, - String mic, - String ticker, - String name, - @Field("occurredAt") Instant occurredAt, - Instant updatedAt) {} + @Id String assetId, + String mic, + String ticker, + String name, + @Field("occurredAt") Instant occurredAt, + Instant updatedAt) {} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/AssetMapper.java b/src/main/java/io/autoinvestor/infrastructure/repositories/AssetMapper.java index 67f13f0..a4c1d9b 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/AssetMapper.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/AssetMapper.java @@ -1,31 +1,31 @@ package io.autoinvestor.infrastructure.repositories; import io.autoinvestor.domain.Asset; -import org.springframework.stereotype.Component; import java.util.Date; +import org.springframework.stereotype.Component; + @Component class AssetMapper { - AssetDocument toDocument(Asset domain) { - return new AssetDocument( - domain.getId().value(), - domain.mic(), - domain.ticker(), - domain.name(), - domain.getCreatedAt().toInstant(), - domain.getUpdatedAt().toInstant()); - } + AssetDocument toDocument(Asset domain) { + return new AssetDocument( + domain.getId().value(), + domain.mic(), + domain.ticker(), + domain.name(), + domain.getCreatedAt().toInstant(), + domain.getUpdatedAt().toInstant()); + } - public Asset toDomain(AssetDocument assetDocument) { - return Asset.from( - assetDocument.assetId(), - assetDocument.mic(), - assetDocument.ticker(), - assetDocument.name(), - Date.from(assetDocument.occurredAt()), - Date.from(assetDocument.updatedAt())); - } + public Asset toDomain(AssetDocument assetDocument) { + return Asset.from( + assetDocument.assetId(), + assetDocument.mic(), + assetDocument.ticker(), + assetDocument.name(), + Date.from(assetDocument.occurredAt()), + Date.from(assetDocument.updatedAt())); + } } - diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java index 7dec94d..ff9135f 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryAssetRepository.java @@ -3,38 +3,39 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetId; import io.autoinvestor.domain.AssetRepository; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Repository; import java.util.*; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + @Repository @Profile("local") public class InMemoryAssetRepository implements AssetRepository { - private final Map assetStore = new HashMap<>(); - - @Override - public void save(Asset asset) { - assetStore.put(asset.id(), asset); - } - - @Override - public boolean exists(String mic, String ticker) { - return assetStore.values().stream() - .anyMatch(asset -> asset.mic().equals(mic) && asset.ticker().equals(ticker)); - } - - @Override - 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(); - } + private final Map assetStore = new HashMap<>(); + + @Override + public void save(Asset asset) { + assetStore.put(asset.id(), asset); + } + + @Override + public boolean exists(String mic, String ticker) { + return assetStore.values().stream() + .anyMatch(asset -> asset.mic().equals(mic) && asset.ticker().equals(ticker)); + } + + @Override + 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 6d85c2a..3e8ea26 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoAssetRepository.java @@ -3,59 +3,56 @@ import io.autoinvestor.domain.Asset; import io.autoinvestor.domain.AssetId; import io.autoinvestor.domain.AssetRepository; -import org.springframework.context.annotation.Profile; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -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; +import org.springframework.context.annotation.Profile; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Repository; @Repository @Profile("prod") class MongoAssetRepository implements AssetRepository { - private final MongoTemplate template; - private final AssetMapper mapper; - - public MongoAssetRepository(MongoTemplate template, AssetMapper mapper) { - this.template = template; - this.mapper = mapper; - } - - @Override - public void save(Asset asset) { - template.save(mapper.toDocument(asset)); - } - - @Override - public boolean exists(String mic, String ticker) { - var q = Query.query(Criteria - .where("mic").is(mic) - .and("ticker").is(ticker)); - return template.exists(q, AssetDocument.class); - } - - @Override - public Optional findById(AssetId assetId) { - String id = assetId.value(); - var q = Query.query(Criteria.where("_id").is(id)); - AssetDocument doc = template.findOne(q, AssetDocument.class); - return Optional.ofNullable(doc) - .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; + private final MongoTemplate template; + private final AssetMapper mapper; + + public MongoAssetRepository(MongoTemplate template, AssetMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void save(Asset asset) { + template.save(mapper.toDocument(asset)); + } + + @Override + public boolean exists(String mic, String ticker) { + var q = Query.query(Criteria.where("mic").is(mic).and("ticker").is(ticker)); + return template.exists(q, AssetDocument.class); + } + + @Override + public Optional findById(AssetId assetId) { + String id = assetId.value(); + var q = Query.query(Criteria.where("_id").is(id)); + AssetDocument doc = template.findOne(q, AssetDocument.class); + return Optional.ofNullable(doc).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 index da84225..bdb84f1 100644 --- a/src/main/java/io/autoinvestor/ui/AssetDTO.java +++ b/src/main/java/io/autoinvestor/ui/AssetDTO.java @@ -3,9 +3,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; public record AssetDTO( - @JsonFormat(shape = JsonFormat.Shape.STRING) - String assetId, - String mic, - String ticker, - String name -) {} + @JsonFormat(shape = JsonFormat.Shape.STRING) String assetId, + String mic, + String ticker, + String name) {} diff --git a/src/main/java/io/autoinvestor/ui/ErrorResponse.java b/src/main/java/io/autoinvestor/ui/ErrorResponse.java index 568827b..48c86f0 100644 --- a/src/main/java/io/autoinvestor/ui/ErrorResponse.java +++ b/src/main/java/io/autoinvestor/ui/ErrorResponse.java @@ -1,7 +1,7 @@ package io.autoinvestor.ui; public record ErrorResponse(int status, String error) { - public static ErrorResponseBuilder builder() { - return new ErrorResponseBuilder(); - } + public static ErrorResponseBuilder builder() { + return new ErrorResponseBuilder(); + } } diff --git a/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java b/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java index 298b554..371baad 100644 --- a/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java +++ b/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java @@ -4,31 +4,28 @@ import org.springframework.http.ResponseEntity; public class ErrorResponseBuilder { - private int status = HttpStatus.INTERNAL_SERVER_ERROR.value(); - private String message = "An unexpected error occurred"; + private int status = HttpStatus.INTERNAL_SERVER_ERROR.value(); + private String message = "An unexpected error occurred"; - ErrorResponseBuilder() {} + ErrorResponseBuilder() {} - public ErrorResponseBuilder status(HttpStatus status) { - this.status = status.value(); - return this; - } + public ErrorResponseBuilder status(HttpStatus status) { + this.status = status.value(); + return this; + } - public ErrorResponseBuilder status(int status) { - this.status = status; - return this; - } + public ErrorResponseBuilder status(int status) { + this.status = status; + return this; + } - public ErrorResponseBuilder message(String message) { - this.message = message; - return this; - } + public ErrorResponseBuilder message(String message) { + this.message = message; + return this; + } - public ResponseEntity build() { - ErrorResponse errorResponse = new ErrorResponse( - this.status, - this.message - ); - return ResponseEntity.status(this.status).body(errorResponse); - } + public ResponseEntity build() { + ErrorResponse errorResponse = new ErrorResponse(this.status, this.message); + return ResponseEntity.status(this.status).body(errorResponse); + } } diff --git a/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java b/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java index d181a87..57be914 100644 --- a/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java +++ b/src/main/java/io/autoinvestor/ui/GetAllAssetsController.java @@ -2,28 +2,30 @@ 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; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + @RestController @RequestMapping("/assets") public class GetAllAssetsController { - private final GetAllAssetsCommandHandler handler; + private final GetAllAssetsCommandHandler handler; - public GetAllAssetsController(GetAllAssetsCommandHandler handler) { - this.handler = 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); - } + @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 index e1931b8..6b2e6a7 100644 --- a/src/main/java/io/autoinvestor/ui/GetAssetController.java +++ b/src/main/java/io/autoinvestor/ui/GetAssetController.java @@ -3,6 +3,7 @@ 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.*; @@ -10,21 +11,17 @@ @RequestMapping("/assets") public class GetAssetController { - private final GetAssetCommandHandler handler; + private final GetAssetCommandHandler handler; - public GetAssetController(GetAssetCommandHandler handler) { - this.handler = 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); - } + @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/GetAssetPriceController.java b/src/main/java/io/autoinvestor/ui/GetAssetPriceController.java index 8ad76bc..37baa38 100644 --- a/src/main/java/io/autoinvestor/ui/GetAssetPriceController.java +++ b/src/main/java/io/autoinvestor/ui/GetAssetPriceController.java @@ -3,36 +3,34 @@ import io.autoinvestor.application.GetAssetPriceCommand; import io.autoinvestor.application.GetAssetPriceCommandHandler; import io.autoinvestor.application.GetAssetPriceResponse; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; import java.time.Instant; import java.util.Date; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + @RestController @RequestMapping("/assets") public class GetAssetPriceController { - private final GetAssetPriceCommandHandler handler; + private final GetAssetPriceCommandHandler handler; - public GetAssetPriceController(GetAssetPriceCommandHandler handler) { - this.handler = handler; - } + public GetAssetPriceController(GetAssetPriceCommandHandler handler) { + this.handler = handler; + } - @GetMapping("/{assetId}/price") - public ResponseEntity getPrice( - @PathVariable String assetId, - @RequestParam(name = "at", required = false) - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date at) { + @GetMapping("/{assetId}/price") + public ResponseEntity getPrice( + @PathVariable String assetId, + @RequestParam(name = "at", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + Date at) { - Date date = at != null ? at : Date.from(Instant.now()); + Date date = at != null ? at : Date.from(Instant.now()); - GetAssetPriceResponse response = handler.handle(new GetAssetPriceCommand(assetId, date)); - PriceDTO dto = new PriceDTO( - response.date(), - Math.round(response.price() * 100) - ); - return ResponseEntity.ok(dto); - } + GetAssetPriceResponse response = handler.handle(new GetAssetPriceCommand(assetId, date)); + PriceDTO dto = new PriceDTO(response.date(), Math.round(response.price() * 100)); + return ResponseEntity.ok(dto); + } } diff --git a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java b/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java index 82f8031..a7df645 100644 --- a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java +++ b/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java @@ -3,56 +3,54 @@ import io.autoinvestor.exceptions.*; import io.autoinvestor.infrastructure.fetchers.PriceFetchFailedException; import io.autoinvestor.infrastructure.fetchers.PriceNotAvailableException; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestControllerAdvice(assignableTypes = { - RegisterAssetController.class, - GetAllAssetsController.class, - GetAssetController.class, - GetAssetPriceController.class, -}) +@RestControllerAdvice( + assignableTypes = { + RegisterAssetController.class, + GetAllAssetsController.class, + GetAssetController.class, + GetAssetPriceController.class, + }) public class GlobalExceptionHandler { - @ExceptionHandler(DuplicatedException.class) - public ResponseEntity handleDuplicatedException(DuplicatedException ex) { - return ErrorResponse.builder() - .status(HttpStatus.CONFLICT) - .message(ex.getMessage()) - .build(); - } - - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException ex) { - return ErrorResponse.builder() - .status(HttpStatus.BAD_REQUEST) - .message(ex.getMessage()) - .build(); - } - - @ExceptionHandler(PriceNotAvailableException.class) - public ResponseEntity handlePriceNotAvailableException(PriceNotAvailableException ex) { - return ErrorResponse.builder() - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .message(ex.getMessage()) - .build(); - } - - @ExceptionHandler(PriceFetchFailedException.class) - public ResponseEntity handlePriceFetchFailedException(PriceFetchFailedException ex) { - return ErrorResponse.builder() - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .message(ex.getMessage()) - .build(); - } - - @ExceptionHandler(InternalErrorException.class) - public ResponseEntity handleInternalErrorException(InternalErrorException ex) { - return ErrorResponse.builder() - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .message(ex.getMessage()) - .build(); - } + @ExceptionHandler(DuplicatedException.class) + public ResponseEntity handleDuplicatedException(DuplicatedException ex) { + return ErrorResponse.builder().status(HttpStatus.CONFLICT).message(ex.getMessage()).build(); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException ex) { + return ErrorResponse.builder().status(HttpStatus.BAD_REQUEST).message(ex.getMessage()).build(); + } + + @ExceptionHandler(PriceNotAvailableException.class) + public ResponseEntity handlePriceNotAvailableException( + PriceNotAvailableException ex) { + return ErrorResponse.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .message(ex.getMessage()) + .build(); + } + + @ExceptionHandler(PriceFetchFailedException.class) + public ResponseEntity handlePriceFetchFailedException( + PriceFetchFailedException ex) { + return ErrorResponse.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .message(ex.getMessage()) + .build(); + } + + @ExceptionHandler(InternalErrorException.class) + public ResponseEntity handleInternalErrorException(InternalErrorException ex) { + return ErrorResponse.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .message(ex.getMessage()) + .build(); + } } diff --git a/src/main/java/io/autoinvestor/ui/InvalidRegisterAssetInputException.java b/src/main/java/io/autoinvestor/ui/InvalidRegisterAssetInputException.java index 22af6ca..0ec4fc8 100644 --- a/src/main/java/io/autoinvestor/ui/InvalidRegisterAssetInputException.java +++ b/src/main/java/io/autoinvestor/ui/InvalidRegisterAssetInputException.java @@ -3,7 +3,7 @@ import io.autoinvestor.exceptions.BadRequestException; public class InvalidRegisterAssetInputException extends BadRequestException { - public InvalidRegisterAssetInputException(String message) { - super(message); - } + public InvalidRegisterAssetInputException(String message) { + super(message); + } } diff --git a/src/main/java/io/autoinvestor/ui/PriceDTO.java b/src/main/java/io/autoinvestor/ui/PriceDTO.java index 08cf987..e83337d 100644 --- a/src/main/java/io/autoinvestor/ui/PriceDTO.java +++ b/src/main/java/io/autoinvestor/ui/PriceDTO.java @@ -1,10 +1,7 @@ package io.autoinvestor.ui; -import com.fasterxml.jackson.annotation.JsonFormat; - import java.util.Date; -public record PriceDTO( - @JsonFormat(shape = JsonFormat.Shape.STRING) - Date date, - int price) { } +import com.fasterxml.jackson.annotation.JsonFormat; + +public record PriceDTO(@JsonFormat(shape = JsonFormat.Shape.STRING) Date date, int price) {} diff --git a/src/main/java/io/autoinvestor/ui/RegisterAssetController.java b/src/main/java/io/autoinvestor/ui/RegisterAssetController.java index 6ea98ba..a10fdfc 100644 --- a/src/main/java/io/autoinvestor/ui/RegisterAssetController.java +++ b/src/main/java/io/autoinvestor/ui/RegisterAssetController.java @@ -3,34 +3,30 @@ 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; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - @RestController @RequestMapping("/assets") public class RegisterAssetController { - private final RegisterAssetCommandHandler commandHandler; + private final RegisterAssetCommandHandler commandHandler; - public RegisterAssetController(RegisterAssetCommandHandler commandHandler) { - this.commandHandler = commandHandler; - } + public RegisterAssetController(RegisterAssetCommandHandler commandHandler) { + this.commandHandler = commandHandler; + } - @PostMapping - public ResponseEntity handle(@RequestBody RegisterAssetDTO queryDto) { - RegisterAssetResponse response = this.commandHandler.handle(new RegisterAssetCommand( - queryDto.mic(), queryDto.ticker(), queryDto.name() - )); - AssetDTO dto = new AssetDTO( - response.assetId(), - response.mic(), - response.ticker(), - response.name() - ); - return ResponseEntity.ok(dto); - } + @PostMapping + public ResponseEntity handle(@RequestBody RegisterAssetDTO queryDto) { + RegisterAssetResponse response = + this.commandHandler.handle( + new RegisterAssetCommand(queryDto.mic(), queryDto.ticker(), queryDto.name())); + 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/RegisterAssetDTO.java b/src/main/java/io/autoinvestor/ui/RegisterAssetDTO.java index 6ff0dda..68a9395 100644 --- a/src/main/java/io/autoinvestor/ui/RegisterAssetDTO.java +++ b/src/main/java/io/autoinvestor/ui/RegisterAssetDTO.java @@ -3,23 +3,28 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - public record RegisterAssetDTO(String mic, String ticker, String name) { - @JsonCreator - public RegisterAssetDTO(@JsonProperty("mic") String mic, - @JsonProperty("ticker") String ticker, - @JsonProperty("name") String name) { - if (mic == null || ticker == null || name == null || mic.isEmpty() || ticker.isEmpty() || name.isEmpty()) { - throw new InvalidRegisterAssetInputException("All fields should not be null"); - } - - if (mic.trim().isEmpty() || ticker.trim().isEmpty() || name.trim().isEmpty()) { - throw new InvalidRegisterAssetInputException("All fields should not be empty"); - } + @JsonCreator + public RegisterAssetDTO( + @JsonProperty("mic") String mic, + @JsonProperty("ticker") String ticker, + @JsonProperty("name") String name) { + if (mic == null + || ticker == null + || name == null + || mic.isEmpty() + || ticker.isEmpty() + || name.isEmpty()) { + throw new InvalidRegisterAssetInputException("All fields should not be null"); + } - this.mic = mic; - this.ticker = ticker; - this.name = name; + if (mic.trim().isEmpty() || ticker.trim().isEmpty() || name.trim().isEmpty()) { + throw new InvalidRegisterAssetInputException("All fields should not be empty"); } -} \ No newline at end of file + + this.mic = mic; + this.ticker = ticker; + this.name = name; + } +}