diff --git a/.gitignore b/.gitignore
index ecfb160..ff6bd83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,154 @@
-.DS_Store
-.idea
+# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,gradle,kotlin
+# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,gradle,kotlin
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignore everything but code style settings and run configurations
+# that are supposed to be shared within teams.
+
+.idea/*
+
+!.idea/codeStyles
+!.idea/runConfigurations
+
+### Kotlin ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Gradle ###
.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+### Gradle Patch ###
+# Java heap dump
+*.hprof
+
+# Fixture Monkey
+
+.jqwik-database
+
+# out package exclusion
+
+!src/main/kotlin/com/example/estdelivery/application/port/out
+!src/test/kotlin/com/example/estdelivery/application/port/out
+
+# End of https://www.toptal.com/developers/gitignore/api/intellij+all,gradle,kotlin
diff --git a/README.md b/README.md
index be90e7f..8fe7da8 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,183 @@
-# 2주차
+# 2주차
+
## 이츠 페이먼츠 ( EST's-Payments )
-이스트소프트는 최근 금융사업과 관련하여 송금 서비스를 제공하기로 결정되었다. 각 서비스들은 MSA로 분리되어있다.
-
-베타서비스를 위해, A2개발팀은 송금과 관련된 요구사항이 부여되었고 개발자들은 해당 요구사항을 토대로 개발을 진행해야한다.
+이스트소프트는 최근 금융사업과 관련하여 송금 서비스를 제공하기로 결정되었다. 각 서비스들은 MSA로 분리되어있다.
+
+
+베타서비스를 위해, A2개발팀은 송금과 관련된 요구사항이 부여되었고 개발자들은 해당 요구사항을 토대로 개발을 진행해야한다.
+
+## 요구사항
-## 요구사항
1. 사용자는 돈이 없으면 송금할 수 없다.
2. 동일한 송금은 두번 요청할 수 없다.
3. 송금 후에는 계좌잔액을 노출해야 한다.
4. 송금 내역은 5개월동안 보장되고 그 이후의 내역은 삭제되어야 한다.
+> 송금 이력이 사라지기 때문에 계좌 잔액을 영속화하거나 송금 기록을 제외한 입출금 내역만을 관리해 계좌 잔액을 계산해야 한다.
+
+## 요구사항 정제
+
+- `계좌 번호`를 입력해 `계좌`를 생성한다.
+ - `계좌 번호`는 10자리로 구성되어있다.
+ - `계좌 번호`는 중복되어서는 안된다.
+- `자신 계좌 번호`와 `상대방 계좌 번호` 그리고 `돈의 수량`을 입력해 송금을 진행 한 후 계좌 잔액을 노출한다.
+ - `상대방 계좌 번호`가 존재하지 않으면 안된다.
+ - `자신 계좌 번호`가 존재하지 않으면 안된다.
+ - `자신 계좌 번호`와 `상대방 계좌 번호`가 동일하면 안된다.
+ - `돈의 수량`은 0보다 커야한다.
+ - `자신 계좌`에 잔액이 입력한 `돈의 수량`보다 많아야 한다.
+ - 동일한 송금은 두번 요청할 수 없다.
+ - `자신 계좌`에서 `상대방의 계좌`로 돈을 전달한다.
+ - 송금한 기록을 남긴다. 송금 기록에는 `자신의 계좌 번호`, `상대방 계좌 번호`, `송금한 돈의 수량`, `송금한 일자`가 포함된다.
+ - 송금 후 `자신 계좌` 잔액을 노출한다.
+- `계좌 번호`, `돈의 수량`를 입력해 돈을 입급한다.
+ - `계좌 번호`가 존재하지 않으면 안된다.
+ - `돈의 수량`은 0보다 커야한다.
+- `계좌 번호`, `돈의 수량`를 입력해 돈을 출금한다.
+ - `계좌 번호`가 존재하지 않으면 안된다.
+ - `돈의 수량`은 0보다 커야한다.
+ - `계좌`에 잔액이 입력한 `돈의 수량`보다 많아야 한다.
+
+## 모델링
+
+| 한글 | 영어 | 설명 |
+|-----------|--------------------|-----------------------------------------------------------------|
+| 송금 | Transfer | 송금에는 `송금한 계좌`, `송금받은 계좌`, `송금한 돈의 수량`, `송금한 일자`가 포함된다. |
+| 송금한 계좌 | sourceAccount | 송금하려는 계좌 정보. |
+| 송금받은 계좌 | targetAccount | 송금받는 계좌 정보. |
+| 송금한 돈의 수량 | transferMoney | 송금할 돈의 수량. |
+| 송금한 일자 | transferDate | 송금한 일자는 송금한 날짜. |
+| 계좌 | Account | 계좌에는 `계좌 번호`, `생성 일자 정보`, `잔액 정보`, `송금 이력`을 포함한다. |
+| 계좌 번호 | AccountNumber | 10자리로 구성된 고유한 값. |
+| 생성 일자 | createDate | 계좌를 생성한 날짜. |
+| 잔액 | balance | 계좌에 남은 돈의 수량. |
+| 송금 이력 | transferHistories | 송금 이력에는 `송금한 계좌 번호`, `송금받은 계좌 번호`, `송금한 돈의 수량`, `송금한 일자`가 포함된다. |
+| 돈 | Money | 주고받는 돈의 기본 단위. 돈의 수량은 0 이상이어야 한다. |
+| 입출금 이력 | transactionHistory | 입출금 이력에는 `계좌 번호`, `입출금 타입`, `돈의 수량`, `입출금 일자`가 포함된다. |
+
+### 입금
+
+- `계좌 번호(AccountNumber)`와 `돈의 수량(Money)`을 입력해 돈을 입금한다.
+- `계좌 번호(AccountNumber)`가 존재하지 않으면 안된다.
+- `돈의 수량(Money)`은 0보다 커야한다.
+- `입출금 이력(transactionHisotry)`에 `입금 기록(DEPOSIT)`을 추가한다.
+
+### 출금
+
+- `계좌 번호(AccountNumber)`와 `돈의 수량(Money)`을 입력해 돈을 출금한다.
+- `계좌 번호(AccountNumber)`가 존재하지 않으면 안된다.
+- `돈의 수량(Money)`은 0보다 커야한다.
+- `계좌`에 잔액이 입력한 `돈의 수량(Money)`보다 많아야 한다.
+- `입출금 이력(transactionHisotry)`에 `출금 기록(WITHDRAW)`을 추가한다.
+
+### 송금
+
+- `자신 계좌(sourceAccount)`와 `상대방 계좌(targetAccount)`, `송금 일자(transferDate)` 그리고 `돈의 수량(transferMoney)`을 입력해 송금한다.
+- `상대방 계좌(targetAccount)`가 존재하지 않으면 안된다.
+- `자신 계좌(sourceAccount)`가 존재하지 않으면 안된다.
+- `자신 계좌(sourceAccount)`와 `상대방 계좌(targetAccount)`가 동일하면 안된다.
+- `돈의 수량(transferMoney)`은 0보다 커야한다.
+- `자신 계좌(sourceAccount)`의 `입출금 이력(transactionHisotry)`에는 `출금 기록(WITHDRAW)`을 추가한다.
+- `상대방 계좌(targetAccount)`의 `입출금 이력(transactionHisotry)`에는 `입금 기록(DEPOSIT)`을 추가한다.
+
+### 계좌
+
+- 계좌에는 `계좌 번호(AccountNumber)`, `생성 일자 정보(createDate)`, `잔액 정보(balance)`, `송금 이력(transferHistories)`, `입출금 이력(transactionHisotry)`를 포함한다.
+- `입출금 이력(transactionHisotry)` 에는 `계좌 번호(AccountNumber)`, `입출금 타입(type)`, `돈의 수량(Money)`, `입출금 일자(transactionDate)`가 포함된다.
+- `입출금 이력(transactionHisotry)`으로 `잔액 정보(balance)`를 계산한다.
+- `계좌 번호(AccountNumber)`는 10자리로 구성되어있다.
+- `잔액 정보(balance)`는 0 이상이어야 한다.
+- `송금 이력(transferHistories)` 에는 `송금한 계좌(sourceAccountNumber)`, `송금받은 계좌(targetAccountNumber)`, `송금한 돈의 수량(transferMoney)`, `송금한 일자(transferDate)`가 포함된다.
+
+### 돈(Money)
+
+- `금액 정보(amount)`가 포함된다.
+
+### 클래스 다이어그램
+
+```mermaid
+classDiagram
+ class Withdraw {
+ - Account targetAccount
+ - Money withdrawMoney
+ - Date withdrawDate
+ }
+
+ class Deposit {
+ - Account targetAccount
+ - Money depositMoney
+ - Date depositDate
+ }
+
+ class Transfer {
+ - Account sourceAccount
+ - Account targetAccount
+ - money transferMoney
+ - Date transferDate
+ }
+
+ class Account {
+ - AccountNumber number
+ - Money balance
+ - List~TransferHistory~ transferHistories
+ - Date createdDate
+ }
+
+ class AccountNumber {
+ - String number
+ }
+
+ class TransferHistory {
+ - AccountNumber sourceAccountNumber
+ - AccountNumber targetAccountNumber
+ - Money transferMoney
+ - TransferDate transferDate
+ }
+
+ class Money {
+ - int amount
+ }
+
+ Withdraw *-- Account
+ Withdraw *-- Money
+ Deposit *-- Account
+ Deposit *-- Money
+ Account *-- AccountNumber
+ Account *-- Money
+ Account *-- TransferHistory
+ Transfer *-- Money
+ Transfer *-- Account
+ TransferHistory *-- Money
+ TransferHistory *-- AccountNumber
+```
+
+## erd
+
+```mermaid
+erDiagram
+ transfer_history {
+ id long pk
+ source_account_number varchar(200)
+ target_account_number varchar(200)
+ transfer_money int
+ trasfer_date date
+ }
+
+ account {
+ id long pk
+ account_number varchar(200)
+ created_date date
+ }
+
+ account_transaction {
+ id long pk
+ account_id long fk
+ type enum "WITHDRAW, DEPOSIT"
+ money int
+ transaction_date date
+ }
+
+ account ||--o{ transfer_history: has
+ account ||--o{ account_transaction: contains
+```
diff --git a/build.gradle.kts b/build.gradle.kts
index c1d8c88..b13c770 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,12 +24,16 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")
testImplementation("io.kotest", "kotest-runner-junit5", "5.4.0")
+ testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter-kotlin:1.0.13")
+ testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jakarta-validation:1.0.0")
+ testImplementation("io.mockk:mockk:1.13.9")
}
tasks.withType {
diff --git a/src/main/kotlin/com/example/estdelivery/application/CreateAccountService.kt b/src/main/kotlin/com/example/estdelivery/application/CreateAccountService.kt
new file mode 100644
index 0000000..e1170c8
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/CreateAccountService.kt
@@ -0,0 +1,27 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.application.port.`in`.CreateAccountUseCase
+import com.example.estdelivery.application.port.`in`.command.CreateAccountCommand
+import com.example.estdelivery.application.port.out.CreateAccountPort
+import com.example.estdelivery.application.port.out.GenerateAccountNumberPort
+import com.example.estdelivery.domain.Account
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.domain.TransferHistories
+import org.springframework.stereotype.Service
+
+@Service
+class CreateAccountService(
+ private val createAccountPort: CreateAccountPort,
+ private val generateAccountNumber: GenerateAccountNumberPort,
+) : CreateAccountUseCase {
+ /**
+ * 1. 계좌를 생성한다.
+ * 2. 계좌 정보를 저장한다.
+ */
+ override fun createAccount(command: CreateAccountCommand) {
+ val accountNumber = generateAccountNumber.generate()
+ val newAccount =
+ Account(accountNumber, TransferHistories(), AccountTransactions(), command.createTime)
+ createAccountPort.create(newAccount)
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/DepositService.kt b/src/main/kotlin/com/example/estdelivery/application/DepositService.kt
new file mode 100644
index 0000000..1460651
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/DepositService.kt
@@ -0,0 +1,30 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.application.port.`in`.AccountResponse
+import com.example.estdelivery.application.port.`in`.DepositUseCase
+import com.example.estdelivery.application.port.`in`.command.DepositCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.domain.Deposit
+import org.springframework.stereotype.Service
+
+@Service
+class DepositService(
+ private val loadAccountPort: LoadAccountPort,
+ private val updateAccountPort: UpdateAccountPort
+) : DepositUseCase {
+ /**
+ * 1. 계좌를 조회한다.
+ * 2. 계좌에 입금한다.
+ * 3. 계좌 정보를 업데이트한다.
+ *
+ * @param command 입금 명령
+ */
+ override fun deposit(command: DepositCommand): AccountResponse {
+ val account = loadAccountPort.findByAccountNumber(command.accountNumber)
+ val depositCommand = Deposit(account, command.amount, command.depositTime)
+ depositCommand.deposit()
+ updateAccountPort.update(account)
+ return AccountResponse(account.balance())
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/TransferService.kt b/src/main/kotlin/com/example/estdelivery/application/TransferService.kt
new file mode 100644
index 0000000..b016741
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/TransferService.kt
@@ -0,0 +1,31 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.application.port.`in`.AccountResponse
+import com.example.estdelivery.application.port.`in`.TransferUseCase
+import com.example.estdelivery.application.port.`in`.command.TransferCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.domain.Transfer
+import org.springframework.stereotype.Service
+
+@Service
+class TransferService(
+ private val loadAccountPort: LoadAccountPort,
+ private val updateAccountPort: UpdateAccountPort
+) : TransferUseCase {
+ /**
+ * 1. 두 계좌를 조회한다.
+ * 2. 계좌를 이체한다.
+ * 3. 계좌 정보를 업데이트한다.
+ *
+ * @param command 이체 명령
+ */
+ override fun transfer(command: TransferCommand): AccountResponse {
+ val sourceAccount = loadAccountPort.findByAccountNumber(command.source)
+ val targetAccount = loadAccountPort.findByAccountNumber(command.target)
+ Transfer(sourceAccount, targetAccount, command.amount, command.transferTime).transfer()
+ updateAccountPort.update(sourceAccount)
+ updateAccountPort.update(targetAccount)
+ return AccountResponse(sourceAccount.balance())
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/WithdrawService.kt b/src/main/kotlin/com/example/estdelivery/application/WithdrawService.kt
new file mode 100644
index 0000000..a8bd6f9
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/WithdrawService.kt
@@ -0,0 +1,30 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.application.port.`in`.AccountResponse
+import com.example.estdelivery.application.port.`in`.WithdrawUseCase
+import com.example.estdelivery.application.port.`in`.command.WithdrawCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.domain.Withdrawal
+import org.springframework.stereotype.Service
+
+@Service
+class WithdrawService(
+ private val loadAccountPort: LoadAccountPort,
+ private val updateAccountPort: UpdateAccountPort
+) : WithdrawUseCase {
+ /**
+ * 1. 계좌를 조회한다.
+ * 2. 계좌에서 출금한다.
+ * 3. 계좌 정보를 업데이트한다.
+ *
+ * @param command 출금 명령
+ */
+ override fun withdraw(command: WithdrawCommand): AccountResponse {
+ val account = loadAccountPort.findByAccountNumber(command.accountNumber)
+ val withdrawalCommand = Withdrawal(account, command.amount, command.withdrawalTime)
+ withdrawalCommand.withdraw()
+ updateAccountPort.update(account)
+ return AccountResponse(account.balance())
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/AccountResponse.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/AccountResponse.kt
new file mode 100644
index 0000000..396ab09
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/AccountResponse.kt
@@ -0,0 +1,7 @@
+package com.example.estdelivery.application.port.`in`
+
+import com.example.estdelivery.domain.Money
+
+class AccountResponse(
+ val balance: Money
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/CreateAccountUseCase.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/CreateAccountUseCase.kt
new file mode 100644
index 0000000..ad86629
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/CreateAccountUseCase.kt
@@ -0,0 +1,12 @@
+package com.example.estdelivery.application.port.`in`
+
+import com.example.estdelivery.application.port.`in`.command.CreateAccountCommand
+
+interface CreateAccountUseCase {
+ /**
+ * 계좌를 생성한다. 생성되는 계좌는 고유하며 잔액은 0원이다.
+ *
+ * @param command
+ */
+ fun createAccount(command: CreateAccountCommand)
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/DepositUseCase.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/DepositUseCase.kt
new file mode 100644
index 0000000..a592361
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/DepositUseCase.kt
@@ -0,0 +1,11 @@
+package com.example.estdelivery.application.port.`in`
+
+import com.example.estdelivery.application.port.`in`.command.DepositCommand
+
+interface DepositUseCase {
+ /**
+ * 계좌에 돈을 입금한다.
+ * @param command
+ */
+ fun deposit(command: DepositCommand) : AccountResponse
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/TransferUseCase.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/TransferUseCase.kt
new file mode 100644
index 0000000..b4c73a0
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/TransferUseCase.kt
@@ -0,0 +1,11 @@
+package com.example.estdelivery.application.port.`in`
+
+import com.example.estdelivery.application.port.`in`.command.TransferCommand
+
+interface TransferUseCase {
+ /**
+ * 계좌 간 돈을 이체한다. 이체할 계좌 잔액은 이체할 금액보다 많아야 한다.
+ * @param command
+ */
+ fun transfer(command: TransferCommand) : AccountResponse
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/WithdrawUseCase.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/WithdrawUseCase.kt
new file mode 100644
index 0000000..16bba87
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/WithdrawUseCase.kt
@@ -0,0 +1,12 @@
+package com.example.estdelivery.application.port.`in`
+
+import com.example.estdelivery.application.port.`in`.command.WithdrawCommand
+
+interface WithdrawUseCase {
+ /**
+ * 돈을 출금한다. 계좌 잔액은 출금 금액보다 많거나 같아야 한다.
+ *
+ * @param command 출금 명령
+ */
+ fun withdraw(command: WithdrawCommand) : AccountResponse
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/command/CreateAccountCommand.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/command/CreateAccountCommand.kt
new file mode 100644
index 0000000..08330b7
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/command/CreateAccountCommand.kt
@@ -0,0 +1,9 @@
+package com.example.estdelivery.application.port.`in`.command
+
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+data class CreateAccountCommand(
+ @field:PastOrPresent
+ val createTime: LocalDateTime
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/command/DepositCommand.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/command/DepositCommand.kt
new file mode 100644
index 0000000..56d58ca
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/command/DepositCommand.kt
@@ -0,0 +1,13 @@
+package com.example.estdelivery.application.port.`in`.command
+
+import com.example.estdelivery.domain.AccountNumber
+import com.example.estdelivery.domain.Money
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+data class DepositCommand(
+ val accountNumber: AccountNumber,
+ val amount: Money,
+ @field:PastOrPresent
+ val depositTime: LocalDateTime
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/command/TransferCommand.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/command/TransferCommand.kt
new file mode 100644
index 0000000..28cd7d2
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/command/TransferCommand.kt
@@ -0,0 +1,12 @@
+package com.example.estdelivery.application.port.`in`.command
+
+import com.example.estdelivery.domain.AccountNumber
+import com.example.estdelivery.domain.Money
+import java.time.LocalDateTime
+
+data class TransferCommand(
+ val source: AccountNumber,
+ val target: AccountNumber,
+ val amount: Money,
+ val transferTime: LocalDateTime
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/command/WithdrawCommand.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/command/WithdrawCommand.kt
new file mode 100644
index 0000000..6052d1d
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/command/WithdrawCommand.kt
@@ -0,0 +1,13 @@
+package com.example.estdelivery.application.port.`in`.command
+
+import com.example.estdelivery.domain.AccountNumber
+import com.example.estdelivery.domain.Money
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+data class WithdrawCommand(
+ val accountNumber: AccountNumber,
+ val amount: Money,
+ @field:PastOrPresent
+ val withdrawalTime: LocalDateTime
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/in/web/AccountController.kt b/src/main/kotlin/com/example/estdelivery/application/port/in/web/AccountController.kt
new file mode 100644
index 0000000..4ec6808
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/in/web/AccountController.kt
@@ -0,0 +1,36 @@
+package com.example.estdelivery.application.port.`in`.web
+
+import com.example.estdelivery.application.port.`in`.CreateAccountUseCase
+import com.example.estdelivery.application.port.`in`.DepositUseCase
+import com.example.estdelivery.application.port.`in`.TransferUseCase
+import com.example.estdelivery.application.port.`in`.WithdrawUseCase
+import com.example.estdelivery.application.port.`in`.command.CreateAccountCommand
+import com.example.estdelivery.application.port.`in`.command.DepositCommand
+import com.example.estdelivery.application.port.`in`.command.TransferCommand
+import com.example.estdelivery.application.port.`in`.command.WithdrawCommand
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/accounts")
+class AccountController(
+ private val createAccountUseCase: CreateAccountUseCase,
+ private val depositUseCase: DepositUseCase,
+ private val withdrawUseCase: WithdrawUseCase,
+ private val transferUseCase: TransferUseCase
+) {
+ @PostMapping
+ fun createAccount(createAccountCommand: CreateAccountCommand) {
+ createAccountUseCase.createAccount(createAccountCommand)
+ }
+
+ @PostMapping("/deposit")
+ fun deposit(depositCommand: DepositCommand) = depositUseCase.deposit(depositCommand)
+
+ @PostMapping("/withdraw")
+ fun withdraw(withdrawCommand: WithdrawCommand) = withdrawUseCase.withdraw(withdrawCommand)
+
+ @PostMapping("/transfer")
+ fun transfer(transferCommand: TransferCommand) = transferUseCase.transfer(transferCommand)
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/CreateAccountPort.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/CreateAccountPort.kt
new file mode 100644
index 0000000..70c5127
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/CreateAccountPort.kt
@@ -0,0 +1,7 @@
+package com.example.estdelivery.application.port.out
+
+import com.example.estdelivery.domain.Account
+
+interface CreateAccountPort {
+ fun create(account: Account)
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/GenerateAccountNumberPort.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/GenerateAccountNumberPort.kt
new file mode 100644
index 0000000..52889f3
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/GenerateAccountNumberPort.kt
@@ -0,0 +1,7 @@
+package com.example.estdelivery.application.port.out
+
+import com.example.estdelivery.domain.AccountNumber
+
+interface GenerateAccountNumberPort {
+ fun generate(): AccountNumber
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/LoadAccountPort.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/LoadAccountPort.kt
new file mode 100644
index 0000000..3840599
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/LoadAccountPort.kt
@@ -0,0 +1,9 @@
+package com.example.estdelivery.application.port.out
+
+import com.example.estdelivery.domain.Account
+import com.example.estdelivery.domain.AccountNumber
+
+interface LoadAccountPort {
+ fun existsByAccountNumber(accountNumber: AccountNumber): Boolean
+ fun findByAccountNumber(accountNumber: AccountNumber): Account
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/UpdateAccountPort.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/UpdateAccountPort.kt
new file mode 100644
index 0000000..5b4809c
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/UpdateAccountPort.kt
@@ -0,0 +1,7 @@
+package com.example.estdelivery.application.port.out
+
+import com.example.estdelivery.domain.Account
+
+interface UpdateAccountPort {
+ fun update(account: Account)
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/AccountAdapter.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/AccountAdapter.kt
new file mode 100644
index 0000000..0d4c3c9
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/AccountAdapter.kt
@@ -0,0 +1,35 @@
+package com.example.estdelivery.application.port.out.persistence
+
+import com.example.estdelivery.application.port.out.CreateAccountPort
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.application.port.out.persistence.mapper.fromAccountEntity
+import com.example.estdelivery.application.port.out.persistence.mapper.toAccountEntity
+import com.example.estdelivery.application.port.out.persistence.infra.AccountEntityRepository
+import com.example.estdelivery.domain.Account
+import com.example.estdelivery.domain.AccountNumber
+import jakarta.transaction.Transactional
+import org.springframework.stereotype.Component
+
+@Component
+class AccountAdapter(
+ private val accountEntityRepository: AccountEntityRepository
+) : CreateAccountPort, LoadAccountPort, UpdateAccountPort {
+ @Transactional
+ override fun create(account: Account) {
+ accountEntityRepository.save(toAccountEntity(account))
+ }
+
+ @Transactional
+ override fun update(account: Account) {
+ accountEntityRepository.save(toAccountEntity(account))
+ }
+
+ override fun existsByAccountNumber(accountNumber: AccountNumber) =
+ accountEntityRepository.existsByAccountNumber(accountNumber)
+
+ override fun findByAccountNumber(accountNumber: AccountNumber) =
+ fromAccountEntity(
+ accountEntityRepository.findByAccountNumber(accountNumber) ?: throw NoSuchElementException("Account not found")
+ )
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/GenerateAccountNumberAdapter.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/GenerateAccountNumberAdapter.kt
new file mode 100644
index 0000000..04aa139
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/GenerateAccountNumberAdapter.kt
@@ -0,0 +1,21 @@
+package com.example.estdelivery.application.port.out.persistence
+
+import com.example.estdelivery.application.port.out.GenerateAccountNumberPort
+import com.example.estdelivery.application.port.out.persistence.infra.AccountEntityRepository
+import com.example.estdelivery.application.port.out.persistence.infra.AccountNumberSequence
+import com.example.estdelivery.domain.AccountNumber
+import org.springframework.stereotype.Component
+
+@Component
+class GenerateAccountNumberAdapter(
+ private val accountNumberSequence: AccountNumberSequence,
+ private val accountEntityRepository: AccountEntityRepository
+) : GenerateAccountNumberPort {
+ override fun generate(): AccountNumber {
+ var accountNumber: AccountNumber
+ do {
+ accountNumber = AccountNumber(accountNumberSequence.next())
+ } while (accountEntityRepository.existsByAccountNumber(accountNumber))
+ return accountNumber
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountEntity.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountEntity.kt
new file mode 100644
index 0000000..bac432f
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountEntity.kt
@@ -0,0 +1,31 @@
+package com.example.estdelivery.application.port.out.persistence.entity
+
+import com.example.estdelivery.domain.AccountNumber
+import jakarta.persistence.Column
+import jakarta.persistence.Embedded
+import jakarta.persistence.Entity
+import jakarta.persistence.GeneratedValue
+import jakarta.persistence.GenerationType
+import jakarta.persistence.Id
+import jakarta.persistence.JoinColumn
+import jakarta.persistence.OneToMany
+import jakarta.persistence.Table
+import java.time.LocalDateTime
+
+@Entity
+@Table(name = "account")
+class AccountEntity(
+ @Embedded
+ val accountNumber: AccountNumber,
+ @OneToMany
+ @JoinColumn(name = "account_id")
+ val transferHistories: List,
+ @OneToMany
+ @JoinColumn(name = "account_id")
+ val transactions: List,
+ val createdDate: LocalDateTime,
+ @Id
+ @Column(name = "account_id")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long?
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountTransactionEntity.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountTransactionEntity.kt
new file mode 100644
index 0000000..c506099
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/AccountTransactionEntity.kt
@@ -0,0 +1,24 @@
+package com.example.estdelivery.application.port.out.persistence.entity
+
+import com.example.estdelivery.domain.Money
+import com.example.estdelivery.domain.TransactionType
+import jakarta.persistence.Embedded
+import jakarta.persistence.Entity
+import jakarta.persistence.EnumType
+import jakarta.persistence.Enumerated
+import jakarta.persistence.GeneratedValue
+import jakarta.persistence.GenerationType
+import jakarta.persistence.Id
+import java.time.LocalDateTime
+
+@Entity
+class AccountTransactionEntity(
+ @Embedded
+ val amount: Money,
+ @Enumerated(EnumType.STRING)
+ val type: TransactionType,
+ val transactionDateTime: LocalDateTime,
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long? = null,
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/TransferHistoryEntity.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/TransferHistoryEntity.kt
new file mode 100644
index 0000000..97873c1
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/entity/TransferHistoryEntity.kt
@@ -0,0 +1,29 @@
+package com.example.estdelivery.application.port.out.persistence.entity
+
+import com.example.estdelivery.domain.AccountNumber
+import com.example.estdelivery.domain.Money
+import jakarta.persistence.AttributeOverride
+import jakarta.persistence.Column
+import jakarta.persistence.Embedded
+import jakarta.persistence.Entity
+import jakarta.persistence.GeneratedValue
+import jakarta.persistence.GenerationType
+import jakarta.persistence.Id
+import java.time.LocalDateTime
+
+@Entity
+class TransferHistoryEntity(
+ @Embedded
+ @AttributeOverride(name = "accountNumber", column = Column(name = "source_account_number"))
+ val sourceAccountNumber: AccountNumber,
+ @Embedded
+ @AttributeOverride(name = "accountNumber", column = Column(name = "target_account_number"))
+ val targetAccountNumber: AccountNumber,
+ @Embedded
+ val amount: Money,
+ val transferDate: LocalDateTime,
+ @Id
+ @Column(name = "transfer_id")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long? = null
+)
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountEntityRepository.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountEntityRepository.kt
new file mode 100644
index 0000000..c438492
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountEntityRepository.kt
@@ -0,0 +1,10 @@
+package com.example.estdelivery.application.port.out.persistence.infra
+
+import com.example.estdelivery.application.port.out.persistence.entity.AccountEntity
+import com.example.estdelivery.domain.AccountNumber
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface AccountEntityRepository: JpaRepository {
+ fun existsByAccountNumber(accountNumber: AccountNumber): Boolean
+ fun findByAccountNumber(accountNumber: AccountNumber): AccountEntity?
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountNumberSequence.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountNumberSequence.kt
new file mode 100644
index 0000000..b860231
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/infra/AccountNumberSequence.kt
@@ -0,0 +1,19 @@
+package com.example.estdelivery.application.port.out.persistence.infra
+
+import org.springframework.stereotype.Component
+import kotlin.random.Random
+
+@Component
+class AccountNumberSequence {
+ /**
+ * 숫자 10 자리를 포함한 문자를 반환한다.
+ */
+ fun next(): String {
+ val random = Random(System.currentTimeMillis())
+ val sb = StringBuilder(10)
+ for (i in 0 until 10) {
+ sb.append(random.nextInt(10))
+ }
+ return sb.toString()
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/mapper/AccountMapper.kt b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/mapper/AccountMapper.kt
new file mode 100644
index 0000000..4c16642
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/application/port/out/persistence/mapper/AccountMapper.kt
@@ -0,0 +1,61 @@
+package com.example.estdelivery.application.port.out.persistence.mapper
+
+import com.example.estdelivery.application.port.out.persistence.entity.AccountEntity
+import com.example.estdelivery.application.port.out.persistence.entity.AccountTransactionEntity
+import com.example.estdelivery.application.port.out.persistence.entity.TransferHistoryEntity
+import com.example.estdelivery.domain.Account
+import com.example.estdelivery.domain.AccountTransaction
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.domain.TransferHistories
+import com.example.estdelivery.domain.TransferHistory
+
+fun toAccountEntity(newAccount: Account): AccountEntity {
+ return AccountEntity(
+ accountNumber = newAccount.showNumber(),
+ transferHistories = newAccount.showHistories().map { toTransferHistoryEntity(it) },
+ transactions = newAccount.showTransactions().map { toAccountTransactionEntity(it) },
+ createdDate = newAccount.showCreatedDate(),
+ id = newAccount.id
+ )
+}
+
+fun fromAccountEntity(accountEntity: AccountEntity): Account {
+ return Account(
+ number = accountEntity.accountNumber,
+ transferHistories = TransferHistories(accountEntity.transferHistories.map { fromTransferHistoryEntity(it) }),
+ transactions = AccountTransactions(accountEntity.transactions.map { fromAccountTransactionEntity(it) }),
+ createdDate = accountEntity.createdDate,
+ id = accountEntity.id
+ )
+}
+
+private fun toAccountTransactionEntity(it: AccountTransaction) =
+ AccountTransactionEntity(
+ amount = it.showAmount(),
+ type = it.showType(),
+ transactionDateTime = it.showTransactionTime()
+ )
+
+private fun fromAccountTransactionEntity(it: AccountTransactionEntity) =
+ AccountTransaction(
+ amount = it.amount,
+ type = it.type,
+ transactionDateTime = it.transactionDateTime
+ )
+
+private fun toTransferHistoryEntity(it: TransferHistory) =
+ TransferHistoryEntity(
+ amount = it.showAmount(),
+ sourceAccountNumber = it.showSource(),
+ targetAccountNumber = it.showTarget(),
+ transferDate = it.showTransferDate()
+ )
+
+private fun fromTransferHistoryEntity(it: TransferHistoryEntity) =
+ TransferHistory(
+ money = it.amount,
+ source = it.sourceAccountNumber,
+ target = it.targetAccountNumber,
+ transferDate = it.transferDate
+ )
+
diff --git a/src/main/kotlin/com/example/estdelivery/domain/Account.kt b/src/main/kotlin/com/example/estdelivery/domain/Account.kt
new file mode 100644
index 0000000..eaa6efd
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/Account.kt
@@ -0,0 +1,42 @@
+package com.example.estdelivery.domain
+
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+class Account(
+ private val number: AccountNumber,
+ private val transferHistories: TransferHistories,
+ private val transactions: AccountTransactions,
+ @field:PastOrPresent
+ private val createdDate: LocalDateTime,
+ val id: Long? = null
+) {
+ fun balance() = transactions.balance()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Account
+
+ return id == other.id
+ }
+
+ override fun hashCode() = id?.hashCode() ?: 0
+
+ override fun toString() =
+ "Account(number=$number, balance=${balance()}, transferHistories=$transferHistories, createdDate=$createdDate, id=$id)"
+
+ fun deposit(amount: Money, transactionTime: LocalDateTime) {
+ transactions.deposit(amount, transactionTime)
+ }
+
+ fun withdraw(amount: Money, transactionTime: LocalDateTime) {
+ transactions.withdraw(amount, transactionTime)
+ }
+
+ fun showTransactions() = transactions.showTransactions()
+ fun showHistories() = transferHistories.showHistories()
+ fun showNumber() = number
+ fun showCreatedDate() = createdDate
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/AccountNumber.kt b/src/main/kotlin/com/example/estdelivery/domain/AccountNumber.kt
new file mode 100644
index 0000000..971f059
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/AccountNumber.kt
@@ -0,0 +1,10 @@
+package com.example.estdelivery.domain
+
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+
+data class AccountNumber(
+ @field:Size(min = 10, max = 10)
+ @field:Pattern(regexp = "^[0-9]*$")
+ private val accountNumber: String
+)
diff --git a/src/main/kotlin/com/example/estdelivery/domain/AccountTransaction.kt b/src/main/kotlin/com/example/estdelivery/domain/AccountTransaction.kt
new file mode 100644
index 0000000..d192017
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/AccountTransaction.kt
@@ -0,0 +1,15 @@
+package com.example.estdelivery.domain
+
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+data class AccountTransaction(
+ val amount: Money,
+ val type: TransactionType,
+ @field:PastOrPresent
+ val transactionDateTime: LocalDateTime,
+) {
+ fun showAmount() = amount
+ fun showTransactionTime() = transactionDateTime
+ fun showType() = type
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/AccountTransactions.kt b/src/main/kotlin/com/example/estdelivery/domain/AccountTransactions.kt
new file mode 100644
index 0000000..a14fefe
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/AccountTransactions.kt
@@ -0,0 +1,26 @@
+package com.example.estdelivery.domain
+
+import java.time.LocalDateTime
+
+class AccountTransactions(
+ private var accountTransactions: List = listOf()
+) {
+ fun deposit(amount: Money, transactionTime: LocalDateTime) {
+ accountTransactions = accountTransactions + AccountTransaction(amount, TransactionType.DEPOSIT, transactionTime)
+ }
+
+ fun withdraw(amount: Money, transactionTime: LocalDateTime) {
+ accountTransactions =
+ accountTransactions + AccountTransaction(amount, TransactionType.WITHDRAWAL, transactionTime)
+ }
+
+ fun balance() = accountTransactions.sortedBy { it.transactionDateTime }
+ .fold(Money.ZERO) { acc, accountTransaction ->
+ when (accountTransaction.type) {
+ TransactionType.DEPOSIT -> acc + accountTransaction.amount
+ TransactionType.WITHDRAWAL -> acc - accountTransaction.amount
+ }
+ }
+
+ fun showTransactions() = accountTransactions
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/Deposit.kt b/src/main/kotlin/com/example/estdelivery/domain/Deposit.kt
new file mode 100644
index 0000000..b5f3659
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/Deposit.kt
@@ -0,0 +1,13 @@
+package com.example.estdelivery.domain
+
+import java.time.LocalDateTime
+
+class Deposit(
+ private val account: Account,
+ private val amount: Money,
+ private val transactionTime: LocalDateTime
+) {
+ fun deposit() {
+ account.deposit(amount, transactionTime)
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/Money.kt b/src/main/kotlin/com/example/estdelivery/domain/Money.kt
new file mode 100644
index 0000000..b5490f7
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/Money.kt
@@ -0,0 +1,23 @@
+package com.example.estdelivery.domain
+
+import jakarta.validation.constraints.PositiveOrZero
+import java.math.BigInteger
+
+data class Money(
+ @field:PositiveOrZero
+ private val amount: BigInteger
+) {
+ init {
+ require(amount >= BigInteger.ZERO) { "금액은 0원 이상이어야 합니다." }
+ }
+
+ companion object {
+ val ZERO: Money = Money(BigInteger.ZERO)
+ }
+
+ operator fun plus(amount: Money) = Money(this.amount.add(amount.amount))
+
+ operator fun minus(amount: Money) = Money(this.amount.subtract(amount.amount))
+
+ operator fun compareTo(amount: Money) = this.amount.compareTo(amount.amount)
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/TransactionType.kt b/src/main/kotlin/com/example/estdelivery/domain/TransactionType.kt
new file mode 100644
index 0000000..8b1eb40
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/TransactionType.kt
@@ -0,0 +1,6 @@
+package com.example.estdelivery.domain
+
+enum class TransactionType {
+ WITHDRAWAL,
+ DEPOSIT
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/Transfer.kt b/src/main/kotlin/com/example/estdelivery/domain/Transfer.kt
new file mode 100644
index 0000000..6f70298
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/Transfer.kt
@@ -0,0 +1,15 @@
+package com.example.estdelivery.domain
+
+import java.time.LocalDateTime
+
+class Transfer(
+ private val source: Account,
+ private val target: Account,
+ private val amount: Money,
+ private val transactionTime: LocalDateTime
+) {
+ fun transfer() {
+ Withdrawal(source, amount, transactionTime).withdraw()
+ Deposit(target, amount, transactionTime).deposit()
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/TransferHistories.kt b/src/main/kotlin/com/example/estdelivery/domain/TransferHistories.kt
new file mode 100644
index 0000000..799f2e0
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/TransferHistories.kt
@@ -0,0 +1,19 @@
+package com.example.estdelivery.domain
+
+class TransferHistories(
+ private var transferHistories: List = listOf()
+) {
+ fun showHistories(): List {
+ return transferHistories
+ }
+
+ fun addHistory(transferHistory: TransferHistory) {
+ transferHistories = transferHistories + transferHistory
+ }
+
+ fun removeHistory(transferHistory: TransferHistory) {
+ transferHistories = transferHistories - transferHistory
+ }
+
+ override fun toString() = "TransferHistories(transferHistories=$transferHistories)"
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/TransferHistory.kt b/src/main/kotlin/com/example/estdelivery/domain/TransferHistory.kt
new file mode 100644
index 0000000..84c028a
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/TransferHistory.kt
@@ -0,0 +1,21 @@
+package com.example.estdelivery.domain
+
+import jakarta.validation.constraints.PastOrPresent
+import java.time.LocalDateTime
+
+data class TransferHistory(
+ private val source: AccountNumber,
+ private val target: AccountNumber,
+ private val money: Money,
+ @field:PastOrPresent
+ private val transferDate: LocalDateTime
+) {
+ fun showAmount()= money
+ fun showSource() = source
+ fun showTarget() = target
+ fun showTransferDate() = transferDate
+
+ init {
+ require(source != target) { "출금 계좌와 입금 계좌는 같을 수 없습니다." }
+ }
+}
diff --git a/src/main/kotlin/com/example/estdelivery/domain/Withdrawal.kt b/src/main/kotlin/com/example/estdelivery/domain/Withdrawal.kt
new file mode 100644
index 0000000..c7cbdfd
--- /dev/null
+++ b/src/main/kotlin/com/example/estdelivery/domain/Withdrawal.kt
@@ -0,0 +1,17 @@
+package com.example.estdelivery.domain
+
+import java.time.LocalDateTime
+
+class Withdrawal(
+ private val account: Account,
+ private val amount: Money,
+ private val transactionTime: LocalDateTime
+) {
+ init {
+ require(account.balance() >= amount) { "잔액이 부족합니다." }
+ }
+
+ fun withdraw() {
+ account.withdraw(amount, transactionTime)
+ }
+}
diff --git a/src/test/kotlin/com/example/estdelivery/FixtureMonkeyUtil.kt b/src/test/kotlin/com/example/estdelivery/FixtureMonkeyUtil.kt
new file mode 100644
index 0000000..5bb6466
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/FixtureMonkeyUtil.kt
@@ -0,0 +1,34 @@
+package com.example.estdelivery
+
+import com.example.estdelivery.application.port.`in`.command.CreateAccountCommand
+import com.example.estdelivery.application.port.`in`.command.DepositCommand
+import com.example.estdelivery.domain.Account
+import com.example.estdelivery.domain.AccountNumber
+import com.example.estdelivery.domain.AccountTransaction
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.domain.Money
+import com.example.estdelivery.domain.TransferHistories
+import com.example.estdelivery.domain.TransferHistory
+import com.navercorp.fixturemonkey.FixtureMonkey
+import com.navercorp.fixturemonkey.api.introspector.ConstructorPropertiesArbitraryIntrospector
+import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin
+import com.navercorp.fixturemonkey.kotlin.KotlinPlugin
+import com.navercorp.fixturemonkey.kotlin.giveMeBuilder
+import net.jqwik.api.Arbitraries
+import java.time.LocalDateTime
+
+private val fixtureMonkey: FixtureMonkey = FixtureMonkey.builder()
+ .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
+ .plugin(KotlinPlugin())
+ .plugin(JakartaValidationPlugin())
+ .build()
+
+fun accountCommandArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun moneyArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun accountArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun accountNumberArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun transferHistoryArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun transferHistoriesArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun accountTransactionArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun accountTransactionsArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
+fun depositCommandArbitraryBuilder() = fixtureMonkey.giveMeBuilder()
diff --git a/src/test/kotlin/com/example/estdelivery/application/CreateAccountServiceTest.kt b/src/test/kotlin/com/example/estdelivery/application/CreateAccountServiceTest.kt
new file mode 100644
index 0000000..04c7444
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/application/CreateAccountServiceTest.kt
@@ -0,0 +1,36 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.accountCommandArbitraryBuilder
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.application.port.out.CreateAccountPort
+import com.example.estdelivery.application.port.out.GenerateAccountNumberPort
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.core.spec.style.FreeSpec
+import io.mockk.every
+import io.mockk.mockk
+
+class CreateAccountServiceTest : FreeSpec({
+
+ lateinit var createAccountPort: CreateAccountPort
+ lateinit var generateAccountNumberPort: GenerateAccountNumberPort
+ lateinit var createAccountService: CreateAccountService
+
+ beforeTest {
+ createAccountPort = mockk()
+ generateAccountNumberPort = mockk()
+ createAccountService = CreateAccountService(createAccountPort, generateAccountNumberPort)
+ }
+
+ "계좌를 생성한다." {
+ val accountNumber = accountNumberArbitraryBuilder().sample()
+ val accountCommand = accountCommandArbitraryBuilder().sample()
+
+ every { generateAccountNumberPort.generate() } returns accountNumber
+ every { createAccountPort.create(any()) } returns Unit
+
+ // then
+ shouldNotThrowAny {
+ createAccountService.createAccount(accountCommand)
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/application/DepositServiceTest.kt b/src/test/kotlin/com/example/estdelivery/application/DepositServiceTest.kt
new file mode 100644
index 0000000..cf13e2a
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/application/DepositServiceTest.kt
@@ -0,0 +1,52 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.application.port.`in`.command.DepositCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.depositCommandArbitraryBuilder
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.moneyArbitraryBuilder
+import com.navercorp.fixturemonkey.kotlin.set
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+
+class DepositServiceTest : FreeSpec({
+
+ lateinit var updateAccountPort: UpdateAccountPort
+ lateinit var loadAccountPort: LoadAccountPort
+ lateinit var depositService: DepositService
+
+ beforeTest {
+ updateAccountPort = mockk()
+ loadAccountPort = mockk()
+ depositService = DepositService(loadAccountPort, updateAccountPort)
+ }
+
+ "계좌에 돈을 입금한다." {
+ // given
+ val money = moneyArbitraryBuilder().sample()
+ val accountNumber = accountNumberArbitraryBuilder().sample()
+ val depositCommand = depositCommandArbitraryBuilder()
+ .set(DepositCommand::accountNumber, accountNumber)
+ .set(DepositCommand::amount, money)
+ .sample()
+ val account = accountArbitraryBuilder()
+ .set("accountNumber", accountNumber)
+ .set("transactions", AccountTransactions())
+ .sample()
+
+ // when
+ every { loadAccountPort.findByAccountNumber(depositCommand.accountNumber) } returns account
+ every { updateAccountPort.update(account) } returns Unit
+
+ depositService.deposit(depositCommand)
+
+ // then
+ account.balance() shouldBe money
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/application/TransferServiceTest.kt b/src/test/kotlin/com/example/estdelivery/application/TransferServiceTest.kt
new file mode 100644
index 0000000..bf41970
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/application/TransferServiceTest.kt
@@ -0,0 +1,63 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.application.port.`in`.command.TransferCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.domain.Money
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import java.time.LocalDateTime
+
+class TransferServiceTest : FreeSpec({
+
+ lateinit var updateAccountPort: UpdateAccountPort
+ lateinit var loadAccountPort: LoadAccountPort
+ lateinit var transferService: TransferService
+
+ beforeTest {
+ updateAccountPort = mockk()
+ loadAccountPort = mockk()
+ transferService = TransferService(loadAccountPort, updateAccountPort)
+ }
+
+ "계좌 간 돈을 이체한다." {
+ // given
+ val money = moneyArbitraryBuilder().sample()
+ val sourceAccountNumber = accountNumberArbitraryBuilder().sample()
+ val targetAccountNumber = accountNumberArbitraryBuilder().sample()
+ val sourceAccount = accountArbitraryBuilder()
+ .set("accountNumber", sourceAccountNumber)
+ .set("transactions", AccountTransactions())
+ .sample()
+ val targetAccount = accountArbitraryBuilder()
+ .set("accountNumber", targetAccountNumber)
+ .set("transactions", AccountTransactions())
+ .sample()
+
+ sourceAccount.deposit(money, LocalDateTime.now().minusDays(1))
+ val transferCommand = TransferCommand(
+ sourceAccountNumber,
+ targetAccountNumber,
+ money,
+ LocalDateTime.now()
+ )
+
+ // when
+ every { loadAccountPort.findByAccountNumber(transferCommand.source) } returns sourceAccount
+ every { loadAccountPort.findByAccountNumber(transferCommand.target) } returns targetAccount
+ every { updateAccountPort.update(sourceAccount) } returns Unit
+ every { updateAccountPort.update(targetAccount) } returns Unit
+
+ transferService.transfer(transferCommand)
+
+ // then
+ sourceAccount.balance() shouldBe Money.ZERO
+ targetAccount.balance() shouldBe transferCommand.amount
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/application/WithdrawServiceTest.kt b/src/test/kotlin/com/example/estdelivery/application/WithdrawServiceTest.kt
new file mode 100644
index 0000000..3bfb694
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/application/WithdrawServiceTest.kt
@@ -0,0 +1,50 @@
+package com.example.estdelivery.application
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.application.port.`in`.command.WithdrawCommand
+import com.example.estdelivery.application.port.out.LoadAccountPort
+import com.example.estdelivery.application.port.out.UpdateAccountPort
+import com.example.estdelivery.domain.AccountTransactions
+import com.example.estdelivery.domain.Money
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.time.LocalDateTime
+
+class WithdrawServiceTest : FreeSpec({
+
+ lateinit var loadAccountPort: LoadAccountPort
+ lateinit var updateAccountPort: UpdateAccountPort
+ lateinit var withdrawService: WithdrawService
+
+ beforeTest {
+ loadAccountPort = mockk()
+ updateAccountPort = mockk()
+ withdrawService = WithdrawService(loadAccountPort, updateAccountPort)
+ }
+
+ "계좌에서 돈을 출금한다." {
+ // given
+ val accountNumber = accountNumberArbitraryBuilder().sample()
+ val money = moneyArbitraryBuilder().sample()
+ val account = accountArbitraryBuilder()
+ .set("accountNumber", accountNumber)
+ .set("transactions", AccountTransactions())
+ .sample()
+ account.deposit(money, LocalDateTime.now().minusDays(1))
+ val withdrawCommand = WithdrawCommand(accountNumber, money, LocalDateTime.now())
+
+ // when
+ every { loadAccountPort.findByAccountNumber(withdrawCommand.accountNumber) } returns account
+ every { updateAccountPort.update(account) } returns Unit
+
+ withdrawService.withdraw(withdrawCommand)
+
+ // then
+ account.balance() shouldBe Money.ZERO
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/AccountNumberTest.kt b/src/test/kotlin/com/example/estdelivery/domain/AccountNumberTest.kt
new file mode 100644
index 0000000..e235de0
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/AccountNumberTest.kt
@@ -0,0 +1,47 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.core.spec.style.FreeSpec
+import net.jqwik.api.Arbitraries
+
+class AccountNumberTest : FreeSpec({
+ "계좌 번호를 생성 할 때" - {
+ "10자리 임의 번호를 생성해" - {
+ for (i in 0..300) {
+ "$i 번째 계좌 번호를 생성한다." {
+ shouldNotThrow {
+ accountNumberArbitraryBuilder().sample()
+ }
+ }
+ }
+ }
+
+ "경계값 - 계좌번호는 10자리 미만인" - {
+ val arbitrarily = Arbitraries.strings().numeric().ofMinLength(0).ofMaxLength(9)
+
+ for (i in 0..100) {
+ val number = arbitrarily.sample()
+ "${number}로 생성하면 예외가 발생한다." {
+ shouldThrowAny {
+ accountNumberArbitraryBuilder().set("accountNumber", number).sample()
+ }
+ }
+ }
+ }
+
+ "경계값 - 계좌번호가 10자리 초과인." - {
+ val arbitrarily = Arbitraries.strings().numeric().ofMinLength(11).ofMaxLength(20)
+
+ for (i in 0..100) {
+ val number = arbitrarily.sample()
+ "${number}로 생성하면 예외가 발생한다." {
+ shouldThrowAny {
+ accountNumberArbitraryBuilder().set("accountNumber", number).sample()
+ }
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/AccountTest.kt b/src/test/kotlin/com/example/estdelivery/domain/AccountTest.kt
new file mode 100644
index 0000000..d0cb955
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/AccountTest.kt
@@ -0,0 +1,42 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.transferHistoryArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.core.spec.style.FreeSpec
+
+
+class AccountTest : FreeSpec({
+ fun transferHistories(myAccount: AccountNumber): TransferHistories {
+ val 계좌_과거_이력 = TransferHistories(listOf())
+ for (i in 0..10) {
+ 계좌_과거_이력.addHistory(
+ transferHistoryArbitraryBuilder()
+ .set("source", myAccount)
+ .sample()
+ )
+ 계좌_과거_이력.addHistory(
+ transferHistoryArbitraryBuilder()
+ .set("target", myAccount)
+ .sample()
+ )
+ }
+ return 계좌_과거_이력
+ }
+
+ "10번 계좌 생성 중" - {
+ val 내_계좌 = accountNumberArbitraryBuilder().sample()
+ val 계좌_과거_이력 = transferHistories(내_계좌)
+ for (i in 0..10) {
+ "$i 번째 계좌를 생성한다." {
+ shouldNotThrowAny {
+ accountArbitraryBuilder()
+ .set("number", 내_계좌)
+ .set("transferHistories", 계좌_과거_이력)
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionTest.kt b/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionTest.kt
new file mode 100644
index 0000000..1abb184
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionTest.kt
@@ -0,0 +1,29 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountTransactionArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.core.spec.style.FreeSpec
+import java.time.LocalDateTime
+
+class AccountTransactionTest : FreeSpec({
+ "계좌 거래 내역을 생성한다." - {
+ for (i in 0..300) {
+ "$i 번째 거래 내역을 생성한다." {
+ shouldNotThrowAny {
+ accountTransactionArbitraryBuilder()
+ .sample()
+ }
+ }
+ }
+ }
+
+ "계좌 거래 일자가 미래일 수 없다." {
+ val futureDate = LocalDateTime.now().plusDays(1)
+ shouldNotThrowAny {
+ accountTransactionArbitraryBuilder()
+ .set("transactionTime", futureDate)
+ .sample()
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionsTest.kt b/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionsTest.kt
new file mode 100644
index 0000000..a95753e
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/AccountTransactionsTest.kt
@@ -0,0 +1,31 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountTransactionsArbitraryBuilder
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.core.spec.style.FreeSpec
+import java.time.LocalDateTime
+
+class AccountTransactionsTest : FreeSpec({
+ val money = moneyArbitraryBuilder().sample()
+ val accountTransactions = AccountTransactions()
+
+ "돈 입금 기록을 추가한다." {
+ shouldNotThrowAny {
+ accountTransactions.deposit(money, LocalDateTime.now())
+ }
+ }
+
+ "돈 출금 기록을 추가한다." {
+ shouldNotThrowAny {
+ accountTransactions.withdraw(money, LocalDateTime.now())
+ }
+ }
+
+ "잔액을 조회한다." {
+ shouldNotThrowAny {
+ accountTransactions.balance()
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/DepositTest.kt b/src/test/kotlin/com/example/estdelivery/domain/DepositTest.kt
new file mode 100644
index 0000000..7dfb3ca
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/DepositTest.kt
@@ -0,0 +1,30 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import java.time.LocalDateTime
+
+class DepositTest : FreeSpec({
+ "입금하는데, " - {
+ for (i in 0..300) {
+ // given
+ val account = accountArbitraryBuilder()
+ .set("transactions", AccountTransactions(listOf()))
+ .sample()
+ var beforeBalance = account.balance()
+ "무려 $i 번째 입금이다." {
+ val money = moneyArbitraryBuilder().sample()
+ val deposit = Deposit(account, money, LocalDateTime.now())
+
+ // when
+ deposit.deposit()
+
+ // then
+ account.balance() shouldBe beforeBalance + money
+ beforeBalance = account.balance()
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/MoneyTest.kt b/src/test/kotlin/com/example/estdelivery/domain/MoneyTest.kt
new file mode 100644
index 0000000..fde04ea
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/MoneyTest.kt
@@ -0,0 +1,32 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.core.spec.style.FreeSpec
+import net.jqwik.api.Arbitraries
+import java.math.BigInteger
+
+class MoneyTest : FreeSpec({
+ "0이상의 숫자를 넣어" - {
+ for (i in 0..300) {
+ "$i 번째 금액을 생성한다." {
+ val money = moneyArbitraryBuilder()
+ shouldNotThrow { money.sample() }
+ }
+ }
+ }
+
+ "금액은 0원 이상이어야 한다." - {
+ val money = moneyArbitraryBuilder()
+ .set("amount", Arbitraries.bigIntegers().lessOrEqual(BigInteger.valueOf(-1L)))
+
+ for (i in 0..200) {
+ "$i 번째 금액을 생성한다." {
+ shouldThrowAny {
+ money.sample()
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/TransferHistoriesTest.kt b/src/test/kotlin/com/example/estdelivery/domain/TransferHistoriesTest.kt
new file mode 100644
index 0000000..a552c03
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/TransferHistoriesTest.kt
@@ -0,0 +1,40 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.transferHistoriesArbitraryBuilder
+import com.example.estdelivery.transferHistoryArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrow
+import io.kotest.core.spec.style.FreeSpec
+
+
+class TransferHistoriesTest : FreeSpec({
+ "이체 내역을 생성한다." {
+ shouldNotThrow {
+ transferHistoriesArbitraryBuilder().sample()
+ }
+ }
+
+ "이체 내역을 조회한다." {
+ val histories = transferHistoriesArbitraryBuilder().sample()
+ shouldNotThrow {
+ histories.showHistories()
+ }
+ }
+
+ "이체 내역을 추가할 때" - {
+ val histories = transferHistoriesArbitraryBuilder().sample()
+ for (i in 0..100) {
+ val transferHistory = transferHistoryArbitraryBuilder().sample()
+ "$i 번째 이력을 추가한다." {
+ shouldNotThrow {
+ histories.addHistory(transferHistory)
+ }
+ }
+
+ "$i 번째 이력을 제거한다." {
+ shouldNotThrow {
+ histories.removeHistory(transferHistory)
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/TransferHistoryTest.kt b/src/test/kotlin/com/example/estdelivery/domain/TransferHistoryTest.kt
new file mode 100644
index 0000000..c31902c
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/TransferHistoryTest.kt
@@ -0,0 +1,39 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountNumberArbitraryBuilder
+import com.example.estdelivery.moneyArbitraryBuilder
+import com.example.estdelivery.transferHistoryArbitraryBuilder
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.FreeSpec
+import java.time.LocalDateTime
+
+class TransferHistoryTest : FreeSpec({
+ "300번 중" - {
+ for (i in 0..300) {
+ "$i 번째 송금 내역을 생성한다." {
+ shouldNotThrowAny {
+ transferHistoryArbitraryBuilder().sample()
+ }
+ }
+ }
+ }
+
+ "입금 계좌와 출금 계좌가 같을 수 없다." {
+ val sameAccountNumber = accountNumberArbitraryBuilder().sample()
+ val money = moneyArbitraryBuilder().sample()
+
+ shouldThrow {
+ TransferHistory(sameAccountNumber, sameAccountNumber, money, LocalDateTime.now())
+ }
+ }
+
+ "이체 일자는 현재 시간보다 이전이어야 한다." {
+ val futureDateTime = LocalDateTime.now().plusDays(1)
+ shouldThrow {
+ transferHistoryArbitraryBuilder()
+ .set("transferDate", futureDateTime)
+ .sample()
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/TransferTest.kt b/src/test/kotlin/com/example/estdelivery/domain/TransferTest.kt
new file mode 100644
index 0000000..f869c59
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/TransferTest.kt
@@ -0,0 +1,42 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import net.jqwik.api.Arbitraries
+import java.math.BigInteger
+import java.time.LocalDateTime
+
+class TransferTest : FreeSpec({
+ "이체하는데, " - {
+ // given
+ val source = accountArbitraryBuilder()
+ .set("transactions", AccountTransactions(listOf()))
+ .sample()
+ val target = accountArbitraryBuilder()
+ .set("transactions", AccountTransactions(listOf()))
+ .sample()
+
+ source.deposit(Money(6_000_000.toBigInteger()), LocalDateTime.now())
+ var beforeSourceBalance = source.balance()
+ var beforeTargetBalance = target.balance()
+ for (i in 0..300) {
+ "무려 $i 번째 이체이다." {
+ val money = moneyArbitraryBuilder()
+ .set("amount", Arbitraries.bigIntegers().between(BigInteger.ZERO, 20_000.toBigInteger()))
+ .sample()
+ val transfer = Transfer(source, target, money, LocalDateTime.now())
+
+ // when
+ transfer.transfer()
+
+ // then
+ source.balance() shouldBe beforeSourceBalance - money
+ target.balance() shouldBe beforeTargetBalance + money
+ beforeSourceBalance = source.balance()
+ beforeTargetBalance = target.balance()
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/com/example/estdelivery/domain/WithdrawalTest.kt b/src/test/kotlin/com/example/estdelivery/domain/WithdrawalTest.kt
new file mode 100644
index 0000000..db53db7
--- /dev/null
+++ b/src/test/kotlin/com/example/estdelivery/domain/WithdrawalTest.kt
@@ -0,0 +1,50 @@
+package com.example.estdelivery.domain
+
+import com.example.estdelivery.accountArbitraryBuilder
+import com.example.estdelivery.moneyArbitraryBuilder
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+import net.jqwik.api.Arbitraries
+import java.math.BigInteger
+import java.time.LocalDateTime
+
+class WithdrawalTest : FreeSpec({
+
+ "출금하는데, " - {
+ // given
+ val account = accountArbitraryBuilder()
+ .set("transactions", AccountTransactions(listOf()))
+ .sample()
+ account.deposit(Money(6_000_000.toBigInteger()), LocalDateTime.now())
+
+ var beforeBalance = account.balance()
+ for (i in 0..300) {
+ "무려 $i 번째 출금이다." {
+ val money = moneyArbitraryBuilder()
+ .set("amount", Arbitraries.bigIntegers().between(BigInteger.ZERO, 20_000.toBigInteger()))
+ .sample()
+ val withdrawal = Withdrawal(account, money, LocalDateTime.now())
+
+ // when
+ withdrawal.withdraw()
+
+ // then
+ account.balance() shouldBe beforeBalance - money
+ beforeBalance = account.balance()
+ }
+ }
+ }
+
+ "출금하는데 잔액이 부족하면 예외가 발생한다." {
+ val account = accountArbitraryBuilder()
+ .set("transactions", AccountTransactions(listOf()))
+ .sample()
+ val money = moneyArbitraryBuilder().sample()
+
+ shouldThrow {
+ Withdrawal(account, money, LocalDateTime.now())
+ }
+ }
+
+})