From cdff13c395728b70e498ce36027cdc0a203952b5 Mon Sep 17 00:00:00 2001 From: Michal Zeman <> Date: Sun, 22 Sep 2019 10:58:03 +0200 Subject: [PATCH] Account aggregate implementation --- .../account/domain/AccountAggregate.java | 67 ++++++++ .../account/domain/AccountState.java | 3 + .../domain/command/AccountCommandHandler.java | 24 ++- .../account/domain/event/AccountCreated.java | 9 + .../domain/event/AccountEventApplier.java | 8 +- .../account/domain/event/MoneyWithdrawn.java | 9 + .../account/domain/AccountAggregateTest.java | 158 ++++++++++++++++++ .../command/AccountCommandHandlerTest.java | 70 ++++++++ .../domain/event/AccountEventApplierTest.java | 57 +++++++ .../domain/TransactionAggregate.java | 11 ++ .../command/TransactionCommandHandler.java | 26 ++- .../domain/event/FinishTransactionFailed.java | 15 ++ .../domain/event/TransactionEventApplier.java | 16 ++ .../domain/TransactionAggregateTest.java | 8 + .../ddd/common/api/valueobject/Money.java | 41 ++++- 15 files changed, 512 insertions(+), 10 deletions(-) create mode 100644 bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountAggregateTest.java create mode 100644 bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandlerTest.java create mode 100644 bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplierTest.java create mode 100644 bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/FinishTransactionFailed.java diff --git a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountAggregate.java b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountAggregate.java index 345bdcd..f85cdc8 100644 --- a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountAggregate.java +++ b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountAggregate.java @@ -2,9 +2,76 @@ package com.mz.reactor.ddd.reactorddd.account.domain; import com.mz.reactor.ddd.common.api.valueobject.Id; import com.mz.reactor.ddd.common.api.valueobject.Money; +import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount; +import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney; +import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney; +import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated; +import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited; +import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn; +import org.apache.commons.lang3.text.translate.AggregateTranslator; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; public class AccountAggregate { private Id aggregateId; private Money amount; + + private List openedTransactions = new ArrayList<>(); + + public AccountAggregate(String aggregateId) { + this.aggregateId = new Id(aggregateId); + this.amount = new Money(BigDecimal.ZERO); + } + + public MoneyWithdrawn validateWithdrawMoney(WithdrawMoney command) { + return Money.validateValue + .andThen(v -> Money.validateWithdraw.apply(this.amount.getAmount(), command.amount())) + .andThen(v -> MoneyWithdrawn.from(command)) + .apply(command.amount()); + } + + public MoneyDeposited validateDepositMoney(DepositMoney depositMoney) { + return Money.validateDepositMoney + .andThen(v -> + MoneyDeposited.builder() + .correlationId(depositMoney.correlationId()) + .aggregateId(aggregateId.getValue()) + .amount(v) + .build()) + .apply(depositMoney.amount()); + } + + public AccountCreated validateCreateAccount(CreateAccount command) { + return Money.validateValue + .andThen(v -> AccountCreated.from(command)) + .apply(command.balance()); + } + + public AccountAggregate applyMoneyDeposited(MoneyDeposited moneyDeposited) { + this.amount = this.amount.depositMoney(moneyDeposited.amount()); + return this; + } + + public AccountAggregate applyAccountCreated(AccountCreated event) { + this.amount = this.amount.depositMoney(event.balance()); + return this; + } + + public AccountAggregate applyMoneyWithdrawn(MoneyWithdrawn event) { + this.amount = this.amount.withdrawMoney(event.amount()); + return this; + } + + public AccountState getState() { + return AccountState.builder() + .aggregateId(this.aggregateId.getValue()) + .addAllOpenedTransactions(this.openedTransactions.stream().map(Id::getValue).collect(Collectors.toList())) + .amount(this.amount.getAmount()) + .build(); + } } diff --git a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountState.java b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountState.java index 8e6a13a..d82ad43 100644 --- a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountState.java +++ b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/AccountState.java @@ -3,6 +3,7 @@ package com.mz.reactor.ddd.reactorddd.account.domain; import org.immutables.value.Value; import java.math.BigDecimal; +import java.util.List; @Value.Immutable public interface AccountState { @@ -10,6 +11,8 @@ public interface AccountState { BigDecimal amount(); + List openedTransactions(); + static ImmutableAccountState.Builder builder() { return ImmutableAccountState.builder(); } diff --git a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandler.java b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandler.java index 6e6e6e3..a50397b 100644 --- a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandler.java +++ b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandler.java @@ -2,7 +2,11 @@ package com.mz.reactor.ddd.reactorddd.account.domain.command; import com.mz.reactor.ddd.common.api.command.CommandHandler; import com.mz.reactor.ddd.common.api.command.CommandResult; +import com.mz.reactor.ddd.common.api.command.ImmutableCommandResult; import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate; +import com.mz.reactor.ddd.reactorddd.account.domain.event.DepositMoneyFailed; + +import java.util.Optional; public class AccountCommandHandler implements CommandHandler { @@ -15,7 +19,25 @@ public class AccountCommandHandler implements CommandHandler CommandResult.builder() + .statusCode(CommandResult.StatusCode.OK) + .addEvents(e) + .build()) + .orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified()); + } catch (RuntimeException e) { + return CommandResult.builder() + .addEvents(DepositMoneyFailed.builder() + .aggregateId(command.aggregateId()) + .amount(command.amount()) + .correlationId(command.correlationId()) + .build()) + .statusCode(CommandResult.StatusCode.FAILED) + .error(e) + .build(); + } } @Override diff --git a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountCreated.java b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountCreated.java index 12b2ef1..b759e3c 100644 --- a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountCreated.java +++ b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountCreated.java @@ -1,5 +1,6 @@ package com.mz.reactor.ddd.reactorddd.account.domain.event; +import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount; import org.immutables.value.Value; import java.math.BigDecimal; @@ -12,4 +13,12 @@ public interface AccountCreated extends AccountEvent { static ImmutableAccountCreated.Builder builder() { return ImmutableAccountCreated.builder(); } + + static AccountCreated from(CreateAccount createAccount) { + return builder() + .aggregateId(createAccount.aggregateId()) + .balance(createAccount.balance()) + .correlationId(createAccount.correlationId()) + .build(); + } } diff --git a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplier.java b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplier.java index 77eeffb..219a9bd 100644 --- a/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplier.java +++ b/bank-account/account-domain/src/main/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplier.java @@ -3,14 +3,10 @@ package com.mz.reactor.ddd.reactorddd.account.domain.event; import com.mz.reactor.ddd.common.api.event.EventApplier; import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiFunction; - public class AccountEventApplier implements EventApplier { private AccountAggregate applyAccountCreated(AccountAggregate aggregate, AccountCreated event) { - return aggregate; + return aggregate.applyAccountCreated(event); } private AccountAggregate applyMoneyWithdrawn(AccountAggregate aggregate, MoneyWithdrawn event) { @@ -18,7 +14,7 @@ public class AccountEventApplier implements EventApplier aggregate.validateDepositMoney(depositMoney)); + } + + @Test + void applyMoneyDeposited() { + //given + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var aggregate = new AccountAggregate(aggregateId); + var event = MoneyDeposited.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .amount(BigDecimal.TEN) + .build(); + Assertions.assertEquals(aggregate.getState().amount(), BigDecimal.ZERO); + + //when + var state = aggregate.applyMoneyDeposited(event).getState(); + + //then + Assertions.assertEquals(state.aggregateId(), aggregateId); + Assertions.assertEquals(state.amount(), event.amount()); + } + + @Test + void validateWithdrawMoney_MoneyWithdrawn() { + //given + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var aggregate = new AccountAggregate(aggregateId); + var accountCreated = AccountCreated.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .balance(BigDecimal.TEN) + .build(); + + aggregate.applyAccountCreated(accountCreated).getState(); + + var withdrawMoney = WithdrawMoney.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .amount(BigDecimal.valueOf(5)) + .build(); + + //when + var event = aggregate.validateWithdrawMoney(withdrawMoney); + var state = aggregate.applyMoneyWithdrawn(event).getState(); + + //then + Assertions.assertNotNull(event); + Assertions.assertEquals(event.correlationId().get(), correlationId); + Assertions.assertEquals(event.aggregateId(), aggregateId); + Assertions.assertTrue(event instanceof MoneyWithdrawn); + Assertions.assertEquals(state.amount(), BigDecimal.valueOf(5)); + } + + @Test + void validateWithdrawMoney_Failed() { + //given + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var aggregate = new AccountAggregate(aggregateId); + var accountCreated = AccountCreated.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .balance(BigDecimal.TEN) + .build(); + + aggregate.applyAccountCreated(accountCreated); + + //when + var withdrawMoney = WithdrawMoney.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .amount(BigDecimal.valueOf(15)) + .build(); + + //then + Assertions.assertThrows(RuntimeException.class, () -> aggregate.validateWithdrawMoney(withdrawMoney)); + } +} \ No newline at end of file diff --git a/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandlerTest.java b/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandlerTest.java new file mode 100644 index 0000000..5b743d5 --- /dev/null +++ b/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/command/AccountCommandHandlerTest.java @@ -0,0 +1,70 @@ +package com.mz.reactor.ddd.reactorddd.account.domain.command; + +import com.mz.reactor.ddd.common.api.command.CommandResult; +import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate; +import com.mz.reactor.ddd.reactorddd.account.domain.event.DepositMoneyFailed; +import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class AccountCommandHandlerTest { + + AccountCommandHandler accountCommandHandler = new AccountCommandHandler(); + + @Test + void executeDepositMoneyOK() { + //given + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var accountAggregate = new AccountAggregate(aggregateId); + var command = DepositMoney.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .amount(BigDecimal.TEN) + .build(); + + //when + var commandResult = accountCommandHandler.execute(accountAggregate, command); + + //then + Assertions.assertEquals(commandResult.statusCode(), CommandResult.StatusCode.OK); + Assertions.assertTrue(commandResult.events().stream().allMatch(e -> e instanceof MoneyDeposited)); + } + + @Test + void executeDepositMoneyBadCommand() { + //given + var accountAggregate = new AccountAggregate(UUID.randomUUID().toString()); + + //when + var commandResult = accountCommandHandler.execute(accountAggregate, null); + + //then + Assertions.assertEquals(commandResult.statusCode(), CommandResult.StatusCode.BAD_COMMAND); + } + + @Test + void executeDepositMoneyFailed() { + //given + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var accountAggregate = new AccountAggregate(aggregateId); + var command = DepositMoney.builder() + .aggregateId(aggregateId) + .correlationId(correlationId) + .amount(BigDecimal.ZERO) + .build(); + + //when + var commandResult = accountCommandHandler.execute(accountAggregate, command); + + //then + Assertions.assertEquals(commandResult.statusCode(), CommandResult.StatusCode.FAILED); + Assertions.assertTrue(commandResult.events().stream().allMatch(e -> e instanceof DepositMoneyFailed)); + } +} \ No newline at end of file diff --git a/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplierTest.java b/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplierTest.java new file mode 100644 index 0000000..6356387 --- /dev/null +++ b/bank-account/account-domain/src/test/java/com/mz/reactor/ddd/reactorddd/account/domain/event/AccountEventApplierTest.java @@ -0,0 +1,57 @@ +package com.mz.reactor.ddd.reactorddd.account.domain.event; + +import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class AccountEventApplierTest { + + AccountEventApplier applier = new AccountEventApplier(); + + @Test + void applyAccountCreated() { + //give + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var accountAggregate = new AccountAggregate(aggregateId); + var event = AccountCreated.builder() + .correlationId(correlationId) + .aggregateId(aggregateId) + .balance(BigDecimal.TEN) + .build(); + assertEquals(accountAggregate.getState().amount().compareTo(BigDecimal.ZERO), 0); + + //when + var result = applier.apply(accountAggregate, event).getState(); + + //then + assertEquals(result.amount().compareTo(BigDecimal.TEN), 0); + assertEquals(result.aggregateId(), aggregateId); + } + + @Test + void applyMoneyDeposited() { + //give + var aggregateId = UUID.randomUUID().toString(); + var correlationId = UUID.randomUUID().toString(); + var accountAggregate = new AccountAggregate(aggregateId); + var event = MoneyDeposited.builder() + .correlationId(correlationId) + .aggregateId(aggregateId) + .amount(BigDecimal.TEN) + .build(); + assertEquals(accountAggregate.getState().amount().compareTo(BigDecimal.ZERO), 0); + + //when + var result = applier.apply(accountAggregate, event).getState(); + + //then + assertEquals(result.amount().compareTo(BigDecimal.TEN), 0); + assertEquals(result.aggregateId(), aggregateId); + } + +} \ No newline at end of file diff --git a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/TransactionAggregate.java b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/TransactionAggregate.java index 7111132..7970e73 100644 --- a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/TransactionAggregate.java +++ b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/TransactionAggregate.java @@ -4,6 +4,7 @@ import com.mz.reactor.ddd.common.api.valueobject.Id; import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction; import com.mz.reactor.ddd.reactorddd.transaction.domain.command.FinishTransaction; import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated; +import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed; import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished; import java.math.BigDecimal; @@ -73,6 +74,16 @@ public class TransactionAggregate { return this; } + public TransactionAggregate applyTransactionFinished(TransactionFinished event) { + this.state = State.FINISHED; + return this; + } + + public TransactionAggregate applyTransactionFailed(TransactionFailed event) { + this.state = State.FAILED; + return this; + } + public TransactionState getState() { return TransactionState.builder() .amount(amount) diff --git a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/command/TransactionCommandHandler.java b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/command/TransactionCommandHandler.java index 3b89f27..c24f4eb 100644 --- a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/command/TransactionCommandHandler.java +++ b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/command/TransactionCommandHandler.java @@ -4,6 +4,7 @@ import com.mz.reactor.ddd.common.api.command.CommandHandler; import com.mz.reactor.ddd.common.api.command.CommandResult; import com.mz.reactor.ddd.common.api.command.ImmutableCommandResult; import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate; +import com.mz.reactor.ddd.reactorddd.transaction.domain.event.FinishTransactionFailed; import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed; import java.util.List; @@ -17,9 +18,10 @@ public class TransactionCommandHandler implements CommandHandler CommandResult.builder() - .events(List.of(e)) + .addEvents(e) .statusCode(CommandResult.StatusCode.OK) .build()) .orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified()); @@ -32,6 +34,26 @@ public class TransactionCommandHandler implements CommandHandler CommandResult.builder().build()) + .orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified()); + } catch (RuntimeException e) { + return CommandResult.builder() + .statusCode(CommandResult.StatusCode.FAILED) + .error(e) + .addEvents(FinishTransactionFailed.builder() + .aggregateId(commad.aggregateId()) + .correlationId(commad.correlationId()) + .fromAccountId(commad.fromAccountId()) + .toAccountId(commad.toAccountId()) + .build()) + .build(); + } + } + @Override public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) { if (command instanceof CreateTransaction) { diff --git a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/FinishTransactionFailed.java b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/FinishTransactionFailed.java new file mode 100644 index 0000000..5912fdf --- /dev/null +++ b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/FinishTransactionFailed.java @@ -0,0 +1,15 @@ +package com.mz.reactor.ddd.reactorddd.transaction.domain.event; + +import org.immutables.value.Value; + +@Value.Immutable +public interface FinishTransactionFailed extends TransactionEvent { + + String fromAccountId(); + + String toAccountId(); + + static ImmutableFinishTransactionFailed.Builder builder() { + return ImmutableFinishTransactionFailed.builder(); + } +} diff --git a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/TransactionEventApplier.java b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/TransactionEventApplier.java index 0f145ab..b3f7afa 100644 --- a/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/TransactionEventApplier.java +++ b/bank-account/transaction-domain/src/main/java/com/mz/reactor/ddd/reactorddd/transaction/domain/event/TransactionEventApplier.java @@ -10,12 +10,28 @@ public class TransactionEventApplier implements EventApplier validateValue = value -> + Objects.requireNonNull(value, "Money amount can't be null!"); + + public static final Function validateGreaterThenZero = value -> { + if (BigDecimal.ZERO.compareTo(value) == 0) { + throw new RuntimeException(String.format("Can't deposit %s money", value)); + } + return value; + }; + + public static final Function validateDepositMoney = value -> + validateValue.andThen(validateGreaterThenZero).apply(value); + + public static final BinaryOperator validateWithdraw = (currentValue, value) -> + validateValue + .andThen(v -> { + if (currentValue.compareTo(v) < 0) { + throw new RuntimeException(String.format( + "Can not withdraw amount %s, current balance is %s", + value, + currentValue + )); + } + return v; + }) + .apply(value); + }