transfer money operation
This commit is contained in:
1
bank-account/account-adapters/build.gradle
Normal file
1
bank-account/account-adapters/build.gradle
Normal file
@@ -0,0 +1 @@
|
||||
description('account-adapter')
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.api;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
|
||||
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;
|
||||
@@ -16,4 +18,6 @@ public interface AccountApplicationService {
|
||||
|
||||
Mono<MoneyWithdrawn> execute(WithdrawMoney cmd);
|
||||
|
||||
<R extends DomainEvent> Mono<R> execute(AccountCommand cmd, Class<R> eventType);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.mz.reactor.ddd.reactorddd.account.api;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.http.HttpHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.model.*;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
@@ -17,8 +18,22 @@ public class AccountHandler implements HttpHandler {
|
||||
|
||||
private final AccountApplicationService service;
|
||||
|
||||
public AccountHandler(AccountApplicationService service) {
|
||||
private final AccountQuery accountQuery;
|
||||
|
||||
public AccountHandler(AccountApplicationService service, AccountQuery accountQuery) {
|
||||
this.service = service;
|
||||
this.accountQuery = accountQuery;
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getById(ServerRequest request) {
|
||||
return accountQuery.findById(request.pathVariable("id"))
|
||||
.flatMap(this::mapToResponse);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getAll(ServerRequest request) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(accountQuery.getAll(), AccountState.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createAccount(ServerRequest request) {
|
||||
@@ -51,8 +66,10 @@ public class AccountHandler implements HttpHandler {
|
||||
public RouterFunction<ServerResponse> route() {
|
||||
var route = RouterFunctions
|
||||
.route(POST("").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::createAccount)
|
||||
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getAll)
|
||||
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getById)
|
||||
.andRoute(PUT("/moneys/withdraw").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::withdrawMoney)
|
||||
.andRoute(PUT("/moneys/deposit").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::withdrawMoney);
|
||||
.andRoute(PUT("/moneys/deposit").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::depositMoney);
|
||||
|
||||
return RouterFunctions.route()
|
||||
.nest(path("/accounts"), () -> route)
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.api;
|
||||
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AccountQuery {
|
||||
Mono<AccountState> findById(String id);
|
||||
|
||||
Flux<AccountState> getAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.common.api.view.DomainView;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableAccountState.class)
|
||||
@JsonDeserialize(as = ImmutableAccountState.class)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public interface AccountState extends DomainView {
|
||||
|
||||
default String id() {
|
||||
return aggregateId();
|
||||
}
|
||||
|
||||
String aggregateId();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
@JsonIgnore
|
||||
List<String> openedTransactions();
|
||||
|
||||
static ImmutableAccountState.Builder builder() {
|
||||
return ImmutableAccountState.builder();
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,18 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableCreateAccount.class)
|
||||
@JsonDeserialize(as = ImmutableCreateAccount.class)
|
||||
public interface CreateAccount extends AccountCommand {
|
||||
|
||||
@Value.Default
|
||||
default String aggregateId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
BigDecimal balance();
|
||||
|
||||
static ImmutableCreateAccount.Builder builder() {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.command;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableDepositTransferMoney.class)
|
||||
@JsonDeserialize(as = ImmutableDepositTransferMoney.class)
|
||||
public interface DepositTransferMoney extends TransferMoneyCommand {
|
||||
|
||||
static ImmutableDepositTransferMoney.Builder builder() {
|
||||
return ImmutableDepositTransferMoney.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.command;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public interface TransferMoneyCommand extends AccountCommand {
|
||||
|
||||
String transactionId();
|
||||
|
||||
String fromAccount();
|
||||
|
||||
String toAccount();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.command;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableWithdrawTransferMoney.class)
|
||||
@JsonDeserialize(as = ImmutableWithdrawTransferMoney.class)
|
||||
public interface WithdrawTransferMoney extends TransferMoneyCommand {
|
||||
|
||||
static ImmutableWithdrawTransferMoney.Builder builder() {
|
||||
return ImmutableWithdrawTransferMoney.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableDepositTransferMoneyFailed.class)
|
||||
@JsonDeserialize(as = ImmutableDepositTransferMoneyFailed.class)
|
||||
public interface DepositTransferMoneyFailed extends TransferMoneyFailed {
|
||||
|
||||
static ImmutableDepositTransferMoneyFailed.Builder builder() {
|
||||
return ImmutableDepositTransferMoneyFailed.builder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableTransferMoneyDeposited.class)
|
||||
@JsonDeserialize(as = ImmutableTransferMoneyDeposited.class)
|
||||
public interface TransferMoneyDeposited extends TransferMoneyEvent {
|
||||
|
||||
static ImmutableTransferMoneyDeposited.Builder builder() {
|
||||
return ImmutableTransferMoneyDeposited.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public interface TransferMoneyEvent extends AccountEvent {
|
||||
|
||||
String transactionId();
|
||||
|
||||
String fromAccount();
|
||||
|
||||
String toAccount();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public interface TransferMoneyFailed extends AccountEvent {
|
||||
|
||||
String transactionId();
|
||||
|
||||
String fromAccount();
|
||||
|
||||
String toAccount();
|
||||
|
||||
BigDecimal amount();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawTransferMoney;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableTransferMoneyWithdrawn.class)
|
||||
@JsonDeserialize(as = ImmutableTransferMoneyWithdrawn.class)
|
||||
public interface TransferMoneyWithdrawn extends TransferMoneyEvent {
|
||||
|
||||
static ImmutableTransferMoneyWithdrawn.Builder builder() {
|
||||
return ImmutableTransferMoneyWithdrawn.builder();
|
||||
}
|
||||
|
||||
static TransferMoneyWithdrawn from(WithdrawTransferMoney cmd) {
|
||||
return TransferMoneyWithdrawn.builder()
|
||||
.transactionId(cmd.transactionId())
|
||||
.aggregateId(cmd.aggregateId())
|
||||
.amount(cmd.amount())
|
||||
.correlationId(cmd.correlationId())
|
||||
.fromAccount(cmd.fromAccount())
|
||||
.toAccount(cmd.toAccount())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableWithdrawTransferMoneyFailed.class)
|
||||
@JsonDeserialize(as = ImmutableWithdrawTransferMoneyFailed.class)
|
||||
public interface WithdrawTransferMoneyFailed extends TransferMoneyFailed {
|
||||
static ImmutableWithdrawTransferMoneyFailed.Builder builder() {
|
||||
return ImmutableWithdrawTransferMoneyFailed.builder();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,8 @@ 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 com.mz.reactor.ddd.reactorddd.account.domain.command.*;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
@@ -50,6 +46,27 @@ public class AccountAggregate {
|
||||
.apply(command.balance());
|
||||
}
|
||||
|
||||
public TransferMoneyWithdrawn validateWithdrawTransferMoney(WithdrawTransferMoney command) {
|
||||
return Money.validateValue
|
||||
.andThen(v -> Money.validateWithdraw.apply(this.amount.getAmount(), command.amount()))
|
||||
.andThen(v -> TransferMoneyWithdrawn.from(command))
|
||||
.apply(command.amount());
|
||||
}
|
||||
|
||||
public TransferMoneyDeposited validateDepositTransferMoney(DepositTransferMoney depositMoney) {
|
||||
return Money.validateDepositMoney
|
||||
.andThen(v ->
|
||||
TransferMoneyDeposited.builder()
|
||||
.correlationId(depositMoney.correlationId())
|
||||
.aggregateId(aggregateId.getValue())
|
||||
.amount(v)
|
||||
.transactionId(depositMoney.transactionId())
|
||||
.fromAccount(depositMoney.fromAccount())
|
||||
.toAccount(depositMoney.toAccount())
|
||||
.build())
|
||||
.apply(depositMoney.amount());
|
||||
}
|
||||
|
||||
public AccountAggregate applyMoneyDeposited(MoneyDeposited moneyDeposited) {
|
||||
this.amount = this.amount.depositMoney(moneyDeposited.amount());
|
||||
return this;
|
||||
@@ -65,6 +82,16 @@ public class AccountAggregate {
|
||||
return this;
|
||||
}
|
||||
|
||||
public AccountAggregate applyTransferMoneyWithdrawn(TransferMoneyWithdrawn event) {
|
||||
this.amount = this.amount.withdrawMoney(event.amount());
|
||||
return this;
|
||||
}
|
||||
|
||||
public AccountAggregate applyTransferMoneyDeposited(TransferMoneyDeposited event) {
|
||||
this.amount = this.amount.depositMoney(event.amount());
|
||||
return this;
|
||||
}
|
||||
|
||||
public AccountState getState() {
|
||||
return AccountState.builder()
|
||||
.aggregateId(this.aggregateId.getValue())
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.command.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.command.AccountCommand;
|
||||
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.CreateAccountFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.DepositMoneyFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.WithdrawMoneyFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.*;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class AccountCommandHandler implements CommandHandler<AccountAggregate, AccountCommand> {
|
||||
|
||||
private final Function<AccountCommand, ImmutableCommandResult> commandNotModified = cmd ->
|
||||
(ImmutableCommandResult) CommandResult.notModified(cmd);
|
||||
|
||||
|
||||
@Override
|
||||
public CommandResult execute(AccountAggregate aggregate, AccountCommand command) {
|
||||
if (command instanceof WithdrawMoney) {
|
||||
return doWithdrawMoney(aggregate, (WithdrawMoney) command);
|
||||
} else if (command instanceof CreateAccount) {
|
||||
return doCreateAccount(aggregate, (CreateAccount) command);
|
||||
} else if (command instanceof DepositMoney) {
|
||||
return doDepositMoney(aggregate, (DepositMoney) command);
|
||||
} else if (command instanceof WithdrawTransferMoney) {
|
||||
return doWithdrawTransferMoney(aggregate, (WithdrawTransferMoney) command);
|
||||
} else if (command instanceof DepositTransferMoney) {
|
||||
return doDepositTransferMoney(aggregate, (DepositTransferMoney) command);
|
||||
} else {
|
||||
return CommandResult.badCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResult doWithdrawMoney(AccountAggregate aggregate, WithdrawMoney command) {
|
||||
try {
|
||||
@@ -28,22 +37,15 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.addEvents(e)
|
||||
.events(List.of(e))
|
||||
.build())
|
||||
.orElseGet(() -> commandNotModified.apply(command));
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.addEvents(WithdrawMoneyFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.amount(command.amount())
|
||||
.correlationId(command.correlationId())
|
||||
.build())
|
||||
.statusCode(CommandResult.StatusCode.FAILED)
|
||||
.error(e)
|
||||
.build();
|
||||
return CommandResult.fromError(e, List.of(WithdrawMoneyFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.amount(command.amount())
|
||||
.correlationId(command.correlationId())
|
||||
.build()), command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,22 +56,19 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.addEvents(e)
|
||||
.events(List.of(e))
|
||||
.build())
|
||||
.orElseGet(() -> commandNotModified.apply(command));
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.addEvents(CreateAccountFailed.builder()
|
||||
return CommandResult.fromError(
|
||||
e,
|
||||
List.of(CreateAccountFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.balance(command.balance())
|
||||
.correlationId(command.correlationId())
|
||||
.build())
|
||||
.statusCode(CommandResult.StatusCode.FAILED)
|
||||
.error(e)
|
||||
.build();
|
||||
.build()),
|
||||
command
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,35 +79,71 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.addEvents(e)
|
||||
.events(List.of(e))
|
||||
.build())
|
||||
.orElseGet(() -> commandNotModified.apply(command));
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.addEvents(DepositMoneyFailed.builder()
|
||||
return CommandResult.fromError(
|
||||
e,
|
||||
List.of(DepositMoneyFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.amount(command.amount())
|
||||
.correlationId(command.correlationId())
|
||||
.build())
|
||||
.statusCode(CommandResult.StatusCode.FAILED)
|
||||
.error(e)
|
||||
.build();
|
||||
.build()),
|
||||
command
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(AccountAggregate aggregate, AccountCommand command) {
|
||||
if (command instanceof WithdrawMoney) {
|
||||
return doWithdrawMoney(aggregate, (WithdrawMoney) command);
|
||||
} else if (command instanceof CreateAccount) {
|
||||
return doCreateAccount(aggregate, (CreateAccount) command);
|
||||
} else if (command instanceof DepositMoney) {
|
||||
return doDepositMoney(aggregate, (DepositMoney) command);
|
||||
} else {
|
||||
return CommandResult.badCommand(command);
|
||||
private CommandResult doWithdrawTransferMoney(AccountAggregate aggregate, WithdrawTransferMoney command) {
|
||||
try {
|
||||
return Optional.of(command)
|
||||
.map(aggregate::validateWithdrawTransferMoney)
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.events(List.of(e))
|
||||
.build())
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.fromError(
|
||||
e,
|
||||
List.of(WithdrawTransferMoneyFailed.builder()
|
||||
.transactionId(command.transactionId())
|
||||
.aggregateId(command.aggregateId())
|
||||
.amount(command.amount())
|
||||
.correlationId(command.correlationId())
|
||||
.fromAccount(command.fromAccount())
|
||||
.toAccount(command.toAccount())
|
||||
.build()),
|
||||
command
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResult doDepositTransferMoney(AccountAggregate aggregate, DepositTransferMoney command) {
|
||||
try {
|
||||
return Optional.of(command)
|
||||
.map(aggregate::validateDepositTransferMoney)
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.events(List.of(e))
|
||||
.build())
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.fromError(
|
||||
e,
|
||||
List.of(DepositTransferMoneyFailed.builder()
|
||||
.transactionId(command.transactionId())
|
||||
.aggregateId(command.aggregateId())
|
||||
.amount(command.amount())
|
||||
.correlationId(command.correlationId())
|
||||
.fromAccount(command.fromAccount())
|
||||
.toAccount(command.toAccount())
|
||||
.build()),
|
||||
command
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.domain;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.event.EventApplier;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountEvent;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
|
||||
|
||||
public class AccountEventApplier implements EventApplier<AccountAggregate, AccountEvent> {
|
||||
public class AccountEventApplier implements EventApplier<AccountAggregate> {
|
||||
|
||||
@Override
|
||||
public <E extends DomainEvent> AccountAggregate apply(AccountAggregate aggregate, E event) {
|
||||
if (event instanceof AccountCreated) {
|
||||
return applyAccountCreated(aggregate, (AccountCreated) event);
|
||||
} else if (event instanceof MoneyWithdrawn) {
|
||||
return applyMoneyWithdrawn(aggregate, (MoneyWithdrawn) event);
|
||||
} else if (event instanceof MoneyDeposited) {
|
||||
return applyMoneyDeposited(aggregate, (MoneyDeposited) event);
|
||||
} else if (event instanceof TransferMoneyWithdrawn) {
|
||||
return applyTransferMoneyWithdrawn(aggregate, (TransferMoneyWithdrawn) event);
|
||||
} else if (event instanceof TransferMoneyDeposited) {
|
||||
return applyTransferMoneyDeposited(aggregate, (TransferMoneyDeposited) event);
|
||||
} else {
|
||||
return aggregate;
|
||||
}
|
||||
}
|
||||
|
||||
private AccountAggregate applyTransferMoneyDeposited(AccountAggregate aggregate, TransferMoneyDeposited event) {
|
||||
return aggregate.applyTransferMoneyDeposited(event);
|
||||
}
|
||||
|
||||
private AccountAggregate applyAccountCreated(AccountAggregate aggregate, AccountCreated event) {
|
||||
return aggregate.applyAccountCreated(event);
|
||||
@@ -21,16 +39,7 @@ public class AccountEventApplier implements EventApplier<AccountAggregate, Accou
|
||||
return aggregate.applyMoneyDeposited(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountAggregate apply(AccountAggregate aggregate, AccountEvent event) {
|
||||
if (event instanceof AccountCreated) {
|
||||
return applyAccountCreated(aggregate, (AccountCreated) event);
|
||||
} else if (event instanceof MoneyWithdrawn) {
|
||||
return applyMoneyWithdrawn(aggregate, (MoneyWithdrawn) event);
|
||||
} else if (event instanceof MoneyDeposited) {
|
||||
return applyMoneyDeposited(aggregate, (MoneyDeposited) event);
|
||||
} else {
|
||||
return aggregate;
|
||||
}
|
||||
private AccountAggregate applyTransferMoneyWithdrawn(AccountAggregate aggregate, TransferMoneyWithdrawn event) {
|
||||
return aggregate.applyTransferMoneyWithdrawn(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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 {
|
||||
String aggregateId();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
List<String> openedTransactions();
|
||||
|
||||
static ImmutableAccountState.Builder builder() {
|
||||
return ImmutableAccountState.builder();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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.WithdrawTransferMoney;
|
||||
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;
|
||||
@@ -151,6 +152,40 @@ class AccountAggregateTest {
|
||||
.build();
|
||||
|
||||
//then
|
||||
Assertions.assertThrows(RuntimeException.class, () -> aggregate.validateWithdrawMoney(withdrawMoney));
|
||||
Assertions.assertThrows(RuntimeException.class, () -> aggregate.validateWithdrawMoney(withdrawMoney));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMoneyTransferred() {
|
||||
//given
|
||||
var toAccountId = UUID.randomUUID().toString();
|
||||
var aggregateId = UUID.randomUUID().toString();
|
||||
var correlationId = UUID.randomUUID().toString();
|
||||
var transactionId = 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 command = WithdrawTransferMoney.builder()
|
||||
.transactionId(transactionId)
|
||||
.aggregateId(aggregateId)
|
||||
.fromAccount(aggregateId)
|
||||
.toAccount(toAccountId)
|
||||
.amount(BigDecimal.TEN)
|
||||
.build();
|
||||
Assertions.assertEquals(aggregate.getState().amount(), BigDecimal.TEN);
|
||||
|
||||
//when
|
||||
var event = aggregate.validateWithdrawTransferMoney(command);
|
||||
var state = aggregate.applyTransferMoneyWithdrawn(event).getState();
|
||||
|
||||
//then
|
||||
Assertions.assertEquals(state.aggregateId(), aggregateId);
|
||||
Assertions.assertEquals(state.amount(), BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,108 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.AccountApplicationService;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
|
||||
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.AccountEvent;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.*;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AccountApplicationServiceImpl implements AccountApplicationService {
|
||||
|
||||
private final AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> aggregateFacade;
|
||||
private static final Log log = LogFactory.getLog(AccountApplicationServiceImpl.class);
|
||||
|
||||
private final AggregateFacade<AccountAggregate, AccountCommand, AccountState> aggregateFacade;
|
||||
|
||||
public AccountApplicationServiceImpl(
|
||||
ApplicationMessageBus bus,
|
||||
@Qualifier("accountAggregateFacade") AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> aggregateFacade
|
||||
@Qualifier("accountAggregateFacade") AggregateFacade<AccountAggregate, AccountCommand, AccountState> aggregateFacade
|
||||
) {
|
||||
this.aggregateFacade = aggregateFacade;
|
||||
handleTransactionCreated(bus);
|
||||
handleTransferMoneyWithdrawn(bus);
|
||||
}
|
||||
|
||||
public <R extends DomainEvent> Mono<R> execute(AccountCommand cmd, Class<R> eventType) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), eventType)
|
||||
.cast(eventType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AccountCreated> execute(CreateAccount cmd) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(AccountCreated.class);
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), AccountCreated.class)
|
||||
.cast(AccountCreated.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MoneyDeposited> execute(DepositMoney cmd) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(MoneyDeposited.class);
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), MoneyDeposited.class)
|
||||
.cast(MoneyDeposited.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MoneyWithdrawn> execute(WithdrawMoney cmd) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(MoneyWithdrawn.class);
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), MoneyWithdrawn.class)
|
||||
.cast(MoneyWithdrawn.class);
|
||||
}
|
||||
|
||||
// private void handleTransactionFailed(ApplicationMessageBus bus) {
|
||||
// bus.messagesStream()
|
||||
// .filter(m -> m instanceof TransactionFailed)
|
||||
// .cast(TransactionFailed.class)
|
||||
// .flatMap(e -> execute(DepositMoney.builder()
|
||||
// .correlationId(e.correlationId())
|
||||
// .aggregateId(e.fromAccountId())
|
||||
// .amount(e.amount())
|
||||
// .build()))
|
||||
// .map(Optional::of)
|
||||
// .doOnError(e -> log.error("handleTransactionFailed -> ", e))
|
||||
// .retry()
|
||||
// .subscribe();
|
||||
// }
|
||||
|
||||
private void handleTransactionCreated(ApplicationMessageBus bus) {
|
||||
bus.messagesStream()
|
||||
.filter(m -> m instanceof TransactionCreated)
|
||||
.cast(TransactionCreated.class)
|
||||
.flatMap(e -> execute(WithdrawTransferMoney.builder()
|
||||
.aggregateId(e.fromAccountId())
|
||||
.transactionId(e.aggregateId())
|
||||
.correlationId(e.correlationId())
|
||||
.fromAccount(e.fromAccountId())
|
||||
.toAccount(e.toAccountId())
|
||||
.amount(e.amount())
|
||||
.build(), TransferMoneyWithdrawn.class))
|
||||
.doOnError(e -> log.error("handleTransactionCreated -> ", e))
|
||||
.retry()
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private void handleTransferMoneyWithdrawn(ApplicationMessageBus bus) {
|
||||
bus.messagesStream()
|
||||
.filter(m -> m instanceof TransferMoneyWithdrawn)
|
||||
.cast(TransferMoneyWithdrawn.class)
|
||||
.flatMap(e -> execute(DepositTransferMoney.builder()
|
||||
.aggregateId(e.toAccount())
|
||||
.transactionId(e.transactionId())
|
||||
.correlationId(e.correlationId())
|
||||
.fromAccount(e.fromAccount())
|
||||
.toAccount(e.toAccount())
|
||||
.amount(e.amount())
|
||||
.build(), TransferMoneyDeposited.class))
|
||||
.doOnError(e -> log.error("handleTransferMoneyWithdrawn -> ", e))
|
||||
.retry()
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.mz.reactor.ddd.reactorddd.account.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.AccountQuery;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import com.mz.reactor.ddd.reactorddd.account.wiring.AccountConfiguration;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class AccountQueryImpl implements AccountQuery {
|
||||
|
||||
private final ViewRepository<AccountState> repository;
|
||||
|
||||
public AccountQueryImpl(
|
||||
@Qualifier(AccountConfiguration.ACCOUNT_VIEW_REPOSITORY) ViewRepository<AccountState> repository,
|
||||
ApplicationMessageBus bus
|
||||
) {
|
||||
this.repository = Objects.requireNonNull(repository);
|
||||
Objects.requireNonNull(bus).messagesStream()
|
||||
.filter(m -> m instanceof AccountState)
|
||||
.cast(AccountState.class)
|
||||
.subscribe(repository::addView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AccountState> findById(@NonNull String id) {
|
||||
return repository.findBy(v -> v.id().equals(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AccountState> getAll() {
|
||||
return repository.findAllBy(v -> true);
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,19 @@ package com.mz.reactor.ddd.reactorddd.account.wiring;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.common.components.bus.impl.ApplicationMessageBusImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountCommandHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountEventApplier;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountEvent;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateFacadeImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateRepositoryImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.impl.ViewRepositoryImpl;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.function.Function;
|
||||
@@ -24,23 +23,31 @@ import java.util.function.Function;
|
||||
public class AccountConfiguration {
|
||||
|
||||
public static final String ACCOUNT_AGGREGATE_REPOSITORY = "accountAggregateRepository";
|
||||
public static final String ACCOUNT_AGGREGATE_FACADE = "accountAggregateFacade";
|
||||
public static final String ACCOUNT_VIEW_REPOSITORY = "accountViewRepository";
|
||||
|
||||
private final AccountEventApplier accountEventApplier = new AccountEventApplier();
|
||||
private final AccountCommandHandler accountCommandHandler = new AccountCommandHandler();
|
||||
private final Function<Id, AccountAggregate> aggregateFactory = id -> new AccountAggregate(id.toString());
|
||||
private final Function<Id, AccountAggregate> aggregateFactory = id -> new AccountAggregate(id.getValue());
|
||||
private final Function<AccountAggregate, AccountState> stateFactory = AccountAggregate::getState;
|
||||
|
||||
@Bean(ACCOUNT_AGGREGATE_REPOSITORY)
|
||||
public AggregateRepository<AccountAggregate, AccountCommand, AccountEvent, AccountState> getAggregateRepository() {
|
||||
public AggregateRepository<AccountAggregate, AccountCommand, AccountState> getAggregateRepository() {
|
||||
return new AggregateRepositoryImpl<>(accountCommandHandler, accountEventApplier, aggregateFactory, stateFactory);
|
||||
}
|
||||
|
||||
@Bean("accountAggregateFacade")
|
||||
public AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> getAggregateFacade(
|
||||
@Qualifier(ACCOUNT_AGGREGATE_REPOSITORY) AggregateRepository aggregateRepository,
|
||||
@Bean(ACCOUNT_AGGREGATE_FACADE)
|
||||
public AggregateFacade<AccountAggregate, AccountCommand, AccountState> getAggregateFacade(
|
||||
@Qualifier(ACCOUNT_AGGREGATE_REPOSITORY) AggregateRepository<AccountAggregate, AccountCommand, AccountState> aggregateRepository,
|
||||
@Qualifier(ACCOUNT_VIEW_REPOSITORY) ViewRepository<AccountState> viewRepository,
|
||||
ApplicationMessageBus bus
|
||||
) {
|
||||
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, s -> {});
|
||||
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, bus::publishMessage);
|
||||
}
|
||||
|
||||
@Bean(ACCOUNT_VIEW_REPOSITORY)
|
||||
public ViewRepository<AccountState> getViewRepository() {
|
||||
return new ViewRepositoryImpl<AccountState>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.mz.reactor.ddd.reactorddd.application;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import reactor.core.publisher.Hooks;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class BankAccountApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Hooks.onOperatorDebug();
|
||||
SpringApplication.run(BankAccountApp.class, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.mz.reactor.ddd.reactorddd.application;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.http.HttpErrorHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.AccountHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionHandler;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
@@ -14,17 +16,21 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = {"com.mz.reactor.ddd.*"})
|
||||
@Import({com.mz.reactor.ddd.reactorddd.account.wiring.AccountConfiguration.class})
|
||||
//@Import({AccountConfiguration.class, TransactionConfiguration.class})
|
||||
public class BankAccountAppConfiguration {
|
||||
|
||||
private static final Log log = LogFactory.getLog(BankAccountAppConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> statisticRoute(AccountHandler handler) {
|
||||
public RouterFunction<ServerResponse> statisticRoute(AccountHandler accountHandler, TransactionHandler transactionHandler) {
|
||||
return RouterFunctions.route()
|
||||
.add(handler.route())
|
||||
.add(accountHandler.route())
|
||||
.add(transactionHandler.route())
|
||||
.GET("/health", req -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(Mono.just("Tick"), String.class))
|
||||
.onError(Throwable.class, HttpErrorHandler.FN::onError)
|
||||
.onError(Throwable.class,
|
||||
(throwable, serverRequest) -> HttpErrorHandler.FN.onError(throwable, serverRequest, error -> log.error("Error: ", error)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.mz.reactor.ddd.reactorddd.application;
|
||||
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.model.CreateAccountRequest;
|
||||
import com.mz.reactor.ddd.reactorddd.account.api.model.CreateAccountResponse;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionRequest;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionResponse;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.registerCustomDateFormat;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class BankAccountAppTest {
|
||||
|
||||
@Autowired
|
||||
WebTestClient webTestClient;
|
||||
|
||||
@Test
|
||||
public void testApp() {
|
||||
var correlationId = "testScenarioCreateAccountsAndTransferMoney";
|
||||
|
||||
var accountId1 = "account_1";
|
||||
var accountId2 = "account_2";
|
||||
|
||||
createAccount(accountId1, correlationId);
|
||||
createAccount(accountId2, correlationId);
|
||||
|
||||
assertThat(getAllAccounts().size()).isEqualTo(2);
|
||||
|
||||
var transactionId = createTransaction(accountId1, accountId2, correlationId, BigDecimal.TEN).payload().aggregateId();
|
||||
|
||||
getTransaction(transactionId);
|
||||
|
||||
assertThat(getAccount(accountId1).amount().compareTo(BigDecimal.valueOf(90))).isEqualTo(0);
|
||||
assertThat(getAccount(accountId2).amount().compareTo(BigDecimal.valueOf(110))).isEqualTo(0);
|
||||
}
|
||||
|
||||
private CreateAccountResponse createAccount(String accountId, String correlationId) {
|
||||
var createAccount1 = CreateAccountRequest.builder()
|
||||
.payload(CreateAccount.builder()
|
||||
.aggregateId(accountId)
|
||||
.balance(BigDecimal.valueOf(100))
|
||||
.correlationId(correlationId)
|
||||
.build())
|
||||
.build();
|
||||
return webTestClient.post().uri("/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(BodyInserters.fromObject(createAccount1))
|
||||
.exchange()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(CreateAccountResponse.class)
|
||||
.returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private List<AccountState> getAllAccounts() {
|
||||
return webTestClient.get().uri("/accounts")
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(List.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private CreateTransactionResponse createTransaction(String fromAccount, String toAccount, String correlationId, BigDecimal amount) {
|
||||
var createTransactionRequest = CreateTransactionRequest.builder()
|
||||
.payload(CreateTransaction.builder()
|
||||
.amount(amount)
|
||||
.correlationId(correlationId)
|
||||
.fromAccountId(fromAccount)
|
||||
.toAccountId(toAccount)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return webTestClient.post().uri("/transactions")
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(BodyInserters.fromObject(createTransactionRequest))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(CreateTransactionResponse.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private AccountState getAccount(String id) {
|
||||
return webTestClient.get().uri("/accounts/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(AccountState.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private String getAccountString(String id) {
|
||||
return webTestClient.get().uri("/accounts/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(String.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private TransactionState getTransaction(String id) {
|
||||
return webTestClient.get().uri("/transactions/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(TransactionState.class).returnResult().getResponseBody();
|
||||
}
|
||||
}
|
||||
1
bank-account/transaction-adapters/build.gradle
Normal file
1
bank-account/transaction-adapters/build.gradle
Normal file
@@ -0,0 +1 @@
|
||||
description('transactions-adapters')
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.adapters.account;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionApplicationService;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CancelTransaction;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.FinishTransaction;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class AccountChangeStreamAdapter {
|
||||
|
||||
private static final Log log = LogFactory.getLog(AccountChangeStreamAdapter.class);
|
||||
|
||||
private final ApplicationMessageBus messageBus;
|
||||
|
||||
private final TransactionApplicationService transactionApplicationService;
|
||||
|
||||
private final TransactionQuery query;
|
||||
|
||||
public AccountChangeStreamAdapter(
|
||||
ApplicationMessageBus messageBus,
|
||||
TransactionApplicationService transactionApplicationService,
|
||||
TransactionQuery query
|
||||
) {
|
||||
this.messageBus = Objects.requireNonNull(messageBus);
|
||||
this.transactionApplicationService = Objects.requireNonNull(transactionApplicationService);
|
||||
this.query = Objects.requireNonNull(query);
|
||||
handleTransferMoneyDeposited(messageBus);
|
||||
handleTransferMoneyFailed(messageBus);
|
||||
}
|
||||
|
||||
private void handleTransferMoneyDeposited(ApplicationMessageBus bus) {
|
||||
log.debug("handleTransferMoneyDeposited ->");
|
||||
bus.messagesStream()
|
||||
.filter(m -> m instanceof TransferMoneyDeposited)
|
||||
.cast(TransferMoneyDeposited.class)
|
||||
.flatMap(e -> query.findById(e.transactionId()))
|
||||
.flatMap(s -> transactionApplicationService.execute(FinishTransaction.builder()
|
||||
.aggregateId(s.aggregateId())
|
||||
.fromAccountId(s.fromAccountId())
|
||||
.toAccountId(s.toAccountId())
|
||||
.build()))
|
||||
.doOnError(error -> log.error("handleMoneyDeposited -> ", error))
|
||||
.log()
|
||||
.retry()
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private void handleTransferMoneyFailed(ApplicationMessageBus bus) {
|
||||
log.debug("handleTransferMoneyFailed ->");
|
||||
bus.messagesStream()
|
||||
.filter(m -> m instanceof TransferMoneyFailed)
|
||||
.cast(TransferMoneyFailed.class)
|
||||
.flatMap(e -> query.findById(e.transactionId()))
|
||||
.flatMap(s -> transactionApplicationService.execute(CancelTransaction.builder()
|
||||
.aggregateId(s.aggregateId())
|
||||
.fromAccountId(s.fromAccountId())
|
||||
.toAccountId(s.toAccountId())
|
||||
.build(), TransactionFailed.class))
|
||||
.doOnError(error -> log.error("handleTransferMoneyFailed -> ", error))
|
||||
.log()
|
||||
.retry()
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
1
bank-account/transaction-api/build.gradle
Normal file
1
bank-account/transaction-api/build.gradle
Normal file
@@ -0,0 +1 @@
|
||||
description('transaction-api module')
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.api;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
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.command.TransactionCommand;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface TransactionApplicationService {
|
||||
|
||||
Mono<TransactionCreated> execute(CreateTransaction createTransaction);
|
||||
|
||||
Mono<TransactionFinished> execute(FinishTransaction cmd);
|
||||
|
||||
<R extends DomainEvent> Mono<R> execute(TransactionCommand cmd, Class<R> eventType);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.api;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.http.HttpHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionRequest;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionResponse;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
|
||||
|
||||
@Component
|
||||
public class TransactionHandler implements HttpHandler {
|
||||
|
||||
private final TransactionApplicationService service;
|
||||
|
||||
private final TransactionQuery query;
|
||||
|
||||
public TransactionHandler(TransactionApplicationService transactionApplicationService, TransactionQuery query) {
|
||||
this.service = Objects.requireNonNull(transactionApplicationService);
|
||||
this.query = Objects.requireNonNull(query);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getAll(ServerRequest request) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(query.getAll(), TransactionState.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getById(ServerRequest request) {
|
||||
return query.findById(request.pathVariable("id"))
|
||||
.flatMap(this::mapToResponse);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createTransaction(ServerRequest request) {
|
||||
return request
|
||||
.bodyToMono(CreateTransactionRequest.class)
|
||||
.map(CreateTransactionRequest::payload)
|
||||
.flatMap(service::execute)
|
||||
.map(CreateTransactionResponse::from)
|
||||
.flatMap(this::mapToResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> route() {
|
||||
var route = RouterFunctions
|
||||
.route(POST("").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::createTransaction)
|
||||
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getAll)
|
||||
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getById);
|
||||
|
||||
return RouterFunctions.route()
|
||||
.nest(path("/transactions"), () -> route)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.api;
|
||||
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface TransactionQuery {
|
||||
Mono<TransactionState> findById(String id);
|
||||
|
||||
Flux<TransactionState> getAll();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.api.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableCreateTransactionRequest.class)
|
||||
@JsonDeserialize(as = ImmutableCreateTransactionRequest.class)
|
||||
public interface CreateTransactionRequest {
|
||||
|
||||
CreateTransaction payload();
|
||||
|
||||
static ImmutableCreateTransactionRequest.Builder builder() {
|
||||
return ImmutableCreateTransactionRequest.builder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.api.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableCreateTransactionResponse.class)
|
||||
@JsonDeserialize(as = ImmutableCreateTransactionResponse.class)
|
||||
public interface CreateTransactionResponse {
|
||||
|
||||
TransactionCreated payload();
|
||||
|
||||
static CreateTransactionResponse from(TransactionCreated transactionCreated) {
|
||||
return builder()
|
||||
.payload(transactionCreated)
|
||||
.build();
|
||||
}
|
||||
|
||||
static ImmutableCreateTransactionResponse.Builder builder() {
|
||||
return ImmutableCreateTransactionResponse.builder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.common.api.view.DomainView;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableTransactionState.class)
|
||||
@JsonDeserialize(as = ImmutableTransactionState.class)
|
||||
public interface TransactionState extends DomainView {
|
||||
|
||||
default String id() {
|
||||
return aggregateId();
|
||||
}
|
||||
|
||||
String aggregateId();
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
String toAccountId();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
TransactionStatus status();
|
||||
|
||||
static ImmutableTransactionState.Builder builder() {
|
||||
return ImmutableTransactionState.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.util.concurrent.TransferQueue;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableCancelTransaction.class)
|
||||
@JsonDeserialize(as = ImmutableCancelTransaction.class)
|
||||
public interface CancelTransaction extends TransactionCommand {
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
String toAccountId();
|
||||
|
||||
static ImmutableCancelTransaction.Builder builder() {
|
||||
return ImmutableCancelTransaction.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableCreateTransaction.class)
|
||||
@JsonDeserialize(as = ImmutableCreateTransaction.class)
|
||||
public interface CreateTransaction extends TransactionCommand {
|
||||
|
||||
@Value.Default
|
||||
default String aggregateId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
String toAccountId();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableFinishTransaction.class)
|
||||
@JsonDeserialize(as = ImmutableFinishTransaction.class)
|
||||
public interface FinishTransaction extends TransactionCommand {
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableTransactionCreated.class)
|
||||
@JsonDeserialize(as = ImmutableTransactionCreated.class)
|
||||
public interface TransactionCreated extends TransactionEvent {
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CancelTransaction;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
@JsonSerialize(as = ImmutableTransactionFailed.class)
|
||||
@JsonDeserialize(as = ImmutableTransactionFailed.class)
|
||||
public interface TransactionFailed extends TransactionEvent {
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CancelTransaction;
|
||||
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;
|
||||
@@ -8,6 +9,7 @@ import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionAggregate {
|
||||
|
||||
@@ -59,6 +61,20 @@ public class TransactionAggregate {
|
||||
}
|
||||
}
|
||||
|
||||
public TransactionFailed validateCancelTransaction(CancelTransaction command) {
|
||||
if (status == TransactionStatus.CREATED) {
|
||||
return TransactionFailed.builder()
|
||||
.aggregateId(aggregateId.getValue())
|
||||
.correlationId(command.correlationId())
|
||||
.fromAccountId(fromAccount.getValue())
|
||||
.toAccountId(toAccount.getValue())
|
||||
.amount(this.amount)
|
||||
.build();
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Transaction in the state: %s can't be canceled!", status));
|
||||
}
|
||||
}
|
||||
|
||||
public TransactionAggregate applyTransactionCreated(TransactionCreated created) {
|
||||
this.fromAccount = new Id(created.fromAccountId());
|
||||
this.toAccount = new Id(created.toAccountId());
|
||||
@@ -77,7 +93,7 @@ public class TransactionAggregate {
|
||||
return this;
|
||||
}
|
||||
|
||||
public TransactionState getStatus() {
|
||||
public TransactionState getState() {
|
||||
return TransactionState.builder()
|
||||
.amount(amount)
|
||||
.fromAccountId(fromAccount.getValue())
|
||||
|
||||
@@ -5,6 +5,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.command.CancelTransaction;
|
||||
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.command.TransactionCommand;
|
||||
@@ -21,13 +22,54 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
|
||||
public TransactionCommandHandler() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) {
|
||||
if (command instanceof CreateTransaction) {
|
||||
return doCreateTransaction(aggregate, (CreateTransaction) command);
|
||||
} else if (command instanceof FinishTransaction) {
|
||||
return doFinishTransaction(aggregate, (FinishTransaction) command);
|
||||
} else if (command instanceof CancelTransaction) {
|
||||
return doCancelTransaction(aggregate, (CancelTransaction) command);
|
||||
} else {
|
||||
return CommandResult.badCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResult doCancelTransaction(TransactionAggregate aggregate, CancelTransaction command) {
|
||||
try {
|
||||
return Optional.of(command)
|
||||
.map(aggregate::validateCancelTransaction)
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.events(List.of(e))
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.build())
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.events(List.of(TransactionFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.correlationId(command.correlationId())
|
||||
.fromAccountId(command.fromAccountId())
|
||||
.toAccountId(command.toAccountId())
|
||||
.amount(aggregate.getState().amount())
|
||||
.build()))
|
||||
.statusCode(CommandResult.StatusCode.FAILED)
|
||||
.error(e)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResult doCreateTransaction(TransactionAggregate aggregate, CreateTransaction command) {
|
||||
try {
|
||||
return Optional.of(command)
|
||||
.map(aggregate::validateCreateTransaction)
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.addEvents(e)
|
||||
.events(List.of(e))
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.build())
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
@@ -47,7 +89,13 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
|
||||
try {
|
||||
return Optional.of(command)
|
||||
.map(aggregate::validateFinishTransaction)
|
||||
.map(e -> CommandResult.builder().build())
|
||||
.map(e -> CommandResult.builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.events(List.of(e))
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.build())
|
||||
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
|
||||
} catch (RuntimeException e) {
|
||||
return CommandResult.builder()
|
||||
@@ -56,25 +104,13 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.statusCode(CommandResult.StatusCode.FAILED)
|
||||
.error(e)
|
||||
.addEvents(FinishTransactionFailed.builder()
|
||||
.events(List.of(FinishTransactionFailed.builder()
|
||||
.aggregateId(command.aggregateId())
|
||||
.correlationId(command.correlationId())
|
||||
.fromAccountId(command.fromAccountId())
|
||||
.toAccountId(command.toAccountId())
|
||||
.build())
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) {
|
||||
if (command instanceof CreateTransaction) {
|
||||
return doCreateTransaction(aggregate, (CreateTransaction) command);
|
||||
}
|
||||
if (command instanceof FinishTransaction) {
|
||||
return doFinishTransaction(aggregate, (FinishTransaction) command);
|
||||
}else {
|
||||
return CommandResult.badCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.event.EventApplier;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
@@ -7,11 +8,11 @@ import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionEvent;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
|
||||
|
||||
public class TransactionEventApplier implements EventApplier<TransactionAggregate, TransactionEvent> {
|
||||
public class TransactionEventApplier implements EventApplier<TransactionAggregate> {
|
||||
|
||||
|
||||
@Override
|
||||
public TransactionAggregate apply(TransactionAggregate aggregate, TransactionEvent event) {
|
||||
public <E extends DomainEvent> TransactionAggregate apply(TransactionAggregate aggregate, E event) {
|
||||
if (event instanceof TransactionCreated) {
|
||||
return applyTransactionCreated(aggregate, (TransactionCreated) event);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.domain;
|
||||
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Value.Immutable
|
||||
public interface TransactionState {
|
||||
|
||||
String aggregateId();
|
||||
|
||||
String fromAccountId();
|
||||
|
||||
String toAccountId();
|
||||
|
||||
BigDecimal amount();
|
||||
|
||||
TransactionStatus status();
|
||||
|
||||
static ImmutableTransactionState.Builder builder() {
|
||||
return ImmutableTransactionState.builder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class TransactionAggregateTest {
|
||||
.build();
|
||||
|
||||
var aggregate = new TransactionAggregate(aggregateId);
|
||||
var state = aggregate.applyTransactionCreated(event).getStatus();
|
||||
var state = aggregate.applyTransactionCreated(event).getState();
|
||||
|
||||
assertEquals(state.aggregateId(), aggregateId);
|
||||
assertEquals(state.toAccountId(), toAccountId);
|
||||
@@ -149,7 +149,7 @@ class TransactionAggregateTest {
|
||||
.fromAccountId(fromAccountId)
|
||||
.aggregateId(aggregateId)
|
||||
.build();
|
||||
var state = aggregate.applyTransactionFinished(transactionFinished).getStatus();
|
||||
var state = aggregate.applyTransactionFinished(transactionFinished).getState();
|
||||
|
||||
//then
|
||||
assertEquals(state.status(), TransactionStatus.FINISHED);
|
||||
@@ -181,7 +181,7 @@ class TransactionAggregateTest {
|
||||
.aggregateId(aggregateId)
|
||||
.amount(BigDecimal.TEN)
|
||||
.build();
|
||||
var state = aggregate.applyTransactionFailed(transactionFailed).getStatus();
|
||||
var state = aggregate.applyTransactionFailed(transactionFailed).getState();
|
||||
|
||||
//then
|
||||
assertEquals(state.status(), TransactionStatus.FAILED);
|
||||
|
||||
@@ -30,7 +30,7 @@ class TransactionEventApplierTest {
|
||||
var aggregate = new TransactionAggregate(aggregateId);
|
||||
|
||||
//when
|
||||
var status = subject.apply(aggregate, transactionCreated).getStatus();
|
||||
var status = subject.apply(aggregate, transactionCreated).getState();
|
||||
|
||||
//then
|
||||
Assertions.assertEquals(status.amount(), BigDecimal.TEN);
|
||||
@@ -61,7 +61,7 @@ class TransactionEventApplierTest {
|
||||
subject.apply(aggregate, transactionCreated);
|
||||
|
||||
//when
|
||||
var status = subject.apply(aggregate, transactionFinished).getStatus();
|
||||
var status = subject.apply(aggregate, transactionFinished).getState();
|
||||
|
||||
//then
|
||||
Assertions.assertEquals(status.amount(), BigDecimal.TEN);
|
||||
@@ -93,7 +93,7 @@ class TransactionEventApplierTest {
|
||||
subject.apply(aggregate, transactionCreated);
|
||||
|
||||
//when
|
||||
var status = subject.apply(aggregate, transactionFailed).getStatus();
|
||||
var status = subject.apply(aggregate, transactionFailed).getState();
|
||||
|
||||
//then
|
||||
Assertions.assertEquals(status.amount(), BigDecimal.TEN);
|
||||
|
||||
1
bank-account/transaction-impl/build.gradle
Normal file
1
bank-account/transaction-impl/build.gradle
Normal file
@@ -0,0 +1 @@
|
||||
description('transaction-impl module')
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionApplicationService;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
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.command.TransactionCommand;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.mz.reactor.ddd.reactorddd.transaction.wiring.TransactionConfiguration.TRANSACTION_AGGREGATE_FACADE;
|
||||
|
||||
@Service
|
||||
public class TransactionApplicationServiceImpl implements TransactionApplicationService {
|
||||
|
||||
private static final Log log = LogFactory.getLog(TransactionApplicationServiceImpl.class);
|
||||
|
||||
private final AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> aggregateFacade;
|
||||
|
||||
public TransactionApplicationServiceImpl(
|
||||
@Qualifier(TRANSACTION_AGGREGATE_FACADE) AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> aggregateFacade,
|
||||
ApplicationMessageBus bus,
|
||||
TransactionQuery query
|
||||
) {
|
||||
this.aggregateFacade = Objects.requireNonNull(aggregateFacade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TransactionCreated> execute(CreateTransaction createTransaction) {
|
||||
return aggregateFacade.executeReturnEvent(createTransaction, UUID.randomUUID().toString(), TransactionCreated.class)
|
||||
.cast(TransactionCreated.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TransactionFinished> execute(FinishTransaction cmd) {
|
||||
return aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), TransactionFinished.class)
|
||||
.cast(TransactionFinished.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R extends DomainEvent> Mono<R> execute(TransactionCommand cmd, Class<R> eventType) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), eventType)
|
||||
.cast(eventType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.mz.reactor.ddd.reactorddd.transaction.wiring.TransactionConfiguration.TRANSACTION_VIEW_REPOSITORY;
|
||||
|
||||
@Service
|
||||
public class TransactionQueryImpl implements TransactionQuery {
|
||||
|
||||
private final ViewRepository<TransactionState> repository;
|
||||
|
||||
public TransactionQueryImpl(
|
||||
@Qualifier(TRANSACTION_VIEW_REPOSITORY) ViewRepository<TransactionState> repository,
|
||||
ApplicationMessageBus bus
|
||||
) {
|
||||
this.repository = Objects.requireNonNull(repository);
|
||||
Objects.requireNonNull(bus).messagesStream()
|
||||
.filter(m -> m instanceof TransactionState)
|
||||
.cast(TransactionState.class)
|
||||
.subscribe(repository::addView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TransactionState> findById(@NonNull String id) {
|
||||
return repository.findBy(v -> v.id().equals(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TransactionState> getAll() {
|
||||
return repository.findAllBy(v -> true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.mz.reactor.ddd.reactorddd.transaction.wiring;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateFacadeImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateRepositoryImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.impl.ViewRepositoryImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionCommandHandler;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionEventApplier;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.TransactionCommand;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@Configuration
|
||||
public class TransactionConfiguration {
|
||||
|
||||
public static final String TRANSACTION_AGGREGATE_REPOSITORY = "transactionAggregateRepository";
|
||||
public static final String TRANSACTION_AGGREGATE_FACADE = "transactionAggregateFacade";
|
||||
public static final String TRANSACTION_VIEW_REPOSITORY = "transactionViewRepository";
|
||||
|
||||
private final TransactionEventApplier transactionEventApplier = new TransactionEventApplier();
|
||||
private final TransactionCommandHandler transactionCommandHandler = new TransactionCommandHandler();
|
||||
private final Function<Id, TransactionAggregate> aggregateFactory = id -> new TransactionAggregate(id.getValue());
|
||||
private final Function<TransactionAggregate, TransactionState> stateFactory = TransactionAggregate::getState;
|
||||
|
||||
@Bean(TRANSACTION_AGGREGATE_REPOSITORY)
|
||||
public AggregateRepository<TransactionAggregate, TransactionCommand, TransactionState> getAggregateRepository() {
|
||||
return new AggregateRepositoryImpl<>(transactionCommandHandler, transactionEventApplier, aggregateFactory, stateFactory);
|
||||
}
|
||||
|
||||
@Bean(TRANSACTION_AGGREGATE_FACADE)
|
||||
public AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> getAggregateFacade(
|
||||
@Qualifier(TRANSACTION_AGGREGATE_REPOSITORY) AggregateRepository<TransactionAggregate, TransactionCommand, TransactionState> aggregateRepository,
|
||||
@Qualifier(TRANSACTION_VIEW_REPOSITORY) ViewRepository<TransactionState> viewRepository,
|
||||
ApplicationMessageBus bus
|
||||
) {
|
||||
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, bus::publishMessage);
|
||||
}
|
||||
|
||||
@Bean(TRANSACTION_VIEW_REPOSITORY)
|
||||
public ViewRepository<TransactionState> getTransactionViewRepository() {
|
||||
return new ViewRepositoryImpl<>();
|
||||
}
|
||||
}
|
||||
75
build.gradle
75
build.gradle
@@ -16,7 +16,6 @@ buildscript {
|
||||
}
|
||||
|
||||
plugins {
|
||||
// id 'org.springframework.boot' version "$springBootVersion"
|
||||
id 'io.spring.dependency-management' version "$springDependencyMavagementVersion"
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
@@ -48,7 +47,6 @@ subprojects {
|
||||
sourceCompatibility = '11'
|
||||
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/com.google.guava/guava
|
||||
implementation group: 'com.google.guava', name: 'guava', version: '28.1-jre'
|
||||
annotationProcessor 'org.immutables:value:2.7.5'
|
||||
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine'
|
||||
@@ -73,10 +71,6 @@ project(':bank-account:bank-account-application') {
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
// springBoot {
|
||||
// mainClassName = 'com.mz.reactor.ddd.reactorddd.application.BankAccountApp'
|
||||
// }
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation project(':common-api')
|
||||
@@ -85,7 +79,11 @@ project(':bank-account:bank-account-application') {
|
||||
implementation project(':bank-account:account-impl')
|
||||
implementation project(':bank-account:account-api')
|
||||
implementation project(':bank-account:account-domain')
|
||||
implementation project(':bank-account:account-adapters')
|
||||
implementation project(':bank-account:transaction-impl')
|
||||
implementation project(':bank-account:transaction-api')
|
||||
implementation project(':bank-account:transaction-domain')
|
||||
implementation project(':bank-account:transaction-adapters')
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
@@ -108,20 +106,55 @@ project(':bank-account:account-api') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':common-components')
|
||||
api project(':shared-spring-dependecies')
|
||||
api project(':shared-spring-dependencies')
|
||||
api project(':bank-account:account-domain-api')
|
||||
}
|
||||
}
|
||||
|
||||
project(':bank-account:transaction-api') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':common-components')
|
||||
api project(':shared-spring-dependencies')
|
||||
api project(':bank-account:transaction-domain-api')
|
||||
}
|
||||
}
|
||||
|
||||
project(':bank-account:account-impl') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':common-components')
|
||||
api project(':common-persistence')
|
||||
api project(':shared-spring-dependecies')
|
||||
implementation project(':bank-account:account-domain-api')
|
||||
implementation project(':bank-account:account-api')
|
||||
implementation project(':bank-account:account-domain')
|
||||
api project(':shared-spring-dependencies')
|
||||
api project(':shared-spring-dependencies')
|
||||
api project(':bank-account:transaction-domain-api')
|
||||
api project(':bank-account:account-domain-api')
|
||||
api project(':bank-account:account-api')
|
||||
api project(':bank-account:account-domain')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
project(':bank-account:transaction-adapters') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':bank-account:transaction-domain-api')
|
||||
api project(':bank-account:transaction-api')
|
||||
api project(':bank-account:account-domain-api')
|
||||
api project(':shared-spring-dependencies')
|
||||
}
|
||||
}
|
||||
|
||||
project(':bank-account:transaction-impl') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':common-components')
|
||||
api project(':common-persistence')
|
||||
api project(':shared-spring-dependencies')
|
||||
api project(':bank-account:transaction-domain-api')
|
||||
api project(':bank-account:account-domain-api')
|
||||
api project(':bank-account:transaction-api')
|
||||
api project(':bank-account:transaction-domain')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +165,16 @@ project(':bank-account:transaction-domain') {
|
||||
}
|
||||
}
|
||||
|
||||
project(':bank-account:account-adapters') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':bank-account:transaction-domain-api')
|
||||
api project(':bank-account:account-api')
|
||||
api project(':bank-account:account-domain-api')
|
||||
api project(':shared-spring-dependencies')
|
||||
}
|
||||
}
|
||||
|
||||
project(':bank-account:transaction-domain-api') {
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
@@ -141,12 +184,13 @@ project(':bank-account:transaction-domain-api') {
|
||||
project(':shared-dependencies') {
|
||||
dependencies {
|
||||
api 'org.immutables:value:2.7.5'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.10.1'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.1'
|
||||
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
|
||||
}
|
||||
}
|
||||
|
||||
project(':shared-spring-dependecies') {
|
||||
project(':shared-spring-dependencies') {
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
dependencies {
|
||||
@@ -161,7 +205,8 @@ project(':common-persistence') {
|
||||
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
implementation group: 'io.projectreactor', name: 'reactor-core'
|
||||
// implementation group: 'io.projectreactor', name: 'reactor-core'
|
||||
api project(':shared-spring-dependencies')
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
}
|
||||
@@ -171,7 +216,7 @@ project(':common-components') {
|
||||
|
||||
dependencies {
|
||||
api project(':common-api')
|
||||
api project(':shared-spring-dependecies')
|
||||
api project(':shared-spring-dependencies')
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.mz.reactor.ddd.common.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.immutables.value.Value;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -7,6 +8,7 @@ import java.util.Optional;
|
||||
|
||||
public interface Message {
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
Optional<String> correlationId();
|
||||
|
||||
@Value.Default
|
||||
|
||||
@@ -5,6 +5,6 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
@FunctionalInterface
|
||||
public interface CommandHandler<A, C extends Command> {
|
||||
|
||||
<E extends DomainEvent> CommandResult<E> execute(A aggregate, C command);
|
||||
CommandResult execute(A aggregate, C command);
|
||||
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Value.Immutable
|
||||
public interface CommandResult<E extends DomainEvent> {
|
||||
public interface CommandResult {
|
||||
|
||||
enum StatusCode {
|
||||
OK,
|
||||
BAD_COMMAND,
|
||||
FAILED,
|
||||
NOT_MODIFIED;
|
||||
NOT_MODIFIED
|
||||
}
|
||||
|
||||
String commandId();
|
||||
|
||||
StatusCode statusCode();
|
||||
|
||||
List<E> events();
|
||||
List<? extends DomainEvent> events();
|
||||
|
||||
Optional<RuntimeException> error();
|
||||
|
||||
@@ -29,23 +29,24 @@ public interface CommandResult<E extends DomainEvent> {
|
||||
return ImmutableCommandResult.builder();
|
||||
}
|
||||
|
||||
static <D extends DomainEvent> CommandResult<D> fromError(RuntimeException error, D event, Command command) {
|
||||
static CommandResult fromError(RuntimeException error, List<? extends DomainEvent> events, Command command) {
|
||||
return builder()
|
||||
.commandId(Optional.ofNullable(command)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.statusCode(StatusCode.BAD_COMMAND)
|
||||
.events(Optional.ofNullable(event).map(List::of).orElseGet(List::of))
|
||||
.statusCode(StatusCode.FAILED)
|
||||
.events(events)
|
||||
.error(error)
|
||||
.build();
|
||||
}
|
||||
|
||||
static <D extends DomainEvent> CommandResult<D> badCommand(Command cmd) {
|
||||
static CommandResult badCommand(Command cmd) {
|
||||
return builder()
|
||||
.commandId(Optional.ofNullable(cmd)
|
||||
.map(Command::commandId)
|
||||
.orElseGet(() -> UUID.randomUUID().toString()))
|
||||
.statusCode(StatusCode.BAD_COMMAND)
|
||||
.events(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.mz.reactor.ddd.common.api.event;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface EventApplier<A, E extends DomainEvent> {
|
||||
public interface EventApplier<A> {
|
||||
|
||||
A apply(A aggregate, E event);
|
||||
<E extends DomainEvent> A apply(A aggregate, E event);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ public class Id extends StringValue {
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Id{" +
|
||||
"value='" + value + '\'' +
|
||||
'}';
|
||||
public static Id of(String id) {
|
||||
return new Id(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.mz.reactor.ddd.common.api.view;
|
||||
|
||||
public interface DomainView {
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
|
||||
public interface DomainView extends DomainEvent {
|
||||
@JsonIgnore
|
||||
String id();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.mz.reactor.ddd.common.components.bus.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.Message;
|
||||
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.DirectProcessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
import reactor.core.publisher.ReplayProcessor;
|
||||
@@ -14,17 +17,21 @@ import java.util.Optional;
|
||||
@Service
|
||||
public class ApplicationMessageBusImpl implements ApplicationMessageBus {
|
||||
|
||||
private final ReplayProcessor<Message> messages = ReplayProcessor.create(1);
|
||||
private static final Log log = LogFactory.getLog(ApplicationMessageBusImpl.class);
|
||||
|
||||
private final DirectProcessor<Message> messages = DirectProcessor.create();
|
||||
|
||||
private final FluxSink<Message> messagesSink = messages.sink();
|
||||
|
||||
@Override
|
||||
public <M extends Message> void publishMessage(M message) {
|
||||
log.info(String.format("publishMessage -> messageBusId: %s, message: %s",this.hashCode(), message));
|
||||
Optional.ofNullable(message).ifPresent(messagesSink::next);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Message> messagesStream() {
|
||||
log.info("messagesStream -> " + this.hashCode());
|
||||
return messages.publishOn(Schedulers.parallel());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,15 @@ import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
|
||||
|
||||
public enum HttpErrorHandler {
|
||||
FN;
|
||||
|
||||
public <E extends Throwable> Mono<ServerResponse> onError(E e, ServerRequest req) {
|
||||
public <E extends Throwable> Mono<ServerResponse> onError(E e, ServerRequest req, Consumer<E> logger) {
|
||||
logger.accept(e);
|
||||
return Mono.just(ErrorMessage.builder().error(e.getMessage()).build())
|
||||
.flatMap(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mz.reactor.ddd.common.components.http;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -8,9 +9,12 @@ import static org.springframework.web.reactive.function.BodyInserters.fromObject
|
||||
|
||||
public interface HttpHandler {
|
||||
|
||||
RouterFunction<ServerResponse> route();
|
||||
|
||||
default <T> Mono<ServerResponse> mapToResponse(T result) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8).body(fromObject(result));
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(fromObject(result));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface AggregateActor<A, C extends Command, E extends DomainEvent> {
|
||||
public interface AggregateActor<A, C extends Command> {
|
||||
|
||||
<S> Mono<S> getState(Function<A, S> stateFactory);
|
||||
|
||||
void onDestroy();
|
||||
|
||||
Mono<CommandResult<E>> execute(C cmd);
|
||||
Mono<CommandResult> execute(C cmd);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import com.mz.reactor.ddd.common.api.command.Command;
|
||||
import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AggregateFacade<A, C extends Command, E extends DomainEvent,S> {
|
||||
public interface AggregateFacade<A, C extends Command,S> {
|
||||
|
||||
Mono<S> execute(C command, String aggregateID);
|
||||
|
||||
Mono<E> executeReturnEvent(C command, String aggregateID);
|
||||
Mono<? extends DomainEvent> executeReturnEvent(C command, String aggregateID, Class<? extends DomainEvent> event);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AggregateRepository<A, C extends Command, E extends DomainEvent,S> {
|
||||
public interface AggregateRepository<A, C extends Command,S> {
|
||||
|
||||
Mono<CommandResult<E>> execute(C cmd, Id aggregateId);
|
||||
Mono<CommandResult> execute(C cmd, Id aggregateId);
|
||||
|
||||
Mono<S> findById(Id id);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.event.EventApplier;
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateActor;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.ReplayProcessor;
|
||||
@@ -19,15 +21,17 @@ import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> implements AggregateActor<A, C, E> {
|
||||
public class AggregateActorImpl<A, C extends Command> implements AggregateActor<A, C> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(AggregateActorImpl.class);
|
||||
|
||||
private final Id id;
|
||||
|
||||
private final CommandHandler<A, C> commandHandler;
|
||||
|
||||
private final EventApplier<A, E> eventApplier;
|
||||
private final EventApplier<A> eventApplier;
|
||||
|
||||
private final BiFunction<Id, List<E>, List<E>> persistAll;
|
||||
private final BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll;
|
||||
|
||||
private A aggregate;
|
||||
|
||||
@@ -35,17 +39,17 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
|
||||
|
||||
private final FluxSink<C> commandSink = commandProcessor.sink();
|
||||
|
||||
private final ReplayProcessor<CommandResult<E>> commandResultReplayProcessor = ReplayProcessor.create();
|
||||
private final ReplayProcessor<CommandResult> commandResultReplayProcessor = ReplayProcessor.create();
|
||||
|
||||
private final FluxSink<CommandResult<E>> commandResultSink = commandResultReplayProcessor.sink();
|
||||
private final FluxSink<CommandResult> commandResultSink = commandResultReplayProcessor.sink();
|
||||
|
||||
public AggregateActorImpl(
|
||||
Id id,
|
||||
CommandHandler<A, C> commandHandler,
|
||||
EventApplier<A, E> eventApplier,
|
||||
EventApplier<A> eventApplier,
|
||||
Function<Id, A> aggregateFactory,
|
||||
List<E> domainEvents,
|
||||
BiFunction<Id, List<E>, List<E>> persistAll
|
||||
List<? extends DomainEvent> domainEvents,
|
||||
BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll
|
||||
) {
|
||||
this.id = id;
|
||||
this.commandHandler = commandHandler;
|
||||
@@ -60,11 +64,12 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
|
||||
commandProcessor
|
||||
.publishOn(Schedulers.newSingle(String.format("AggregateActor: %s", id)))
|
||||
.log()
|
||||
.doOnError(error -> log.error("handleCommand -> ", error))
|
||||
.subscribe(this::handleCommand);
|
||||
}
|
||||
|
||||
private void handleCommand(C cmd) {
|
||||
var commandResult = commandHandler.<E>execute(this.aggregate, cmd);
|
||||
var commandResult = commandHandler.execute(this.aggregate, cmd);
|
||||
this.aggregate = (A) persistAll
|
||||
.andThen(events -> events.stream()
|
||||
.reduce(this.aggregate, eventApplier::apply, (a1, a2) -> a2))
|
||||
@@ -73,8 +78,8 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CommandResult<E>> execute(C cmd) {
|
||||
Mono<CommandResult<E>> result = commandResultReplayProcessor.publishOn(Schedulers.elastic())
|
||||
public Mono<CommandResult> execute(C cmd) {
|
||||
Mono<CommandResult> result = commandResultReplayProcessor.publishOn(Schedulers.elastic())
|
||||
.filter(r -> r.commandId().equals(cmd.commandId()))
|
||||
.next();
|
||||
commandSink.next(cmd);
|
||||
|
||||
@@ -7,20 +7,25 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
||||
public class AggregateFacadeImpl<A, C extends Command, E extends DomainEvent ,S> implements AggregateFacade<A, C, E, S> {
|
||||
public class AggregateFacadeImpl<A, C extends Command, S> implements AggregateFacade<A, C, S> {
|
||||
|
||||
private final AggregateRepository<A, C, E, S> aggregateRepository;
|
||||
private static final Log log = LogFactory.getLog(AggregateFacadeImpl.class);
|
||||
|
||||
private final AggregateRepository<A, C, S> aggregateRepository;
|
||||
private final Consumer<Message> publishChanged;
|
||||
private final Consumer<S> publishDocument;
|
||||
|
||||
public AggregateFacadeImpl(
|
||||
AggregateRepository<A, C, E, S> aggregateRepository,
|
||||
AggregateRepository<A, C, S> aggregateRepository,
|
||||
Consumer<Message> publishChanged,
|
||||
Consumer<S> publishDocument
|
||||
) {
|
||||
@@ -32,29 +37,60 @@ public class AggregateFacadeImpl<A, C extends Command, E extends DomainEvent ,S>
|
||||
@Override
|
||||
public Mono<S> execute(C command, String aggregateID) {
|
||||
return aggregateRepository.execute(command, new Id(aggregateID))
|
||||
.flatMap(processResult(aggregateID));
|
||||
.flatMap(cr -> processResult(aggregateID, cr))
|
||||
.doOnError(error -> log.error("execute -> ", error));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<E> executeReturnEvent(C command, String aggregateID) {
|
||||
public Mono<? extends DomainEvent> executeReturnEvent(C command, String aggregateID, Class<? extends DomainEvent> eventType) {
|
||||
var result = aggregateRepository.execute(command, new Id(aggregateID));
|
||||
return result.map(r -> r.events().stream().findAny().get());
|
||||
return result.flatMap(cr -> processResult(aggregateID, eventType, cr))
|
||||
.doOnError(error -> log.error("execute -> event type: "+eventType, error));
|
||||
}
|
||||
|
||||
private Function<CommandResult<E>, Mono<S>> processResult(String aggregateId) {
|
||||
return result -> {
|
||||
switch (result.statusCode()) {
|
||||
case OK:
|
||||
return aggregateRepository.findById(new Id(aggregateId))
|
||||
.doOnSuccess(publishDocument)
|
||||
.doOnSuccess(s -> result.events().forEach(publishChanged));
|
||||
case FAILED:
|
||||
return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error")));
|
||||
case NOT_MODIFIED:
|
||||
default:
|
||||
return Mono.empty();
|
||||
}
|
||||
};
|
||||
private Mono<? extends DomainEvent> processResult(String aggregateId, Class<? extends DomainEvent> eventType, CommandResult result) {
|
||||
switch (result.statusCode()) {
|
||||
case OK:
|
||||
return publishChanges(aggregateId, result)
|
||||
.map(state -> result.events().stream()
|
||||
.filter(e -> isInstance(e, eventType))
|
||||
.map(eventType::cast)
|
||||
.findAny())
|
||||
.map(Optional::get);
|
||||
case FAILED:
|
||||
return onFailed(result);
|
||||
case NOT_MODIFIED:
|
||||
default:
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<S> processResult(String aggregateId, CommandResult result) {
|
||||
switch (result.statusCode()) {
|
||||
case OK:
|
||||
return publishChanges(aggregateId, result);
|
||||
case FAILED:
|
||||
return onFailed(result);
|
||||
case NOT_MODIFIED:
|
||||
default:
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> Mono<T> onFailed(CommandResult result) {
|
||||
result.events().forEach(publishChanged);
|
||||
return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error")));
|
||||
}
|
||||
|
||||
private Mono<S> publishChanges(String aggregateId, CommandResult result) {
|
||||
return aggregateRepository.findById(new Id(aggregateId))
|
||||
.doOnSuccess(publishDocument)
|
||||
.doOnSuccess(s -> result.events().forEach(publishChanged));
|
||||
}
|
||||
|
||||
protected <E> boolean isInstance(Object obj, Class<E> type) {
|
||||
return Optional.ofNullable(type)
|
||||
.flatMap(t -> Optional.ofNullable(obj).map(t::isInstance)).orElse(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,19 +21,19 @@ import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent,S> implements AggregateRepository<A, C, E, S> {
|
||||
public class AggregateRepositoryImpl<A, C extends Command,S> implements AggregateRepository<A, C, S> {
|
||||
|
||||
private final AtomicReference<Map<Id, List<E>>> eventSource = new AtomicReference<>(new HashMap<>());
|
||||
private final AtomicReference<Map<Id, List<? extends DomainEvent>>> eventSource = new AtomicReference<>(new HashMap<>());
|
||||
|
||||
private final Cache<Id, AggregateActor<A, C, E>> cache = CacheBuilder.newBuilder()
|
||||
private final Cache<Id, AggregateActor<A, C>> cache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(Duration.ofMinutes(10))
|
||||
.removalListener((RemovalListener<Id, AggregateActor<A, C, E>>) notification -> notification.getValue().onDestroy())
|
||||
.removalListener((RemovalListener<Id, AggregateActor<A, C>>) notification -> notification.getValue().onDestroy())
|
||||
.build();
|
||||
|
||||
|
||||
private final CommandHandler<A, C> commandHandler;
|
||||
|
||||
private final EventApplier<A, E> eventApplier;
|
||||
private final EventApplier<A> eventApplier;
|
||||
|
||||
private final Function<Id, A> aggregateFactory;
|
||||
|
||||
@@ -41,7 +41,7 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
|
||||
|
||||
public AggregateRepositoryImpl(
|
||||
CommandHandler<A, C> commandHandler,
|
||||
EventApplier<A, E> eventApplier,
|
||||
EventApplier<A> eventApplier,
|
||||
Function<Id, A> aggregateFactory,
|
||||
Function<A, S> stateFactory
|
||||
) {
|
||||
@@ -51,28 +51,28 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
|
||||
this.stateFactory = stateFactory;
|
||||
}
|
||||
|
||||
private List<E> persistAll(Id id, List<E> events) {
|
||||
private List<? extends DomainEvent> persistAll(Id id, List<? extends DomainEvent> events) {
|
||||
eventSource.updateAndGet(esMap -> {
|
||||
var eventsToStore = Optional.ofNullable(esMap.get(id))
|
||||
.map(es -> Stream.concat(es.stream(), events.stream())
|
||||
.sorted(Comparator.comparing(DomainEvent::createdAt))
|
||||
.collect(toList()))
|
||||
.orElse(events);
|
||||
.orElse((List<DomainEvent>) events);
|
||||
esMap.put(id, eventsToStore);
|
||||
return esMap;
|
||||
});
|
||||
return events;
|
||||
}
|
||||
|
||||
private Mono<AggregateActor<A, C, E>> getAggregate(Id id) {
|
||||
private Mono<AggregateActor<A, C>> getAggregate(Id id) {
|
||||
return Mono.just(id)
|
||||
.map(this::getFromCache);
|
||||
}
|
||||
|
||||
private AggregateActor<A, C, E> getFromCache(Id id) {
|
||||
private AggregateActor<A, C> getFromCache(Id id) {
|
||||
try {
|
||||
return cache.get(id, () ->
|
||||
new AggregateActorImpl<>(
|
||||
new AggregateActorImpl<A, C>(
|
||||
id,
|
||||
commandHandler,
|
||||
eventApplier,
|
||||
@@ -85,7 +85,7 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CommandResult<E>> execute(C cmd, Id aggregateId) {
|
||||
public Mono<CommandResult> execute(C cmd, Id aggregateId) {
|
||||
return getAggregate(aggregateId)
|
||||
.flatMap(a -> a.execute(cmd));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class AggregateActorTest {
|
||||
.withValue(10)
|
||||
.build();
|
||||
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
|
||||
new Id(UUID.randomUUID().toString()),
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
@@ -31,7 +31,7 @@ class AggregateActorTest {
|
||||
TestFunctions.FN.persistAll
|
||||
);
|
||||
|
||||
Mono<CommandResult<TestAggregateEvent>> result = subject.execute(command);
|
||||
Mono<CommandResult> result = subject.execute(command);
|
||||
StepVerifier.create(result)
|
||||
.expectNextMatches(r ->
|
||||
r.commandId().equals(commandId) && r.statusCode().equals(CommandResult.StatusCode.OK))
|
||||
@@ -40,7 +40,7 @@ class AggregateActorTest {
|
||||
|
||||
@Test
|
||||
void onDestroy() {
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
|
||||
new Id(UUID.randomUUID().toString()),
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
@@ -60,7 +60,7 @@ class AggregateActorTest {
|
||||
|
||||
@Test
|
||||
void executeParallel() {
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
|
||||
new Id(UUID.randomUUID().toString()),
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
@@ -84,7 +84,7 @@ class AggregateActorTest {
|
||||
|
||||
@Test
|
||||
public void testRecoveryState() {
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
|
||||
new Id(UUID.randomUUID().toString()),
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
@@ -105,7 +105,7 @@ class AggregateActorTest {
|
||||
|
||||
@Test
|
||||
public void test_RecoveryStateAndExecuteCommand() {
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
|
||||
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
|
||||
new Id(UUID.randomUUID().toString()),
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
|
||||
@@ -18,7 +18,7 @@ class AggregateRepositoryImplTest {
|
||||
|
||||
private final Function<TestAggregate, Tuple2<Id, Long>> getState = a -> Tuples.of(a.getId(), a.getValue());
|
||||
|
||||
AggregateRepositoryImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent,Tuple2<Id, Long>> subject =
|
||||
AggregateRepositoryImpl<TestAggregate, TestAggregateCommand,Tuple2<Id, Long>> subject =
|
||||
new AggregateRepositoryImpl<>(
|
||||
TestFunctions.FN.commandHandler,
|
||||
TestFunctions.FN.eventApplier,
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl;
|
||||
|
||||
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.event.DomainEvent;
|
||||
import com.mz.reactor.ddd.common.api.event.EventApplier;
|
||||
import com.mz.reactor.ddd.common.api.valueobject.Id;
|
||||
|
||||
@@ -14,13 +15,13 @@ public enum TestFunctions {
|
||||
|
||||
public final CommandHandler<TestAggregate, TestAggregateCommand> commandHandler = new CommandHandler<TestAggregate, TestAggregateCommand>() {
|
||||
@Override
|
||||
public CommandResult<TestAggregateEvent> execute(TestAggregate aggregate, TestAggregateCommand command) {
|
||||
public CommandResult execute(TestAggregate aggregate, TestAggregateCommand command) {
|
||||
try {
|
||||
TestAggregateEvent event = aggregate.validate(command);
|
||||
return CommandResult.builder()
|
||||
.commandId(command.commandId())
|
||||
.statusCode(CommandResult.StatusCode.OK)
|
||||
.addEvents(event)
|
||||
.events(List.of(event))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return CommandResult.fromError(
|
||||
@@ -32,11 +33,16 @@ public enum TestFunctions {
|
||||
}
|
||||
};
|
||||
|
||||
public final EventApplier<TestAggregate, TestAggregateEvent> eventApplier = (aggregate, event) -> aggregate.apply((TestAggregateEvent) event);
|
||||
public final EventApplier<TestAggregate> eventApplier = new EventApplier<TestAggregate>() {
|
||||
@Override
|
||||
public <E extends DomainEvent> TestAggregate apply(TestAggregate aggregate, E event) {
|
||||
return aggregate.apply((TestAggregateEvent) event);
|
||||
}
|
||||
};
|
||||
|
||||
public final Function<Id, TestAggregate> aggregateFactory = TestAggregate::new;
|
||||
|
||||
public final BiFunction<Id, List<TestAggregateEvent>, List<TestAggregateEvent>> persistAll = (id, events) -> events;
|
||||
public final BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll = (id, events) -> events;
|
||||
|
||||
public Function<String, String> mapTest1(String val1, String val2) {
|
||||
return v -> val1 + " and " + val2 + " not " + v;
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.mz.reactor.ddd.reactorddd.persistance.model;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.view.DomainView;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TestView implements DomainView {
|
||||
private final String id;
|
||||
|
||||
private final String value;
|
||||
|
||||
private TestView(Builder builder) {
|
||||
@@ -25,6 +27,11 @@ public class TestView implements DomainView {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> correlationId() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ include 'bank-account:bank-account-application'
|
||||
include 'bank-account:account-domain'
|
||||
include 'bank-account:transaction-domain'
|
||||
include 'shared-dependencies'
|
||||
include 'shared-spring-dependecies'
|
||||
include 'shared-spring-dependencies'
|
||||
include 'common-persistence'
|
||||
include 'common-components'
|
||||
include 'bank-account:account-domain-api'
|
||||
include 'bank-account:transaction-domain-api'
|
||||
include 'bank-account:account-api'
|
||||
include 'bank-account:account-impl'
|
||||
include 'bank-account:account-impl'
|
||||
include 'bank-account:transaction-impl'
|
||||
include 'bank-account:transaction-api'
|
||||
include 'bank-account:account-adapters'
|
||||
include 'bank-account:transaction-adapters'
|
||||
|
||||
Reference in New Issue
Block a user