diff --git a/src/main/java/com/eventsourcing/bankAccount/commands/BankAccountCommandHandler.java b/src/main/java/com/eventsourcing/bankAccount/commands/BankAccountCommandHandler.java index c3b7499..6fac090 100644 --- a/src/main/java/com/eventsourcing/bankAccount/commands/BankAccountCommandHandler.java +++ b/src/main/java/com/eventsourcing/bankAccount/commands/BankAccountCommandHandler.java @@ -5,16 +5,18 @@ import com.eventsourcing.bankAccount.domain.BankAccountAggregate; import com.eventsourcing.es.EventStoreDB; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; @RequiredArgsConstructor @Slf4j +@Service public class BankAccountCommandHandler implements BankAccountCommandService{ private final EventStoreDB eventStoreDB; @Override public String handle(CreateBankAccountCommand command) { - final var aggregate = eventStoreDB.load(command.aggregateID(), BankAccountAggregate.class); + final var aggregate = new BankAccountAggregate(command.aggregateID()); aggregate.createBankAccount(command.email(), command.address(), command.userName()); eventStoreDB.save(aggregate); diff --git a/src/main/java/com/eventsourcing/bankAccount/delivery/BankAccountController.java b/src/main/java/com/eventsourcing/bankAccount/delivery/BankAccountController.java index 26aa23e..152d4e6 100644 --- a/src/main/java/com/eventsourcing/bankAccount/delivery/BankAccountController.java +++ b/src/main/java/com/eventsourcing/bankAccount/delivery/BankAccountController.java @@ -1,22 +1,66 @@ package com.eventsourcing.bankAccount.delivery; +import com.eventsourcing.bankAccount.commands.*; +import com.eventsourcing.bankAccount.dto.*; +import com.eventsourcing.bankAccount.queries.BankAccountQueryService; +import com.eventsourcing.bankAccount.queries.GetBankAccountByIDQuery; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; @RestController @RequestMapping(path = "/api/v1/bank") @Slf4j +@RequiredArgsConstructor public class BankAccountController { + private final BankAccountCommandService commandService; + private final BankAccountQueryService queryService; @GetMapping("{aggregateId}") - public ResponseEntity getBankAccount(@PathVariable String aggregateId) { - log.info("GET bank account: {}", aggregateId); - return ResponseEntity.ok("OK"); + public ResponseEntity getBankAccount(@PathVariable String aggregateId) { + final var query = new GetBankAccountByIDQuery(aggregateId); + log.info("GET bank account query: {}", query); + final var result = queryService.handle(query); + log.info("GET bank account result: {}", result); + return ResponseEntity.ok(result); + } + + @PostMapping + public ResponseEntity createBankAccount(@RequestBody CreateBankAccountRequestDTO dto) { + final var aggregateID = UUID.randomUUID().toString(); + final var command = new CreateBankAccountCommand(aggregateID, dto.email(), dto.userName(), dto.address()); + final var id = commandService.handle(command); + log.info("CREATE bank account id: {}", id); + return ResponseEntity.status(HttpStatus.CREATED).body(id); + } + + @PostMapping(path = "/deposit/{aggregateId}") + public ResponseEntity depositAmount(@RequestBody DepositAmountRequestDTO dto, @PathVariable String aggregateId) { + final var command = new DepositAmountCommand(aggregateId, dto.amount()); + commandService.handle(command); + log.info("DepositAmountCommand command: {}", command); + return ResponseEntity.ok().build(); + } + + @PostMapping(path = "/email/{aggregateId}") + public ResponseEntity changeEmail(@RequestBody ChangeEmailRequestDTO dto, @PathVariable String aggregateId) { + final var command = new ChangeEmailCommand(aggregateId, dto.newEmail()); + commandService.handle(command); + log.info("ChangeEmailCommand command: {}", command); + return ResponseEntity.ok().build(); + } + + @PostMapping(path = "/address/{aggregateId}") + public ResponseEntity changeAddress(@RequestBody ChangeAddressRequestDTO dto, @PathVariable String aggregateId) { + final var command = new ChangeAddressCommand(aggregateId, dto.newAddress()); + commandService.handle(command); + log.info("changeAddress command: {}", command); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/eventsourcing/bankAccount/domain/BankAccountAggregate.java b/src/main/java/com/eventsourcing/bankAccount/domain/BankAccountAggregate.java index de1e957..6013a7e 100644 --- a/src/main/java/com/eventsourcing/bankAccount/domain/BankAccountAggregate.java +++ b/src/main/java/com/eventsourcing/bankAccount/domain/BankAccountAggregate.java @@ -11,6 +11,7 @@ import com.eventsourcing.es.exceptions.InvalidEventTypeException; import lombok.*; import java.math.BigDecimal; +import java.util.Objects; @Data @Builder @@ -55,14 +56,19 @@ public class BankAccountAggregate extends AggregateRoot { } private void handle(final EmailChangedEvent event) { + Objects.requireNonNull(event.getNewEmail()); + if (event.getNewEmail().isBlank()) throw new RuntimeException("invalid email address"); this.email = event.getNewEmail(); } private void handle(final AddressUpdatedEvent event) { + Objects.requireNonNull(event.getNewAddress()); + if (event.getNewAddress().isBlank()) throw new RuntimeException("invalid address"); this.address = event.getNewAddress(); } private void handle(final BalanceDepositedEvent event) { + Objects.requireNonNull(event.getAmount()); this.balance = this.balance.add(event.getAmount()); } diff --git a/src/main/java/com/eventsourcing/bankAccount/queries/BankAccountQueryHandler.java b/src/main/java/com/eventsourcing/bankAccount/queries/BankAccountQueryHandler.java index 7ecc9b7..76ddd6e 100644 --- a/src/main/java/com/eventsourcing/bankAccount/queries/BankAccountQueryHandler.java +++ b/src/main/java/com/eventsourcing/bankAccount/queries/BankAccountQueryHandler.java @@ -6,10 +6,12 @@ import com.eventsourcing.es.EventStoreDB; import com.eventsourcing.mappers.BankAccountMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; @Slf4j @RequiredArgsConstructor +@Service public class BankAccountQueryHandler implements BankAccountQueryService { private final EventStoreDB eventStoreDB; diff --git a/src/main/java/com/eventsourcing/es/EventStore.java b/src/main/java/com/eventsourcing/es/EventStore.java index fb3c1de..acefb2e 100644 --- a/src/main/java/com/eventsourcing/es/EventStore.java +++ b/src/main/java/com/eventsourcing/es/EventStore.java @@ -3,6 +3,7 @@ package com.eventsourcing.es; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -48,7 +49,7 @@ public class EventStore implements EventStoreDB { " from events e where e.aggregate_id = :aggregate_id and e.version > :version ORDER BY e.version ASC", Map.of("aggregate_id", aggregateId, "version", version), (rs, rowNum) -> { - Event.builder() + Event event = Event.builder() .aggregateId(rs.getString("aggregate_id")) .aggregateType(rs.getString("aggregate_type")) .eventType(rs.getString("event_type")) @@ -57,7 +58,7 @@ public class EventStore implements EventStoreDB { .version(rs.getLong("version")) .timeStamp(rs.getTimestamp("timestamp").toLocalDateTime()) .build(); - return null; + return event; }); log.info("(loadEvents) events list: {}", events); @@ -83,7 +84,10 @@ public class EventStore implements EventStoreDB { @Override @Transactional public void save(T aggregate) { - this.handleConcurrency(aggregate.getId()); + if (aggregate.getVersion() > 1) { + this.handleConcurrency(aggregate.getId()); + } + this.saveEvents(aggregate.getChanges()); if (aggregate.getVersion() % 3 == 0) { this.saveSnapshot(aggregate); @@ -92,8 +96,14 @@ public class EventStore implements EventStoreDB { } private void handleConcurrency(String aggregateId) { - int update = jdbcTemplate.update("SELECT aggregate_id FROM events e WHERE e.aggregate_id = ? LIMIT 1 FOR UPDATE", Map.of("aggregate_id", aggregateId)); - log.info("(handleConcurrency) result: {}", update); + try { + String aggregateID = jdbcTemplate.queryForObject("SELECT aggregate_id FROM events e " + + "WHERE e.aggregate_id = :aggregate_id LIMIT 1 FOR UPDATE", Map.of("aggregate_id", aggregateId), String.class); + log.info("(handleConcurrency) aggregateID for lock: {}", aggregateID); + } catch (EmptyResultDataAccessException e) { + log.info("(handleConcurrency) EmptyResultDataAccessException: {}", e.getMessage()); + } + log.info("(handleConcurrency) aggregateID for lock: {}", aggregateId); } private Optional loadSnapshot(String aggregateId) { @@ -139,9 +149,11 @@ public class EventStore implements EventStoreDB { final List events = this.loadEvents(aggregateId, aggregate.getVersion()); events.forEach(event -> { aggregate.raiseEvent(event); - log.info("raise event: {}", event.getVersion()); + log.info("raise event version: {}", event.getVersion()); }); + if (aggregate.getVersion() == 0) throw new RuntimeException("aggregate not found id:" + aggregateId); + return aggregate; } diff --git a/src/main/java/com/eventsourcing/filters/GlobalControllerAdvice.java b/src/main/java/com/eventsourcing/filters/GlobalControllerAdvice.java index 8cf72de..1b6cd7f 100644 --- a/src/main/java/com/eventsourcing/filters/GlobalControllerAdvice.java +++ b/src/main/java/com/eventsourcing/filters/GlobalControllerAdvice.java @@ -27,7 +27,8 @@ public class GlobalControllerAdvice { @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntimeException(RuntimeException ex, WebRequest request) { final var response = new InternalServerErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), LocalDateTime.now().toString()); - log.error("OrderNotFoundException response: {} ", response); + log.error("RuntimeException response: {} ", response); + ex.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); }