diff --git a/build.gradle b/build.gradle index fd79852..8a43ec2 100644 --- a/build.gradle +++ b/build.gradle @@ -21,20 +21,24 @@ 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 'org.springframework.security:spring-security-core:' - implementation 'io.jsonwebtoken:jjwt:0.12.6' - implementation 'io.github.cdimascio:java-dotenv:5.2.2' - implementation platform("com.google.cloud:spring-cloud-gcp-dependencies:6.1.1") - implementation 'com.google.cloud:spring-cloud-gcp-starter-pubsub' + + 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 'com.google.cloud:google-cloud-pubsub:1.113.7' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("org.springframework.cloud:spring-cloud-gcp-starter-pubsub:1.2.5.RELEASE") - implementation("org.springframework.integration:spring-integration-core") - implementation 'com.google.cloud:spring-cloud-gcp-starter-pubsub' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:gcloud' + + compileOnly 'org.projectlombok:lombok:1.18.38' + annotationProcessor 'org.projectlombok:lombok:1.18.38' + testCompileOnly 'org.projectlombok:lombok:1.18.38' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.38' } tasks.named('test') { diff --git a/src/main/java/io/autoinvestor/application/HoldingsReadModel.java b/src/main/java/io/autoinvestor/application/HoldingsReadModel.java new file mode 100644 index 0000000..3de436a --- /dev/null +++ b/src/main/java/io/autoinvestor/application/HoldingsReadModel.java @@ -0,0 +1,9 @@ +package io.autoinvestor.application; + +import java.util.List; + +public interface HoldingsReadModel { + void add(HoldingsReadModelDTO dto); + void update(HoldingsReadModelDTO dto); + List getHoldings(String userId); +} diff --git a/src/main/java/io/autoinvestor/application/ComplexReadModelDTO.java b/src/main/java/io/autoinvestor/application/HoldingsReadModelDTO.java similarity index 79% rename from src/main/java/io/autoinvestor/application/ComplexReadModelDTO.java rename to src/main/java/io/autoinvestor/application/HoldingsReadModelDTO.java index 3dfa652..efad3c3 100644 --- a/src/main/java/io/autoinvestor/application/ComplexReadModelDTO.java +++ b/src/main/java/io/autoinvestor/application/HoldingsReadModelDTO.java @@ -1,6 +1,6 @@ package io.autoinvestor.application; -public record ComplexReadModelDTO( +public record HoldingsReadModelDTO( String userId, String assetId, Integer amount, diff --git a/src/main/java/io/autoinvestor/application/NewHoldingUseCase/NewHoldingCommandHandler.java b/src/main/java/io/autoinvestor/application/NewHoldingUseCase/NewHoldingCommandHandler.java index 53fdb71..0845184 100644 --- a/src/main/java/io/autoinvestor/application/NewHoldingUseCase/NewHoldingCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/NewHoldingUseCase/NewHoldingCommandHandler.java @@ -1,12 +1,15 @@ package io.autoinvestor.application.NewHoldingUseCase; -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.application.ReadModel; -import io.autoinvestor.domain.*; +import io.autoinvestor.application.HoldingsReadModel; +import io.autoinvestor.application.HoldingsReadModelDTO; +import io.autoinvestor.application.UsersWalletReadModel; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventPublisher; +import io.autoinvestor.domain.events.WalletEventStoreRepository; +import io.autoinvestor.domain.model.AssetId; +import io.autoinvestor.domain.model.Wallet; +import io.autoinvestor.domain.model.WalletId; import io.autoinvestor.exceptions.UserWithoutPortfolio; -import io.autoinvestor.infrastructure.EventMessageMapper; -import io.autoinvestor.infrastructure.EventPublisherQueue; -import io.autoinvestor.infrastructure.HoldingAddedOrUpdatedMessage; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -16,29 +19,37 @@ @RequiredArgsConstructor public class NewHoldingCommandHandler { - private final WalletRepository eventRepository; - private final ReadModel readModel; - private final EventMessageMapper messageMapper; - private final EventPublisherQueue eventPublisher; + private final WalletEventStoreRepository eventStore; + private final UsersWalletReadModel usersWalletReadModel; + private final HoldingsReadModel holdingsReadModel; + private final EventPublisher eventPublisher; - public void handle (NewHoldingCommand command) { - String walletId = readModel.getWalletId(command.userId()); + public void handle(NewHoldingCommand command) { + String walletId = this.usersWalletReadModel.getWalletId(command.userId()); if (walletId == null) { throw UserWithoutPortfolio.with(command.userId()); } - Wallet wallet = eventRepository.get(walletId); - wallet.newHolding(command.userId(), command.assetId(), command.amount(), command.boughtPrice()); - List> uncommittedEvents = wallet.releaseEvents(); - this.eventRepository.save(uncommittedEvents); - ComplexReadModelDTO dto = new ComplexReadModelDTO( - wallet.getState().userId().value(), + + Wallet wallet = this.eventStore.get(WalletId.of(walletId)) + .orElseThrow(() -> UserWithoutPortfolio.with(command.userId())); + + wallet.createHolding(command.userId(), command.assetId(), command.amount(), command.boughtPrice()); + + List> events = wallet.getUncommittedEvents(); + + this.eventStore.save(wallet); + + HoldingsReadModelDTO dto = new HoldingsReadModelDTO( + wallet.getState().getUserId().value(), command.assetId(), - wallet.getState().holdings().get(AssetId.of(command.assetId())).amount().value(), - wallet.getState().holdings().get(AssetId.of(command.assetId())).boughtPrice().value() + wallet.getState().getHoldings().get(AssetId.of(command.assetId())).amount().value(), + wallet.getState().getHoldings().get(AssetId.of(command.assetId())).boughtPrice().value() ); - readModel.add(dto); - List holdingAddedMessages = this.messageMapper.mapToHoldingAddedMessage(uncommittedEvents); - this.eventPublisher.publishHoldingAddedOrUpdated(holdingAddedMessages); + this.holdingsReadModel.add(dto); + + this.eventPublisher.publish(events); + + wallet.markEventsAsCommitted(); } } diff --git a/src/main/java/io/autoinvestor/application/QueryHoldingsUseCase/GetHoldingsQueryHandler.java b/src/main/java/io/autoinvestor/application/QueryHoldingsUseCase/GetHoldingsQueryHandler.java index 3c9556a..65b7a48 100644 --- a/src/main/java/io/autoinvestor/application/QueryHoldingsUseCase/GetHoldingsQueryHandler.java +++ b/src/main/java/io/autoinvestor/application/QueryHoldingsUseCase/GetHoldingsQueryHandler.java @@ -1,7 +1,7 @@ package io.autoinvestor.application.QueryHoldingsUseCase; -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.application.ReadModel; +import io.autoinvestor.application.HoldingsReadModel; +import io.autoinvestor.application.HoldingsReadModelDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,9 +11,9 @@ @RequiredArgsConstructor public class GetHoldingsQueryHandler { - private final ReadModel readModel; + private final HoldingsReadModel readModel; - public List handle (GetHoldingsQuery query) { - return readModel.getHoldings(query.userId()); + public List handle (GetHoldingsQuery query) { + return this.readModel.getHoldings(query.userId()); } } diff --git a/src/main/java/io/autoinvestor/application/ReadModel.java b/src/main/java/io/autoinvestor/application/ReadModel.java deleted file mode 100644 index 6afbba0..0000000 --- a/src/main/java/io/autoinvestor/application/ReadModel.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.autoinvestor.application; - -import io.autoinvestor.domain.Holding; -import io.autoinvestor.infrastructure.ReadModel.ComplexReadModelDocument; - -import java.util.List; -import java.util.Map; - -public interface ReadModel { - void add(SimpleReadModelDTO dto); - void add(ComplexReadModelDTO dto); - String getWalletId(String userId); - void update(ComplexReadModelDTO dto); - List getHoldings (String userId); -} diff --git a/src/main/java/io/autoinvestor/application/UptdateHoldingUseCase/UpdateHoldingCommandHandler.java b/src/main/java/io/autoinvestor/application/UptdateHoldingUseCase/UpdateHoldingCommandHandler.java index 07be2b3..1ef4ccb 100644 --- a/src/main/java/io/autoinvestor/application/UptdateHoldingUseCase/UpdateHoldingCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/UptdateHoldingUseCase/UpdateHoldingCommandHandler.java @@ -1,15 +1,15 @@ package io.autoinvestor.application.UptdateHoldingUseCase; -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.application.ReadModel; -import io.autoinvestor.domain.AssetId; -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.Wallet; -import io.autoinvestor.domain.WalletRepository; +import io.autoinvestor.application.HoldingsReadModel; +import io.autoinvestor.application.HoldingsReadModelDTO; +import io.autoinvestor.application.UsersWalletReadModel; +import io.autoinvestor.domain.events.EventPublisher; +import io.autoinvestor.domain.model.AssetId; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.model.Wallet; +import io.autoinvestor.domain.events.WalletEventStoreRepository; +import io.autoinvestor.domain.model.WalletId; import io.autoinvestor.exceptions.UserWithoutPortfolio; -import io.autoinvestor.infrastructure.EventMessageMapper; -import io.autoinvestor.infrastructure.EventPublisherQueue; -import io.autoinvestor.infrastructure.HoldingAddedOrUpdatedMessage; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,29 +19,36 @@ @RequiredArgsConstructor public class UpdateHoldingCommandHandler { - private final ReadModel readModel; - private final WalletRepository eventRepository; - private final EventMessageMapper messageMapper; - private final EventPublisherQueue eventPublisher; + private final UsersWalletReadModel usersWalletReadModel; + private final HoldingsReadModel holdingsReadModel; + private final WalletEventStoreRepository eventStore; + private final EventPublisher eventPublisher; public void handle (UpdateHoldingCommand command) { - String walletId = readModel.getWalletId(command.userId()); + String walletId = this.usersWalletReadModel.getWalletId(command.userId()); if (walletId == null) { throw UserWithoutPortfolio.with(command.userId()); } - Wallet wallet = eventRepository.get(walletId); + + Wallet wallet = this.eventStore.get(WalletId.of(walletId)) + .orElseThrow(() -> UserWithoutPortfolio.with(command.userId())); + wallet.updateHolding(command.userId(), command.assetId(), command.amount(), command.boughtPrice()); - List> uncommittedEvents = wallet.releaseEvents(); - this.eventRepository.save(uncommittedEvents); - ComplexReadModelDTO dto = new ComplexReadModelDTO( - wallet.getState().userId().value(), - command.assetId(), - wallet.getState().holdings().get(AssetId.of(command.assetId())).amount().value(), - wallet.getState().holdings().get(AssetId.of(command.assetId())).boughtPrice().value() + List> events = wallet.getUncommittedEvents(); + + this.eventStore.save(wallet); + + HoldingsReadModelDTO dto = new HoldingsReadModelDTO( + wallet.getState().getUserId().value(), + command.assetId(), + wallet.getState().getHoldings().get(AssetId.of(command.assetId())).amount().value(), + wallet.getState().getHoldings().get(AssetId.of(command.assetId())).boughtPrice().value() ); - this.readModel.update(dto); - List holdingUpdatedMessages = this.messageMapper.mapToHoldingUpdatedMessage(uncommittedEvents); - this.eventPublisher.publishHoldingAddedOrUpdated(holdingUpdatedMessages); + this.holdingsReadModel.update(dto); + + this.eventPublisher.publish(events); + + wallet.markEventsAsCommitted(); } } diff --git a/src/main/java/io/autoinvestor/application/UsersWalletReadModel.java b/src/main/java/io/autoinvestor/application/UsersWalletReadModel.java new file mode 100644 index 0000000..7b641fe --- /dev/null +++ b/src/main/java/io/autoinvestor/application/UsersWalletReadModel.java @@ -0,0 +1,6 @@ +package io.autoinvestor.application; + +public interface UsersWalletReadModel { + void add(UsersWalletReadModelDTO dto); + String getWalletId(String userId); +} diff --git a/src/main/java/io/autoinvestor/application/SimpleReadModelDTO.java b/src/main/java/io/autoinvestor/application/UsersWalletReadModelDTO.java similarity index 70% rename from src/main/java/io/autoinvestor/application/SimpleReadModelDTO.java rename to src/main/java/io/autoinvestor/application/UsersWalletReadModelDTO.java index 42e5674..9dbdf1a 100644 --- a/src/main/java/io/autoinvestor/application/SimpleReadModelDTO.java +++ b/src/main/java/io/autoinvestor/application/UsersWalletReadModelDTO.java @@ -1,6 +1,6 @@ package io.autoinvestor.application; -public record SimpleReadModelDTO( +public record UsersWalletReadModelDTO( String walletId, String userId ) { diff --git a/src/main/java/io/autoinvestor/application/WalletCreatedUseCase/WalletCreatedHandler.java b/src/main/java/io/autoinvestor/application/WalletCreatedUseCase/WalletCreatedHandler.java index 4d573ba..16b7ee6 100644 --- a/src/main/java/io/autoinvestor/application/WalletCreatedUseCase/WalletCreatedHandler.java +++ b/src/main/java/io/autoinvestor/application/WalletCreatedUseCase/WalletCreatedHandler.java @@ -1,9 +1,10 @@ package io.autoinvestor.application.WalletCreatedUseCase; -import io.autoinvestor.application.SimpleReadModelDTO; -import io.autoinvestor.application.ReadModel; -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.WalletRepository; -import io.autoinvestor.domain.Wallet; +import io.autoinvestor.application.UsersWalletReadModel; +import io.autoinvestor.application.UsersWalletReadModelDTO; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.WalletEventStoreRepository; +import io.autoinvestor.domain.events.EventPublisher; +import io.autoinvestor.domain.model.Wallet; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,21 +14,26 @@ @RequiredArgsConstructor public class WalletCreatedHandler { - private final WalletRepository eventRepository; - private final ReadModel readModel; + private final WalletEventStoreRepository eventStore; + private final EventPublisher eventPublisher; + private final UsersWalletReadModel readModel; public void handle(WalletCreateCommand command) { - System.out.println("Mensaje recibido en WalletCreatedHandler: " + command.userId()); Wallet wallet = Wallet.create(command.userId()); - List> uncomittedEvents = wallet.releaseEvents(); - this.eventRepository.save(uncomittedEvents); - SimpleReadModelDTO dto = new SimpleReadModelDTO( - wallet.getState().walletId().value(), - wallet.getState().userId().value() + + List> events = wallet.getUncommittedEvents(); + + this.eventStore.save(wallet); + + UsersWalletReadModelDTO dto = new UsersWalletReadModelDTO( + wallet.getState().getWalletId().value(), + wallet.getState().getUserId().value() ); - readModel.add(dto); + this.readModel.add(dto); + this.eventPublisher.publish(events); + wallet.markEventsAsCommitted(); } } diff --git a/src/main/java/io/autoinvestor/domain/AggregateRoot.java b/src/main/java/io/autoinvestor/domain/AggregateRoot.java deleted file mode 100644 index 34af3dd..0000000 --- a/src/main/java/io/autoinvestor/domain/AggregateRoot.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.ArrayList; -import java.util.List; - -public class AggregateRoot { - private final List> appliedEvents; - private Integer version; - - protected AggregateRoot(List> stream) { - if (!stream.isEmpty()) { - for (Event event : stream) { - this.when(event); - } - this.version = stream.size(); - } else { - this.version = 0; - } - appliedEvents = new ArrayList<>(); - } - - protected void when(Event event) { - } - - protected void recordEvent(Event event) { - this.appliedEvents.add(event); - } - - protected void apply(Event event) { - this.when(event); - this.appliedEvents.add(event); - } - - public List> releaseEvents() { - List> events = new ArrayList<>(this.appliedEvents); - this.appliedEvents.clear(); - return events; - } -} diff --git a/src/main/java/io/autoinvestor/domain/Event.java b/src/main/java/io/autoinvestor/domain/Event.java deleted file mode 100644 index f50c707..0000000 --- a/src/main/java/io/autoinvestor/domain/Event.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.Date; - -public abstract class Event { - private final EventId id; - private final Id aggregateId; - private final String type; - private final EventPayload payload; - private final Date occurredAt; - private final int version; - - protected Event(Id aggregateId, String type, EventPayload payload) { - this(aggregateId, type, payload, 1, new Date()); - } - - protected Event(Id aggregateId, String type, EventPayload payload, int version, Date occurredAt) { - this.id = EventId.generate(); - this.aggregateId = aggregateId; - this.type = type; - this.payload = payload; - this.occurredAt = occurredAt; - this.version = version; - } - - public EventId getId() { - return id; - } - - public Id getAggregateId() { - return aggregateId; - } - - public String getType() { - return type; - } - - public EventPayload getPayload() { - return payload; - } - - public Date getOccurredAt() { - return occurredAt; - } - - public int getVersion() { - return version; - } -} diff --git a/src/main/java/io/autoinvestor/domain/Holding.java b/src/main/java/io/autoinvestor/domain/Holding.java deleted file mode 100644 index d2c97c0..0000000 --- a/src/main/java/io/autoinvestor/domain/Holding.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.autoinvestor.domain; - -public record Holding( - Amount amount, - BoughtPrice boughtPrice -) {} diff --git a/src/main/java/io/autoinvestor/domain/HoldingWasCreatedEvent.java b/src/main/java/io/autoinvestor/domain/HoldingWasCreatedEvent.java deleted file mode 100644 index ce4237a..0000000 --- a/src/main/java/io/autoinvestor/domain/HoldingWasCreatedEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.Date; - -public class HoldingWasCreatedEvent extends Event{ - protected HoldingWasCreatedEvent(Id aggregateId, HoldingWasCreatedEventPayload payload) { - super(aggregateId, "PORTFOLIO_ASSET_ADDED", payload); - } - - protected HoldingWasCreatedEvent(WalletId walletId, HoldingWasCreatedEventPayload payload, int version, Date occurredAt) { - super(walletId, "PORTFOLIO_ASSET_ADDED", payload, version, occurredAt); - } - public static HoldingWasCreatedEvent with( - WalletId walletId, - UserId userId, - AssetId assetId, - Amount amount, - BoughtPrice boughtPrice - ) { - var payload = new HoldingWasCreatedEventPayload(userId, assetId, amount, boughtPrice); - return new HoldingWasCreatedEvent(walletId, payload); - } - - public static HoldingWasCreatedEvent hydrate( - WalletId walletId, - HoldingWasCreatedEventPayload payload, - int version, - Date occurredAt - ) { - return new HoldingWasCreatedEvent(walletId, payload, version, occurredAt); - } - -} diff --git a/src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEvent.java b/src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEvent.java deleted file mode 100644 index 39fea38..0000000 --- a/src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.Date; - -public class HoldingWasUpdatedEvent extends Event{ - protected HoldingWasUpdatedEvent(Id aggregateId, HoldingWasUpdatedEventPayload payload) { - super(aggregateId, "PORTFOLIO_ASSET_UPDATED", payload); - } - - protected HoldingWasUpdatedEvent(WalletId walletId, HoldingWasUpdatedEventPayload payload, int version, Date occurredAt) { - super(walletId, "PORTFOLIO_ASSET_UPDATED", payload, version, occurredAt); - } - - public static HoldingWasUpdatedEvent with( - WalletId walletId, - UserId userId, - AssetId assetId, - Amount amount, - BoughtPrice boughtPrice - ) { - var payload = new HoldingWasUpdatedEventPayload(userId, assetId, amount, boughtPrice); - return new HoldingWasUpdatedEvent(walletId, payload); - } - - public static HoldingWasUpdatedEvent hydrate( - WalletId walletId, - HoldingWasUpdatedEventPayload payload, - int version, - Date occurredAt - ) { - return new HoldingWasUpdatedEvent(walletId, payload, version, occurredAt); - } - -} diff --git a/src/main/java/io/autoinvestor/domain/Id.java b/src/main/java/io/autoinvestor/domain/Id.java index 2960264..5c43c52 100644 --- a/src/main/java/io/autoinvestor/domain/Id.java +++ b/src/main/java/io/autoinvestor/domain/Id.java @@ -4,7 +4,7 @@ import java.util.UUID; public abstract class Id { - private final String id; + protected final String id; public Id(String id) { this.id = id; @@ -24,11 +24,16 @@ public boolean equals(Object o) { return true; if (!(o instanceof Id that)) return false; - return Objects.equals(id, that.id); + return id.equals(that.id); } @Override public int hashCode() { return Objects.hash(id); } + + @Override + public String toString() { + return id; + } } diff --git a/src/main/java/io/autoinvestor/domain/Wallet.java b/src/main/java/io/autoinvestor/domain/Wallet.java deleted file mode 100644 index c6e3d01..0000000 --- a/src/main/java/io/autoinvestor/domain/Wallet.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.autoinvestor.domain; - -import lombok.Getter; - -import java.util.ArrayList; -import java.util.List; - -public class Wallet extends AggregateRoot{ - - @Getter - private WalletState walletState; - - private Wallet(List> stream) { - super(stream); - } - - public static Wallet from(List> stream) { - return new Wallet(stream); - } - - @Override - protected void when(Event event) { - switch (event.getType()) { - case "WALLET_CREATED" : - this.whenWalletCreated((WalletWasCreatedEvent) event); - break; - case "PORTFOLIO_ASSET_ADDED" : - this.whenHoldingCreated((HoldingWasCreatedEvent) event); - break; - case "PORTFOLIO_ASSET_UPDATED" : - this.whenHoldingUpdated((HoldingWasUpdatedEvent) event); - - } - } - - private void whenWalletCreated(WalletWasCreatedEvent event) { - this.walletState = WalletState.withWalletCreated(event); - } - private void whenHoldingCreated(HoldingWasCreatedEvent event) { - this.walletState = this.walletState.withHoldingCreated(event); - } - private void whenHoldingUpdated(HoldingWasUpdatedEvent event) { - this.walletState = this.walletState.withHoldingUpdated(event); - } - - public static Wallet empty() { - return new Wallet(new ArrayList<>()); - } - - - - - - public static Wallet create(String userId) { - WalletId id = WalletId.generate(); - UserId userIdDTO = new UserId(userId); - Wallet wallet = Wallet.empty(); - wallet.createWallet(id, userIdDTO); - return wallet; - } - - public Wallet newHolding(String userId, String assetId, Integer amount, Integer boughtPrice) { - UserId userIdDTO = UserId.of(userId); - AssetId assetIdDTO = AssetId.of(assetId); - Amount amountDTO = new Amount(amount); - BoughtPrice boughtPriceDTO = new BoughtPrice(boughtPrice); - createHolding(userIdDTO, assetIdDTO, amountDTO, boughtPriceDTO); - return this; - } - - public Wallet updateHolding(String userId, String assetId, Integer amount, Integer boughtPrice) { - UserId userIdDTO = UserId.of(userId); - AssetId assetIdDTO = AssetId.of(assetId); - Amount amountDTO = new Amount(amount); - BoughtPrice boughtPriceDTO = new BoughtPrice(boughtPrice); - updateHoldingApplier(userIdDTO, assetIdDTO, amountDTO, boughtPriceDTO); - return this; - } - - - public void createWallet(WalletId walletId, UserId userId) { - this.apply(WalletWasCreatedEvent.with(walletId, userId)); - } - - public void createHolding(UserId userId, AssetId assetId, Amount amount, BoughtPrice boughtPrice) { - this.apply(HoldingWasCreatedEvent.with( - walletState.walletId(), userId, assetId, amount, boughtPrice)); - } - public void updateHoldingApplier(UserId userId, AssetId assetId, Amount amount, BoughtPrice boughtPrice) { - this.apply(HoldingWasUpdatedEvent.with(walletState.walletId(), userId, assetId, amount, boughtPrice)); - } - - public WalletState getState () { - return walletState; - } -} diff --git a/src/main/java/io/autoinvestor/domain/WalletRepository.java b/src/main/java/io/autoinvestor/domain/WalletRepository.java deleted file mode 100644 index 98fe033..0000000 --- a/src/main/java/io/autoinvestor/domain/WalletRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.List; - -public interface WalletRepository { - void save(List> walletEvents); - Wallet get (String walletId); -} diff --git a/src/main/java/io/autoinvestor/domain/WalletState.java b/src/main/java/io/autoinvestor/domain/WalletState.java deleted file mode 100644 index 12d4453..0000000 --- a/src/main/java/io/autoinvestor/domain/WalletState.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.autoinvestor.domain; - - -import java.util.HashMap; -import java.util.Map; - -public record WalletState( - WalletId walletId, - UserId userId, - Map holdings -) { - public static WalletState withWalletCreated(WalletWasCreatedEvent event) { - WalletWasCreatedPayload payload = event.getPayload(); - return new WalletState( - (WalletId) event.getAggregateId(), - payload.userId(), - new HashMap<>() - ); - } - - public WalletState withHoldingCreated(HoldingWasCreatedEvent event) { - HoldingWasCreatedEventPayload payload = event.getPayload(); - WalletId walletId = (WalletId) event.getAggregateId(); - UserId userId = payload.userId(); - Holding newHolding = new Holding(payload.amount(), payload.boughtPrice()); - Map holdingsUpdated = new HashMap<>(this.holdings); - holdingsUpdated.put(payload.assetId(), newHolding); - return new WalletState( - walletId, - userId, - holdingsUpdated - ); - } - - public WalletState withHoldingUpdated(HoldingWasUpdatedEvent event) { - HoldingWasUpdatedEventPayload payload = event.getPayload(); - Holding holdingUpdated = new Holding(payload.amount(), payload.boughtPrice()); - holdings.put(payload.assetId(), holdingUpdated); - return new WalletState( - this.walletId, - this.userId, - this.holdings - ); - } - -} diff --git a/src/main/java/io/autoinvestor/domain/WalletWasCreatedEvent.java b/src/main/java/io/autoinvestor/domain/WalletWasCreatedEvent.java deleted file mode 100644 index dad8fd5..0000000 --- a/src/main/java/io/autoinvestor/domain/WalletWasCreatedEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.Date; - -public class WalletWasCreatedEvent extends Event{ - - private WalletWasCreatedEvent(Id aggregateId, WalletWasCreatedPayload payload) { - super(aggregateId, "WALLET_CREATED", payload); - } - - public static WalletWasCreatedEvent with( - WalletId holdingId, - UserId userId - ) { - var payload = new WalletWasCreatedPayload(userId); - return new WalletWasCreatedEvent(holdingId, payload); - } - - protected WalletWasCreatedEvent(WalletId walletId, WalletWasCreatedPayload payload, int version, Date occurredAt) { - super(walletId, "WALLET_CREATED", payload, version, occurredAt); - } - - public static WalletWasCreatedEvent hydrate(WalletId walletId, - WalletWasCreatedPayload payload, - int version, - Date occurredAt) { - return new WalletWasCreatedEvent(walletId, payload, version, occurredAt); - } - -} diff --git a/src/main/java/io/autoinvestor/domain/events/Event.java b/src/main/java/io/autoinvestor/domain/events/Event.java new file mode 100644 index 0000000..940207c --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/Event.java @@ -0,0 +1,38 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.Id; +import lombok.Getter; + +import java.util.Date; + +@Getter +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; + } + + protected Event(EventId id, Id aggregateId, String type, P payload, Date occurredAt, int version) { + this.id = id; + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = occurredAt; + this.version = version; + } +} diff --git a/src/main/java/io/autoinvestor/domain/EventId.java b/src/main/java/io/autoinvestor/domain/events/EventId.java similarity index 53% rename from src/main/java/io/autoinvestor/domain/EventId.java rename to src/main/java/io/autoinvestor/domain/events/EventId.java index 8601083..1b08665 100644 --- a/src/main/java/io/autoinvestor/domain/EventId.java +++ b/src/main/java/io/autoinvestor/domain/events/EventId.java @@ -1,4 +1,6 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.Id; public class EventId extends Id { EventId(String id) { @@ -8,4 +10,8 @@ public class EventId extends Id { public static EventId generate() { return new EventId(generateId()); } + + public static EventId of(String id) { + return new EventId(id); + } } diff --git a/src/main/java/io/autoinvestor/domain/EventPayload.java b/src/main/java/io/autoinvestor/domain/events/EventPayload.java similarity index 70% rename from src/main/java/io/autoinvestor/domain/EventPayload.java rename to src/main/java/io/autoinvestor/domain/events/EventPayload.java index 48b01d4..afa0a55 100644 --- a/src/main/java/io/autoinvestor/domain/EventPayload.java +++ b/src/main/java/io/autoinvestor/domain/events/EventPayload.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.Map; diff --git a/src/main/java/io/autoinvestor/domain/EventPublisher.java b/src/main/java/io/autoinvestor/domain/events/EventPublisher.java similarity index 72% rename from src/main/java/io/autoinvestor/domain/EventPublisher.java rename to src/main/java/io/autoinvestor/domain/events/EventPublisher.java index 799870e..623689f 100644 --- a/src/main/java/io/autoinvestor/domain/EventPublisher.java +++ b/src/main/java/io/autoinvestor/domain/events/EventPublisher.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.List; diff --git a/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java b/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java new file mode 100644 index 0000000..e9b67b0 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java @@ -0,0 +1,40 @@ +package io.autoinvestor.domain.events; + +import java.util.ArrayList; +import java.util.List; + +public abstract class EventSourcedEntity { + private final List> appliedEvents = new ArrayList<>(); + private int version; + + protected EventSourcedEntity(List> stream) { + if (!stream.isEmpty()) { + for (Event e : stream) { + when(e); + } + version = stream.size(); + } else { + version = 0; + } + } + + protected EventSourcedEntity() { + this(null); + } + + protected void apply(Event e) { + appliedEvents.add(e); + when(e); + } + + protected abstract void when(Event e); + + public List> getUncommittedEvents() { + return new ArrayList<>(appliedEvents); + } + + public void markEventsAsCommitted() { + appliedEvents.clear(); + } +} + diff --git a/src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEvent.java b/src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEvent.java new file mode 100644 index 0000000..9fd39cd --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEvent.java @@ -0,0 +1,41 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.*; +import io.autoinvestor.domain.model.*; + +import java.util.Date; + +public class HoldingWasCreatedEvent extends Event { + public static final String TYPE = "PORTFOLIO_ASSET_ADDED"; + + private HoldingWasCreatedEvent(Id aggregateId, HoldingWasCreatedEventPayload payload) { + super(aggregateId, TYPE, payload); + } + + protected HoldingWasCreatedEvent(EventId id, + Id aggregateId, + HoldingWasCreatedEventPayload payload, + Date occurredAt, + int version) { + super(id, aggregateId, TYPE, payload, occurredAt, version); + } + + public static HoldingWasCreatedEvent with(WalletId walletId, + UserId userId, + AssetId assetId, + Amount amount, + BoughtPrice boughtPrice) { + HoldingWasCreatedEventPayload payload = new HoldingWasCreatedEventPayload( + userId.value(), assetId.value(), amount.value(), boughtPrice.value() + ); + return new HoldingWasCreatedEvent(walletId, payload); + } + + public static HoldingWasCreatedEvent hydrate(EventId id, + Id aggregateId, + HoldingWasCreatedEventPayload payload, + Date occurredAt, + int version) { + return new HoldingWasCreatedEvent(id, aggregateId, payload, occurredAt, version); + } +} diff --git a/src/main/java/io/autoinvestor/domain/HoldingWasCreatedEventPayload.java b/src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEventPayload.java similarity index 65% rename from src/main/java/io/autoinvestor/domain/HoldingWasCreatedEventPayload.java rename to src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEventPayload.java index 3261ccc..c2e38b0 100644 --- a/src/main/java/io/autoinvestor/domain/HoldingWasCreatedEventPayload.java +++ b/src/main/java/io/autoinvestor/domain/events/HoldingWasCreatedEventPayload.java @@ -1,13 +1,13 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.Map; public record HoldingWasCreatedEventPayload( - UserId userId, - AssetId assetId, - Amount amount, - BoughtPrice boughtPrice -) implements EventPayload{ + String userId, + String assetId, + int amount, + int boughtPrice +) implements EventPayload { @Override public Map asMap() { return Map.of( @@ -17,4 +17,4 @@ public Map asMap() { "boughtPrice", boughtPrice ); } -} \ No newline at end of file +} diff --git a/src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEvent.java b/src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEvent.java new file mode 100644 index 0000000..17547bd --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEvent.java @@ -0,0 +1,41 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.*; +import io.autoinvestor.domain.model.*; + +import java.util.Date; + +public class HoldingWasUpdatedEvent extends Event { + public static final String TYPE = "PORTFOLIO_ASSET_UPDATED"; + + private HoldingWasUpdatedEvent(Id aggregateId, HoldingWasUpdatedEventPayload payload) { + super(aggregateId, TYPE, payload); + } + + protected HoldingWasUpdatedEvent(EventId id, + Id aggregateId, + HoldingWasUpdatedEventPayload payload, + Date occurredAt, + int version) { + super(id, aggregateId, TYPE, payload, occurredAt, version); + } + + public static HoldingWasUpdatedEvent with(WalletId walletId, + UserId userId, + AssetId assetId, + Amount amount, + BoughtPrice boughtPrice) { + HoldingWasUpdatedEventPayload payload = new HoldingWasUpdatedEventPayload( + userId.value(), assetId.value(), amount.value(), boughtPrice.value() + ); + return new HoldingWasUpdatedEvent(walletId, payload); + } + + public static HoldingWasUpdatedEvent hydrate(EventId id, + Id aggregateId, + HoldingWasUpdatedEventPayload payload, + Date occurredAt, + int version) { + return new HoldingWasUpdatedEvent(id, aggregateId, payload, occurredAt, version); + } +} diff --git a/src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEventPayload.java b/src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEventPayload.java similarity index 65% rename from src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEventPayload.java rename to src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEventPayload.java index 2b64812..26af7fb 100644 --- a/src/main/java/io/autoinvestor/domain/HoldingWasUpdatedEventPayload.java +++ b/src/main/java/io/autoinvestor/domain/events/HoldingWasUpdatedEventPayload.java @@ -1,13 +1,13 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.Map; public record HoldingWasUpdatedEventPayload( - UserId userId, - AssetId assetId, - Amount amount, - BoughtPrice boughtPrice -) implements EventPayload{ + String userId, + String assetId, + int amount, + int boughtPrice +) implements EventPayload { @Override public Map asMap() { return Map.of( diff --git a/src/main/java/io/autoinvestor/domain/events/WalletEventStoreRepository.java b/src/main/java/io/autoinvestor/domain/events/WalletEventStoreRepository.java new file mode 100644 index 0000000..01cee38 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/WalletEventStoreRepository.java @@ -0,0 +1,11 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.model.Wallet; +import io.autoinvestor.domain.model.WalletId; + +import java.util.Optional; + +public interface WalletEventStoreRepository { + Optional get(WalletId walletId); + void save(Wallet wallet); +} diff --git a/src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEvent.java b/src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEvent.java new file mode 100644 index 0000000..7061b81 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEvent.java @@ -0,0 +1,39 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.Id; +import io.autoinvestor.domain.model.*; + +import java.util.Date; + +public class WalletWasCreatedEvent extends Event { + public static final String TYPE = "WALLET_CREATED"; + + private WalletWasCreatedEvent(Id aggregateId, WalletWasCreatedEventPayload payload) { + super(aggregateId, TYPE, payload); + } + + protected WalletWasCreatedEvent(EventId id, + Id aggregateId, + WalletWasCreatedEventPayload payload, + Date occurredAt, + int version) { + super(id, aggregateId, TYPE, payload, occurredAt, version); + } + + public static WalletWasCreatedEvent with(WalletId walletId, + UserId userId) { + WalletWasCreatedEventPayload payload = new WalletWasCreatedEventPayload( + userId.value() + ); + return new WalletWasCreatedEvent(walletId, payload); + } + + public static WalletWasCreatedEvent hydrate(EventId id, + Id aggregateId, + WalletWasCreatedEventPayload payload, + Date occurredAt, + int version) { + return new WalletWasCreatedEvent(id, aggregateId, payload, occurredAt, version); + } + +} diff --git a/src/main/java/io/autoinvestor/domain/WalletWasCreatedPayload.java b/src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEventPayload.java similarity index 53% rename from src/main/java/io/autoinvestor/domain/WalletWasCreatedPayload.java rename to src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEventPayload.java index c531387..5a3fd0d 100644 --- a/src/main/java/io/autoinvestor/domain/WalletWasCreatedPayload.java +++ b/src/main/java/io/autoinvestor/domain/events/WalletWasCreatedEventPayload.java @@ -1,12 +1,10 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.Map; -public record WalletWasCreatedPayload( - UserId userId -) implements EventPayload{ - - +public record WalletWasCreatedEventPayload( + String userId +) implements EventPayload { @Override public Map asMap() { return Map.of( diff --git a/src/main/java/io/autoinvestor/domain/Amount.java b/src/main/java/io/autoinvestor/domain/model/Amount.java similarity index 65% rename from src/main/java/io/autoinvestor/domain/Amount.java rename to src/main/java/io/autoinvestor/domain/model/Amount.java index 53fa3d6..0ebde88 100644 --- a/src/main/java/io/autoinvestor/domain/Amount.java +++ b/src/main/java/io/autoinvestor/domain/model/Amount.java @@ -1,22 +1,26 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.model; import io.autoinvestor.exceptions.InvalidAmount; public class Amount { private final Integer amount; - public Amount(Integer amount) { - validate(amount); + Amount(Integer amount) { this.amount = amount; } + public static Amount of(Integer amount) { + validate(amount); + return new Amount(amount); + } + private static void validate(Integer amount) { if (amount < 1) { throw InvalidAmount.with(amount); } } - public Integer value() { + public int value() { return this.amount; } } diff --git a/src/main/java/io/autoinvestor/domain/AssetId.java b/src/main/java/io/autoinvestor/domain/model/AssetId.java similarity index 78% rename from src/main/java/io/autoinvestor/domain/AssetId.java rename to src/main/java/io/autoinvestor/domain/model/AssetId.java index c2bcfb1..ea5b2ad 100644 --- a/src/main/java/io/autoinvestor/domain/AssetId.java +++ b/src/main/java/io/autoinvestor/domain/model/AssetId.java @@ -1,4 +1,6 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.model; + +import io.autoinvestor.domain.Id; public class AssetId extends Id { AssetId(String id) { @@ -8,6 +10,7 @@ public class AssetId extends Id { public static AssetId generate() { return new AssetId(generateId()); } + public static AssetId of(String assetId) { return new AssetId(assetId); } diff --git a/src/main/java/io/autoinvestor/domain/BoughtPrice.java b/src/main/java/io/autoinvestor/domain/model/BoughtPrice.java similarity index 65% rename from src/main/java/io/autoinvestor/domain/BoughtPrice.java rename to src/main/java/io/autoinvestor/domain/model/BoughtPrice.java index 62a1070..00feae2 100644 --- a/src/main/java/io/autoinvestor/domain/BoughtPrice.java +++ b/src/main/java/io/autoinvestor/domain/model/BoughtPrice.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.model; import io.autoinvestor.exceptions.InvalidBoughtPrice; @@ -6,18 +6,22 @@ public class BoughtPrice { private final Integer boughtPrice; - public BoughtPrice(Integer boughtPrice) { - validate(boughtPrice); + BoughtPrice(Integer boughtPrice) { this.boughtPrice = boughtPrice; } + public static BoughtPrice of(Integer boughtPrice) { + validate(boughtPrice); + return new BoughtPrice(boughtPrice); + } + private static void validate (Integer boughtPrice) { if (boughtPrice < 1) { throw InvalidBoughtPrice.with(boughtPrice); } } - public Integer value () { + public int value() { return this.boughtPrice; } } diff --git a/src/main/java/io/autoinvestor/domain/model/Holding.java b/src/main/java/io/autoinvestor/domain/model/Holding.java new file mode 100644 index 0000000..bb35446 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/Holding.java @@ -0,0 +1,10 @@ +package io.autoinvestor.domain.model; + +public record Holding( + Amount amount, + BoughtPrice boughtPrice +) { + public static Holding of(Amount amount, BoughtPrice boughtPrice) { + return new Holding(amount, boughtPrice); + } +} diff --git a/src/main/java/io/autoinvestor/domain/UserId.java b/src/main/java/io/autoinvestor/domain/model/UserId.java similarity index 56% rename from src/main/java/io/autoinvestor/domain/UserId.java rename to src/main/java/io/autoinvestor/domain/model/UserId.java index 4c38e77..353d71d 100644 --- a/src/main/java/io/autoinvestor/domain/UserId.java +++ b/src/main/java/io/autoinvestor/domain/model/UserId.java @@ -1,10 +1,12 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.model; -public class UserId extends Id{ +import io.autoinvestor.domain.Id; +public class UserId extends Id { public UserId(String id){ super(id); } + public static UserId generate() { return new UserId(generateId()); } @@ -12,4 +14,8 @@ public static UserId generate() { public static UserId of(String userId){ return new UserId(userId); } + + public static UserId empty() { + return new UserId(""); + } } diff --git a/src/main/java/io/autoinvestor/domain/model/Wallet.java b/src/main/java/io/autoinvestor/domain/model/Wallet.java new file mode 100644 index 0000000..f342f9d --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/Wallet.java @@ -0,0 +1,91 @@ +package io.autoinvestor.domain.model; + +import io.autoinvestor.domain.events.*; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class Wallet extends EventSourcedEntity { + private WalletState state; + + private Wallet(List> stream) { + super(stream); + + if (stream.isEmpty()) { + this.state = WalletState.empty(); + } + } + + public static Wallet empty() { + return new Wallet(new ArrayList<>()); + } + + public static Wallet from(List> stream) { + return new Wallet(stream); + } + + public static Wallet create(String userId) { + Wallet wallet = Wallet.empty(); + + wallet.apply(WalletWasCreatedEvent.with( + wallet.getState().getWalletId(), + UserId.of(userId)) + ); + + return wallet; + } + + public void createHolding(String userId, String assetId, Integer amount, Integer boughtPrice) { + this.apply(HoldingWasCreatedEvent.with( + this.state.getWalletId(), + UserId.of(userId), + AssetId.of(assetId), + Amount.of(amount), + BoughtPrice.of(boughtPrice) + )); + } + + public void updateHolding(String userId, String assetId, Integer amount, Integer boughtPrice) { + this.apply(HoldingWasUpdatedEvent.with( + this.state.getWalletId(), + UserId.of(userId), + AssetId.of(assetId), + Amount.of(amount), + BoughtPrice.of(boughtPrice) + )); + } + + @Override + protected void when(Event e) { + switch (e.getType()) { + case WalletWasCreatedEvent.TYPE: + whenWalletCreated((WalletWasCreatedEvent) e); + break; + case HoldingWasCreatedEvent.TYPE: + whenHoldingCreated((HoldingWasCreatedEvent) e); + break; + case HoldingWasUpdatedEvent.TYPE: + whenHoldingUpdated((HoldingWasUpdatedEvent) e); + break; + default: + throw new IllegalArgumentException("Unknown event type"); + } + } + + private void whenWalletCreated(WalletWasCreatedEvent event) { + if (this.state == null) { + this.state = WalletState.empty(); + } + this.state = this.state.withWalletCreated(event); + } + + private void whenHoldingCreated(HoldingWasCreatedEvent event) { + this.state = this.state.withHoldingCreated(event); + } + + private void whenHoldingUpdated(HoldingWasUpdatedEvent event) { + this.state = this.state.withHoldingUpdated(event); + } +} diff --git a/src/main/java/io/autoinvestor/domain/WalletId.java b/src/main/java/io/autoinvestor/domain/model/WalletId.java similarity index 67% rename from src/main/java/io/autoinvestor/domain/WalletId.java rename to src/main/java/io/autoinvestor/domain/model/WalletId.java index 4545203..de7ace2 100644 --- a/src/main/java/io/autoinvestor/domain/WalletId.java +++ b/src/main/java/io/autoinvestor/domain/model/WalletId.java @@ -1,6 +1,8 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.model; -public class WalletId extends Id{ +import io.autoinvestor.domain.Id; + +public class WalletId extends Id { WalletId(String id) { super(id); } diff --git a/src/main/java/io/autoinvestor/domain/model/WalletState.java b/src/main/java/io/autoinvestor/domain/model/WalletState.java new file mode 100644 index 0000000..9e1c503 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/WalletState.java @@ -0,0 +1,59 @@ +package io.autoinvestor.domain.model; + + +import io.autoinvestor.domain.events.*; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class WalletState { + private final WalletId walletId; + private final UserId userId; + private final Map holdings; + + private WalletState(WalletId walletId, UserId userId, Map holdings) { + this.walletId = walletId; + this.userId = userId; + this.holdings = holdings; + } + + public static WalletState empty() { + return new WalletState(WalletId.generate(), UserId.empty(), new HashMap<>()); + } + + public WalletState withWalletCreated(WalletWasCreatedEvent event) { + WalletWasCreatedEventPayload payload = event.getPayload(); + return new WalletState( + WalletId.of(event.getAggregateId().value()), + UserId.of(payload.userId()), + new HashMap<>() + ); + } + + public WalletState withHoldingCreated(HoldingWasCreatedEvent event) { + HoldingWasCreatedEventPayload payload = event.getPayload(); + WalletId walletId = (WalletId) event.getAggregateId(); + UserId userId = UserId.of(payload.userId()); + Holding newHolding = Holding.of(Amount.of(payload.amount()), BoughtPrice.of(payload.boughtPrice())); + Map holdingsUpdated = new HashMap<>(this.holdings); + holdingsUpdated.put(AssetId.of(payload.assetId()), newHolding); + return new WalletState( + walletId, + userId, + holdingsUpdated + ); + } + + public WalletState withHoldingUpdated(HoldingWasUpdatedEvent event) { + HoldingWasUpdatedEventPayload payload = event.getPayload(); + Holding holdingUpdated = Holding.of(Amount.of(payload.amount()), BoughtPrice.of(payload.boughtPrice())); + holdings.put(AssetId.of(payload.assetId()), holdingUpdated); + return new WalletState( + this.walletId, + this.userId, + this.holdings + ); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/EventMessageMapper.java b/src/main/java/io/autoinvestor/infrastructure/EventMessageMapper.java deleted file mode 100644 index c9e812a..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/EventMessageMapper.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.HoldingWasCreatedEventPayload; -import io.autoinvestor.domain.HoldingWasUpdatedEventPayload; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -@Component -public class EventMessageMapper { - - public List mapToHoldingAddedMessage(List> holdingAddedEvent) { - List holdingAddedMessages = new ArrayList<>(); - for (Event holdingWasAdded : holdingAddedEvent) { - HoldingWasCreatedEventPayload payload = (HoldingWasCreatedEventPayload) holdingWasAdded.getPayload(); - HoldingAddedMessagePayload holdingAddedMessagePayload = new HoldingAddedMessagePayload( - payload.userId().value(), - payload.assetId().value(), - payload.amount().value(), - payload.boughtPrice().value() - ); - HoldingAddedOrUpdatedMessage holdingAddedMessage = new HoldingAddedOrUpdatedMessage( - holdingWasAdded.getId().value(), - holdingWasAdded.getOccurredAt(), - holdingWasAdded.getAggregateId().value(), - holdingWasAdded.getVersion(), - holdingWasAdded.getType(), - holdingAddedMessagePayload - ); - holdingAddedMessages.add(holdingAddedMessage); - } - return holdingAddedMessages; - } - - public List mapToHoldingUpdatedMessage(List> holdingAddedEvent) { - List holdingAddedMessages = new ArrayList<>(); - for (Event holdingWasAdded : holdingAddedEvent) { - HoldingWasUpdatedEventPayload payload = (HoldingWasUpdatedEventPayload) holdingWasAdded.getPayload(); - HoldingAddedMessagePayload holdingAddedMessagePayload = new HoldingAddedMessagePayload( - payload.userId().value(), - payload.assetId().value(), - payload.amount().value(), - payload.boughtPrice().value() - ); - HoldingAddedOrUpdatedMessage holdingAddedMessage = new HoldingAddedOrUpdatedMessage( - holdingWasAdded.getId().value(), - holdingWasAdded.getOccurredAt(), - holdingWasAdded.getAggregateId().value(), - holdingWasAdded.getVersion(), - holdingWasAdded.getType(), - holdingAddedMessagePayload - ); - holdingAddedMessages.add(holdingAddedMessage); - } - return holdingAddedMessages; - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/EventPublisherQueue.java b/src/main/java/io/autoinvestor/infrastructure/EventPublisherQueue.java deleted file mode 100644 index 30da364..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/EventPublisherQueue.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.spring.pubsub.core.PubSubTemplate; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class EventPublisherQueue { - - private static final String TOPIC = "portfolio"; - private final PubSubTemplate pubSubTemplate; - - public void publishHoldingAddedOrUpdated(List holdingAddedMessages) { - ObjectMapper objectMapper = new ObjectMapper(); - for (HoldingAddedOrUpdatedMessage message : holdingAddedMessages) { - try { - String jsonMessage = objectMapper.writeValueAsString(message); - pubSubTemplate.publish(TOPIC, jsonMessage); - } catch (JsonProcessingException e) { - throw new RuntimeException("Error serializing message to JSON", e); - } - } - - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/HoldingAddedMessagePayload.java b/src/main/java/io/autoinvestor/infrastructure/HoldingAddedMessagePayload.java deleted file mode 100644 index 32488bc..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/HoldingAddedMessagePayload.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record HoldingAddedMessagePayload( - @JsonProperty("userId") String userId, - @JsonProperty("assetId") String assetId, - @JsonProperty("amount") Integer amount, - @JsonProperty("boughtPrice") Integer boughtPrice) - {} diff --git a/src/main/java/io/autoinvestor/infrastructure/HoldingAddedOrUpdatedMessage.java b/src/main/java/io/autoinvestor/infrastructure/HoldingAddedOrUpdatedMessage.java deleted file mode 100644 index c5ce032..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/HoldingAddedOrUpdatedMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Date; - -public record HoldingAddedOrUpdatedMessage(@JsonProperty("eventId") String eventId, - @JsonProperty("occurredAt") Date occurredAt, - @JsonProperty("aggregateId") String aggregateId, - @JsonProperty("version") int version, - @JsonProperty("type") String type, - @JsonProperty("payload") HoldingAddedMessagePayload payload) -{} - diff --git a/src/main/java/io/autoinvestor/infrastructure/HoldingEventMapperDocument.java b/src/main/java/io/autoinvestor/infrastructure/HoldingEventMapperDocument.java deleted file mode 100644 index 00e4c76..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/HoldingEventMapperDocument.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.*; -import org.springframework.stereotype.Component; - -import java.util.Date; - -@Component -public class HoldingEventMapperDocument { - WalletEventDocument toDocument(Event walletEvent){ - switch (walletEvent.getType()) { - case "WALLET_CREATED" -> { - WalletWasCreatedPayload payloadAdded = (WalletWasCreatedPayload) walletEvent.getPayload(); - - return new WalletEventDocument( - walletEvent.getId().value(), - walletEvent.getType(), - walletEvent.getAggregateId().value(), - payloadAdded.userId().value(), - null, - null, - null, - walletEvent.getOccurredAt().toInstant(), - walletEvent.getVersion() - ); - } - case "PORTFOLIO_ASSET_ADDED" -> { - HoldingWasCreatedEventPayload payload = (HoldingWasCreatedEventPayload) walletEvent.getPayload(); - return new WalletEventDocument( - walletEvent.getId().value(), - walletEvent.getType(), - walletEvent.getAggregateId().value(), - payload.userId().value(), - payload.assetId().value(), - payload.amount().value(), - payload.boughtPrice().value(), - walletEvent.getOccurredAt().toInstant(), - walletEvent.getVersion() - ); - } - case "PORTFOLIO_ASSET_UPDATED" -> { - HoldingWasUpdatedEventPayload payload = (HoldingWasUpdatedEventPayload) walletEvent.getPayload(); - return new WalletEventDocument( - walletEvent.getId().value(), - walletEvent.getType(), - walletEvent.getAggregateId().value(), - payload.userId().value(), - payload.assetId().value(), - payload.amount().value(), - payload.boughtPrice().value(), - walletEvent.getOccurredAt().toInstant(), - walletEvent.getVersion() - ); - } - default -> throw new IllegalArgumentException ("Unknown event type " + walletEvent.getType()); - } - } - public Event toDomainEvent(WalletEventDocument doc) { - - WalletId walletId = WalletId.of(doc.walletId()); - Date occurredAt = Date.from(doc.ocurredAt()); - int version = doc.version(); - switch (doc.eventType()) { - case "WALLET_CREATED" -> { - WalletWasCreatedPayload payload = new WalletWasCreatedPayload(UserId.of(doc.userId())); - - return WalletWasCreatedEvent.hydrate( - walletId, - payload, - version, - occurredAt - ); - } - case "PORTFOLIO_ASSET_ADDED" -> { - HoldingWasCreatedEventPayload payload = new HoldingWasCreatedEventPayload( - UserId.of(doc.userId()), - AssetId.of(doc.assetId()), - new Amount(doc.amount()), - new BoughtPrice(doc.boughtPrice()) - ); - return HoldingWasCreatedEvent.hydrate( - walletId, - payload, - version, - occurredAt - ); - } - case "PORTFOLIO_ASSET_UPDATED" -> { - HoldingWasUpdatedEventPayload payload = new HoldingWasUpdatedEventPayload( - UserId.of(doc.userId()), - AssetId.of(doc.assetId()), - new Amount(doc.amount()), - new BoughtPrice(doc.boughtPrice()) - ); - return HoldingWasUpdatedEvent.hydrate( - walletId, - payload, - version, - occurredAt - ); - } - default -> throw new IllegalArgumentException("Unknown event type: " + doc.eventType()); - } - - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/MongoWalletRepository.java b/src/main/java/io/autoinvestor/infrastructure/MongoWalletRepository.java deleted file mode 100644 index e21b7ba..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/MongoWalletRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.Wallet; -import io.autoinvestor.domain.WalletRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Primary; -import org.springframework.data.domain.Sort; -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.List; -import java.util.stream.Collectors; - -@Repository -@Primary -@RequiredArgsConstructor -public class MongoWalletRepository implements WalletRepository { - private final MongoTemplate template; - private final HoldingEventMapperDocument mapper; - - - @Override - public void save(List> walletEvents) { - List documents = walletEvents.stream() - .map(mapper::toDocument) - .collect(Collectors.toList()); - template.insertAll(documents); - } - - @Override - public Wallet get(String walletId) { - Query query = new Query(Criteria.where("walletId").is(walletId)); - query.with(Sort.by(Sort.Direction.ASC, "version")); - List documents = template.find(query, WalletEventDocument.class); - if (documents.isEmpty()) { - return null; - } - List> events = documents.stream() - .map(mapper::toDomainEvent) - .collect(Collectors.toList()); - if (events.isEmpty()) { - return Wallet.empty(); - } - return Wallet.from(events); - - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMapper.java b/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMapper.java deleted file mode 100644 index 79f4947..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMapper.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.autoinvestor.infrastructure.ReadModel; - -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.application.SimpleReadModelDTO; -import org.springframework.stereotype.Component; - -@Component -public class ReadModelMapper { - public SimpleReadModelDocument toWalletUserDocument(SimpleReadModelDTO dto) { - return new SimpleReadModelDocument( - dto.walletId(), - dto.userId() - ); - } - - public ComplexReadModelDocument toComplexDocument(ComplexReadModelDTO dto) { - return new ComplexReadModelDocument( - dto.userId(), dto.assetId(), dto.amount(), dto. boughtPrice() - ); - } - - public SimpleReadModelDTO toDTOSimple(SimpleReadModelDocument document) { - return new SimpleReadModelDTO( - document.walletId(), - document.userId() - ); - } - - public ComplexReadModelDTO toDTOComplex(ComplexReadModelDocument document) { - return new ComplexReadModelDTO( - document.userId(), - document.assetId(), - document.amount(), - document.boughtPrice() - ); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMongo.java b/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMongo.java deleted file mode 100644 index 47ad1f2..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ReadModelMongo.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.autoinvestor.infrastructure.ReadModel; - -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.application.ReadModel; -import io.autoinvestor.application.SimpleReadModelDTO; -import io.autoinvestor.domain.Holding; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Primary; -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.data.mongodb.core.query.Update; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Map; - -@Repository -@Primary -public class ReadModelMongo implements ReadModel{ - @Autowired - private MongoTemplate mongoTemplate; - - @Autowired - private ReadModelMapper mapper; - - @Override - public void add(SimpleReadModelDTO dto) { - mongoTemplate.save(mapper.toWalletUserDocument(dto)); - } - - @Override - public void add(ComplexReadModelDTO dto) { - mongoTemplate.save(mapper.toComplexDocument(dto)); - } - - @Override - public String getWalletId(String userId) { - Query query = new Query(Criteria.where("userId").is(userId)); - String walletId = mongoTemplate.findOne(query, SimpleReadModelDocument.class).walletId(); - return walletId; - - -} - - @Override - public void update(ComplexReadModelDTO dto) { - Query query = new Query(Criteria.where("userId").is(dto.userId()) - .and("assetId").is(dto.assetId())); - Update update = new Update() - .set("amount", dto.amount()) - .set("boughtPrice", dto.boughtPrice()); - mongoTemplate.updateFirst(query, update, ComplexReadModelDocument.class); - } - - @Override - public List getHoldings(String userId) { - Query query = new Query(Criteria.where("userId").is(userId)); - return mongoTemplate.find(query, ComplexReadModelDocument.class) - .stream().map(mapper::toDTOComplex).toList(); - } -} - diff --git a/src/main/java/io/autoinvestor/infrastructure/ReadModel/SimpleReadModelDocument.java b/src/main/java/io/autoinvestor/infrastructure/ReadModel/SimpleReadModelDocument.java deleted file mode 100644 index dd8ad1d..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/ReadModel/SimpleReadModelDocument.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.autoinvestor.infrastructure.ReadModel; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -@Document(collection = "walletUser") -public record SimpleReadModelDocument( - String walletId, - String userId -) { -} diff --git a/src/main/java/io/autoinvestor/infrastructure/WalletEventDocument.java b/src/main/java/io/autoinvestor/infrastructure/WalletEventDocument.java deleted file mode 100644 index 79e01a6..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/WalletEventDocument.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.mongodb.lang.Nullable; -import jakarta.validation.constraints.Null; -import org.checkerframework.checker.units.qual.N; -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 = "walletEvent") -public record WalletEventDocument( - @Id String id, - String eventType, - String walletId, - String userId, - @Nullable String assetId, - @Nullable Integer amount, - @Nullable Integer boughtPrice, - @Field("occurredAt")Instant ocurredAt, - Integer version - ) {} diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java new file mode 100644 index 0000000..831d947 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java @@ -0,0 +1,43 @@ +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.events.Event; +import io.autoinvestor.exceptions.InternalErrorException; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + + +final class EventMessageMapper { + + private final 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().value()); + 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"); + } + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java similarity index 80% rename from src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java rename to src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java index 929bf30..edbe2f1 100644 --- a/src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java @@ -1,13 +1,15 @@ -package io.autoinvestor.infrastructure; +package io.autoinvestor.infrastructure.event_publishers; -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.EventPublisher; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventPublisher; 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; diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java new file mode 100644 index 0000000..488a965 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java @@ -0,0 +1,62 @@ +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.events.Event; +import io.autoinvestor.domain.events.EventPublisher; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +@Component +@Profile("prod") +public class PubsubEventPublisher implements EventPublisher { + + private final Publisher publisher; + private final EventMessageMapper mapper; + + public PubsubEventPublisher( + @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(); + + log.info("Pub/Sub publisher created for topic {}", topicName); + } + + @Override + public void publish(List> events) { + if (events.isEmpty()) { + log.debug("publish invoked with empty list — nothing to do"); + return; + } + + log.info("Publishing {} domain event(s)", events.size()); + + events.stream() + .map(mapper::toMessage) + .forEach(msg -> { + publisher.publish(msg).addListener( + () -> log.debug("Published msgId={}", msg.getMessageId()), + Runnable::run + ); + }); + } + + @PreDestroy + public void shutdown() throws Exception { + log.info("Shutting down Pub/Sub publisher..."); + publisher.shutdown(); + publisher.awaitTermination(1, TimeUnit.MINUTES); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEvent.java b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEvent.java index fa2f8ee..4d65e64 100644 --- a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEvent.java +++ b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEvent.java @@ -1,5 +1,6 @@ package io.autoinvestor.infrastructure.listeners; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -7,22 +8,14 @@ import java.util.Map; -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@Data @Builder @NoArgsConstructor @AllArgsConstructor public class PubSubEvent { - private String eventId; - private long occurredAt; - private String aggregateId; - private int version; - private String type; - private Map payload; } diff --git a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEventMapper.java b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEventMapper.java index d016848..5e46779 100644 --- a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEventMapper.java +++ b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubEventMapper.java @@ -1,7 +1,6 @@ package io.autoinvestor.infrastructure.listeners; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSuscriber.java b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSubscriber.java similarity index 50% rename from src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSuscriber.java rename to src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSubscriber.java index 66884f8..777a642 100644 --- a/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSuscriber.java +++ b/src/main/java/io/autoinvestor/infrastructure/listeners/PubSubUsersSubscriber.java @@ -9,17 +9,20 @@ import com.google.pubsub.v1.PubsubMessage; import io.autoinvestor.application.WalletCreatedUseCase.WalletCreateCommand; import io.autoinvestor.application.WalletCreatedUseCase.WalletCreatedHandler; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import com.google.cloud.pubsub.v1.Subscriber; -import javax.annotation.PostConstruct; import java.util.Map; +@Slf4j @Component - -public class PubSubUsersSuscriber { +@Profile("prod") +public class PubSubUsersSubscriber { private final WalletCreatedHandler commandHandler; private final ProjectSubscriptionName subscriptionName; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -27,10 +30,10 @@ public class PubSubUsersSuscriber { private Subscriber subscriber; - public PubSubUsersSuscriber (WalletCreatedHandler walletCreatedHandler, + public PubSubUsersSubscriber(WalletCreatedHandler walletCreatedHandler, PubSubEventMapper mapper, - @Value("${spring.cloud.gcp.pubsub.project-id}") String projectId, - @Value("${PUBSUB_SUBSCRIPTION_PORTFOLIO}") String subscriptionId) + @Value("${GCP_PROJECT}") String projectId, + @Value("${PUBSUB_SUBSCRIPTION_USERS}") String subscriptionId) { this.commandHandler = walletCreatedHandler; this.eventMapper = mapper; @@ -39,41 +42,58 @@ public PubSubUsersSuscriber (WalletCreatedHandler walletCreatedHandler, } @PostConstruct - public void listen () { + public void listen() { + log.info("Starting Pub/Sub subscriber for {}", subscriptionName); + MessageReceiver receiver = this::processMessage; this.subscriber = Subscriber.newBuilder(subscriptionName, receiver).build(); this.subscriber.addListener(new ApiService.Listener() { - @Override public void failed(ApiService.State from, Throwable failure) {} + @Override public void failed(ApiService.State from, Throwable failure) { + log.error("Subscriber failed from state {}: {}", from, failure.toString(), failure); // ERROR + } }, Runnable::run); - subscriber.startAsync().awaitRunning(); + this.subscriber.startAsync().awaitRunning(); + log.info("Subscriber running"); } @PreDestroy public void stop() { - if (subscriber != null) { - subscriber.stopAsync(); + if (this.subscriber != null) { + log.info("Stopping subscriber..."); + this.subscriber.stopAsync(); } } private void processMessage(PubsubMessage message, AckReplyConsumer consumer) { + String msgId = message.getMessageId(); + log.debug("Received message msgId={} size={}B", msgId, message.getData().size()); + try { - Map raw = this.objectMapper.readValue( - message.getData().toByteArray(), new TypeReference>() { - } - ); - PubSubEvent event = this.eventMapper.fromMap(raw); - if("USER_CREATED".equals(event.getType())) { + Map raw = objectMapper.readValue(message.getData().toByteArray(), new TypeReference<>() {}); + PubSubEvent event = eventMapper.fromMap(raw); + log.info("Processing event type={} msgId={}", event.getType(), msgId); + + if ("USER_CREATED".equals(event.getType())) { + if (event.getAggregateId() == null) { + log.warn("Malformed event: Skipping USER_CREATED event with missing aggregateId msgId={}", msgId); + consumer.ack(); + return; + } + WalletCreateCommand command = new WalletCreateCommand( - (String) event.getAggregateId() + event.getAggregateId() ); this.commandHandler.handle(command); + log.info("Decision registered for userId={} msgId={}", command.userId(), msgId); + } else { + log.debug("Ignored unsupported event type={} msgId={}", event.getType(), msgId); } - + consumer.ack(); } catch (Exception ex) { + log.error("Failed to handle msgId={} — nacking", msgId, ex); consumer.nack(); } } - } diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/DocumentMapper.java b/src/main/java/io/autoinvestor/infrastructure/read_models/DocumentMapper.java new file mode 100644 index 0000000..e4d8c62 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/DocumentMapper.java @@ -0,0 +1,37 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.HoldingsReadModelDTO; +import io.autoinvestor.application.UsersWalletReadModelDTO; +import org.springframework.stereotype.Component; + +@Component +public class DocumentMapper { + public MongoUsersWalletReadModelDocument toDocument(UsersWalletReadModelDTO dto) { + return new MongoUsersWalletReadModelDocument( + dto.walletId(), + dto.userId() + ); + } + + public MongoHoldingsReadModelDocument toDocument(HoldingsReadModelDTO dto) { + return new MongoHoldingsReadModelDocument( + dto.userId(), dto.assetId(), dto.amount(), dto. boughtPrice() + ); + } + + public UsersWalletReadModelDTO toDTO(MongoUsersWalletReadModelDocument document) { + return new UsersWalletReadModelDTO( + document.walletId(), + document.userId() + ); + } + + public HoldingsReadModelDTO toDTO(MongoHoldingsReadModelDocument document) { + return new HoldingsReadModelDTO( + document.userId(), + document.assetId(), + document.amount(), + document.boughtPrice() + ); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryHoldingsReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryHoldingsReadModel.java new file mode 100644 index 0000000..7d3f345 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryHoldingsReadModel.java @@ -0,0 +1,27 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.HoldingsReadModel; +import io.autoinvestor.application.HoldingsReadModelDTO; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@Profile("local") +public class InMemoryHoldingsReadModel implements HoldingsReadModel { + @Override + public void add(HoldingsReadModelDTO dto) { + + } + + @Override + public void update(HoldingsReadModelDTO dto) { + + } + + @Override + public List getHoldings(String userId) { + return List.of(); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersWalletReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersWalletReadModel.java new file mode 100644 index 0000000..db45fdd --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersWalletReadModel.java @@ -0,0 +1,20 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.UsersWalletReadModel; +import io.autoinvestor.application.UsersWalletReadModelDTO; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("local") +public class InMemoryUsersWalletReadModel implements UsersWalletReadModel { + @Override + public void add(UsersWalletReadModelDTO dto) { + + } + + @Override + public String getWalletId(String userId) { + return ""; + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModel.java new file mode 100644 index 0000000..4be19b2 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModel.java @@ -0,0 +1,48 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.HoldingsReadModel; +import io.autoinvestor.application.HoldingsReadModelDTO; +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.data.mongodb.core.query.Update; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@Profile("prod") +public class MongoHoldingsReadModel implements HoldingsReadModel { + private final MongoTemplate template; + private final DocumentMapper mapper; + + public MongoHoldingsReadModel(MongoTemplate template, DocumentMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void add(HoldingsReadModelDTO dto) { + this.template.save(mapper.toDocument(dto)); + } + + @Override + public void update(HoldingsReadModelDTO dto) { + Query query = new Query(Criteria.where("userId").is(dto.userId()) + .and("assetId").is(dto.assetId())); + + Update update = new Update() + .set("amount", dto.amount()) + .set("boughtPrice", dto.boughtPrice()); + + this.template.updateFirst(query, update, MongoHoldingsReadModelDocument.class); + } + + @Override + public List getHoldings(String userId) { + Query query = new Query(Criteria.where("userId").is(userId)); + return this.template.find(query, MongoHoldingsReadModelDocument.class) + .stream().map(mapper::toDTO).toList(); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ComplexReadModelDocument.java b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModelDocument.java similarity index 59% rename from src/main/java/io/autoinvestor/infrastructure/ReadModel/ComplexReadModelDocument.java rename to src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModelDocument.java index cfeeae5..b52b798 100644 --- a/src/main/java/io/autoinvestor/infrastructure/ReadModel/ComplexReadModelDocument.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoHoldingsReadModelDocument.java @@ -1,10 +1,9 @@ -package io.autoinvestor.infrastructure.ReadModel; +package io.autoinvestor.infrastructure.read_models; -import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @Document(collection = "holdings") -public record ComplexReadModelDocument( +public record MongoHoldingsReadModelDocument( String userId, String assetId, Integer amount, diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModel.java new file mode 100644 index 0000000..02dd7b3 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModel.java @@ -0,0 +1,35 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.UsersWalletReadModel; +import io.autoinvestor.application.UsersWalletReadModelDTO; +import org.springframework.context.annotation.Profile; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("prod") +public class MongoUsersWalletReadModel implements UsersWalletReadModel { + private final MongoTemplate template; + private final DocumentMapper mapper; + + public MongoUsersWalletReadModel(MongoTemplate template, DocumentMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void add(UsersWalletReadModelDTO dto) { + this.template.save(mapper.toDocument(dto)); + } + + @Override + public String getWalletId(String userId) { + UsersWalletReadModelDTO dto = template.findById(userId, UsersWalletReadModelDTO.class); + + if (dto != null) { + return dto.walletId(); + } + + return null; + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModelDocument.java b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModelDocument.java new file mode 100644 index 0000000..87c7912 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersWalletReadModelDocument.java @@ -0,0 +1,11 @@ +package io.autoinvestor.infrastructure.read_models; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "users") +public record MongoUsersWalletReadModelDocument( + @Id String userId, + String walletId +) { +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java b/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java new file mode 100644 index 0000000..8db9923 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java @@ -0,0 +1,50 @@ +package io.autoinvestor.infrastructure.repositories; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.Date; +import java.util.Map; + +@Getter +@Setter +@Document(collection = "events") +public class EventDocument { + + @Id + private String id; + + @Field + private String aggregateId; + + @Field + private String type; + + @Field + private Map payload; + + @Field + private Date occurredAt; + + @Field + private int version; + + public EventDocument() { } + + public EventDocument(String id, + String aggregateId, + String type, + Map payload, + Date occurredAt, + int version) { + this.id = id; + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = occurredAt; + this.version = version; + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java b/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java new file mode 100644 index 0000000..7247558 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java @@ -0,0 +1,60 @@ +package io.autoinvestor.infrastructure.repositories; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.autoinvestor.domain.events.*; +import io.autoinvestor.domain.model.*; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Map; + +@Component +public class EventMapper { + + private final ObjectMapper json = new ObjectMapper(); + + public

EventDocument toDocument(Event

evt) { + Map payloadMap = + json.convertValue(evt.getPayload(), new TypeReference>() {}); + + return new EventDocument( + evt.getId().value(), + evt.getAggregateId().value(), + evt.getType(), + payloadMap, + evt.getOccurredAt(), + evt.getVersion() + ); + } + + public Event toDomain(EventDocument doc) { + EventId id = EventId.of(doc.getId()); + WalletId aggId = WalletId.of(doc.getAggregateId()); + Date occurred = doc.getOccurredAt(); + int version = doc.getVersion(); + + switch (doc.getType()) { + case WalletWasCreatedEvent.TYPE -> { + WalletWasCreatedEventPayload payload = + json.convertValue(doc.getPayload(), WalletWasCreatedEventPayload.class); + + return WalletWasCreatedEvent.hydrate(id, aggId, payload, occurred, version); + } + case HoldingWasCreatedEvent.TYPE -> { + HoldingWasCreatedEventPayload payload = + json.convertValue(doc.getPayload(), HoldingWasCreatedEventPayload.class); + + return HoldingWasCreatedEvent.hydrate(id, aggId, payload, occurred, version); + } + case HoldingWasUpdatedEvent.TYPE -> { + HoldingWasUpdatedEventPayload payload = + json.convertValue(doc.getPayload(), HoldingWasUpdatedEventPayload.class); + + return HoldingWasUpdatedEvent.hydrate(id, aggId, payload, occurred, version); + } + default -> throw new IllegalArgumentException("Unknown event type: " + doc.getType() + ); + } + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java new file mode 100644 index 0000000..345cb8b --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java @@ -0,0 +1,40 @@ +package io.autoinvestor.infrastructure.repositories; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.WalletEventStoreRepository; +import io.autoinvestor.domain.model.Wallet; +import io.autoinvestor.domain.model.WalletId; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +@Repository +@Profile("local") +public class InMemoryEventStoreRepository implements WalletEventStoreRepository { + + private final List> eventStore = new CopyOnWriteArrayList<>(); + + @Override + public void save(Wallet wallet) { + eventStore.addAll(wallet.getUncommittedEvents()); + } + + @Override + public Optional get(WalletId walletId) { + List> events = eventStore.stream() + .filter(e -> e.getAggregateId().value().equals(walletId.value())) + .sorted(Comparator.comparingLong(Event::getVersion)) + .collect(Collectors.toList()); + + if (events.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(Wallet.from(events)); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java new file mode 100644 index 0000000..df884be --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java @@ -0,0 +1,64 @@ +package io.autoinvestor.infrastructure.repositories; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.WalletEventStoreRepository; +import io.autoinvestor.domain.model.Wallet; +import io.autoinvestor.domain.model.WalletId; +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Sort; +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.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +@Profile("prod") +public class MongoEventStoreRepository implements WalletEventStoreRepository { + private static final String COLLECTION = "events"; + + private final MongoTemplate template; + private final EventMapper mapper; + + public MongoEventStoreRepository(MongoTemplate template, EventMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void save(Wallet wallet) { + List docs = wallet.getUncommittedEvents() + .stream() + .map(mapper::toDocument) + .collect(Collectors.toList()); + template.insertAll(docs); + } + + @Override + public Optional get(WalletId walletId) { + Query q = Query.query( + Criteria.where("aggregateId") + .is(walletId.value()) + ) + .with(Sort.by("version")); + + List docs = template.find(q, EventDocument.class, COLLECTION); + + if (docs.isEmpty()) { + return Optional.empty(); + } + + List> events = docs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + + if (events.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(Wallet.from(events)); + } +} diff --git a/src/main/java/io/autoinvestor/ui/GetHoldingResponseDocumentMapper.java b/src/main/java/io/autoinvestor/ui/GetHoldingResponseDocumentMapper.java index 7a4d02b..facd335 100644 --- a/src/main/java/io/autoinvestor/ui/GetHoldingResponseDocumentMapper.java +++ b/src/main/java/io/autoinvestor/ui/GetHoldingResponseDocumentMapper.java @@ -1,13 +1,12 @@ package io.autoinvestor.ui; -import io.autoinvestor.application.ComplexReadModelDTO; -import io.autoinvestor.infrastructure.ReadModel.ComplexReadModelDocument; +import io.autoinvestor.application.HoldingsReadModelDTO; import org.springframework.stereotype.Service; @Service public class GetHoldingResponseDocumentMapper { -public GetHoldingResponse map(ComplexReadModelDTO document) { +public GetHoldingResponse map(HoldingsReadModelDTO document) { return new GetHoldingResponse( document.assetId(), document.amount(), diff --git a/src/main/java/io/autoinvestor/ui/HoldingRequestDTO.java b/src/main/java/io/autoinvestor/ui/HoldingRequestDTO.java index 167aaab..58ab875 100644 --- a/src/main/java/io/autoinvestor/ui/HoldingRequestDTO.java +++ b/src/main/java/io/autoinvestor/ui/HoldingRequestDTO.java @@ -1,6 +1,5 @@ package io.autoinvestor.ui; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record HoldingRequestDTO( diff --git a/src/main/java/io/autoinvestor/ui/PortfolioController.java b/src/main/java/io/autoinvestor/ui/PortfolioController.java index 73248ee..b01bc72 100644 --- a/src/main/java/io/autoinvestor/ui/PortfolioController.java +++ b/src/main/java/io/autoinvestor/ui/PortfolioController.java @@ -1,6 +1,6 @@ package io.autoinvestor.ui; -import io.autoinvestor.application.ComplexReadModelDTO; +import io.autoinvestor.application.HoldingsReadModelDTO; import io.autoinvestor.application.NewHoldingUseCase.NewHoldingCommand; import io.autoinvestor.application.NewHoldingUseCase.NewHoldingCommandHandler; import io.autoinvestor.application.QueryHoldingsUseCase.GetHoldingsQuery; @@ -47,7 +47,7 @@ public ResponseEntity addHolding( public ResponseEntity> getHoldings( @RequestHeader(value = "X-User-Id", required = true) String userId ) { - List documents = getHoldingsQueryHandler.handle(new GetHoldingsQuery(userId)); + List documents = getHoldingsQueryHandler.handle(new GetHoldingsQuery(userId)); return ResponseEntity.ok(documents.stream().map(mapperGetHoldingResponse::map).toList()); } diff --git a/src/main/java/io/autoinvestor/ui/ErrorResponse.java b/src/main/java/io/autoinvestor/ui/error_handling/ErrorResponse.java similarity index 78% rename from src/main/java/io/autoinvestor/ui/ErrorResponse.java rename to src/main/java/io/autoinvestor/ui/error_handling/ErrorResponse.java index 568827b..8f28101 100644 --- a/src/main/java/io/autoinvestor/ui/ErrorResponse.java +++ b/src/main/java/io/autoinvestor/ui/error_handling/ErrorResponse.java @@ -1,4 +1,4 @@ -package io.autoinvestor.ui; +package io.autoinvestor.ui.error_handling; public record ErrorResponse(int status, String error) { public static ErrorResponseBuilder builder() { diff --git a/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java b/src/main/java/io/autoinvestor/ui/error_handling/ErrorResponseBuilder.java similarity index 95% rename from src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java rename to src/main/java/io/autoinvestor/ui/error_handling/ErrorResponseBuilder.java index 9c70f1e..1457ef4 100644 --- a/src/main/java/io/autoinvestor/ui/ErrorResponseBuilder.java +++ b/src/main/java/io/autoinvestor/ui/error_handling/ErrorResponseBuilder.java @@ -1,4 +1,4 @@ -package io.autoinvestor.ui; +package io.autoinvestor.ui.error_handling; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java b/src/main/java/io/autoinvestor/ui/error_handling/GlobalExceptionHandler.java similarity index 91% rename from src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java rename to src/main/java/io/autoinvestor/ui/error_handling/GlobalExceptionHandler.java index a2be99e..3d95533 100644 --- a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java +++ b/src/main/java/io/autoinvestor/ui/error_handling/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ -package io.autoinvestor.ui; +package io.autoinvestor.ui.error_handling; import io.autoinvestor.exceptions.UserWithoutPortfolio; +import io.autoinvestor.ui.PortfolioController; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..1ed44a7 --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,7 @@ +spring.autoconfigure.exclude=\ + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + +spring.autoconfigure.exclude+=,\ + com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..9d07ce3 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,5 @@ +spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.database=${MONGODB_DB} +GCP_PROJECT=${GCP_PROJECT} +PUBSUB_TOPIC=${PUBSUB_TOPIC} +PUBSUB_SUBSCRIPTION_USERS=${PUBSUB_SUBSCRIPTION_USERS} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d99340c..b7fb837 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,2 @@ spring.application.name=portfolio -spring.cloud.gcp.pubsub.project-id=autoinvestor-tfm -spring.data.mongodb.uri=${MONGODB_URI} -spring.data.mongodb.database=${MONGODB_DB} -PUBSUB_TOPIC=${PUBSUB_TOPIC} -PUBSUB_SUBSCRIPTION_PORTFOLIO=${PUBSUB_SUBSCRIPTION_USERS} - - - +spring.profiles.active=local diff --git a/src/test/java/io/autoinvestor/PortfolioApplicationTests.java b/src/test/java/io/autoinvestor/PortfolioApplicationTests.java deleted file mode 100644 index d0355bc..0000000 --- a/src/test/java/io/autoinvestor/PortfolioApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.autoinvestor; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - - -class PortfolioApplicationTests { - - @Test - void dummyTest() { - assert(true); - } -}