replaced bookreview app with cashpal
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "account")
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
class AccountJpaEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Activity;
|
||||||
|
import io.reflectoring.cashpal.domain.Activity.ActivityId;
|
||||||
|
import io.reflectoring.cashpal.domain.ActivityWindow;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class AccountMapper {
|
||||||
|
|
||||||
|
Account mapToDomainEntity(
|
||||||
|
AccountJpaEntity account,
|
||||||
|
List<ActivityJpaEntity> activities,
|
||||||
|
Long withdrawalBalance,
|
||||||
|
Long depositBalance) {
|
||||||
|
|
||||||
|
Money baselineBalance = Money.subtract(
|
||||||
|
Money.of(depositBalance),
|
||||||
|
Money.of(withdrawalBalance));
|
||||||
|
|
||||||
|
return Account.withId(
|
||||||
|
new AccountId(account.getId()),
|
||||||
|
baselineBalance,
|
||||||
|
mapToActivityWindow(activities));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivityWindow mapToActivityWindow(List<ActivityJpaEntity> activities) {
|
||||||
|
List<Activity> mappedActivities = new ArrayList<>();
|
||||||
|
|
||||||
|
for (ActivityJpaEntity activity : activities) {
|
||||||
|
mappedActivities.add(new Activity(
|
||||||
|
new ActivityId(activity.getId()),
|
||||||
|
new AccountId(activity.getOwnerAccountId()),
|
||||||
|
new AccountId(activity.getSourceAccountId()),
|
||||||
|
new AccountId(activity.getTargetAccountId()),
|
||||||
|
activity.getTimestamp(),
|
||||||
|
Money.of(activity.getAmount())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ActivityWindow(mappedActivities);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.out.LoadAccountPort;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.UpdateAccountStatePort;
|
||||||
|
import io.reflectoring.cashpal.testdata.PersistenceAdapter;
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Activity;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@PersistenceAdapter
|
||||||
|
class AccountPersistenceAdapter implements
|
||||||
|
LoadAccountPort,
|
||||||
|
UpdateAccountStatePort {
|
||||||
|
|
||||||
|
private final AccountRepository accountRepository;
|
||||||
|
private final ActivityRepository activityRepository;
|
||||||
|
private final AccountMapper accountMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account loadAccount(AccountId accountId, LocalDateTime baselineDate) {
|
||||||
|
|
||||||
|
AccountJpaEntity account =
|
||||||
|
accountRepository.findById(accountId.getValue())
|
||||||
|
.orElseThrow(EntityNotFoundException::new);
|
||||||
|
|
||||||
|
List<ActivityJpaEntity> activities =
|
||||||
|
activityRepository.findByOwnerSince(
|
||||||
|
accountId.getValue(),
|
||||||
|
baselineDate);
|
||||||
|
|
||||||
|
Long withdrawalBalance = orZero(activityRepository
|
||||||
|
.getWithdrawalBalanceUntil(
|
||||||
|
accountId.getValue(),
|
||||||
|
baselineDate));
|
||||||
|
|
||||||
|
Long depositBalance = orZero(activityRepository
|
||||||
|
.getDepositBalanceUntil(
|
||||||
|
accountId.getValue(),
|
||||||
|
baselineDate));
|
||||||
|
|
||||||
|
return accountMapper.mapToDomainEntity(
|
||||||
|
account,
|
||||||
|
activities,
|
||||||
|
withdrawalBalance,
|
||||||
|
depositBalance);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orZero(Long value){
|
||||||
|
return value == null ? 0L : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateActivities(Account account) {
|
||||||
|
for (Activity activity : account.getActivityWindow().getActivities()) {
|
||||||
|
if (activity.getId() == null) {
|
||||||
|
activityRepository.save(mapToJpa(activity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActivityJpaEntity mapToJpa(Activity activity) {
|
||||||
|
return new ActivityJpaEntity(
|
||||||
|
activity.getId() == null ? null : activity.getId().getValue(),
|
||||||
|
activity.getTimestamp(),
|
||||||
|
activity.getOwnerAccountId().getValue(),
|
||||||
|
activity.getSourceAccountId().getValue(),
|
||||||
|
activity.getTargetAccountId().getValue(),
|
||||||
|
activity.getMoney().getAmount().longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
interface AccountRepository extends JpaRepository<AccountJpaEntity, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "activity")
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
class ActivityJpaEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Long ownerAccountId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Long sourceAccountId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Long targetAccountId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Long amount;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
interface ActivityRepository extends JpaRepository<ActivityJpaEntity, Long> {
|
||||||
|
|
||||||
|
@Query("select a from ActivityJpaEntity a " +
|
||||||
|
"where a.ownerAccountId = :ownerAccountId " +
|
||||||
|
"and a.timestamp >= :since")
|
||||||
|
List<ActivityJpaEntity> findByOwnerSince(
|
||||||
|
@Param("ownerAccountId") Long ownerAccountId,
|
||||||
|
@Param("since") LocalDateTime since);
|
||||||
|
|
||||||
|
@Query("select sum(a.amount) from ActivityJpaEntity a " +
|
||||||
|
"where a.targetAccountId = :accountId " +
|
||||||
|
"and a.ownerAccountId = :accountId " +
|
||||||
|
"and a.timestamp < :until")
|
||||||
|
Long getDepositBalanceUntil(
|
||||||
|
@Param("accountId") Long accountId,
|
||||||
|
@Param("until") LocalDateTime until);
|
||||||
|
|
||||||
|
@Query("select sum(a.amount) from ActivityJpaEntity a " +
|
||||||
|
"where a.sourceAccountId = :accountId " +
|
||||||
|
"and a.ownerAccountId = :accountId " +
|
||||||
|
"and a.timestamp < :until")
|
||||||
|
Long getWithdrawalBalanceUntil(
|
||||||
|
@Param("accountId") Long accountId,
|
||||||
|
@Param("until") LocalDateTime until);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.ActivityWindow;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.test.context.jdbc.Sql;
|
||||||
|
import static io.reflectoring.cashpal.testdata.AccountTestData.*;
|
||||||
|
import static io.reflectoring.cashpal.testdata.ActivityTestData.*;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@Import({AccountPersistenceAdapter.class, AccountMapper.class})
|
||||||
|
class AccountPersistenceAdapterTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AccountPersistenceAdapter adapterUnderTest;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActivityRepository activityRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Sql("AccountPersistenceAdapterTest.sql")
|
||||||
|
void loadsAccount() {
|
||||||
|
Account account = adapterUnderTest.loadAccount(new AccountId(1L), LocalDateTime.of(2018, 8, 10, 0, 0));
|
||||||
|
|
||||||
|
assertThat(account.getActivityWindow().getActivities()).hasSize(2);
|
||||||
|
assertThat(account.calculateBalance()).isEqualTo(Money.of(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatesActivities() {
|
||||||
|
Account account = defaultAccount()
|
||||||
|
.withBaselineBalance(Money.of(555L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withId(null)
|
||||||
|
.withMoney(Money.of(1L)).build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
adapterUnderTest.updateActivities(account);
|
||||||
|
|
||||||
|
assertThat(activityRepository.count()).isEqualTo(1);
|
||||||
|
|
||||||
|
ActivityJpaEntity savedActivity = activityRepository.findAll().get(0);
|
||||||
|
assertThat(savedActivity.getAmount()).isEqualTo(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.persistence;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class TestApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(TestApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
insert into account (id) values (1);
|
||||||
|
insert into account (id) values (2);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1, '2018-08-08 08:00:00.0', 1, 1, 2, 500);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (2, '2018-08-08 08:00:00.0', 2, 1, 2, 500);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (3, '2018-08-09 10:00:00.0', 1, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (4, '2018-08-09 10:00:00.0', 2, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (5, '2019-08-09 09:00:00.0', 1, 1, 2, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (6, '2019-08-09 09:00:00.0', 2, 1, 2, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (7, '2019-08-09 10:00:00.0', 1, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (8, '2019-08-09 10:00:00.0', 2, 2, 1, 1000);
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation project(':application')
|
implementation project(':cashpal-application')
|
||||||
api 'org.springframework.boot:spring-boot-starter-web'
|
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
2
adapters/cashpal-web/build/tmp/jar/MANIFEST.MF
Normal file
2
adapters/cashpal-web/build/tmp/jar/MANIFEST.MF
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.web;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase;
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase.SendMoneyCommand;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SendMoneyController {
|
||||||
|
|
||||||
|
private final SendMoneyUseCase sendMoneyUseCase;
|
||||||
|
|
||||||
|
@PostMapping(path = "/sendMoney")
|
||||||
|
void sendMoney(@RequestBody SendMoneyForm form) {
|
||||||
|
|
||||||
|
SendMoneyCommand command = new SendMoneyCommand(
|
||||||
|
new AccountId(form.sourceAccountId),
|
||||||
|
new AccountId(form.targetAccountId),
|
||||||
|
Money.of(form.amount));
|
||||||
|
|
||||||
|
sendMoneyUseCase.sendMoney(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class SendMoneyForm {
|
||||||
|
private Long sourceAccountId;
|
||||||
|
private Long targetAccountId;
|
||||||
|
private Long amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.reflectoring.cashpal.adapter.web.SendMoneyController.SendMoneyForm;
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase;
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase.SendMoneyCommand;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import static org.mockito.BDDMockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
@WebMvcTest(controllers = SendMoneyController.class)
|
||||||
|
class SendMoneyControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private SendMoneyUseCase sendMoneyUseCase;
|
||||||
|
|
||||||
|
private ObjectMapper jsonMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendMoney() throws Exception {
|
||||||
|
|
||||||
|
SendMoneyForm form = new SendMoneyForm(
|
||||||
|
41L,
|
||||||
|
42L,
|
||||||
|
500L
|
||||||
|
);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/sendMoney")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.content(jsonMapper.writeValueAsString(form)))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
then(sendMoneyUseCase).should().sendMoney(any(SendMoneyCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package io.reflectoring.cashpal.adapter.web;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class TestApplication {
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.FindAuthorByIdPort;
|
|
||||||
import io.reflectoring.reviewapp.domain.Author;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
class AuthorPersistenceAdapter implements FindAuthorByIdPort {
|
|
||||||
|
|
||||||
private final AuthorRepository authorRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Author findAuthorById(Long authorId) {
|
|
||||||
return authorRepository.findById(authorId)
|
|
||||||
.orElseThrow(() -> new AuthorNotFoundException(authorId));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Author;
|
|
||||||
import org.springframework.data.repository.CrudRepository;
|
|
||||||
|
|
||||||
interface AuthorRepository extends CrudRepository<Author, Long> {
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.FindBookByTitlePort;
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.PersistBookPort;
|
|
||||||
import io.reflectoring.reviewapp.common.PersistenceAdapter;
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@PersistenceAdapter
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
class BookPersistenceAdapter implements FindBookByTitlePort, PersistBookPort {
|
|
||||||
|
|
||||||
private final BookRepository bookRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Book> findBookByTitle(String title) {
|
|
||||||
return bookRepository.findByTitle(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Book saveBook(Book book) {
|
|
||||||
return bookRepository.save(book);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
import org.springframework.data.jdbc.repository.query.Query;
|
|
||||||
import org.springframework.data.repository.CrudRepository;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
interface BookRepository extends CrudRepository<Book, Long> {
|
|
||||||
|
|
||||||
@Query("select b.* from Book b where b.title = :title")
|
|
||||||
Optional<Book> findByTitle(@Param("title") String title);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
|
|
||||||
import org.springframework.data.jdbc.repository.config.JdbcConfiguration;
|
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableJdbcRepositories
|
|
||||||
@ComponentScan
|
|
||||||
class PersistenceAdapterConfiguration extends JdbcConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) {
|
|
||||||
return new NamedParameterJdbcTemplate(dataSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Author;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.jdbc.Sql;
|
|
||||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
|
||||||
import org.springframework.test.context.jdbc.SqlGroup;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.*;
|
|
||||||
|
|
||||||
@DataJdbcTest
|
|
||||||
class AuthorPersistenceAdapterTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthorPersistenceAdapter authorPersistenceAdapter;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SqlGroup({
|
|
||||||
@Sql(scripts = "single-author.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD),
|
|
||||||
@Sql(scripts = "single-author-reset.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)})
|
|
||||||
void findByAuthorId() {
|
|
||||||
Author author = authorPersistenceAdapter.findAuthorById(42L);
|
|
||||||
assertThat(author).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.jdbc.Sql;
|
|
||||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
|
||||||
import org.springframework.test.context.jdbc.SqlGroup;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.*;
|
|
||||||
|
|
||||||
@DataJdbcTest
|
|
||||||
class BookPersistenceAdapterTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BookPersistenceAdapter bookPersistenceAdapter;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BookRepository bookRepository;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SqlGroup({
|
|
||||||
@Sql(scripts = "single-book.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD),
|
|
||||||
@Sql(scripts = "single-book-reset.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)})
|
|
||||||
void findBookByTitle() {
|
|
||||||
Optional<Book> optionalBook = bookPersistenceAdapter.findBookByTitle("Get Your Hands Dirty on Clean Architecture");
|
|
||||||
assertThat(optionalBook).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SqlGroup({
|
|
||||||
@Sql(scripts = "single-book.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD),
|
|
||||||
@Sql(scripts = "single-book-reset.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)})
|
|
||||||
void savesBook() {
|
|
||||||
Book book = new Book(null, "A Hitchhiker's Guide to the Galaxy", 42L);
|
|
||||||
Book savedBook = bookPersistenceAdapter.saveBook(book);
|
|
||||||
assertThat(bookRepository.findById(savedBook.getId())).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.persistence;
|
|
||||||
|
|
||||||
import org.springframework.boot.SpringBootConfiguration;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
|
|
||||||
@SpringBootConfiguration
|
|
||||||
@Import(PersistenceAdapterConfiguration.class)
|
|
||||||
class PersistenceAdapterTestConfiguration {
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
DELETE
|
|
||||||
FROM AUTHOR
|
|
||||||
WHERE ID = 42;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
INSERT INTO AUTHOR (ID, NAME)
|
|
||||||
VALUES (42, 'Tom');
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DELETE FROM AUTHOR WHERE ID = 42;
|
|
||||||
DELETE FROM BOOK WHERE ID = 1;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
INSERT INTO AUTHOR (ID, NAME)
|
|
||||||
VALUES (42, 'Tom');
|
|
||||||
|
|
||||||
INSERT INTO BOOK (ID, TITLE, AUTHOR_ID)
|
|
||||||
VALUES (1, 'Get Your Hands Dirty on Clean Architecture', 42);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.adapter.web;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.application.port.in.RegisterBookUseCase;
|
|
||||||
import io.reflectoring.reviewapp.application.port.in.RegisterBookUseCase.RegisterBookCommand;
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
class RegisterBookController {
|
|
||||||
|
|
||||||
private final RegisterBookUseCase registerBookUseCase;
|
|
||||||
|
|
||||||
@PostMapping(path = "/books/register")
|
|
||||||
void registerBook(@RequestBody Book book) {
|
|
||||||
RegisterBookCommand command = new RegisterBookCommand(book.getTitle(), book.getAuthorId());
|
|
||||||
registerBookUseCase.registerBook(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
implementation project(':common')
|
|
||||||
|
|
||||||
implementation 'org.springframework:spring-context'
|
|
||||||
implementation 'org.springframework:spring-core'
|
|
||||||
implementation 'javax.validation:validation-api'
|
|
||||||
|
|
||||||
// Needed for Spring Data annotations on domain entities.
|
|
||||||
// This is a shortcut to avoid a mapping step between domain and persistence!
|
|
||||||
implementation 'org.springframework.data:spring-data-commons'
|
|
||||||
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
|
||||||
|
|
||||||
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
|
||||||
exclude group: 'junit' // excluding junit 4
|
|
||||||
}
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.0.1'
|
|
||||||
testImplementation 'org.mockito:mockito-junit-jupiter:2.23.0'
|
|
||||||
testImplementation 'com.tngtech.archunit:archunit:0.9.3'
|
|
||||||
testImplementation 'de.adesso:junit-insights:1.1.0'
|
|
||||||
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
systemProperty 'de.adesso.junitinsights.enabled', 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.port.in;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.common.SelfValidating;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public interface RegisterBookUseCase {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows an author to register a new book to be reviewed.
|
|
||||||
*/
|
|
||||||
void registerBook(RegisterBookCommand command) throws NonUniqueBookTitleException;
|
|
||||||
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
final class RegisterBookCommand extends SelfValidating<RegisterBookCommand> {
|
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
private final String bookTitle;
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private final Long authorId;
|
|
||||||
|
|
||||||
public RegisterBookCommand(String bookTitle, Long authorId) {
|
|
||||||
this.bookTitle = bookTitle;
|
|
||||||
this.authorId = authorId;
|
|
||||||
validateSelf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class NonUniqueBookTitleException extends RuntimeException {
|
|
||||||
|
|
||||||
public NonUniqueBookTitleException(String bookTitle) {
|
|
||||||
super(String.format("A book title must be unique (book title in question: '%s').", bookTitle));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.port.out;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Author;
|
|
||||||
|
|
||||||
public interface FindAuthorByIdPort {
|
|
||||||
|
|
||||||
Author findAuthorById(Long authorId) throws AuthorNotFoundException;
|
|
||||||
|
|
||||||
class AuthorNotFoundException extends RuntimeException {
|
|
||||||
public AuthorNotFoundException(Long authorId) {
|
|
||||||
super(String.format("Author with ID '%d' does not exist!", authorId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.port.out;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface FindBookByTitlePort {
|
|
||||||
|
|
||||||
Optional<Book> findBookByTitle(String title);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.port.out;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
|
|
||||||
public interface PersistBookPort {
|
|
||||||
|
|
||||||
Book saveBook(Book book);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.service;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.application.port.in.RegisterBookUseCase;
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.FindAuthorByIdPort;
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.FindBookByTitlePort;
|
|
||||||
import io.reflectoring.reviewapp.application.port.out.PersistBookPort;
|
|
||||||
import io.reflectoring.reviewapp.domain.Author;
|
|
||||||
import io.reflectoring.reviewapp.domain.Book;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RegisterBookService implements RegisterBookUseCase {
|
|
||||||
|
|
||||||
private final FindAuthorByIdPort findAuthorByIdPort;
|
|
||||||
private final FindBookByTitlePort findBookByTitlePort;
|
|
||||||
private final PersistBookPort saveBookPort;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerBook(RegisterBookCommand command) {
|
|
||||||
|
|
||||||
Author author = findAuthorByIdPort.findAuthorById(command.getAuthorId());
|
|
||||||
|
|
||||||
requireUniqueTitle(command.getBookTitle());
|
|
||||||
requireAuthorHasPremiumAccount(author);
|
|
||||||
|
|
||||||
// more business validations ...
|
|
||||||
|
|
||||||
Book book = new Book(null, command.getBookTitle(), author.getId());
|
|
||||||
saveBookPort.saveBook(book);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requireAuthorHasPremiumAccount(Author author) {
|
|
||||||
// some business validation ...
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requireUniqueTitle(String bookTitle) {
|
|
||||||
if (findBookByTitlePort.findBookByTitle(bookTitle).isPresent()) {
|
|
||||||
throw new NonUniqueBookTitleException(bookTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.domain;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.data.annotation.Id;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Author {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
// imagine some insanely complex business logic methods ...
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.domain;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.data.annotation.Id;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Book {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private Long id;
|
|
||||||
private String title;
|
|
||||||
private Long authorId;
|
|
||||||
|
|
||||||
// imagine some insanely complex business logic methods ...
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
CREATE TABLE BOOK
|
|
||||||
(
|
|
||||||
id BIGINT auto_increment NOT NULL,
|
|
||||||
title VARCHAR(50) NOT NULL,
|
|
||||||
author_id BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE AUTHOR
|
|
||||||
(
|
|
||||||
id BIGINT auto_increment NOT NULL,
|
|
||||||
name VARCHAR(50) NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package io.reflectoring.reviewapp.application.port.in;
|
|
||||||
|
|
||||||
import io.reflectoring.reviewapp.application.port.in.RegisterBookUseCase.RegisterBookCommand;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import javax.validation.ConstraintViolationException;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.*;
|
|
||||||
|
|
||||||
class RegisterBookCommandTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void acceptsValidParameters() {
|
|
||||||
new RegisterBookCommand("Get Your Hands Dirty on Clean Architecture", 42L);
|
|
||||||
// no exception
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsEmptyTitle() {
|
|
||||||
assertThatThrownBy(() -> new RegisterBookCommand("", 42L))
|
|
||||||
.isInstanceOf(ConstraintViolationException.class);
|
|
||||||
assertThatThrownBy(() -> new RegisterBookCommand(null, 42L))
|
|
||||||
.isInstanceOf(ConstraintViolationException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsEmptyAuthor() {
|
|
||||||
assertThatThrownBy(() -> new RegisterBookCommand("Get Your Hands Dirty on Clean Architecture", null))
|
|
||||||
.isInstanceOf(ConstraintViolationException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation project(':application')
|
|
||||||
api 'org.springframework.boot:spring-boot-starter-data-jdbc'
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
runtimeOnly 'com.h2database:h2'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
||||||
|
implementation 'javax.validation:validation-api'
|
||||||
|
implementation 'javax.transaction:javax.transaction-api'
|
||||||
|
|
||||||
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||||
exclude group: 'junit' // excluding junit 4
|
exclude group: 'junit' // excluding junit 4
|
||||||
}
|
}
|
||||||
@@ -15,9 +17,11 @@ dependencies {
|
|||||||
testImplementation 'com.tngtech.archunit:archunit:0.9.3'
|
testImplementation 'com.tngtech.archunit:archunit:0.9.3'
|
||||||
testImplementation 'de.adesso:junit-insights:1.1.0'
|
testImplementation 'de.adesso:junit-insights:1.1.0'
|
||||||
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
|
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
|
||||||
|
testImplementation project(':cashpal-testdata')
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
systemProperty 'de.adesso.junitinsights.enabled', 'true'
|
systemProperty 'de.adesso.junitinsights.enabled', 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package io.reflectoring.cashpal.application.port.in;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
|
||||||
|
public interface GetAccountBalanceQuery {
|
||||||
|
|
||||||
|
Money getAccountBalance(AccountId accountId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package io.reflectoring.cashpal.application.port.in;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.testdata.SelfValidating;
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
public interface SendMoneyUseCase {
|
||||||
|
|
||||||
|
boolean sendMoney(SendMoneyCommand command);
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Account.AccountId sourceAccountId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Account.AccountId targetAccountId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Money money;
|
||||||
|
|
||||||
|
public SendMoneyCommand(Account.AccountId sourceAccountId, Account.AccountId targetAccountId, Money money) {
|
||||||
|
this.sourceAccountId = sourceAccountId;
|
||||||
|
this.targetAccountId = targetAccountId;
|
||||||
|
this.money = money;
|
||||||
|
this.validateSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package io.reflectoring.cashpal.application.port.out;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
|
||||||
|
public interface AccountLock {
|
||||||
|
|
||||||
|
void lockAccount(Account.AccountId accountId);
|
||||||
|
|
||||||
|
void releaseAccount(Account.AccountId accountId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package io.reflectoring.cashpal.application.port.out;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
|
||||||
|
public interface LoadAccountPort {
|
||||||
|
|
||||||
|
Account loadAccount(AccountId accountId, LocalDateTime baselineDate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package io.reflectoring.cashpal.application.port.out;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
|
||||||
|
public interface UpdateAccountStatePort {
|
||||||
|
|
||||||
|
void updateActivities(Account account);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package io.reflectoring.cashpal.application.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.in.GetAccountBalanceQuery;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.LoadAccountPort;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
class GetAccountBalanceService implements GetAccountBalanceQuery {
|
||||||
|
|
||||||
|
private final LoadAccountPort loadAccountPort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Money getAccountBalance(AccountId accountId) {
|
||||||
|
return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
|
||||||
|
.calculateBalance();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package io.reflectoring.cashpal.application.service;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.out.AccountLock;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class NoOpAccountLock implements AccountLock {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lockAccount(AccountId accountId) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseAccount(AccountId accountId) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package io.reflectoring.cashpal.application.service;
|
||||||
|
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.AccountLock;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.LoadAccountPort;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.UpdateAccountStatePort;
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.testdata.UseCase;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@UseCase
|
||||||
|
@Transactional
|
||||||
|
public class SendMoneyService implements SendMoneyUseCase {
|
||||||
|
|
||||||
|
private final LoadAccountPort loadAccountPort;
|
||||||
|
private final AccountLock accountLock;
|
||||||
|
private final UpdateAccountStatePort updateAccountStatePort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sendMoney(SendMoneyCommand command) {
|
||||||
|
LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);
|
||||||
|
|
||||||
|
Account sourceAccount = loadAccountPort.loadAccount(
|
||||||
|
command.getSourceAccountId(),
|
||||||
|
baselineDate);
|
||||||
|
|
||||||
|
Account targetAccount = loadAccountPort.loadAccount(
|
||||||
|
command.getTargetAccountId(),
|
||||||
|
baselineDate);
|
||||||
|
|
||||||
|
accountLock.lockAccount(sourceAccount.getId());
|
||||||
|
if (!sourceAccount.withdraw(command.getMoney(), targetAccount.getId())) {
|
||||||
|
accountLock.releaseAccount(sourceAccount.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountLock.lockAccount(targetAccount.getId());
|
||||||
|
if (!targetAccount.deposit(command.getMoney(), sourceAccount.getId())) {
|
||||||
|
accountLock.releaseAccount(sourceAccount.getId());
|
||||||
|
accountLock.releaseAccount(targetAccount.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccountStatePort.updateActivities(sourceAccount);
|
||||||
|
updateAccountStatePort.updateActivities(targetAccount);
|
||||||
|
|
||||||
|
accountLock.releaseAccount(sourceAccount.getId());
|
||||||
|
accountLock.releaseAccount(targetAccount.getId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account that holds a certain amount of money. An {@link Account} object only
|
||||||
|
* contains a window of the latest account activities. The total balance of the account is
|
||||||
|
* the sum of a baseline balance that was valid before the first activity in the
|
||||||
|
* window and the sum of the activity values.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class Account {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique ID of the account.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private AccountId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The baseline balance of the account. This was the balance of the account before the first
|
||||||
|
* activity in the activityWindow.
|
||||||
|
*/
|
||||||
|
private Money baselineBalance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The window of latest activities on this account.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private ActivityWindow activityWindow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet
|
||||||
|
* persisted.
|
||||||
|
*/
|
||||||
|
public static Account withoutId(Money baselineBalance, ActivityWindow activityWindow) {
|
||||||
|
return new Account(null, baselineBalance, activityWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity.
|
||||||
|
*/
|
||||||
|
public static Account withId(AccountId accountId, Money baselineBalance, ActivityWindow activityWindow) {
|
||||||
|
return new Account(accountId, baselineBalance, activityWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the total balance of the account by adding the activity values to the baseline balance.
|
||||||
|
*/
|
||||||
|
public Money calculateBalance() {
|
||||||
|
return Money.add(this.activityWindow.calculateBalance(this.id), this.baselineBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to withdraw a certain amount of money from this account.
|
||||||
|
* If successful, creates a new activity with a negative value.
|
||||||
|
* @return true if the withdrawal was successful, false if not.
|
||||||
|
*/
|
||||||
|
public boolean withdraw(Money money, AccountId targetAccountId) {
|
||||||
|
|
||||||
|
if(Money.add(this.calculateBalance(), money.negate()).isNegative()){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity withdrawal = new Activity(null, this.id, this.id, targetAccountId, LocalDateTime.now(), money);
|
||||||
|
this.activityWindow.addActivity(withdrawal);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to deposit a certain amount of money to this account.
|
||||||
|
* If sucessful, creates a new activity with a positive value.
|
||||||
|
* @return true if the deposit was successful, false if not.
|
||||||
|
*/
|
||||||
|
public boolean deposit(Money money, AccountId sourceAccountId) {
|
||||||
|
Activity deposit = new Activity(null, this.id, sourceAccountId, this.id, LocalDateTime.now(), money);
|
||||||
|
this.activityWindow.addActivity(deposit);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class AccountId {
|
||||||
|
private Long value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A money transfer activity between {@link Account}s.
|
||||||
|
*/
|
||||||
|
@Value
|
||||||
|
public class Activity {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final ActivityId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account that owns this activity.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final Account.AccountId ownerAccountId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The debited account.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final Account.AccountId sourceAccountId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The credited account.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final Account.AccountId targetAccountId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the activity.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The money that was transferred between the accounts.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final Money money;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class ActivityId {
|
||||||
|
private final Long value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A window of account activities.
|
||||||
|
*/
|
||||||
|
public class ActivityWindow {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of account activities within this window.
|
||||||
|
*/
|
||||||
|
private List<Activity> activities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the first activity within this window.
|
||||||
|
*/
|
||||||
|
public LocalDateTime getStartTimestamp() {
|
||||||
|
return activities.stream()
|
||||||
|
.min(Comparator.comparing(Activity::getTimestamp))
|
||||||
|
.orElseThrow(IllegalStateException::new)
|
||||||
|
.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the last activity within this window.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LocalDateTime getEndTimestamp() {
|
||||||
|
return activities.stream()
|
||||||
|
.max(Comparator.comparing(Activity::getTimestamp))
|
||||||
|
.orElseThrow(IllegalStateException::new)
|
||||||
|
.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the balance by summing up the values of all activities within this window.
|
||||||
|
*/
|
||||||
|
public Money calculateBalance(AccountId accountId) {
|
||||||
|
Money depositBalance = activities.stream()
|
||||||
|
.filter(a -> a.getTargetAccountId().equals(accountId))
|
||||||
|
.map(Activity::getMoney)
|
||||||
|
.reduce(Money.ZERO, Money::add);
|
||||||
|
|
||||||
|
Money withdrawalBalance = activities.stream()
|
||||||
|
.filter(a -> a.getSourceAccountId().equals(accountId))
|
||||||
|
.map(Activity::getMoney)
|
||||||
|
.reduce(Money.ZERO, Money::add);
|
||||||
|
|
||||||
|
return Money.add(depositBalance, withdrawalBalance.negate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityWindow(@NonNull List<Activity> activities) {
|
||||||
|
this.activities = activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityWindow(@NonNull Activity... activities) {
|
||||||
|
this.activities = new ArrayList<>(Arrays.asList(activities));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> getActivities() {
|
||||||
|
return new ArrayList<>(this.activities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addActivity(Activity activity) {
|
||||||
|
this.activities.add(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class Money {
|
||||||
|
|
||||||
|
public static Money ZERO = Money.of(0L);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final BigInteger amount;
|
||||||
|
|
||||||
|
public boolean isPositiveOrZero(){
|
||||||
|
return this.amount.compareTo(BigInteger.ZERO) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNegative(){
|
||||||
|
return this.amount.compareTo(BigInteger.ZERO) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPositive(){
|
||||||
|
return this.amount.compareTo(BigInteger.ZERO) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGreaterThanOrEqualTo(Money money){
|
||||||
|
return this.amount.compareTo(money.amount) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Money of(long value) {
|
||||||
|
return new Money(BigInteger.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Money add(Money a, Money b) {
|
||||||
|
return new Money(a.amount.add(b.amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Money subtract(Money a, Money b) {
|
||||||
|
return new Money(a.amount.subtract(b.amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money negate(){
|
||||||
|
return new Money(this.amount.negate());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package io.reflectoring.cashpal.application.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.application.port.in.SendMoneyUseCase.SendMoneyCommand;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.AccountLock;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.LoadAccountPort;
|
||||||
|
import io.reflectoring.cashpal.application.port.out.UpdateAccountStatePort;
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.BDDMockito.*;
|
||||||
|
|
||||||
|
class SendMoneyServiceTest {
|
||||||
|
|
||||||
|
private final LoadAccountPort loadAccountPort =
|
||||||
|
Mockito.mock(LoadAccountPort.class);
|
||||||
|
|
||||||
|
private final AccountLock accountLock =
|
||||||
|
Mockito.mock(AccountLock.class);
|
||||||
|
|
||||||
|
private final UpdateAccountStatePort updateAccountStatePort =
|
||||||
|
Mockito.mock(UpdateAccountStatePort.class);
|
||||||
|
|
||||||
|
private final SendMoneyService sendMoneyService =
|
||||||
|
new SendMoneyService(loadAccountPort, accountLock, updateAccountStatePort);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void givenWithdrawalFails_thenOnlySourceAccountIsLockedAndReleased() {
|
||||||
|
|
||||||
|
AccountId sourceAccountId = new AccountId(41L);
|
||||||
|
Account sourceAccount = givenAnAccountWithId(sourceAccountId);
|
||||||
|
|
||||||
|
AccountId targetAccountId = new AccountId(42L);
|
||||||
|
Account targetAccount = givenAnAccountWithId(targetAccountId);
|
||||||
|
|
||||||
|
givenWithdrawalWillFail(sourceAccount);
|
||||||
|
givenDepositWillSucceed(targetAccount);
|
||||||
|
|
||||||
|
SendMoneyCommand command = new SendMoneyCommand(
|
||||||
|
sourceAccountId,
|
||||||
|
targetAccountId,
|
||||||
|
Money.of(300L));
|
||||||
|
|
||||||
|
boolean success = sendMoneyService.sendMoney(command);
|
||||||
|
|
||||||
|
assertThat(success).isFalse();
|
||||||
|
|
||||||
|
then(accountLock).should().lockAccount(eq(sourceAccountId));
|
||||||
|
then(accountLock).should().releaseAccount(eq(sourceAccountId));
|
||||||
|
then(accountLock).should(times(0)).lockAccount(eq(targetAccountId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void transactionSucceeds() {
|
||||||
|
|
||||||
|
AccountId sourceAccountId = new AccountId(41L);
|
||||||
|
Account sourceAccount = givenAnAccountWithId(sourceAccountId);
|
||||||
|
|
||||||
|
AccountId targetAccountId = new AccountId(42L);
|
||||||
|
Account targetAccount = givenAnAccountWithId(targetAccountId);
|
||||||
|
|
||||||
|
givenWithdrawalWillSucceed(sourceAccount);
|
||||||
|
givenDepositWillSucceed(targetAccount);
|
||||||
|
|
||||||
|
Money money = Money.of(500L);
|
||||||
|
|
||||||
|
SendMoneyCommand command = new SendMoneyCommand(
|
||||||
|
sourceAccountId,
|
||||||
|
targetAccountId,
|
||||||
|
money);
|
||||||
|
|
||||||
|
boolean success = sendMoneyService.sendMoney(command);
|
||||||
|
|
||||||
|
assertThat(success).isTrue();
|
||||||
|
|
||||||
|
then(accountLock).should().lockAccount(eq(sourceAccountId));
|
||||||
|
then(sourceAccount).should().withdraw(eq(money), eq(targetAccountId));
|
||||||
|
then(accountLock).should().releaseAccount(eq(sourceAccountId));
|
||||||
|
|
||||||
|
then(accountLock).should().lockAccount(eq(targetAccountId));
|
||||||
|
then(targetAccount).should().deposit(eq(money), eq(sourceAccountId));
|
||||||
|
then(accountLock).should().releaseAccount(eq(targetAccountId));
|
||||||
|
|
||||||
|
thenAccountsHaveBeenUpdated(sourceAccountId, targetAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void thenAccountsHaveBeenUpdated(AccountId... accountIds){
|
||||||
|
ArgumentCaptor<Account> accountCaptor = ArgumentCaptor.forClass(Account.class);
|
||||||
|
then(updateAccountStatePort).should(times(accountIds.length))
|
||||||
|
.updateActivities(accountCaptor.capture());
|
||||||
|
|
||||||
|
List<AccountId> updatedAccountIds = accountCaptor.getAllValues()
|
||||||
|
.stream()
|
||||||
|
.map(Account::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
for(AccountId accountId : accountIds){
|
||||||
|
assertThat(updatedAccountIds).contains(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenDepositWillSucceed(Account account) {
|
||||||
|
given(account.deposit(any(Money.class), any(AccountId.class)))
|
||||||
|
.willReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenWithdrawalWillFail(Account account) {
|
||||||
|
given(account.withdraw(any(Money.class), any(AccountId.class)))
|
||||||
|
.willReturn(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenWithdrawalWillSucceed(Account account) {
|
||||||
|
given(account.withdraw(any(Money.class), any(AccountId.class)))
|
||||||
|
.willReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Account givenAnAccountWithId(AccountId id) {
|
||||||
|
Account account = Mockito.mock(Account.class);
|
||||||
|
given(account.getId())
|
||||||
|
.willReturn(id);
|
||||||
|
given(loadAccountPort.loadAccount(eq(account.getId()), any(LocalDateTime.class)))
|
||||||
|
.willReturn(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static io.reflectoring.cashpal.testdata.AccountTestData.*;
|
||||||
|
import static io.reflectoring.cashpal.testdata.ActivityTestData.*;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
class AccountTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void calculatesBalance() {
|
||||||
|
AccountId accountId = new AccountId(1L);
|
||||||
|
Account account = defaultAccount()
|
||||||
|
.withAccountId(accountId)
|
||||||
|
.withBaselineBalance(Money.of(555L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(999L)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(1L)).build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Money balance = account.calculateBalance();
|
||||||
|
|
||||||
|
assertThat(balance).isEqualTo(Money.of(1555L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withdrawalSucceeds() {
|
||||||
|
AccountId accountId = new AccountId(1L);
|
||||||
|
Account account = defaultAccount()
|
||||||
|
.withAccountId(accountId)
|
||||||
|
.withBaselineBalance(Money.of(555L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(999L)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(1L)).build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
boolean success = account.withdraw(Money.of(555L), new AccountId(99L));
|
||||||
|
|
||||||
|
assertThat(success).isTrue();
|
||||||
|
assertThat(account.getActivityWindow().getActivities()).hasSize(3);
|
||||||
|
assertThat(account.calculateBalance()).isEqualTo(Money.of(1000L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withdrawalFailure() {
|
||||||
|
AccountId accountId = new AccountId(1L);
|
||||||
|
Account account = defaultAccount()
|
||||||
|
.withAccountId(accountId)
|
||||||
|
.withBaselineBalance(Money.of(555L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(999L)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(1L)).build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
boolean success = account.withdraw(Money.of(1556L), new AccountId(99L));
|
||||||
|
|
||||||
|
assertThat(success).isFalse();
|
||||||
|
assertThat(account.getActivityWindow().getActivities()).hasSize(2);
|
||||||
|
assertThat(account.calculateBalance()).isEqualTo(Money.of(1555L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void depositSuccess() {
|
||||||
|
AccountId accountId = new AccountId(1L);
|
||||||
|
Account account = defaultAccount()
|
||||||
|
.withAccountId(accountId)
|
||||||
|
.withBaselineBalance(Money.of(555L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(999L)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withTargetAccount(accountId)
|
||||||
|
.withMoney(Money.of(1L)).build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
boolean success = account.deposit(Money.of(445L), new AccountId(99L));
|
||||||
|
|
||||||
|
assertThat(success).isTrue();
|
||||||
|
assertThat(account.getActivityWindow().getActivities()).hasSize(3);
|
||||||
|
assertThat(account.calculateBalance()).isEqualTo(Money.of(2000L));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package io.reflectoring.cashpal.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static io.reflectoring.cashpal.testdata.ActivityTestData.*;
|
||||||
|
|
||||||
|
class ActivityWindowTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void calculatesStartTimestamp() {
|
||||||
|
ActivityWindow window = new ActivityWindow(
|
||||||
|
defaultActivity().withTimestamp(startDate()).build(),
|
||||||
|
defaultActivity().withTimestamp(inBetweenDate()).build(),
|
||||||
|
defaultActivity().withTimestamp(endDate()).build());
|
||||||
|
|
||||||
|
Assertions.assertThat(window.getStartTimestamp()).isEqualTo(startDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void calculatesEndTimestamp() {
|
||||||
|
ActivityWindow window = new ActivityWindow(
|
||||||
|
defaultActivity().withTimestamp(startDate()).build(),
|
||||||
|
defaultActivity().withTimestamp(inBetweenDate()).build(),
|
||||||
|
defaultActivity().withTimestamp(endDate()).build());
|
||||||
|
|
||||||
|
Assertions.assertThat(window.getEndTimestamp()).isEqualTo(endDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void calculatesBalance() {
|
||||||
|
|
||||||
|
AccountId account1 = new AccountId(1L);
|
||||||
|
AccountId account2 = new AccountId(2L);
|
||||||
|
|
||||||
|
ActivityWindow window = new ActivityWindow(
|
||||||
|
defaultActivity()
|
||||||
|
.withSourceAccount(account1)
|
||||||
|
.withTargetAccount(account2)
|
||||||
|
.withMoney(Money.of(999)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withSourceAccount(account1)
|
||||||
|
.withTargetAccount(account2)
|
||||||
|
.withMoney(Money.of(1)).build(),
|
||||||
|
defaultActivity()
|
||||||
|
.withSourceAccount(account2)
|
||||||
|
.withTargetAccount(account1)
|
||||||
|
.withMoney(Money.of(500)).build());
|
||||||
|
|
||||||
|
Assertions.assertThat(window.calculateBalance(account1)).isEqualTo(Money.of(-500));
|
||||||
|
Assertions.assertThat(window.calculateBalance(account2)).isEqualTo(Money.of(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime startDate() {
|
||||||
|
return LocalDateTime.of(2019, 8, 3, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime inBetweenDate() {
|
||||||
|
return LocalDateTime.of(2019, 8, 4, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime endDate() {
|
||||||
|
return LocalDateTime.of(2019, 8, 5, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ plugins {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation project(':application')
|
implementation project(':cashpal-application')
|
||||||
implementation project(':adapters:persistence')
|
implementation project(':adapters:cashpal-persistence')
|
||||||
implementation project(':adapters:web')
|
implementation project(':adapters:cashpal-web')
|
||||||
|
implementation ('org.springframework.boot:spring-boot-starter-web')
|
||||||
|
|
||||||
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||||
exclude group: 'junit' // excluding junit 4
|
exclude group: 'junit' // excluding junit 4
|
||||||
@@ -19,3 +20,9 @@ dependencies {
|
|||||||
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
|
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
systemProperty 'de.adesso.junitinsights.enabled', 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package io.reflectoring.reviewapp;
|
package io.reflectoring.cashpal;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class BookReviewerApplication {
|
public class CashpalApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(BookReviewerApplication.class, args);
|
SpringApplication.run(CashpalApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp;
|
package io.reflectoring.cashpal;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -7,7 +7,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
|||||||
|
|
||||||
@ExtendWith(SpringExtension.class)
|
@ExtendWith(SpringExtension.class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class BookReviewerApplicationTests {
|
class CashpalApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.reflectoring.reviewapp;
|
package io.reflectoring.cashpal;
|
||||||
|
|
||||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||||
import io.reflectoring.reviewapp.archunit.HexagonalArchitecture;
|
import io.reflectoring.cashpal.archunit.HexagonalArchitecture;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
|
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ class DependencyRuleTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validateRegistrationContextArchitecture() {
|
void validateRegistrationContextArchitecture() {
|
||||||
HexagonalArchitecture.boundedContext("io.reflectoring.reviewapp")
|
HexagonalArchitecture.boundedContext("io.reflectoring.cashpal")
|
||||||
|
|
||||||
.withDomainLayer("domain")
|
.withDomainLayer("domain")
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class DependencyRuleTests {
|
|||||||
|
|
||||||
.withConfiguration("configuration")
|
.withConfiguration("configuration")
|
||||||
.check(new ClassFileImporter()
|
.check(new ClassFileImporter()
|
||||||
.importPackages("io.reflectoring.reviewapp.."));
|
.importPackages("io.reflectoring.cashpal.."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.archunit;
|
package io.reflectoring.cashpal.archunit;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.archunit;
|
package io.reflectoring.cashpal.archunit;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.archunit;
|
package io.reflectoring.cashpal.archunit;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.archunit;
|
package io.reflectoring.cashpal.archunit;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
insert into account (id) values (1);
|
||||||
|
insert into account (id) values (2);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1001, '2018-08-08 08:00:00.0', 1, 1, 2, 500);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1002, '2018-08-08 08:00:00.0', 2, 1, 2, 500);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1003, '2018-08-09 10:00:00.0', 1, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1004, '2018-08-09 10:00:00.0', 2, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1005, '2019-08-09 09:00:00.0', 1, 1, 2, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1006, '2019-08-09 09:00:00.0', 2, 1, 2, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1007, '2019-08-09 10:00:00.0', 1, 2, 1, 1000);
|
||||||
|
|
||||||
|
insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount)
|
||||||
|
values (1008, '2019-08-09 10:00:00.0', 2, 2, 1, 1000);
|
||||||
48
cashpal-testdata/src/main/java/io/reflectoring/cashpal/testdata/AccountTestData.java
vendored
Normal file
48
cashpal-testdata/src/main/java/io/reflectoring/cashpal/testdata/AccountTestData.java
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account;
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.ActivityWindow;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
|
||||||
|
public class AccountTestData {
|
||||||
|
|
||||||
|
public static AccountBuilder defaultAccount() {
|
||||||
|
return new AccountBuilder()
|
||||||
|
.withAccountId(new AccountId(42L))
|
||||||
|
.withBaselineBalance(Money.of(999L))
|
||||||
|
.withActivityWindow(new ActivityWindow(
|
||||||
|
ActivityTestData.defaultActivity().build(),
|
||||||
|
ActivityTestData.defaultActivity().build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class AccountBuilder {
|
||||||
|
|
||||||
|
private AccountId accountId;
|
||||||
|
private Money baselineBalance;
|
||||||
|
private ActivityWindow activityWindow;
|
||||||
|
|
||||||
|
public AccountBuilder withAccountId(AccountId accountId) {
|
||||||
|
this.accountId = accountId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountBuilder withBaselineBalance(Money baselineBalance) {
|
||||||
|
this.baselineBalance = baselineBalance;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountBuilder withActivityWindow(ActivityWindow activityWindow) {
|
||||||
|
this.activityWindow = activityWindow;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account build() {
|
||||||
|
return Account.withId(this.accountId, this.baselineBalance, this.activityWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
69
cashpal-testdata/src/main/java/io/reflectoring/cashpal/testdata/ActivityTestData.java
vendored
Normal file
69
cashpal-testdata/src/main/java/io/reflectoring/cashpal/testdata/ActivityTestData.java
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import io.reflectoring.cashpal.domain.Account.AccountId;
|
||||||
|
import io.reflectoring.cashpal.domain.Activity;
|
||||||
|
import io.reflectoring.cashpal.domain.Activity.ActivityId;
|
||||||
|
import io.reflectoring.cashpal.domain.Money;
|
||||||
|
|
||||||
|
public class ActivityTestData {
|
||||||
|
|
||||||
|
public static ActivityBuilder defaultActivity(){
|
||||||
|
return new ActivityBuilder()
|
||||||
|
.withOwnerAccount(new AccountId(42L))
|
||||||
|
.withSourceAccount(new AccountId(42L))
|
||||||
|
.withTargetAccount(new AccountId(41L))
|
||||||
|
.withTimestamp(LocalDateTime.now())
|
||||||
|
.withMoney(Money.of(999L));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ActivityBuilder {
|
||||||
|
private ActivityId id;
|
||||||
|
private AccountId ownerAccountId;
|
||||||
|
private AccountId sourceAccountId;
|
||||||
|
private AccountId targetAccountId;
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
private Money money;
|
||||||
|
|
||||||
|
public ActivityBuilder withId(ActivityId id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityBuilder withOwnerAccount(AccountId accountId) {
|
||||||
|
this.ownerAccountId = accountId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityBuilder withSourceAccount(AccountId accountId) {
|
||||||
|
this.sourceAccountId = accountId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityBuilder withTargetAccount(AccountId accountId) {
|
||||||
|
this.targetAccountId = accountId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityBuilder withTimestamp(LocalDateTime timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityBuilder withMoney(Money money) {
|
||||||
|
this.money = money;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Activity build() {
|
||||||
|
return new Activity(
|
||||||
|
this.id,
|
||||||
|
this.ownerAccountId,
|
||||||
|
this.sourceAccountId,
|
||||||
|
this.targetAccountId,
|
||||||
|
this.timestamp,
|
||||||
|
this.money);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.common;
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
@@ -9,6 +6,9 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Target({ElementType.TYPE})
|
@Target({ElementType.TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.common;
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
@@ -10,6 +6,9 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Target({ElementType.TYPE})
|
@Target({ElementType.TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.common;
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
import javax.validation.ConstraintViolation;
|
||||||
import javax.validation.ConstraintViolationException;
|
import javax.validation.ConstraintViolationException;
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.common;
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
@@ -9,6 +6,9 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Target({ElementType.TYPE})
|
@Target({ElementType.TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.reflectoring.reviewapp.common;
|
package io.reflectoring.cashpal.testdata;
|
||||||
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
import org.springframework.core.annotation.AliasFor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
include 'common'
|
include 'common'
|
||||||
include 'application'
|
include 'cashpal-configuration'
|
||||||
include 'adapters:persistence'
|
|
||||||
include 'adapters:web'
|
include 'adapters:cashpal-web'
|
||||||
include 'configuration'
|
include 'adapters:cashpal-persistence'
|
||||||
|
include 'cashpal-application'
|
||||||
|
include 'cashpal-testdata'
|
||||||
Reference in New Issue
Block a user