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()) + } + } + +})