Refactor account service
This commit is contained in:
@@ -51,6 +51,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-integration</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kbastani</groupId>
|
||||
<artifactId>spring-boot-starter-data-events</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
package demo.account;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import demo.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import demo.account.action.ActivateAccount;
|
||||
import demo.account.action.ArchiveAccount;
|
||||
import demo.account.action.ConfirmAccount;
|
||||
import demo.account.action.SuspendAccount;
|
||||
import demo.account.controller.AccountController;
|
||||
import demo.domain.AbstractEntity;
|
||||
import demo.domain.Command;
|
||||
import demo.event.AccountEvent;
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The {@link Account} domain object contains information related to
|
||||
* a user's account. The status of an account is event sourced using
|
||||
* events logged to the {@link AccountEvent} collection attached to
|
||||
* this resource.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
|
||||
@Entity
|
||||
public class Account extends BaseEntity {
|
||||
public class Account extends AbstractEntity<AccountEvent, Long> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@@ -27,9 +26,6 @@ public class Account extends BaseEntity {
|
||||
private String lastName;
|
||||
private String email;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private Set<AccountEvent> events = new HashSet<>();
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
private AccountStatus status;
|
||||
|
||||
@@ -44,12 +40,14 @@ public class Account extends BaseEntity {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Long getAccountId() {
|
||||
return id;
|
||||
@JsonProperty("accountId")
|
||||
@Override
|
||||
public Long getIdentity() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setAccountId(Long id) {
|
||||
@Override
|
||||
public void setIdentity(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@@ -77,15 +75,6 @@ public class Account extends BaseEntity {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<AccountEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public void setEvents(Set<AccountEvent> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public AccountStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
@@ -94,6 +83,49 @@ public class Account extends BaseEntity {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Command(method = "activate", controller = AccountController.class)
|
||||
public Account activate() {
|
||||
getAction(ActivateAccount.class)
|
||||
.getConsumer()
|
||||
.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Command(method = "archive", controller = AccountController.class)
|
||||
public Account archive() {
|
||||
getAction(ArchiveAccount.class)
|
||||
.getConsumer()
|
||||
.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Command(method = "confirm", controller = AccountController.class)
|
||||
public Account confirm() {
|
||||
getAction(ConfirmAccount.class)
|
||||
.getConsumer()
|
||||
.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Command(method = "suspend", controller = AccountController.class)
|
||||
public Account suspend() {
|
||||
getAction(SuspendAccount.class)
|
||||
.getConsumer()
|
||||
.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
|
||||
*/
|
||||
@Override
|
||||
public Link getId() {
|
||||
return linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(getIdentity())
|
||||
.withSelfRel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Account{" +
|
||||
@@ -101,7 +133,6 @@ public class Account extends BaseEntity {
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", events=" + events +
|
||||
", status=" + status +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package demo.account;
|
||||
|
||||
/**
|
||||
* The {@link AccountCommand} represents an action that can be performed to an
|
||||
* {@link Account} aggregate. Commands initiate an action that can mutate the state of
|
||||
* an account entity as it transitions between {@link AccountStatus} values.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
public enum AccountCommand {
|
||||
CONFIRM_ACCOUNT,
|
||||
ACTIVATE_ACCOUNT,
|
||||
SUSPEND_ACCOUNT,
|
||||
ARCHIVE_ACCOUNT
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package demo.account;
|
||||
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
|
||||
/**
|
||||
* A hypermedia resource that describes the collection of commands that
|
||||
* can be applied to a {@link Account} aggregate.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
public class AccountCommandsResource extends ResourceSupport {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package demo.account;
|
||||
|
||||
import demo.domain.Provider;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.EventService;
|
||||
|
||||
@org.springframework.stereotype.Service
|
||||
public class AccountProvider extends Provider<Account> {
|
||||
|
||||
private final AccountService accountService;
|
||||
private final EventService<AccountEvent, Long> eventService;
|
||||
|
||||
public AccountProvider(AccountService accountService, EventService<AccountEvent, Long> eventService) {
|
||||
this.accountService = accountService;
|
||||
this.eventService = eventService;
|
||||
}
|
||||
|
||||
public AccountService getAccountService() {
|
||||
return accountService;
|
||||
}
|
||||
|
||||
public EventService<AccountEvent, Long> getEventService() {
|
||||
return eventService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountService getDefaultService() {
|
||||
return accountService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventService<AccountEvent, Long> getDefaultEventService() {
|
||||
return eventService;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||
|
||||
Account findAccountByEmail(@Param("email") String email);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,27 @@
|
||||
package demo.account;
|
||||
|
||||
import demo.domain.Service;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import demo.event.EventService;
|
||||
import demo.event.ConsistencyModel;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.CachePut;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import static demo.account.AccountStatus.*;
|
||||
|
||||
/**
|
||||
* The {@link AccountService} provides transactional support for managing {@link Account}
|
||||
* entities. This service also provides event sourcing support for {@link AccountEvent}.
|
||||
* Events can be appended to an {@link Account}, which contains a append-only log of
|
||||
* actions that can be used to support remediation for distributed transactions that encountered
|
||||
* a partial failure.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
@Service
|
||||
@CacheConfig(cacheNames = {"accounts"})
|
||||
public class AccountService {
|
||||
@org.springframework.stereotype.Service
|
||||
public class AccountService extends Service<Account, Long> {
|
||||
|
||||
private final AccountRepository accountRepository;
|
||||
private final EventService eventService;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
public AccountService(AccountRepository accountRepository, EventService eventService, CacheManager cacheManager) {
|
||||
public AccountService(AccountRepository accountRepository) {
|
||||
this.accountRepository = accountRepository;
|
||||
this.eventService = eventService;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()")
|
||||
public Account registerAccount(Account account) {
|
||||
|
||||
account = createAccount(account);
|
||||
|
||||
cacheManager.getCache("accounts")
|
||||
.evict(account.getAccountId());
|
||||
account = create(account);
|
||||
|
||||
// Trigger the account creation event
|
||||
AccountEvent event = appendEvent(account.getAccountId(),
|
||||
new AccountEvent(AccountEventType.ACCOUNT_CREATED));
|
||||
|
||||
// Attach account identifier
|
||||
event.getAccount().setAccountId(account.getAccountId());
|
||||
account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_CREATED, account));
|
||||
|
||||
// Return the result
|
||||
return event.getAccount();
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,8 +30,7 @@ public class AccountService {
|
||||
* @param account is the {@link Account} to create
|
||||
* @return the newly created {@link Account}
|
||||
*/
|
||||
@CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()")
|
||||
public Account createAccount(Account account) {
|
||||
public Account create(Account account) {
|
||||
|
||||
// Assert for uniqueness constraint
|
||||
Assert.isNull(accountRepository.findAccountByEmail(account.getEmail()),
|
||||
@@ -84,34 +48,24 @@ public class AccountService {
|
||||
* @param id is the unique identifier of a {@link Account} entity
|
||||
* @return an {@link Account} entity
|
||||
*/
|
||||
@Cacheable(cacheNames = "accounts", key = "#id.toString()")
|
||||
public Account getAccount(Long id) {
|
||||
public Account get(Long id) {
|
||||
return accountRepository.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an {@link Account} entity with the supplied identifier.
|
||||
*
|
||||
* @param id is the unique identifier of the {@link Account} entity
|
||||
* @param account is the {@link Account} containing updated fields
|
||||
* @return the updated {@link Account} entity
|
||||
*/
|
||||
@CachePut(cacheNames = "accounts", key = "#id.toString()")
|
||||
public Account updateAccount(Long id, Account account) {
|
||||
Assert.notNull(id, "Account id must be present in the resource URL");
|
||||
public Account update(Account account) {
|
||||
Assert.notNull(account.getIdentity(), "Account id must be present in the resource URL");
|
||||
Assert.notNull(account, "Account request body cannot be null");
|
||||
|
||||
if (account.getAccountId() != null) {
|
||||
Assert.isTrue(Objects.equals(id, account.getAccountId()),
|
||||
"The account id in the request body must match the resource URL");
|
||||
} else {
|
||||
account.setAccountId(id);
|
||||
}
|
||||
|
||||
Assert.state(accountRepository.exists(id),
|
||||
Assert.state(accountRepository.exists(account.getIdentity()),
|
||||
"The account with the supplied id does not exist");
|
||||
|
||||
Account currentAccount = accountRepository.findOne(id);
|
||||
Account currentAccount = get(account.getIdentity());
|
||||
currentAccount.setEmail(account.getEmail());
|
||||
currentAccount.setFirstName(account.getFirstName());
|
||||
currentAccount.setLastName(account.getLastName());
|
||||
@@ -125,101 +79,10 @@ public class AccountService {
|
||||
*
|
||||
* @param id is the unique identifier for the {@link Account}
|
||||
*/
|
||||
@CacheEvict(cacheNames = "accounts", key = "#id.toString()")
|
||||
public Boolean deleteAccount(Long id) {
|
||||
public boolean delete(Long id) {
|
||||
Assert.state(accountRepository.exists(id),
|
||||
"The account with the supplied id does not exist");
|
||||
this.accountRepository.delete(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier.
|
||||
*
|
||||
* @param accountId is the unique identifier for the {@link Account}
|
||||
* @param event is the {@link AccountEvent} to append to the {@link Account} entity
|
||||
* @return the newly appended {@link AccountEvent}
|
||||
*/
|
||||
public AccountEvent appendEvent(Long accountId, AccountEvent event) {
|
||||
return appendEvent(accountId, event, ConsistencyModel.ACID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier.
|
||||
*
|
||||
* @param accountId is the unique identifier for the {@link Account}
|
||||
* @param event is the {@link AccountEvent} to append to the {@link Account} entity
|
||||
* @return the newly appended {@link AccountEvent}
|
||||
*/
|
||||
public AccountEvent appendEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) {
|
||||
Account account = getAccount(accountId);
|
||||
Assert.notNull(account, "The account with the supplied id does not exist");
|
||||
event.setAccount(account);
|
||||
event = eventService.createEvent(accountId, event);
|
||||
account.getEvents().add(event);
|
||||
accountRepository.saveAndFlush(account);
|
||||
eventService.raiseEvent(event, consistencyModel);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an {@link AccountCommand} to the {@link Account} with a specified identifier.
|
||||
*
|
||||
* @param id is the unique identifier of the {@link Account}
|
||||
* @param accountCommand is the command to apply to the {@link Account}
|
||||
* @return a hypermedia resource containing the updated {@link Account}
|
||||
*/
|
||||
@CachePut(cacheNames = "accounts", key = "#id.toString()")
|
||||
public Account applyCommand(Long id, AccountCommand accountCommand) {
|
||||
Account account = getAccount(id);
|
||||
|
||||
Assert.notNull(account, "The account for the supplied id could not be found");
|
||||
|
||||
AccountStatus status = account.getStatus();
|
||||
|
||||
switch (accountCommand) {
|
||||
case CONFIRM_ACCOUNT:
|
||||
Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed");
|
||||
|
||||
// Confirm the account
|
||||
Account updateAccount = account;
|
||||
updateAccount.setStatus(ACCOUNT_CONFIRMED);
|
||||
this.updateAccount(id, updateAccount);
|
||||
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED))
|
||||
.getAccount();
|
||||
break;
|
||||
case ACTIVATE_ACCOUNT:
|
||||
Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active");
|
||||
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
|
||||
.contains(status), "The account cannot be activated");
|
||||
|
||||
// Activate the account
|
||||
account.setStatus(ACCOUNT_ACTIVE);
|
||||
this.updateAccount(id, account);
|
||||
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))
|
||||
.getAccount();
|
||||
break;
|
||||
case SUSPEND_ACCOUNT:
|
||||
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
|
||||
|
||||
// Suspend the account
|
||||
account.setStatus(ACCOUNT_SUSPENDED);
|
||||
account = this.updateAccount(id, account);
|
||||
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED));
|
||||
break;
|
||||
case ARCHIVE_ACCOUNT:
|
||||
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
|
||||
|
||||
// Archive the account
|
||||
account.setStatus(ACCOUNT_ARCHIVED);
|
||||
account = this.updateAccount(id, account);
|
||||
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED));
|
||||
break;
|
||||
default:
|
||||
Assert.notNull(accountCommand,
|
||||
"The provided command cannot be applied to this account in its current state");
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package demo.account.action;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountProvider;
|
||||
import demo.account.AccountService;
|
||||
import demo.account.AccountStatus;
|
||||
import demo.domain.Action;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static demo.account.AccountStatus.*;
|
||||
|
||||
/**
|
||||
* Connects an {@link Account} to an Account.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ActivateAccount extends Action<Account> {
|
||||
|
||||
public Consumer<Account> getConsumer() {
|
||||
return (account) -> {
|
||||
Assert.isTrue(account.getStatus() != ACCOUNT_ACTIVE, "The account is already active");
|
||||
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
|
||||
.contains(account.getStatus()), "The account cannot be activated");
|
||||
|
||||
AccountService accountService = account.getProvider(AccountProvider.class)
|
||||
.getDefaultService();
|
||||
|
||||
// Activate the account
|
||||
account.setStatus(AccountStatus.ACCOUNT_ACTIVE);
|
||||
account = accountService.update(account);
|
||||
|
||||
// Trigger the account activated event
|
||||
account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package demo.account.action;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountProvider;
|
||||
import demo.account.AccountService;
|
||||
import demo.account.AccountStatus;
|
||||
import demo.domain.Action;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static demo.account.AccountStatus.ACCOUNT_ACTIVE;
|
||||
|
||||
/**
|
||||
* Connects an {@link Account} to an Account.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ArchiveAccount extends Action<Account> {
|
||||
|
||||
public Consumer<Account> getConsumer() {
|
||||
return (account) -> {
|
||||
Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
|
||||
|
||||
AccountService accountService = account.getProvider(AccountProvider.class)
|
||||
.getDefaultService();
|
||||
|
||||
// Archive the account
|
||||
account.setStatus(AccountStatus.ACCOUNT_ARCHIVED);
|
||||
account = accountService.update(account);
|
||||
|
||||
// Trigger the account archived event
|
||||
account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED, account));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package demo.account.action;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountProvider;
|
||||
import demo.account.AccountService;
|
||||
import demo.account.AccountStatus;
|
||||
import demo.domain.Action;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static demo.account.AccountStatus.ACCOUNT_PENDING;
|
||||
|
||||
/**
|
||||
* Connects an {@link Account} to an Account.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ConfirmAccount extends Action<Account> {
|
||||
|
||||
public Consumer<Account> getConsumer() {
|
||||
return (account) -> {
|
||||
Assert.isTrue(account.getStatus() == ACCOUNT_PENDING, "The account has already been confirmed");
|
||||
|
||||
AccountService accountService = account.getProvider(AccountProvider.class)
|
||||
.getDefaultService();
|
||||
|
||||
// Confirm the account
|
||||
account.setStatus(AccountStatus.ACCOUNT_CONFIRMED);
|
||||
account = accountService.update(account);
|
||||
|
||||
// Trigger the account confirmed
|
||||
account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED, account));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package demo.account.action;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountProvider;
|
||||
import demo.account.AccountService;
|
||||
import demo.account.AccountStatus;
|
||||
import demo.domain.Action;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static demo.account.AccountStatus.ACCOUNT_ACTIVE;
|
||||
|
||||
/**
|
||||
* Connects an {@link Account} to an Account.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class SuspendAccount extends Action<Account> {
|
||||
|
||||
public Consumer<Account> getConsumer() {
|
||||
return (account) -> {
|
||||
Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
|
||||
|
||||
AccountService accountService = account.getProvider(AccountProvider.class)
|
||||
.getDefaultService();
|
||||
|
||||
// Suspend the account
|
||||
account.setStatus(AccountStatus.ACCOUNT_SUSPENDED);
|
||||
account = accountService.update(account);
|
||||
|
||||
// Trigger the account suspended event
|
||||
account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED, account));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
package demo.account;
|
||||
package demo.account.controller;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountService;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEvents;
|
||||
import demo.event.EventController;
|
||||
import demo.event.EventService;
|
||||
import demo.event.Events;
|
||||
import org.springframework.hateoas.LinkBuilder;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
@@ -20,9 +24,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
public class AccountController {
|
||||
|
||||
private final AccountService accountService;
|
||||
private final EventService eventService;
|
||||
private final EventService<AccountEvent, Long> eventService;
|
||||
|
||||
public AccountController(AccountService accountService, EventService eventService) {
|
||||
public AccountController(AccountService accountService, EventService<AccountEvent, Long> eventService) {
|
||||
this.accountService = accountService;
|
||||
this.eventService = eventService;
|
||||
}
|
||||
@@ -41,7 +45,7 @@ public class AccountController {
|
||||
.orElseThrow(() -> new RuntimeException("Account update failed"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}")
|
||||
@RequestMapping(path = "/accounts/{id}")
|
||||
public ResponseEntity getAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(id))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
@@ -50,12 +54,12 @@ public class AccountController {
|
||||
|
||||
@DeleteMapping(path = "/accounts/{id}")
|
||||
public ResponseEntity deleteAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(accountService.deleteAccount(id))
|
||||
return Optional.ofNullable(accountService.delete(id))
|
||||
.map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
|
||||
.orElseThrow(() -> new RuntimeException("Account deletion failed"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/events")
|
||||
@RequestMapping(path = "/accounts/{id}/events")
|
||||
public ResponseEntity getAccountEvents(@PathVariable Long id) {
|
||||
return Optional.of(getAccountEventResources(id))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
@@ -63,47 +67,47 @@ public class AccountController {
|
||||
}
|
||||
|
||||
@PostMapping(path = "/accounts/{id}/events")
|
||||
public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) {
|
||||
public ResponseEntity appendAccountEvent(@PathVariable Long id, @RequestBody AccountEvent event) {
|
||||
return Optional.ofNullable(appendEventResource(id, event))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
||||
.orElseThrow(() -> new RuntimeException("Append account event failed"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/commands")
|
||||
public ResponseEntity getAccountCommands(@PathVariable Long id) {
|
||||
@RequestMapping(path = "/accounts/{id}/commands")
|
||||
public ResponseEntity getCommands(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getCommandsResource(id))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new RuntimeException("The account could not be found"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/commands/confirm")
|
||||
public ResponseEntity confirmAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(
|
||||
accountService.applyCommand(id, AccountCommand.CONFIRM_ACCOUNT)))
|
||||
@RequestMapping(path = "/accounts/{id}/commands/confirm")
|
||||
public ResponseEntity confirm(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(accountService.get(id)
|
||||
.confirm()))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/commands/activate")
|
||||
public ResponseEntity activateAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(
|
||||
accountService.applyCommand(id, AccountCommand.ACTIVATE_ACCOUNT)))
|
||||
@RequestMapping(path = "/accounts/{id}/commands/activate")
|
||||
public ResponseEntity activate(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(accountService.get(id)
|
||||
.activate()))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/commands/suspend")
|
||||
public ResponseEntity suspendAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(
|
||||
accountService.applyCommand(id, AccountCommand.SUSPEND_ACCOUNT)))
|
||||
@RequestMapping(path = "/accounts/{id}/commands/suspend")
|
||||
public ResponseEntity suspend(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(accountService.get(id)
|
||||
.suspend()))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{id}/commands/archive")
|
||||
public ResponseEntity archiveAccount(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(
|
||||
accountService.applyCommand(id, AccountCommand.ARCHIVE_ACCOUNT)))
|
||||
@RequestMapping(path = "/accounts/{id}/commands/archive")
|
||||
public ResponseEntity archive(@PathVariable Long id) {
|
||||
return Optional.ofNullable(getAccountResource(accountService.get(id)
|
||||
.archive()))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||
}
|
||||
@@ -115,17 +119,10 @@ public class AccountController {
|
||||
* @return a hypermedia resource for the fetched {@link Account}
|
||||
*/
|
||||
private Resource<Account> getAccountResource(Long id) {
|
||||
Resource<Account> accountResource = null;
|
||||
|
||||
// Get the account for the provided id
|
||||
Account account = accountService.getAccount(id);
|
||||
Account account = accountService.get(id);
|
||||
|
||||
// If the account exists, wrap the hypermedia response
|
||||
if (account != null)
|
||||
accountResource = getAccountResource(account);
|
||||
|
||||
|
||||
return accountResource;
|
||||
return getAccountResource(account);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +151,8 @@ public class AccountController {
|
||||
* @return a hypermedia resource for the updated {@link Account}
|
||||
*/
|
||||
private Resource<Account> updateAccountResource(Long id, Account account) {
|
||||
return getAccountResource(accountService.updateAccount(id, account));
|
||||
account.setIdentity(id);
|
||||
return getAccountResource(accountService.update(account));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,82 +164,40 @@ public class AccountController {
|
||||
* @return a hypermedia resource for the newly appended {@link AccountEvent}
|
||||
*/
|
||||
private Resource<AccountEvent> appendEventResource(Long accountId, AccountEvent event) {
|
||||
Resource<AccountEvent> eventResource = null;
|
||||
Assert.notNull(event, "Event body must be provided");
|
||||
|
||||
Account account = accountService.get(accountId);
|
||||
Assert.notNull(account, "Account could not be found");
|
||||
|
||||
event = accountService.appendEvent(accountId, event);
|
||||
event.setEntity(account);
|
||||
account.sendAsyncEvent(event);
|
||||
|
||||
if (event != null) {
|
||||
eventResource = new Resource<>(event,
|
||||
linkTo(EventController.class)
|
||||
.slash("events")
|
||||
.slash(event.getEventId())
|
||||
.withSelfRel(),
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(accountId)
|
||||
.withRel("account")
|
||||
);
|
||||
return new Resource<>(event,
|
||||
linkTo(EventController.class)
|
||||
.slash("events")
|
||||
.slash(event.getEventId())
|
||||
.withSelfRel(),
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(accountId)
|
||||
.withRel("account")
|
||||
);
|
||||
}
|
||||
|
||||
private Events getAccountEventResources(Long id) {
|
||||
return eventService.find(id);
|
||||
}
|
||||
|
||||
private LinkBuilder linkBuilder(String name, Long id) {
|
||||
Method method;
|
||||
|
||||
try {
|
||||
method = AccountController.class.getMethod(name, Long.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return eventResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link AccountCommand} hypermedia resource that lists the available commands that can be applied
|
||||
* to an {@link Account} entity.
|
||||
*
|
||||
* @param id is the {@link Account} identifier to provide command links for
|
||||
* @return an {@link AccountCommandsResource} with a collection of embedded command links
|
||||
*/
|
||||
private AccountCommandsResource getCommandsResource(Long id) {
|
||||
// Get the account resource for the identifier
|
||||
Resource<Account> accountResource = getAccountResource(id);
|
||||
|
||||
// Create a new account commands hypermedia resource
|
||||
AccountCommandsResource commandResource = new AccountCommandsResource();
|
||||
|
||||
// Add account command hypermedia links
|
||||
if (accountResource != null) {
|
||||
commandResource.add(
|
||||
getCommandLinkBuilder(id)
|
||||
.slash("confirm")
|
||||
.withRel("confirm"),
|
||||
getCommandLinkBuilder(id)
|
||||
.slash("activate")
|
||||
.withRel("activate"),
|
||||
getCommandLinkBuilder(id)
|
||||
.slash("suspend")
|
||||
.withRel("suspend"),
|
||||
getCommandLinkBuilder(id)
|
||||
.slash("archive")
|
||||
.withRel("archive")
|
||||
);
|
||||
}
|
||||
|
||||
return commandResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link AccountEvents} for the supplied {@link Account} identifier.
|
||||
*
|
||||
* @param id is the unique identifier of the {@link Account}
|
||||
* @return a list of {@link AccountEvent} wrapped in a hypermedia {@link AccountEvents} resource
|
||||
*/
|
||||
private AccountEvents getAccountEventResources(Long id) {
|
||||
return new AccountEvents(id, eventService.getAccountEvents(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a {@link LinkBuilder} for generating the {@link AccountCommandsResource}.
|
||||
*
|
||||
* @param id is the unique identifier for a {@link Account}
|
||||
* @return a {@link LinkBuilder} for the {@link AccountCommandsResource}
|
||||
*/
|
||||
private LinkBuilder getCommandLinkBuilder(Long id) {
|
||||
return linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(id)
|
||||
.slash("commands");
|
||||
return linkTo(AccountController.class, method, id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,23 +207,20 @@ public class AccountController {
|
||||
* @return is a hypermedia enriched resource for the supplied {@link Account} entity
|
||||
*/
|
||||
private Resource<Account> getAccountResource(Account account) {
|
||||
Resource<Account> accountResource;
|
||||
Assert.notNull(account, "Account must not be null");
|
||||
|
||||
// Prepare hypermedia response
|
||||
accountResource = new Resource<>(account,
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(account.getAccountId())
|
||||
.withSelfRel(),
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(account.getAccountId())
|
||||
.slash("events")
|
||||
.withRel("events"),
|
||||
getCommandLinkBuilder(account.getAccountId())
|
||||
.withRel("commands")
|
||||
);
|
||||
// Add command link
|
||||
account.add(linkBuilder("getCommands", account.getIdentity()).withRel("commands"));
|
||||
|
||||
return accountResource;
|
||||
// Add get events link
|
||||
account.add(linkBuilder("getAccountEvents", account.getIdentity()).withRel("events"));
|
||||
|
||||
return new Resource<>(account);
|
||||
}
|
||||
|
||||
private ResourceSupport getCommandsResource(Long id) {
|
||||
Account account = new Account();
|
||||
account.setIdentity(id);
|
||||
return new Resource<>(account.getCommands());
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package demo.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public JedisConnectionFactory redisConnectionFactory(
|
||||
@Value("${spring.redis.port}") Integer redisPort,
|
||||
@Value("${spring.redis.host}") String redisHost) {
|
||||
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
|
||||
|
||||
redisConnectionFactory.setHostName(redisHost);
|
||||
redisConnectionFactory.setPort(redisPort);
|
||||
|
||||
return redisConnectionFactory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate redisTemplate(RedisConnectionFactory cf) {
|
||||
RedisTemplate redisTemplate = new RedisTemplate();
|
||||
redisTemplate.setConnectionFactory(cf);
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisTemplate redisTemplate) {
|
||||
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
|
||||
cacheManager.setDefaultExpiration(50000);
|
||||
cacheManager.setCacheNames(Arrays.asList("accounts", "events"));
|
||||
cacheManager.setUsePrefix(true);
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package demo.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import demo.event.Event;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
|
||||
import javax.persistence.EntityListeners;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class BaseEntity extends ResourceSupport implements Serializable {
|
||||
public abstract class AbstractEntity<E extends Event, T extends Serializable> extends Aggregate<E, T> implements Serializable {
|
||||
|
||||
private T identity;
|
||||
|
||||
@CreatedDate
|
||||
private Long createdAt;
|
||||
@@ -19,7 +23,10 @@ public class BaseEntity extends ResourceSupport implements Serializable {
|
||||
@LastModifiedDate
|
||||
private Long lastModified;
|
||||
|
||||
public BaseEntity() {
|
||||
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<E> events = new ArrayList<>();
|
||||
|
||||
public AbstractEntity() {
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
@@ -38,6 +45,25 @@ public class BaseEntity extends ResourceSupport implements Serializable {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<E> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public void setEvents(List<E> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public void setIdentity(T id) {
|
||||
this.identity = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BaseEntity{" +
|
||||
@@ -45,4 +71,4 @@ public class BaseEntity extends ResourceSupport implements Serializable {
|
||||
", lastModified=" + lastModified +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,40 @@ package demo.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import demo.account.Account;
|
||||
import demo.domain.BaseEntity;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
/**
|
||||
* The domain event {@link AccountEvent} tracks the type and state of events as
|
||||
* applied to the {@link Account} domain object. This event resource can be used
|
||||
* to event source the aggregate state of {@link Account}.
|
||||
* The domain event {@link AccountEvent} tracks the type and state of events as applied to the {@link Account} domain
|
||||
* object. This event resource can be used to event source the aggregate state of {@link Account}.
|
||||
* <p>
|
||||
* This event resource also provides a transaction log that can be used to append
|
||||
* actions to the event.
|
||||
* This event resource also provides a transaction log that can be used to append actions to the event.
|
||||
*
|
||||
* @author kbastani
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Entity
|
||||
public class AccountEvent extends BaseEntity {
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class AccountEvent extends Event<Account, AccountEventType, Long> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long eventId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private AccountEventType type;
|
||||
|
||||
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@JsonIgnore
|
||||
private Account account;
|
||||
private Account entity;
|
||||
|
||||
@CreatedDate
|
||||
private Long createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
private Long lastModified;
|
||||
|
||||
public AccountEvent() {
|
||||
}
|
||||
@@ -37,37 +44,69 @@ public class AccountEvent extends BaseEntity {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public AccountEvent(AccountEventType type, Account entity) {
|
||||
this.type = type;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getEventId() {
|
||||
return id;
|
||||
return eventId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEventId(Long id) {
|
||||
this.id = id;
|
||||
eventId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountEventType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setType(AccountEventType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
@Override
|
||||
public Account getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
@Override
|
||||
public void setEntity(Account entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastModified(Long lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AccountEvent{" +
|
||||
"id=" + id +
|
||||
"eventId=" + eventId +
|
||||
", type=" + type +
|
||||
", account=" + account +
|
||||
", entity=" + entity +
|
||||
", createdAt=" + createdAt +
|
||||
", lastModified=" + lastModified +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package demo.event;
|
||||
|
||||
public interface AccountEventRepository extends EventRepository<AccountEvent, Long> {
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountController;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkBuilder;
|
||||
import org.springframework.hateoas.Resources;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
|
||||
/**
|
||||
* The {@link AccountEvents} is a hypermedia collection of {@link AccountEvent} resources.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
public class AccountEvents extends Resources<AccountEvent> implements Serializable {
|
||||
|
||||
private Long accountId;
|
||||
|
||||
/**
|
||||
* Create a new {@link AccountEvents} hypermedia resources collection for an {@link Account}.
|
||||
*
|
||||
* @param accountId is the unique identifier for the {@link Account}
|
||||
* @param content is the collection of {@link AccountEvents} attached to the {@link Account}
|
||||
*/
|
||||
public AccountEvents(Long accountId, List<AccountEvent> content) {
|
||||
this(content);
|
||||
this.accountId = accountId;
|
||||
|
||||
// Add hypermedia links to resources parent
|
||||
add(linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(accountId)
|
||||
.slash("events")
|
||||
.withSelfRel(),
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(accountId)
|
||||
.withRel("account"));
|
||||
|
||||
LinkBuilder linkBuilder = linkTo(EventController.class);
|
||||
|
||||
// Add hypermedia links to each item of the collection
|
||||
content.stream().parallel().forEach(event -> event.add(
|
||||
linkBuilder.slash("events")
|
||||
.slash(event.getEventId())
|
||||
.withSelfRel()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Resources} instance with the given content and {@link Link}s (optional).
|
||||
*
|
||||
* @param content must not be {@literal null}.
|
||||
* @param links the links to be added to the {@link Resources}.
|
||||
*/
|
||||
private AccountEvents(Iterable<AccountEvent> content, Link... links) {
|
||||
super(content, links);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Account} identifier that the {@link AccountEvents} apply to.
|
||||
*
|
||||
* @return the account identifier
|
||||
*/
|
||||
@JsonIgnore
|
||||
public Long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
public enum ConsistencyModel {
|
||||
BASE,
|
||||
ACID
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1")
|
||||
public class EventController {
|
||||
|
||||
private final EventService eventService;
|
||||
|
||||
public EventController(EventService eventService) {
|
||||
this.eventService = eventService;
|
||||
}
|
||||
|
||||
@PostMapping(path = "/events/{id}")
|
||||
public ResponseEntity createEvent(@RequestBody AccountEvent event, @PathVariable Long id) {
|
||||
return Optional.ofNullable(eventService.createEvent(id, event, ConsistencyModel.ACID))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Event creation failed"));
|
||||
}
|
||||
|
||||
@PutMapping(path = "/events/{id}")
|
||||
public ResponseEntity updateEvent(@RequestBody AccountEvent event, @PathVariable Long id) {
|
||||
return Optional.ofNullable(eventService.updateEvent(id, event))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Event update failed"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/events/{id}")
|
||||
public ResponseEntity getEvent(@PathVariable Long id) {
|
||||
return Optional.ofNullable(eventService.getEvent(id))
|
||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface EventRepository extends JpaRepository<AccountEvent, Long> {
|
||||
Page<AccountEvent> findAccountEventsByAccountId(@Param("accountId") Long accountId, Pageable pageable);
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
import demo.account.Account;
|
||||
import demo.account.AccountController;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cloud.stream.messaging.Source;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
|
||||
/**
|
||||
* The {@link EventService} provides transactional service methods for {@link AccountEvent}
|
||||
* entities of the Account Service. Account domain events are generated with a {@link AccountEventType},
|
||||
* and action logs are appended to the {@link AccountEvent}.
|
||||
*
|
||||
* @author kbastani
|
||||
*/
|
||||
@Service
|
||||
@CacheConfig(cacheNames = {"events"})
|
||||
public class EventService {
|
||||
|
||||
private final Logger log = Logger.getLogger(EventService.class);
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
private final Source accountStreamSource;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public EventService(EventRepository eventRepository, Source accountStreamSource, RestTemplate restTemplate) {
|
||||
this.eventRepository = eventRepository;
|
||||
this.accountStreamSource = accountStreamSource;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AccountEvent} and append it to the event log of the referenced {@link Account}.
|
||||
* After the {@link AccountEvent} has been persisted, send the event to the account stream. Events can
|
||||
* be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}.
|
||||
*
|
||||
* @param accountId is the unique identifier for the {@link Account}
|
||||
* @param event is the {@link AccountEvent} to create
|
||||
* @param consistencyModel is the desired consistency model for the response
|
||||
* @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log
|
||||
*/
|
||||
public AccountEvent createEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) {
|
||||
event = createEvent(accountId, event);
|
||||
return raiseEvent(event, consistencyModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise an {@link AccountEvent} that attempts to transition the state of an {@link Account}.
|
||||
*
|
||||
* @param event is an {@link AccountEvent} that will be raised
|
||||
* @param consistencyModel is the consistency model for this request
|
||||
* @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log
|
||||
*/
|
||||
public AccountEvent raiseEvent(AccountEvent event, ConsistencyModel consistencyModel) {
|
||||
switch (consistencyModel) {
|
||||
case BASE:
|
||||
asyncRaiseEvent(event);
|
||||
break;
|
||||
case ACID:
|
||||
event = raiseEvent(event);
|
||||
break;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise an asynchronous {@link AccountEvent} by sending an AMQP message to the account stream. Any
|
||||
* state changes will be applied to the {@link Account} outside of the current HTTP request context.
|
||||
* <p>
|
||||
* Use this operation when a workflow can be processed asynchronously outside of the current HTTP
|
||||
* request context.
|
||||
*
|
||||
* @param event is an {@link AccountEvent} that will be raised
|
||||
*/
|
||||
private void asyncRaiseEvent(AccountEvent event) {
|
||||
// Append the account event to the stream
|
||||
accountStreamSource.output()
|
||||
.send(MessageBuilder
|
||||
.withPayload(getAccountEventResource(event))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise a synchronous {@link AccountEvent} by sending a HTTP request to the account stream. The response
|
||||
* is a blocking operation, which ensures that the result of a multi-step workflow will not return until
|
||||
* the transaction reaches a consistent state.
|
||||
* <p>
|
||||
* Use this operation when the result of a workflow must be returned within the current HTTP request context.
|
||||
*
|
||||
* @param event is an {@link AccountEvent} that will be raised
|
||||
* @return an {@link AccountEvent} which contains the consistent state of an {@link Account}
|
||||
*/
|
||||
private AccountEvent raiseEvent(AccountEvent event) {
|
||||
try {
|
||||
// Create a new request entity
|
||||
RequestEntity<Resource<AccountEvent>> requestEntity = RequestEntity.post(
|
||||
URI.create("http://localhost:8081/v1/events"))
|
||||
.contentType(MediaTypes.HAL_JSON)
|
||||
.body(getAccountEventResource(event), Resource.class);
|
||||
|
||||
// Update the account entity's status
|
||||
Account result = restTemplate.exchange(requestEntity, Account.class)
|
||||
.getBody();
|
||||
|
||||
log.info(result);
|
||||
event.setAccount(result);
|
||||
} catch (Exception ex) {
|
||||
log.error(ex);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link AccountEvent} and publish it to the account stream.
|
||||
*
|
||||
* @param event is the {@link AccountEvent} to publish to the account stream
|
||||
* @return a hypermedia {@link AccountEvent} resource
|
||||
*/
|
||||
@CacheEvict(cacheNames = "events", key = "#id.toString()")
|
||||
public AccountEvent createEvent(Long id, AccountEvent event) {
|
||||
// Save new event
|
||||
event = addEvent(event);
|
||||
Assert.notNull(event, "The event could not be appended to the account");
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link AccountEvent} with the supplied identifier.
|
||||
*
|
||||
* @param id is the unique identifier for the {@link AccountEvent}
|
||||
* @return an {@link AccountEvent}
|
||||
*/
|
||||
public Resource<AccountEvent> getEvent(Long id) {
|
||||
return getAccountEventResource(eventRepository.findOne(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an {@link AccountEvent} with the supplied identifier.
|
||||
*
|
||||
* @param id is the unique identifier for the {@link AccountEvent}
|
||||
* @param event is the {@link AccountEvent} to update
|
||||
* @return the updated {@link AccountEvent}
|
||||
*/
|
||||
@CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()")
|
||||
public AccountEvent updateEvent(Long id, AccountEvent event) {
|
||||
Assert.notNull(id);
|
||||
Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId()));
|
||||
|
||||
return eventRepository.save(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link AccountEvents} for the supplied {@link Account} identifier.
|
||||
*
|
||||
* @param id is the unique identifier of the {@link Account}
|
||||
* @return a list of {@link AccountEvent} wrapped in a hypermedia {@link AccountEvents} resource
|
||||
*/
|
||||
@Cacheable(cacheNames = "events", key = "#id.toString()")
|
||||
public List<AccountEvent> getAccountEvents(Long id) {
|
||||
return eventRepository.findAccountEventsByAccountId(id,
|
||||
new PageRequest(0, Integer.MAX_VALUE)).getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a hypermedia resource for a {@link AccountEvent} entity.
|
||||
*
|
||||
* @param event is the {@link AccountEvent} to enrich with hypermedia
|
||||
* @return a hypermedia resource for the supplied {@link AccountEvent} entity
|
||||
*/
|
||||
private Resource<AccountEvent> getAccountEventResource(AccountEvent event) {
|
||||
return new Resource<AccountEvent>(event, Arrays.asList(
|
||||
linkTo(AccountController.class)
|
||||
.slash("events")
|
||||
.slash(event.getEventId())
|
||||
.withSelfRel(),
|
||||
linkTo(AccountController.class)
|
||||
.slash("accounts")
|
||||
.slash(event.getAccount().getAccountId())
|
||||
.withRel("account")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link AccountEvent} to an {@link Account} entity.
|
||||
*
|
||||
* @param event is the {@link AccountEvent} to append to an {@link Account} entity
|
||||
* @return the newly appended {@link AccountEvent} entity
|
||||
*/
|
||||
@CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()")
|
||||
private AccountEvent addEvent(AccountEvent event) {
|
||||
event = eventRepository.saveAndFlush(event);
|
||||
return event;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,5 @@ spring:
|
||||
output:
|
||||
destination: account
|
||||
contentType: 'application/json'
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
server:
|
||||
port: 8080
|
||||
@@ -1,6 +1,10 @@
|
||||
package demo.account;
|
||||
|
||||
import demo.account.controller.AccountController;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import demo.event.EventService;
|
||||
import demo.event.Events;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -10,6 +14,8 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@@ -26,18 +32,22 @@ public class AccountControllerTest {
|
||||
private AccountService accountService;
|
||||
|
||||
@MockBean
|
||||
private EventService eventService;
|
||||
private EventService<AccountEvent, Long> eventService;
|
||||
|
||||
@Test
|
||||
public void getUserAccountResourceShouldReturnAccount() throws Exception {
|
||||
String content = "{\"firstName\": \"Jane\", \"lastName\": \"Doe\", \"email\": \"jane.doe@example.com\"}";
|
||||
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
|
||||
given(this.accountService.getAccount(1L))
|
||||
.willReturn(account);
|
||||
given(this.accountService.get(1L)).willReturn(account);
|
||||
given(this.eventService.find(1L)).willReturn(new Events<>(1L, Collections
|
||||
.singletonList(new AccountEvent(AccountEventType
|
||||
.ACCOUNT_CREATED))));
|
||||
|
||||
this.mvc.perform(get("/v1/accounts/1").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(content().json(content));
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().json(content));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,36 @@ package demo.account;
|
||||
import demo.event.AccountEvent;
|
||||
import demo.event.AccountEventType;
|
||||
import demo.event.EventService;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class AccountServiceTests {
|
||||
|
||||
@MockBean
|
||||
private EventService eventService;
|
||||
private EventService<AccountEvent, Long> eventService;
|
||||
|
||||
@MockBean
|
||||
private AccountRepository accountRepository;
|
||||
|
||||
@MockBean
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
accountService = new AccountService(accountRepository, eventService, cacheManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAccountReturnsAccount() throws Exception {
|
||||
Account expected = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(expected);
|
||||
|
||||
Account actual = accountService.getAccount(1L);
|
||||
Account actual = accountService.get(1L);
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getEmail()).isEqualTo("jane.doe@example.com");
|
||||
@@ -49,12 +43,12 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void createAccountReturnsAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setAccountId(1L);
|
||||
account.setIdentity(1L);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.saveAndFlush(account)).willReturn(account);
|
||||
|
||||
Account actual = accountService.createAccount(account);
|
||||
Account actual = accountService.create(account);
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_CREATED);
|
||||
@@ -66,19 +60,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandSuspendsAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_ACTIVE);
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.SUSPEND_ACCOUNT);
|
||||
Account actual = account.suspend();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_SUSPENDED);
|
||||
@@ -87,19 +82,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandUnsuspendsAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_SUSPENDED);
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT);
|
||||
Account actual = account.activate();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE);
|
||||
@@ -108,19 +104,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandArchivesAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_ACTIVE);
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.ARCHIVE_ACCOUNT);
|
||||
Account actual = account.archive();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ARCHIVED);
|
||||
@@ -129,19 +126,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandUnarchivesAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_ARCHIVED);
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT);
|
||||
Account actual = account.activate();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE);
|
||||
@@ -150,19 +148,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandConfirmsAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_PENDING);
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.CONFIRM_ACCOUNT);
|
||||
Account actual = account.confirm();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_CONFIRMED);
|
||||
@@ -171,20 +170,20 @@ public class AccountServiceTests {
|
||||
@Test
|
||||
public void applyCommandActivatesAccount() throws Exception {
|
||||
Account account = new Account("Jane", "Doe", "jane.doe@example.com");
|
||||
account.setIdentity(1L);
|
||||
account.setStatus(AccountStatus.ACCOUNT_CONFIRMED);
|
||||
|
||||
|
||||
AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED);
|
||||
accountEvent.setAccount(account);
|
||||
accountEvent.setEntity(account);
|
||||
accountEvent.setEventId(1L);
|
||||
AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account);
|
||||
|
||||
given(this.accountRepository.findOne(1L)).willReturn(account);
|
||||
given(this.accountRepository.exists(1L)).willReturn(true);
|
||||
given(this.accountRepository.save(account)).willReturn(account);
|
||||
given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
|
||||
.willReturn(accountEvent);
|
||||
given(this.eventService.send(expected)).willReturn(accountEvent);
|
||||
|
||||
Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT);
|
||||
Account actual = account.activate();
|
||||
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE);
|
||||
|
||||
@@ -11,6 +11,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Connects an {@link Order} to an Account.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ConnectAccount extends Action<Order> {
|
||||
|
||||
@@ -25,7 +30,7 @@ public class ConnectAccount extends Action<Order> {
|
||||
order = orderService.update(order);
|
||||
|
||||
// Trigger the account connected event
|
||||
order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED));
|
||||
order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED, order));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Connects a {@link demo.payment.Payment} to an {@link Order}.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ConnectPayment extends Action<Order> {
|
||||
public BiConsumer<Order, Long> getConsumer() {
|
||||
@@ -25,7 +30,7 @@ public class ConnectPayment extends Action<Order> {
|
||||
order = orderService.update(order);
|
||||
|
||||
// Trigger the account connected event
|
||||
order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED));
|
||||
order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED, order));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import org.springframework.web.client.RestTemplate;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Creates a {@link Payment} for an {@link Order}.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class CreatePayment extends Action<Order> {
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Processes a {@link demo.payment.Payment} for an {@link Order}.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ProcessPayment extends Action<Order> {
|
||||
public Consumer<Order> getConsumer() {
|
||||
|
||||
@@ -6,6 +6,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Reserves inventory for an {@link Order}.
|
||||
*
|
||||
* @author Kenny Bastani
|
||||
*/
|
||||
@Service
|
||||
public class ReserveInventory extends Action<Order> {
|
||||
public Consumer<Order> getConsumer() {
|
||||
|
||||
Reference in New Issue
Block a user