├── .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 | } --------------------------------------------------------------------------------