├── .gitignore
├── Dockerfile
├── README.md
├── pom.xml
└── src
├── main
├── kotlin
│ └── com
│ │ └── bsf
│ │ └── moneytransfer
│ │ ├── MoneyTransferApplication.kt
│ │ ├── configuration
│ │ └── Configuration.kt
│ │ ├── controller
│ │ └── AccountController.kt
│ │ ├── dto
│ │ └── DTO.kt
│ │ ├── entity
│ │ └── Account.kt
│ │ ├── exception
│ │ └── Exception.kt
│ │ ├── repository
│ │ └── AccountRepository.kt
│ │ ├── service
│ │ ├── AccountService.kt
│ │ └── impl
│ │ │ └── AccountServiceImpl.kt
│ │ └── utils
│ │ └── ValidationUtils.kt
└── resources
│ ├── application.yml
│ ├── create-schema.sql
│ └── db
│ └── changelog
│ ├── db.changelog-1.0.xml
│ └── db.changelog-master.xml
└── test
└── kotlin
└── com
└── bsf
└── moneytransfer
└── service
└── AccountServiceTest.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jdk-alpine
2 | EXPOSE 8080
3 | VOLUME /tmp
4 | ADD target/money-transfer-0.0.1.jar money-transfer.jar
5 | ENTRYPOINT ["java", "-jar", "money-transfer.jar"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Money Transfer Service
2 | REST API for working with account operations
3 |
4 | ### Technologies
5 | - Kotlin
6 | - Spring Boot
7 | - Maven
8 | - H2 in-memory database
9 | - Liquibase
10 | - JUnit
11 | - Docker
12 |
13 | **Building**
14 | -
15 | git clone https://github.com/Sunagatov/MoneyTransfer.git
16 | cd MoneyTransfer
17 | mvn clean install
18 |
19 | **Running**
20 | -
21 | 1) Build an docker image with the following command:
22 | 'docker build -t money-transfer .'
23 |
24 | 2) Then we can run it by running the following command:
25 | 'docker run -p 8080:8080 money-transfer'
26 |
27 | 3) Then we can open Money Transfer Service API by running the following command:
28 | 'start "" "http://localhost:8080/swagger-ui/"'
29 |
30 |
31 | **API**
32 | -
33 |
34 | **Get an existing account details**
35 |
36 | GET localhost:8080/accounts/{id}
37 |
38 | **Get all existing accounts**
39 |
40 | GET localhost:8080/accounts
41 |
42 | **Create a new account**
43 |
44 | POST localhost:8080/accounts
45 |
46 | **Transfer money from one account to another**
47 |
48 | PATCH localhost:8080/accounts/transfer-money
49 |
50 | **Add money to an existing account**
51 |
52 | PATCH localhost:8080/accounts/add-money
53 |
54 | **Withdraw money from an existing account**
55 |
56 | PATCH localhost:8080/accounts/withdraw-money
57 |
58 | **Delete all existing accounts**
59 |
60 | DELETE localhost:8080/accounts
61 |
62 | **Delete existing account by id**
63 |
64 | DELETE localhost:8080/accounts/{id}
65 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | 4.0.0
6 |
7 |
8 | org.springframework.boot
9 | spring-boot-starter-parent
10 | 2.5.4
11 |
12 |
13 |
14 | com.bsf
15 | money-transfer
16 | 0.0.1
17 | money-transfer
18 | REST API for working with account operations
19 |
20 |
21 |
22 | Zufar Sunagatov
23 | zufar.sunagatov@gmail.com
24 | https://www.linkedin.com/in/zufar-sunagatov/
25 | GMT+3
26 |
27 |
28 |
29 |
30 | 11
31 | 1.4.31
32 | 3.0.0
33 | 2.5.4
34 |
35 |
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-starter-data-jpa
40 |
41 |
42 | org.springframework.boot
43 | spring-boot-starter-web
44 |
45 |
46 | com.fasterxml.jackson.module
47 | jackson-module-kotlin
48 |
49 |
50 | org.liquibase
51 | liquibase-core
52 |
53 |
54 | io.springfox
55 | springfox-boot-starter
56 | ${springfox.version}
57 |
58 |
59 | io.springfox
60 | springfox-swagger-ui
61 | ${springfox.version}
62 |
63 |
64 | com.h2database
65 | h2
66 | runtime
67 |
68 |
69 | org.springframework.boot
70 | spring-boot-starter-test
71 | test
72 |
73 |
74 | org.jetbrains.kotlin
75 | kotlin-stdlib-jdk8
76 | ${kotlin.version}
77 |
78 |
79 |
80 |
81 | src/main/kotlin
82 |
83 |
84 | org.springframework.boot
85 | spring-boot-maven-plugin
86 | ${spring-boot-maven-plugin.version}
87 |
88 |
89 |
90 | org.jetbrains.kotlin
91 | kotlin-maven-plugin
92 | ${kotlin.version}
93 |
94 |
95 | compile
96 | compile
97 |
98 | compile
99 |
100 |
101 |
102 | test-compile
103 | test-compile
104 |
105 | test-compile
106 |
107 |
108 |
109 |
110 |
111 | spring
112 | jpa
113 |
114 | 1.8
115 |
116 |
117 |
118 | org.jetbrains.kotlin
119 | kotlin-maven-allopen
120 | ${kotlin.version}
121 |
122 |
123 | org.jetbrains.kotlin
124 | kotlin-maven-noarg
125 | ${kotlin.version}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/MoneyTransferApplication.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | /**
7 | * Application entry point
8 | *
9 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
10 | */
11 | @SpringBootApplication
12 | class MoneyTransferApplication
13 |
14 | fun main(args: Array) {
15 | runApplication(*args)
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/configuration/Configuration.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.configuration
2 |
3 | import org.springframework.context.annotation.Bean
4 | import org.springframework.context.annotation.Configuration
5 | import springfox.documentation.builders.PathSelectors
6 | import springfox.documentation.builders.RequestHandlerSelectors
7 | import springfox.documentation.spi.DocumentationType
8 | import springfox.documentation.spring.web.plugins.Docket
9 |
10 | /**
11 | * Swagger configuration
12 | *
13 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
14 | */
15 | @Configuration
16 | class SpringFoxConfig {
17 |
18 | @Bean
19 | fun api(): Docket =
20 | Docket(DocumentationType.SWAGGER_2)
21 | .select()
22 | .apis(RequestHandlerSelectors.basePackage("com.bsf.moneytransfer"))
23 | .paths(PathSelectors.any())
24 | .build()
25 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/controller/AccountController.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.controller
2 |
3 | import com.bsf.moneytransfer.dto.AccountUpdateDetails
4 | import com.bsf.moneytransfer.dto.MoneyTransferDetails
5 | import com.bsf.moneytransfer.entity.Account
6 | import com.bsf.moneytransfer.service.AccountService
7 | import io.swagger.v3.oas.annotations.Operation
8 | import io.swagger.v3.oas.annotations.Parameter
9 | import org.springframework.web.bind.annotation.*
10 |
11 | /**
12 | * Account operations endpoints
13 | *
14 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
15 | */
16 | @RestController
17 | @RequestMapping("/accounts")
18 | class AccountController(private val accountService: AccountService) {
19 |
20 | /**
21 | * Get an existing account details
22 | *
23 | * @param id an existing account id
24 | * @return an account details
25 | */
26 | @GetMapping("/{id}")
27 | @ResponseBody
28 | @Operation(summary = "Get an existing account details")
29 | fun getAccountDetails(@Parameter(description = "existing account id") @PathVariable id: Long): Account =
30 | accountService.getAccountDetails(id)
31 |
32 | /**
33 | * Get all existing accounts
34 | *
35 | * @return all existing accounts
36 | */
37 | @GetMapping
38 | @ResponseBody
39 | @Operation(summary = "Get all existing accounts")
40 | fun getAllAccounts(): List = accountService.getAllAccounts()
41 |
42 | /**
43 | * Create a new account
44 | *
45 | * @return a new account
46 | */
47 | @PostMapping
48 | @ResponseBody
49 | @Operation(summary = "Create a new account")
50 | fun createAccount(): Account = accountService.createAccount()
51 |
52 | /**
53 | * Transfer money from one account to another
54 | *
55 | * @param moneyTransferDetails transfer money data
56 | */
57 | @PatchMapping("/transfer-money")
58 | @Operation(summary = "Transfer money from one account to another")
59 | fun transferMoney(@Parameter(description = "Transfer money data") @RequestBody moneyTransferDetails: MoneyTransferDetails) =
60 | accountService.transferMoney(moneyTransferDetails)
61 |
62 | /**
63 | * Add money to an existing account
64 | *
65 | * @param accountUpdateDetails add money data
66 | * @return an account details
67 | */
68 | @PatchMapping("/add-money")
69 | @ResponseBody
70 | @Operation(summary = "Add money to an existing account")
71 | fun addMoney(@Parameter(description = "Add money data") @RequestBody accountUpdateDetails: AccountUpdateDetails): Account =
72 | accountService.addMoney(accountUpdateDetails)
73 |
74 | /**
75 | * Withdraw money from an existing account
76 | *
77 | * @param accountUpdateDetails withdraw money data
78 | * @return an account details
79 | */
80 | @PatchMapping("/withdraw-money")
81 | @ResponseBody
82 | @Operation(summary = "Withdraw money from an existing account by id")
83 | fun withdrawMoney(@Parameter(description = "Withdraw money data") @RequestBody accountUpdateDetails: AccountUpdateDetails): Account =
84 | accountService.withdrawMoney(accountUpdateDetails)
85 |
86 | /**
87 | * Delete all existing accounts
88 | *
89 | * @return http status with description
90 | */
91 | @DeleteMapping
92 | @Operation(summary = "Delete all existing accounts")
93 | fun deleteAllAccount() = accountService.deleteAllAccounts()
94 |
95 | /**
96 | * Delete an existing account
97 | *
98 | * @param id an existing account id
99 | * @return http status
100 | */
101 | @DeleteMapping("/{id}")
102 | @Operation(summary = "Delete an existing account by id")
103 | fun deleteAccount(@Parameter(description = "existing account id") @PathVariable id: Long) =
104 | accountService.deleteAccount(id)
105 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/dto/DTO.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.dto
2 |
3 | import java.math.BigDecimal
4 |
5 | /**
6 | * Information about a money transfer
7 | * */
8 | data class MoneyTransferDetails(val accountFromId: Long,
9 | val accountToId: Long,
10 | val amount: BigDecimal,
11 | val description: String)
12 |
13 | /**
14 | * Information about an account update
15 | * */
16 | data class AccountUpdateDetails(val accountId: Long,
17 | val amount: BigDecimal)
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/entity/Account.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.entity
2 |
3 | import java.math.BigDecimal
4 | import javax.persistence.*
5 |
6 | /**
7 | * Account details
8 | *
9 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
10 | * */
11 | @Entity
12 | @Table(name = "accounts")
13 | data class Account(@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0,
14 | var balance: BigDecimal = BigDecimal.ZERO) {
15 |
16 | override fun equals(other: Any?)
17 | = (other is Account)
18 | && id == other.id
19 | && balance.stripTrailingZeros() == other.balance.stripTrailingZeros()
20 |
21 | override fun hashCode(): Int {
22 | var result = id.hashCode()
23 | result = 31 * result + balance.hashCode()
24 | return result
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/exception/Exception.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.exception
2 |
3 | import org.springframework.http.HttpStatus
4 | import org.springframework.web.bind.annotation.ResponseStatus
5 |
6 | /**
7 | * Describes case when transfer data has negative amount
8 | *
9 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
10 | */
11 | @ResponseStatus(value = HttpStatus.BAD_REQUEST)
12 | class InvalidTransferDetailsException(message: String?) : RuntimeException(message)
13 |
14 | /**
15 | * Describes case when account details has negative balance
16 | *
17 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
18 | */
19 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
20 | class InvalidAccountDetailsException(message: String?) : RuntimeException(message)
21 |
22 | /**
23 | * Describes case when account update data (for withdraw and add money operations) has negative amount
24 | *
25 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
26 | */
27 | @ResponseStatus(value = HttpStatus.BAD_REQUEST)
28 | class InvalidAccountUpdateDetailsException(message: String?) : RuntimeException(message)
29 |
30 | /**
31 | * Describes case when an account which was not found by specific id
32 | *
33 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
34 | */
35 | @ResponseStatus(value = HttpStatus.NOT_FOUND)
36 | class AbsentAccountException(message: String?) : RuntimeException(message)
37 |
38 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/repository/AccountRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.repository
2 |
3 | import com.bsf.moneytransfer.entity.Account
4 | import org.springframework.data.jpa.repository.JpaRepository
5 | import org.springframework.stereotype.Repository
6 |
7 | /**
8 | * CRUD operations on an account in database
9 | *
10 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
11 | */
12 | @Repository
13 | interface AccountRepository : JpaRepository
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/service/AccountService.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.service
2 |
3 | import com.bsf.moneytransfer.dto.AccountUpdateDetails
4 | import com.bsf.moneytransfer.dto.MoneyTransferDetails
5 | import com.bsf.moneytransfer.entity.Account
6 |
7 | /**
8 | * Account operations
9 | *
10 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
11 | */
12 | interface AccountService {
13 |
14 | /**
15 | * Get existing account details
16 | *
17 | * @param id an existing account id
18 | * @return account details
19 | */
20 | fun getAccountDetails(id: Long): Account
21 |
22 | /**
23 | * Get all existing accounts
24 | *
25 | * @return all existing accounts
26 | */
27 | fun getAllAccounts(): MutableList
28 |
29 | /**
30 | * Create new account
31 | *
32 | * @return new account
33 | */
34 | fun createAccount(): Account
35 |
36 | /**
37 | * Create new account with specific account details
38 | *
39 | * @param accountDetails account details for new account
40 | * @return new account
41 | */
42 | fun createAccount(accountDetails: Account): Account
43 |
44 | /**
45 | * Transfer money from one account to another
46 | *
47 | * @param moneyTransferDetails transfer money data
48 | */
49 | fun transferMoney(moneyTransferDetails: MoneyTransferDetails)
50 |
51 | /**
52 | * Add money to an existing account
53 | *
54 | * @param accountUpdateDetails add money data
55 | * @return account details
56 | */
57 | fun addMoney(accountUpdateDetails: AccountUpdateDetails): Account
58 |
59 | /**
60 | * Withdraw money from an existing account
61 | *
62 | * @param accountUpdateDetails withdraw money data
63 | * @return account details
64 | */
65 | fun withdrawMoney(accountUpdateDetails: AccountUpdateDetails): Account
66 |
67 | /**
68 | * Delete all existing accounts
69 | */
70 | fun deleteAllAccounts()
71 |
72 | /**
73 | * Delete existing account by id
74 | *
75 | * @param id existing account id
76 | */
77 | fun deleteAccount(id: Long)
78 |
79 | /**
80 | * Delete existing account by account details
81 | *
82 | * @param accountDetails account details
83 | */
84 | fun deleteAccount(accountDetails: Account)
85 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/service/impl/AccountServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.service.impl
2 |
3 | import com.bsf.moneytransfer.dto.AccountUpdateDetails
4 | import com.bsf.moneytransfer.dto.MoneyTransferDetails
5 | import com.bsf.moneytransfer.entity.Account
6 | import com.bsf.moneytransfer.exception.AbsentAccountException
7 | import com.bsf.moneytransfer.repository.AccountRepository
8 | import com.bsf.moneytransfer.service.AccountService
9 | import com.bsf.moneytransfer.utils.validateAccountDetails
10 | import com.bsf.moneytransfer.utils.validateAccountUpdateDetails
11 | import com.bsf.moneytransfer.utils.validateTransferDetails
12 | import org.slf4j.Logger
13 | import org.slf4j.LoggerFactory
14 | import org.springframework.stereotype.Service
15 | import org.springframework.transaction.annotation.Isolation
16 | import org.springframework.transaction.annotation.Transactional
17 |
18 | /**
19 | * Account operations
20 | *
21 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
22 | */
23 | @Service
24 | class AccountServiceImpl(private val accountRepository: AccountRepository) : AccountService {
25 |
26 | var log: Logger = LoggerFactory.getLogger(AccountServiceImpl::class.java)
27 |
28 | @Transactional(isolation = Isolation.REPEATABLE_READ)
29 | override fun transferMoney(moneyTransferDetails: MoneyTransferDetails) {
30 | log.info("Transfer ${moneyTransferDetails.amount} money with description (${moneyTransferDetails.description})" +
31 | " from the account (id ${moneyTransferDetails.accountFromId})" +
32 | " to the account (id ${moneyTransferDetails.accountToId}) was started")
33 |
34 | validateTransferDetails(moneyTransferDetails)
35 | val (accountFromId, accountToId, amount) = moneyTransferDetails
36 |
37 | val accountFromDetails = getAccountDetails(accountFromId)
38 | accountFromDetails.balance -= amount
39 | validateAccountDetails(accountFromDetails)
40 |
41 | val accountToDetails = getAccountDetails(accountToId)
42 | accountToDetails.balance += amount
43 |
44 | updateAccount(accountFromDetails)
45 | updateAccount(accountToDetails)
46 |
47 | log.info("Transfer ${moneyTransferDetails.amount} money with description (${moneyTransferDetails.description})" +
48 | " from the account (id ${moneyTransferDetails.accountFromId})" +
49 | " to the account (id ${moneyTransferDetails.accountToId}) was finished successfully")
50 | }
51 |
52 | @Transactional(readOnly = true)
53 | override fun getAccountDetails(id: Long): Account =
54 | accountRepository.findById(id).orElseThrow { AbsentAccountException("The account with id=$id is absent") }
55 |
56 | @Transactional(readOnly = true)
57 | override fun getAllAccounts(): MutableList = accountRepository.findAll()
58 |
59 | @Transactional(isolation = Isolation.REPEATABLE_READ)
60 | fun updateAccount(account: Account): Account {
61 | val updatedAccountDetails = accountRepository.save(account)
62 | log.info("Account $updatedAccountDetails was updated successfully")
63 |
64 | return updatedAccountDetails
65 | }
66 |
67 | @Transactional(isolation = Isolation.REPEATABLE_READ)
68 | override fun addMoney(accountUpdateDetails: AccountUpdateDetails): Account = with(accountUpdateDetails) {
69 | validateAccountUpdateDetails(accountUpdateDetails)
70 |
71 | val accountDetails = getAccountDetails(accountId)
72 | accountDetails.balance += amount
73 |
74 | val accountDetailsAfterAddedMoney = accountRepository.save(accountDetails)
75 |
76 | log.info("Added ${accountUpdateDetails.amount} money " +
77 | " from the account (id ${accountUpdateDetails.accountId})" +
78 | " was finished successfully")
79 |
80 | return accountDetailsAfterAddedMoney
81 | }
82 |
83 | @Transactional(isolation = Isolation.REPEATABLE_READ)
84 | override fun withdrawMoney(accountUpdateDetails: AccountUpdateDetails): Account = with(accountUpdateDetails) {
85 | validateAccountUpdateDetails(accountUpdateDetails)
86 | val accountDetails = getAccountDetails(accountId)
87 |
88 | accountDetails.balance -= amount
89 | validateAccountDetails(accountDetails)
90 |
91 | val accountDetailsAfterWithdraw = accountRepository.save(accountDetails)
92 |
93 | log.info("Withdraw ${accountUpdateDetails.amount} money " +
94 | " from the account (id ${accountUpdateDetails.accountId})" +
95 | " was finished successfully")
96 |
97 | return accountDetailsAfterWithdraw
98 | }
99 |
100 | override fun createAccount(): Account {
101 | val newAccount = accountRepository.save(Account())
102 | log.info("New account with id=${newAccount.id} was created successfully")
103 | return newAccount
104 | }
105 |
106 | override fun createAccount(accountDetails: Account): Account {
107 | val newAccount = accountRepository.save(accountDetails)
108 | log.info("New account with id=${newAccount.id} was created successfully")
109 | return newAccount
110 | }
111 |
112 | override fun deleteAllAccounts() {
113 | accountRepository.deleteAll()
114 | log.info("All existing accounts were deleted successfully")
115 | }
116 |
117 | override fun deleteAccount(id: Long) {
118 | accountRepository.deleteById(id)
119 | log.info("Account with id=$id was deleted successfully")
120 | }
121 |
122 | override fun deleteAccount(accountDetails: Account) {
123 | accountRepository.delete(accountDetails)
124 | log.info("Account with id=${accountDetails.id} was deleted successfully")
125 | }
126 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/bsf/moneytransfer/utils/ValidationUtils.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.utils
2 |
3 | import com.bsf.moneytransfer.dto.AccountUpdateDetails
4 | import com.bsf.moneytransfer.dto.MoneyTransferDetails
5 | import com.bsf.moneytransfer.exception.InvalidAccountDetailsException
6 | import com.bsf.moneytransfer.exception.InvalidAccountUpdateDetailsException
7 | import com.bsf.moneytransfer.exception.InvalidTransferDetailsException
8 | import com.bsf.moneytransfer.entity.Account
9 | import com.bsf.moneytransfer.service.impl.AccountServiceImpl
10 | import org.slf4j.Logger
11 | import org.slf4j.LoggerFactory
12 | import java.math.BigDecimal
13 |
14 | var log: Logger = LoggerFactory.getLogger(AccountServiceImpl::class.java)
15 |
16 | fun validateTransferDetails(moneyTransferDetails: MoneyTransferDetails) {
17 | if (isAmountNegative(moneyTransferDetails.amount)) {
18 | val errorMessage = "Transfer details is invalid. Amount is negative {${moneyTransferDetails.amount}}"
19 | log.error(errorMessage)
20 | throw InvalidTransferDetailsException(errorMessage)
21 | }
22 | }
23 |
24 | fun validateAccountDetails(accountDetails: Account) {
25 | if (isAmountNegative(accountDetails.balance)) {
26 | val errorMessage = "Account details is invalid. Balance is negative {${accountDetails.balance}}"
27 | log.error(errorMessage)
28 | throw InvalidAccountDetailsException(errorMessage)
29 | }
30 | }
31 |
32 | fun validateAccountUpdateDetails(accountDetails: AccountUpdateDetails) {
33 | if (isAmountNegative(accountDetails.amount)) {
34 | val errorMessage = "Account update details is invalid. Amount is negative {${accountDetails.amount}}"
35 | log.error(errorMessage)
36 | throw InvalidAccountUpdateDetailsException(errorMessage)
37 | }
38 | }
39 |
40 | fun isAmountNegative(amount: BigDecimal) = amount < BigDecimal.ZERO
41 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | datasource:
3 | url: jdbc:h2:mem:transfer-money;INIT=RUNSCRIPT FROM 'classpath:create-schema.sql'
4 | driverClassName: org.h2.Driver
5 | username: sa
6 | password: sa
7 | h2:
8 | console:
9 | enabled: true
10 | liquibase:
11 | change-log: classpath:db/changelog/db.changelog-master.xml
12 | jpa:
13 | database-platform: org.hibernate.dialect.H2Dialect
14 | generate-ddl: true
15 | database: h2
16 | show-sql: false
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/create-schema.sql:
--------------------------------------------------------------------------------
1 | CREATE SCHEMA IF NOT EXISTS transfer_money_account;
--------------------------------------------------------------------------------
/src/main/resources/db/changelog/db.changelog-1.0.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 | drop table if exists transfer_money_account.accounts;
12 |
13 | create table if not exists transfer_money_account.accounts (
14 | id bigserial primary key,
15 | balance numeric(10,2)
16 | );
17 |
18 | comment on table transfer_money_account.accounts is 'Account details';
19 | comment on column transfer_money_account.accounts.id is 'Unique identifier';
20 | comment on column transfer_money_account.accounts.balance is 'Balance';
21 |
22 | insert into transfer_money_account.accounts (id, balance) values (1, 0.0);
23 | insert into transfer_money_account.accounts (id, balance) values (2, 0.0);
24 | insert into transfer_money_account.accounts (id, balance) values (3, 0.0);
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/main/resources/db/changelog/db.changelog-master.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/bsf/moneytransfer/service/AccountServiceTest.kt:
--------------------------------------------------------------------------------
1 | package com.bsf.moneytransfer.service
2 |
3 | import com.bsf.moneytransfer.MoneyTransferApplication
4 | import com.bsf.moneytransfer.dto.AccountUpdateDetails
5 | import com.bsf.moneytransfer.dto.MoneyTransferDetails
6 | import com.bsf.moneytransfer.exception.InvalidAccountDetailsException
7 | import com.bsf.moneytransfer.exception.InvalidAccountUpdateDetailsException
8 | import com.bsf.moneytransfer.exception.InvalidTransferDetailsException
9 | import com.bsf.moneytransfer.entity.Account
10 | import com.bsf.moneytransfer.exception.AbsentAccountException
11 | import org.junit.jupiter.api.Assertions.*
12 | import org.junit.jupiter.api.Test
13 | import org.springframework.beans.factory.annotation.Autowired
14 | import org.springframework.boot.test.context.SpringBootTest
15 | import java.math.BigDecimal
16 |
17 | /**
18 | * Account operations tests
19 | *
20 | * @author Zufar Sunagatov (zufar.sunagatov@gmail.com)
21 | */
22 | @SpringBootTest(classes = [MoneyTransferApplication::class])
23 | class AccountServiceTest {
24 |
25 | @Autowired
26 | private lateinit var accountService: AccountService
27 |
28 | @Test
29 | fun `test create new account operation is successful`() {
30 | val actualAccountDetails = accountService.createAccount()
31 | val expectedAccountDetails = accountService.getAccountDetails(actualAccountDetails.id)
32 | assertEquals(expectedAccountDetails, actualAccountDetails)
33 | }
34 |
35 | @Test
36 | fun `test get account details operation is successful`() {
37 | val expectedAccountDetails = accountService.createAccount()
38 |
39 | val actualAccountDetails = accountService.getAccountDetails(expectedAccountDetails.id)
40 |
41 | assertEquals(expectedAccountDetails, actualAccountDetails)
42 | }
43 |
44 | @Test
45 | fun `test get account details operation throws AbsentAccountException when account is absent`() {
46 | assertThrows(AbsentAccountException::class.java) {
47 | accountService.getAccountDetails(500)
48 | }
49 | }
50 |
51 | @Test
52 | fun `test get all accounts operation is successful`() {
53 | accountService.deleteAllAccounts()
54 | val account1 = accountService.createAccount()
55 | val account2 = accountService.createAccount()
56 | val account3 = accountService.createAccount()
57 | val expectedAccounts: MutableList = mutableListOf(account1, account2, account3)
58 |
59 | val actualAccounts: MutableList = accountService.getAllAccounts()
60 |
61 | assertEquals(actualAccounts, expectedAccounts)
62 | }
63 |
64 | @Test
65 | fun `test add money operation is successful`() {
66 | val accountDetails = accountService.createAccount(Account(1, BigDecimal(300)))
67 | val expected = Account(accountDetails.id, BigDecimal(500))
68 |
69 | accountService.addMoney(AccountUpdateDetails(accountId = accountDetails.id, amount = BigDecimal(200)))
70 | val actual = accountService.getAccountDetails(accountDetails.id)
71 |
72 | assertEquals(expected, actual)
73 | }
74 |
75 | @Test
76 | fun `test add money operation throws InvalidAccountUpdateDetailsException when amount is negative`() {
77 | val accountDetails = accountService.createAccount()
78 |
79 | assertThrows(InvalidAccountUpdateDetailsException::class.java) {
80 | accountService.addMoney(AccountUpdateDetails(accountId = accountDetails.id, amount = BigDecimal(-200)))
81 | }
82 | }
83 |
84 | @Test
85 | fun `test withdraw money operation is successful`() {
86 | val accountDetails = accountService.createAccount(Account(balance = BigDecimal(300)))
87 | val expected = Account(accountDetails.id, BigDecimal(100))
88 |
89 | accountService.withdrawMoney(AccountUpdateDetails(accountId = accountDetails.id, amount = BigDecimal(200)))
90 |
91 | val actual = accountService.getAccountDetails(accountDetails.id)
92 | assertEquals(expected, actual)
93 | }
94 |
95 | @Test
96 | fun `test withdraw money operation throws InvalidAccountUpdateDetailsException when amount is negative`() {
97 | val accountDetails = accountService.createAccount()
98 |
99 | assertThrows(InvalidAccountUpdateDetailsException::class.java) {
100 | accountService.withdrawMoney(AccountUpdateDetails(accountId = accountDetails.id, amount = BigDecimal(-200)))
101 | }
102 | }
103 |
104 | @Test
105 | fun `test withdraw money operation throws InvalidAccountDetailsException when account balance is negative`() {
106 | val accountDetails = accountService.createAccount()
107 |
108 | assertThrows(InvalidAccountDetailsException::class.java) {
109 | accountService.withdrawMoney(AccountUpdateDetails(accountId = accountDetails.id, amount = BigDecimal(200)))
110 | }
111 | }
112 |
113 | @Test
114 | fun `test delete all accounts operation is successful`() {
115 | accountService.deleteAllAccounts()
116 | assertTrue(accountService.getAllAccounts().isEmpty())
117 | }
118 |
119 | @Test
120 | fun `test delete existing account by id operation is successful`() {
121 | accountService.deleteAllAccounts()
122 | val accountDetails = accountService.createAccount()
123 |
124 | accountService.deleteAccount(accountDetails.id)
125 |
126 | assertTrue(accountService.getAllAccounts().isEmpty())
127 | }
128 |
129 | @Test
130 | fun `test delete existing account by account details operation is successful`() {
131 | accountService.deleteAllAccounts()
132 | val accountDetails = accountService.createAccount()
133 |
134 | accountService.deleteAccount(accountDetails)
135 |
136 | assertTrue(accountService.getAllAccounts().isEmpty())
137 | }
138 |
139 |
140 | @Test
141 | fun `test transfer money operation throws InvalidTransferDetailsException when amount is negative`() {
142 | val accountDetailsFrom = accountService.createAccount()
143 | val accountDetailsTo = accountService.createAccount()
144 |
145 | assertThrows(InvalidTransferDetailsException::class.java) {
146 | accountService.transferMoney(
147 | MoneyTransferDetails(
148 | accountFromId = accountDetailsFrom.id,
149 | accountToId = accountDetailsTo.id,
150 | amount = BigDecimal(-200),
151 | description = "Transfer description"
152 | ))
153 | }
154 | }
155 |
156 | @Test
157 | fun `test transfer money operation throws InvalidAccountDetailsException when account balance is negative`() {
158 | val accountDetailsFrom = accountService.createAccount()
159 | val accountDetailsTo = accountService.createAccount()
160 |
161 | assertThrows(InvalidAccountDetailsException::class.java) {
162 | accountService.transferMoney(
163 | MoneyTransferDetails(
164 | accountFromId = accountDetailsFrom.id,
165 | accountToId = accountDetailsTo.id,
166 | amount = BigDecimal(200),
167 | description = "Transfer description"
168 | ))
169 | }
170 | }
171 |
172 | @Test
173 | fun `test transfer money operation is sucessful`() {
174 | val accountDetailsFrom = accountService.createAccount(Account(balance = BigDecimal(200)))
175 | val accountDetailsTo = accountService.createAccount()
176 |
177 | accountService.transferMoney(
178 | MoneyTransferDetails(
179 | accountFromId = accountDetailsFrom.id,
180 | accountToId = accountDetailsTo.id,
181 | amount = BigDecimal(200),
182 | description = "Transfer description"
183 | ))
184 |
185 | val expectedAccountDetailFrom = Account(accountDetailsFrom.id, BigDecimal.ZERO)
186 | val actualAccountDetailFrom = accountService.getAccountDetails(accountDetailsFrom.id)
187 | val expectedAccountDetailTo = Account(accountDetailsTo.id, BigDecimal(200))
188 | val actualAccountDetailTo = accountService.getAccountDetails(accountDetailsTo.id)
189 |
190 | assertEquals(expectedAccountDetailFrom, actualAccountDetailFrom)
191 | assertEquals(expectedAccountDetailTo, actualAccountDetailTo)
192 | }
193 |
194 | }
--------------------------------------------------------------------------------