Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.autoinvestor.application.HoldingDeleteUseCase;

public record HoldingDeleteCommand(
String userId,
String assetId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.autoinvestor.application.HoldingDeleteUseCase;

import io.autoinvestor.application.HoldingsReadModel;
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.Wallet;
import io.autoinvestor.domain.model.WalletId;
import io.autoinvestor.exceptions.UserWithoutPortfolio;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class HoldingDeleteCommandHandler {

private final UsersWalletReadModel usersWalletReadModel;
private final WalletEventStoreRepository eventStore;
private final HoldingsReadModel holdingsReadModel;
private final EventPublisher eventPublisher;

public void handle (HoldingDeleteCommand command) {
String walletId = this.usersWalletReadModel.getWalletId(command.userId());
if (walletId == null) {
throw UserWithoutPortfolio.with(command.userId());
}
Wallet wallet = this.eventStore.get(WalletId.of(walletId))
.orElseThrow(() -> UserWithoutPortfolio.with(command.userId()));
wallet.deleteHolding(command.userId(), command.assetId());
List<Event<?>> events = wallet.getUncommittedEvents();

this.eventStore.save(wallet);

boolean removed = this.holdingsReadModel.delete(command.userId(), command.assetId());
if (!removed) {
return;
}
this.eventPublisher.publish(events);
wallet.markEventsAsCommitted();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
public interface HoldingsReadModel {
void add(HoldingsReadModelDTO dto);
void update(HoldingsReadModelDTO dto);
boolean delete(String userId, String assetId);
List<HoldingsReadModelDTO> getHoldings(String userId);
boolean assetAlreadyExists(String userIs, String assetId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.autoinvestor.domain.model.AssetId;
import io.autoinvestor.domain.model.Wallet;
import io.autoinvestor.domain.model.WalletId;
import io.autoinvestor.exceptions.AssetAlreadyExists;
import io.autoinvestor.exceptions.UserWithoutPortfolio;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -30,6 +31,9 @@ public void handle(NewHoldingCommand command) {
throw UserWithoutPortfolio.with(command.userId());
}

if (this.holdingsReadModel.assetAlreadyExists(command.userId(), command.assetId())) {
throw AssetAlreadyExists.with(command.userId(), command.assetId());
}
Wallet wallet = this.eventStore.get(WalletId.of(walletId))
.orElseThrow(() -> UserWithoutPortfolio.with(command.userId()));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.autoinvestor.domain.events;

import io.autoinvestor.domain.Id;
import io.autoinvestor.domain.model.Amount;
import io.autoinvestor.domain.model.AssetId;
import io.autoinvestor.domain.model.UserId;
import io.autoinvestor.domain.model.WalletId;

import java.util.Date;

public class HoldingWasDeletedEvent extends Event<HoldingWasDeletedEventPayload> {
public static final String TYPE = "PORTFOLIO_ASSET_REMOVED";

private HoldingWasDeletedEvent(Id aggregateId, HoldingWasDeletedEventPayload payload) {
super (aggregateId, TYPE, payload);
}

protected HoldingWasDeletedEvent(
EventId id,
Id aggregateId,
HoldingWasDeletedEventPayload payload,
Date occurredAt,
int version) {
super(id, aggregateId, TYPE, payload, occurredAt, version);
}

public static HoldingWasDeletedEvent with(WalletId walletId,
UserId userId,
AssetId assetId) {
HoldingWasDeletedEventPayload payload = new HoldingWasDeletedEventPayload(
userId.value(), assetId.value()
);
return new HoldingWasDeletedEvent(walletId, payload);
}

public static HoldingWasDeletedEvent hydrate (EventId id,
Id aggregateId,
HoldingWasDeletedEventPayload payload,
Date occurredAt,
int version) {
return new HoldingWasDeletedEvent(id, aggregateId, payload, occurredAt, version);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.autoinvestor.domain.events;

import java.util.Map;

public record HoldingWasDeletedEventPayload(
String userId,
String assetId
) implements EventPayload {
@Override
public Map<String, Object> asMap() {
return Map.of(
"userId" , userId,
"assetId", assetId
);
}
}
14 changes: 14 additions & 0 deletions src/main/java/io/autoinvestor/domain/model/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public void updateHolding(String userId, String assetId, Integer amount, Integer
));
}

public void deleteHolding(String userId, String assetId) {
this.apply(HoldingWasDeletedEvent.with(
this.state.getWalletId(),
UserId.of(userId),
AssetId.of(assetId)
));
}

@Override
protected void when(Event<?> e) {
switch (e.getType()) {
Expand All @@ -69,6 +77,8 @@ protected void when(Event<?> e) {
case HoldingWasUpdatedEvent.TYPE:
whenHoldingUpdated((HoldingWasUpdatedEvent) e);
break;
case HoldingWasDeletedEvent.TYPE:

default:
throw new IllegalArgumentException("Unknown event type");
}
Expand All @@ -88,4 +98,8 @@ private void whenHoldingCreated(HoldingWasCreatedEvent event) {
private void whenHoldingUpdated(HoldingWasUpdatedEvent event) {
this.state = this.state.withHoldingUpdated(event);
}

private void whenHoldingDeleted(HoldingWasDeletedEvent event) {
this.state = this.state.withHoldingDeleted(event);
}
}
10 changes: 10 additions & 0 deletions src/main/java/io/autoinvestor/domain/model/WalletState.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,14 @@ public WalletState withHoldingUpdated(HoldingWasUpdatedEvent event) {
this.holdings
);
}

public WalletState withHoldingDeleted(HoldingWasDeletedEvent event) {
HoldingWasDeletedEventPayload payload = event.getPayload();
holdings.remove(AssetId.of(payload.assetId()));
return new WalletState(
this.walletId,
this.userId,
this.holdings
);
}
}
12 changes: 12 additions & 0 deletions src/main/java/io/autoinvestor/exceptions/AssetAlreadyExists.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.autoinvestor.exceptions;

public class AssetAlreadyExists extends RuntimeException {
private AssetAlreadyExists(String message) {
super(message);
}

public static AssetAlreadyExists with (String userId, String assetId) {
String message = "Duplicate asset " + assetId + " for user " + userId;
return new AssetAlreadyExists(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ public void update(HoldingsReadModelDTO dto) {

}

@Override
public boolean delete(String userId, String assetId) {
return false;
}

@Override
public List<HoldingsReadModelDTO> getHoldings(String userId) {
return List.of();
}

@Override
public boolean assetAlreadyExists(String userIs, String assetId) {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.autoinvestor.infrastructure.read_models;

import com.mongodb.client.result.DeleteResult;
import io.autoinvestor.application.HoldingsReadModel;
import io.autoinvestor.application.HoldingsReadModelDTO;
import org.springframework.context.annotation.Profile;
Expand Down Expand Up @@ -39,10 +40,26 @@ public void update(HoldingsReadModelDTO dto) {
this.template.updateFirst(query, update, MongoHoldingsReadModelDocument.class);
}

@Override
public boolean delete(String userId, String assetId) {
Query query = new Query(Criteria.where("userId").is(userId)
.and("assetId").is(assetId));
DeleteResult result = this.template.remove(query, MongoHoldingsReadModelDocument.class);
return result.getDeletedCount() > 0;
}

@Override
public List<HoldingsReadModelDTO> getHoldings(String userId) {
Query query = new Query(Criteria.where("userId").is(userId));
return this.template.find(query, MongoHoldingsReadModelDocument.class)
.stream().map(mapper::toDTO).toList();
}

@Override
public boolean assetAlreadyExists(String userId, String assetId) {
Query query = new Query(Criteria.where("userId").is(userId)
.and("assetId").is(assetId));
return this.template.exists(query, MongoHoldingsReadModelDocument.class);
}

}
10 changes: 10 additions & 0 deletions src/main/java/io/autoinvestor/ui/PortfolioController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.autoinvestor.ui;

import io.autoinvestor.application.HoldingDeleteUseCase.HoldingDeleteCommand;
import io.autoinvestor.application.HoldingDeleteUseCase.HoldingDeleteCommandHandler;
import io.autoinvestor.application.HoldingsReadModelDTO;
import io.autoinvestor.application.NewHoldingUseCase.NewHoldingCommand;
import io.autoinvestor.application.NewHoldingUseCase.NewHoldingCommandHandler;
Expand Down Expand Up @@ -27,6 +29,7 @@ public class PortfolioController {
private final WalletCreatedHandler walletCreatedHandler;
private final GetHoldingResponseDocumentMapper mapperGetHoldingResponse;
private final UpdateHoldingCommandHandler updateHoldingCommandHandler;
private final HoldingDeleteCommandHandler holdingDeleteCommandHandler;
@PostMapping
public ResponseEntity<Void> addHolding(
@RequestHeader(value = "X-User-Id", required = true) String userId,
Expand Down Expand Up @@ -67,6 +70,13 @@ public ResponseEntity<Void> putHolding (
return ResponseEntity.status(HttpStatus.CREATED).build();
}

public ResponseEntity<Void> deleteHolding (
@RequestHeader (value = "X-User-Id", required = true) String userId,
@RequestParam(value = "assetId", required = true) String assetId
) {
holdingDeleteCommandHandler.handle(new HoldingDeleteCommand(userId, assetId));
return ResponseEntity.status(HttpStatus.OK).build();
}

@PostMapping("/user")
public ResponseEntity<Void> simulateIncomingUserCreatedMessage (
Expand Down