Account aggregate implementation

This commit is contained in:
Michal Zeman
2019-09-22 10:58:03 +02:00
parent e05f86aa6d
commit cdff13c395
15 changed files with 512 additions and 10 deletions

View File

@@ -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<Id> 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();
}
}

View File

@@ -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<String> openedTransactions();
static ImmutableAccountState.Builder builder() {
return ImmutableAccountState.builder();
}

View File

@@ -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<AccountAggregate, AccountCommand> {
@@ -15,7 +19,25 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
}
private CommandResult doDepositMoney(AccountAggregate aggregate, DepositMoney command) {
return null;
try {
return Optional.of(command)
.map(aggregate::validateDepositMoney)
.map(e -> 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

View File

@@ -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();
}
}

View File

@@ -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<AccountAggregate, AccountEvent> {
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<AccountAggregate, Accou
}
private AccountAggregate applyMoneyDeposited(AccountAggregate aggregate, MoneyDeposited event) {
return aggregate;
return aggregate.applyMoneyDeposited(event);
}
@Override

View File

@@ -1,5 +1,6 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
import org.immutables.value.Value;
import java.math.BigDecimal;
@@ -11,4 +12,12 @@ public interface MoneyWithdrawn extends AccountEvent {
static ImmutableMoneyWithdrawn.Builder builder() {
return ImmutableMoneyWithdrawn.builder();
}
static MoneyWithdrawn from(WithdrawMoney command) {
return builder()
.aggregateId(command.aggregateId())
.correlationId(command.correlationId())
.amount(command.amount())
.build();
}
}

View File

@@ -0,0 +1,158 @@
package com.mz.reactor.ddd.reactorddd.account.domain;
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.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 AccountAggregateTest {
@Test
void validateCreateAccount_Ok() {
//given
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var aggregate = new AccountAggregate(aggregateId);
var command = CreateAccount.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.balance(BigDecimal.TEN)
.build();
//when
var event = aggregate.validateCreateAccount(command);
var state = aggregate.applyAccountCreated(event).getState();
//then
Assertions.assertNotNull(event);
Assertions.assertEquals(event.correlationId().get(), command.correlationId().get());
Assertions.assertEquals(event.aggregateId(), command.aggregateId());
Assertions.assertEquals(event.balance().compareTo(BigDecimal.TEN), 0);
Assertions.assertEquals(state.amount(), BigDecimal.TEN);
}
@Test
void validateDepositMoney_MoneyDeposited() {
//given
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var aggregate = new AccountAggregate(aggregateId);
var depositMoney = DepositMoney.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.amount(BigDecimal.TEN)
.build();
//when
var event = aggregate.validateDepositMoney(depositMoney);
//then
Assertions.assertNotNull(event);
Assertions.assertEquals(event.correlationId().get(), depositMoney.correlationId().get());
Assertions.assertEquals(event.aggregateId(), depositMoney.aggregateId());
Assertions.assertEquals(event.amount().compareTo(BigDecimal.TEN), 0);
}
@Test
void validateDepositMoney_Failed() {
//given
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var aggregate = new AccountAggregate(aggregateId);
var depositMoney = DepositMoney.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.amount(BigDecimal.ZERO)
.build();
//then
Assertions.assertThrows(RuntimeException.class, () -> 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));
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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<TransactionAggr
private CommandResult doCreateTransaction(TransactionAggregate aggregate, CreateTransaction command) {
try {
return Optional.ofNullable(aggregate.validateCreateTransaction(command))
return Optional.of(command)
.map(aggregate::validateCreateTransaction)
.map(e -> 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<TransactionAggr
}
}
private CommandResult doFinishTransaction(TransactionAggregate aggregate, FinishTransaction commad) {
try {
return Optional.of(commad)
.map(aggregate::validateFinishTransaction)
.map(e -> 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) {

View File

@@ -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();
}
}

View File

@@ -10,12 +10,28 @@ public class TransactionEventApplier implements EventApplier<TransactionAggregat
public TransactionAggregate apply(TransactionAggregate aggregate, TransactionEvent event) {
if (event instanceof TransactionCreated) {
return applyTransactionCreated(aggregate, (TransactionCreated) event);
}
if (event instanceof TransactionFinished) {
return applyTransactionFinished(aggregate, (TransactionFinished) event);
}
if (event instanceof TransactionFailed) {
return applyTransactionFailed(aggregate, (TransactionFailed) event);
} else {
return aggregate;
}
}
private TransactionAggregate applyTransactionFailed(TransactionAggregate aggregate, TransactionFailed event) {
return aggregate.applyTransactionFailed(event);
}
private TransactionAggregate applyTransactionFinished(TransactionAggregate aggregate, TransactionFinished event) {
return aggregate.applyTransactionFinished(event);
}
private TransactionAggregate applyTransactionCreated(TransactionAggregate aggregate, TransactionCreated event) {
return aggregate.applyTransactionCreated(event);
}
}

View File

@@ -122,4 +122,12 @@ class TransactionAggregateTest {
.aggregateId(aggregateId)
.build()));
}
@Test
void applyTransactionFinished() {
}
@Test
void applyTransactionFailed() {
}
}

View File

@@ -2,15 +2,54 @@ package com.mz.reactor.ddd.common.api.valueobject;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
public class Money {
private final BigDecimal amount;
public Money(BigDecimal amount) {
this.amount = Objects.requireNonNull(amount, "Money amount can't be null!");
this.amount = validateValue.apply(amount);
}
public Money depositMoney(BigDecimal amount) {
return new Money(this.amount.add(amount));
}
public Money withdrawMoney(BigDecimal amount) {
return new Money(this.amount.add(amount.negate()));
}
public BigDecimal getAmount() {
return amount;
}
public static final Function<BigDecimal, BigDecimal> validateValue = value ->
Objects.requireNonNull(value, "Money amount can't be null!");
public static final Function<BigDecimal, BigDecimal> 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<BigDecimal, BigDecimal> validateDepositMoney = value ->
validateValue.andThen(validateGreaterThenZero).apply(value);
public static final BinaryOperator<BigDecimal> 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);
}