├── screenshots ├── deposit.png ├── withdraw.png ├── check_balance.png ├── check_balance_2.png └── create_account.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── Dockerfile ├── src ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── paul │ │ │ ├── constants │ │ │ ├── ACTION.java │ │ │ └── constants.java │ │ │ ├── Application.java │ │ │ ├── repositories │ │ │ ├── AccountRepository.java │ │ │ └── TransactionRepository.java │ │ │ ├── utils │ │ │ ├── CodeGenerator.java │ │ │ ├── InputValidator.java │ │ │ ├── AccountInput.java │ │ │ ├── CreateAccountInput.java │ │ │ ├── WithdrawInput.java │ │ │ ├── DepositInput.java │ │ │ └── TransactionInput.java │ │ │ ├── services │ │ │ ├── AccountService.java │ │ │ └── TransactionService.java │ │ │ ├── models │ │ │ ├── Transaction.java │ │ │ └── Account.java │ │ │ └── controllers │ │ │ ├── AccountRestController.java │ │ │ └── TransactionRestController.java │ └── resources │ │ ├── application.yaml │ │ ├── schema.sql │ │ └── data.sql └── test │ └── java │ └── com │ └── example │ └── paul │ ├── integration │ ├── CheckBalanceIntegrationTest.java │ └── MakeTransferIntegrationTest.java │ └── unit │ ├── TransactionRestControllerTest.java │ ├── AccountRestControllerTest.java │ ├── AccountServiceTest.java │ └── TransactionServiceTest.java ├── .gitignore ├── .github └── workflows │ └── maven.yml ├── LICENSE ├── README.md ├── pom.xml ├── Banking.postman_collection.json ├── mvnw.cmd └── mvnw /screenshots/deposit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/screenshots/deposit.png -------------------------------------------------------------------------------- /screenshots/withdraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/screenshots/withdraw.png -------------------------------------------------------------------------------- /screenshots/check_balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/screenshots/check_balance.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /screenshots/check_balance_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/screenshots/check_balance_2.png -------------------------------------------------------------------------------- /screenshots/create_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldragoslav/Spring-boot-Banking/HEAD/screenshots/create_account.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.3_7-jdk-alpine 2 | VOLUME /tmp 3 | ARG JAR_FILE 4 | COPY target/Banking-*.jar app.jar 5 | ENTRYPOINT ["java","-jar","/app.jar"] -------------------------------------------------------------------------------- /src/main/java/com/example/paul/constants/ACTION.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.constants; 2 | 3 | public enum ACTION { 4 | DEPOSIT, 5 | WITHDRAW 6 | } 7 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | --- 5 | spring: 6 | profiles: local 7 | datasource: 8 | url: jdbc:h2:mem:online_bank 9 | h2: 10 | console: 11 | enabled: true 12 | path: /h2-console 13 | jpa: 14 | hibernate: 15 | ddl-auto: create 16 | server: 17 | port: 8080 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.paul; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/** 4 | !**/src/test/** 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | 29 | ### VS Code ### 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set the JDK 15 | uses: actions/setup-java@v2 16 | with: 17 | distribution: 'adopt' 18 | java-version: 17 19 | - name: Build with Maven 20 | run: mvn -B package --file pom.xml 21 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/repositories/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.repositories; 2 | 3 | import com.example.paul.models.Account; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface AccountRepository extends JpaRepository { 9 | 10 | Optional findBySortCodeAndAccountNumber(String sortCode, String accountNumber); 11 | Optional findByAccountNumber(String accountNumber); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/repositories/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.repositories; 2 | 3 | import com.example.paul.models.Transaction; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface TransactionRepository extends JpaRepository { 9 | 10 | // TODO Limit to recent transactions and implement separate endpoint to view old transactions 11 | List findBySourceAccountIdOrderByInitiationDate(long id); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/CodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import com.mifmif.common.regex.Generex; 4 | 5 | import static com.example.paul.constants.constants.ACCOUNT_NUMBER_PATTERN_STRING; 6 | import static com.example.paul.constants.constants.SORT_CODE_PATTERN_STRING; 7 | 8 | public class CodeGenerator { 9 | Generex sortCodeGenerex = new Generex(SORT_CODE_PATTERN_STRING); 10 | Generex accountNumberGenerex = new Generex(ACCOUNT_NUMBER_PATTERN_STRING); 11 | 12 | public CodeGenerator(){} 13 | 14 | public String generateSortCode() { 15 | return sortCodeGenerex.random(); 16 | } 17 | 18 | public String generateAccountNumber() { 19 | return accountNumberGenerex.random(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA online_bank; 2 | 3 | CREATE TABLE online_bank.account ( 4 | id bigint NOT NULL PRIMARY KEY, 5 | sort_code CHAR(8) NOT NULL, 6 | account_number CHAR(8) NOT NULL, 7 | current_balance NUMERIC(10,3) NOT NULL, 8 | bank_name VARCHAR(50) NOT NULL, 9 | owner_name VARCHAR(50) NOT NULL, 10 | UNIQUE (sort_code, account_number) 11 | ); 12 | 13 | CREATE SEQUENCE online_bank.transaction_sequence START WITH 5; 14 | CREATE TABLE online_bank.transaction ( 15 | id bigint NOT NULL PRIMARY KEY, 16 | source_account_id bigint NOT NULL REFERENCES online_bank.account(id), 17 | target_account_id bigint NOT NULL REFERENCES online_bank.account(id), 18 | -- Partially denormalize for performance 19 | target_owner_name varchar(50) NOT NULL, 20 | amount NUMERIC(10,3) NOT NULL, 21 | initiation_date timestamp NOT NULL, 22 | completion_date TIMESTAMP, 23 | reference VARCHAR(255), 24 | latitude REAL, 25 | longitude REAL 26 | ); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Dragoslav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO online_bank.account (id, sort_code, account_number, current_balance, bank_name, owner_name) 2 | VALUES (1, '53-68-92', '73084635', 1071.78, 'Challenger Bank', 'Paul Dragoslav'); 3 | INSERT INTO online_bank.account (id, sort_code, account_number, current_balance, bank_name, owner_name) 4 | VALUES (2, '65-93-37', '21956204', 67051.01, 'High Street Bank', 'Scrooge McDuck'); 5 | 6 | INSERT INTO online_bank.transaction (id, source_account_id, target_account_id, target_owner_name, amount, initiation_date, completion_date, reference) 7 | VALUES (1, 1, 2, 'Scrooge McDuck', 100.00, '2019-04-01 10:30', '2019-04-01 10:54', 'Protection charge Apr'); 8 | INSERT INTO online_bank.transaction (id, source_account_id, target_account_id, target_owner_name, amount, initiation_date, completion_date, reference) 9 | VALUES (2, 1, 2, 'Scrooge McDuck', 100.00, '2019-05-01 10:30', '2019-05-01 11:21', 'Protection charge May'); 10 | 11 | INSERT INTO online_bank.transaction (id, source_account_id, target_account_id, target_owner_name, amount, initiation_date, completion_date, reference) 12 | VALUES (3, 2, 1, 'Paul Dragoslav', 10000.00, '2019-05-27 17:21', null, 'Ha Ha I am rich'); 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/constants/constants.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.constants; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class constants { 6 | 7 | public static final String SUCCESS = 8 | "Operation completed successfully"; 9 | public static final String NO_ACCOUNT_FOUND = 10 | "Unable to find an account matching this sort code and account number"; 11 | public static final String INVALID_SEARCH_CRITERIA = 12 | "The provided sort code or account number did not match the expected format"; 13 | 14 | public static final String INSUFFICIENT_ACCOUNT_BALANCE = 15 | "Your account does not have sufficient balance"; 16 | 17 | public static final String SORT_CODE_PATTERN_STRING = "[0-9]{2}-[0-9]{2}-[0-9]{2}"; 18 | 19 | public static final String ACCOUNT_NUMBER_PATTERN_STRING = "[0-9]{8}"; 20 | public static final Pattern SORT_CODE_PATTERN = Pattern.compile("^[0-9]{2}-[0-9]{2}-[0-9]{2}$"); 21 | public static final Pattern ACCOUNT_NUMBER_PATTERN = Pattern.compile("^[0-9]{8}$"); 22 | 23 | public static final String INVALID_TRANSACTION = 24 | "Account information is invalid or transaction has been denied for your protection. Please try again."; 25 | public static final String CREATE_ACCOUNT_FAILED = 26 | "Error happened during creating new account"; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/InputValidator.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import com.example.paul.constants.constants; 4 | 5 | public class InputValidator { 6 | 7 | public static boolean isSearchCriteriaValid(AccountInput accountInput) { 8 | return constants.SORT_CODE_PATTERN.matcher(accountInput.getSortCode()).find() && 9 | constants.ACCOUNT_NUMBER_PATTERN.matcher(accountInput.getAccountNumber()).find(); 10 | } 11 | 12 | public static boolean isAccountNoValid(String accountNo) { 13 | return constants.ACCOUNT_NUMBER_PATTERN.matcher(accountNo).find(); 14 | } 15 | 16 | public static boolean isCreateAccountCriteriaValid(CreateAccountInput createAccountInput) { 17 | return (!createAccountInput.getBankName().isBlank() && !createAccountInput.getOwnerName().isBlank()); 18 | } 19 | 20 | public static boolean isSearchTransactionValid(TransactionInput transactionInput) { 21 | // TODO Add checks for large amounts; consider past history of account holder and location of transfers 22 | 23 | if (!isSearchCriteriaValid(transactionInput.getSourceAccount())) 24 | return false; 25 | 26 | if (!isSearchCriteriaValid(transactionInput.getTargetAccount())) 27 | return false; 28 | 29 | if (transactionInput.getSourceAccount().equals(transactionInput.getTargetAccount())) 30 | return false; 31 | 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/example/paul/integration/CheckBalanceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.integration; 2 | 3 | import com.example.paul.controllers.AccountRestController; 4 | import com.example.paul.models.Account; 5 | import com.example.paul.utils.AccountInput; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ActiveProfiles; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | @ActiveProfiles(value = "local") 15 | class CheckBalanceIntegrationTest { 16 | 17 | @Autowired 18 | private AccountRestController accountRestController; 19 | 20 | @Test 21 | void givenAccountDetails_whenCheckingBalance_thenVerifyAccountCorrect() { 22 | // given 23 | var input = new AccountInput(); 24 | input.setSortCode("53-68-92"); 25 | input.setAccountNumber("73084635"); 26 | 27 | // when 28 | var body = accountRestController.checkAccountBalance(input).getBody(); 29 | 30 | // then 31 | var account = (Account) body; 32 | assertThat(account).isNotNull(); 33 | assertThat(account.getOwnerName()).isEqualTo("Paul Dragoslav"); 34 | assertThat(account.getSortCode()).isEqualTo("53-68-92"); 35 | assertThat(account.getAccountNumber()).isEqualTo("73084635"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/AccountInput.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import java.util.Objects; 5 | 6 | public class AccountInput { 7 | 8 | @NotBlank(message = "Sort code is mandatory") 9 | private String sortCode; 10 | 11 | @NotBlank(message = "Account number is mandatory") 12 | private String accountNumber; 13 | 14 | public AccountInput() {} 15 | 16 | public String getSortCode() { 17 | return sortCode; 18 | } 19 | public void setSortCode(String sortCode) { 20 | this.sortCode = sortCode; 21 | } 22 | public String getAccountNumber() { 23 | return accountNumber; 24 | } 25 | public void setAccountNumber(String accountNumber) { 26 | this.accountNumber = accountNumber; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "AccountInput{" + 32 | "sortCode='" + sortCode + '\'' + 33 | ", accountNumber='" + accountNumber + '\'' + 34 | '}'; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | AccountInput that = (AccountInput) o; 42 | return Objects.equals(sortCode, that.sortCode) && 43 | Objects.equals(accountNumber, that.accountNumber); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(sortCode, accountNumber); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/CreateAccountInput.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import java.util.Objects; 5 | 6 | public class CreateAccountInput { 7 | 8 | @NotBlank(message = "Bank name is mandatory") 9 | private String bankName; 10 | 11 | @NotBlank(message = "Owner name is mandatory") 12 | private String ownerName; 13 | 14 | 15 | public CreateAccountInput() {} 16 | 17 | public String getBankName() { 18 | return bankName; 19 | } 20 | 21 | public void setBankName(String bankName) { 22 | this.bankName = bankName; 23 | } 24 | 25 | public String getOwnerName() { 26 | return ownerName; 27 | } 28 | 29 | public void setOwnerName(String ownerName) { 30 | this.ownerName = ownerName; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "CreateAccountInput{" + 36 | "bankName='" + bankName + '\'' + 37 | ", ownerName='" + ownerName + '\'' + 38 | '}'; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | CreateAccountInput that = (CreateAccountInput) o; 46 | return Objects.equals(bankName, that.bankName) && 47 | Objects.equals(ownerName, that.ownerName); 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hash(bankName, ownerName); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/WithdrawInput.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import javax.validation.constraints.Positive; 4 | import java.util.Objects; 5 | 6 | public class WithdrawInput extends AccountInput{ 7 | String sortCode; 8 | String accountNumber; 9 | 10 | // Prevent fraudulent transfers attempting to abuse currency conversion errors 11 | @Positive(message = "Transfer amount must be positive") 12 | private double amount; 13 | 14 | public WithdrawInput() { 15 | this.sortCode = super.getSortCode(); 16 | this.accountNumber = super.getAccountNumber(); 17 | } 18 | 19 | public double getAmount() { 20 | return amount; 21 | } 22 | 23 | public void setAmount(double amount) { 24 | this.amount = amount; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "AccountInput{" + 30 | "sortCode='" + sortCode + '\'' + 31 | ", accountNumber='" + accountNumber + '\'' + 32 | ", amount='" + amount + '\'' + 33 | '}'; 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) return true; 39 | if (o == null || getClass() != o.getClass()) return false; 40 | WithdrawInput that = (WithdrawInput) o; 41 | return Objects.equals(sortCode, that.sortCode) && 42 | Objects.equals(accountNumber, that.accountNumber) && 43 | Objects.equals(amount, that.amount); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(sortCode, accountNumber, amount); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/DepositInput.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Positive; 5 | import java.util.Objects; 6 | 7 | public class DepositInput { 8 | 9 | @NotBlank(message = "Target account no is mandatory") 10 | private String targetAccountNo; 11 | 12 | // Prevent fraudulent transfers attempting to abuse currency conversion errors 13 | @Positive(message = "Transfer amount must be positive") 14 | private double amount; 15 | 16 | public DepositInput() { 17 | } 18 | 19 | public String getTargetAccountNo() { 20 | return targetAccountNo; 21 | } 22 | 23 | public void setTargetAccountNo(String targetAccountNo) { 24 | this.targetAccountNo = targetAccountNo; 25 | } 26 | 27 | public double getAmount() { 28 | return amount; 29 | } 30 | 31 | public void setAmount(double amount) { 32 | this.amount = amount; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "DepositInput{" + 38 | "targetAccountNo='" + targetAccountNo + '\'' + 39 | ", amount='" + amount + '\'' + 40 | '}'; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | DepositInput that = (DepositInput) o; 48 | return Objects.equals(targetAccountNo, that.targetAccountNo) && 49 | Objects.equals(amount, that.amount); 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return Objects.hash(targetAccountNo, amount); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring-boot Banking 2 | Example project demonstrating the use of Java and Spring-boot to build a microservice to be used by an online bank 3 | 4 | ## Running locally 5 | ``` 6 | ./mvnw clean install -DskipTests=true 7 | ``` 8 | 9 | ``` 10 | java -jar target/Banking-0.0.1.jar 11 | ``` 12 | 13 | ## Running on Docker 14 | ``` 15 | docker build -t "spring-boot:banking" . 16 | ``` 17 | 18 | ``` 19 | docker run -p 8080:8080 spring-boot:banking 20 | ``` 21 | 22 | ## Testing 23 | Import the Postman collection file into the application or copy the request body from there 24 | 25 | ### How to test 26 | 1. Create account 27 | > Use create account API to create an account by providing a `bankName` and `ownerName` 28 | > 29 | ![Create Account](screenshots/create_account.png) 30 | 31 | > Make sure to write down the `sortCode` and the `accountNumber` to proceed with other APIs 32 | 33 | 2. Deposit Cash 34 | >Use noted `accountNumber` as `targetAccountNo` and provide amount greater than zero to deposit cash into an account 35 | 36 | ![Deposit cash](screenshots/deposit.png) 37 | 38 | 3. Check Balance 39 | >Use noted `accountNumber` and `sortCode` to check account balance 40 | 41 | ![Check Balance](screenshots/check_balance.png) 42 | 43 | 4. Withdraw Cash 44 | >Use noted `accountNumber` and `sortCode` and `amount` grater than zero to withdraw cash from an account 45 | 46 | ![Withdraw cash](screenshots/withdraw.png) 47 | 48 | 5. Check Balance again to verify withdrawal 49 | 50 | ![Check Balance](screenshots/check_balance_2.png) 51 | 52 | 53 | 54 | ### Extensions 55 | 1. Use of persisted database 56 | 2. Use of asynchronous programming backed by message queue for transactions 57 | 3. Others mentioned throughout the code -------------------------------------------------------------------------------- /src/test/java/com/example/paul/integration/MakeTransferIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.integration; 2 | 3 | import com.example.paul.controllers.TransactionRestController; 4 | import com.example.paul.utils.AccountInput; 5 | import com.example.paul.utils.TransactionInput; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ActiveProfiles; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | @ActiveProfiles(value = "local") 15 | class MakeTransferIntegrationTest { 16 | 17 | @Autowired 18 | private TransactionRestController transactionRestController; 19 | 20 | @Test 21 | void givenTransactionDetails_whenMakeTransaction_thenVerifyTransactionIsProcessed() { 22 | // given 23 | var sourceAccount = new AccountInput(); 24 | sourceAccount.setSortCode("53-68-92"); 25 | sourceAccount.setAccountNumber("73084635"); 26 | 27 | var targetAccount = new AccountInput(); 28 | targetAccount.setSortCode("65-93-37"); 29 | targetAccount.setAccountNumber("21956204"); 30 | 31 | var input = new TransactionInput(); 32 | input.setSourceAccount(sourceAccount); 33 | input.setTargetAccount(targetAccount); 34 | input.setAmount(27.5); 35 | input.setReference("My reference"); 36 | input.setLatitude(45.0000000); 37 | input.setLongitude(90.0000000); 38 | 39 | // when 40 | var body = transactionRestController.makeTransfer(input).getBody(); 41 | 42 | // then 43 | var isComplete = (Boolean) body; 44 | assertThat(isComplete).isTrue(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/services/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.services; 2 | 3 | import com.example.paul.models.Account; 4 | import com.example.paul.repositories.AccountRepository; 5 | import com.example.paul.repositories.TransactionRepository; 6 | import com.example.paul.utils.CodeGenerator; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Optional; 10 | 11 | @Service 12 | public class AccountService { 13 | 14 | private final AccountRepository accountRepository; 15 | private final TransactionRepository transactionRepository; 16 | 17 | public AccountService(AccountRepository accountRepository, 18 | TransactionRepository transactionRepository) { 19 | this.accountRepository = accountRepository; 20 | this.transactionRepository = transactionRepository; 21 | } 22 | 23 | public Account getAccount(String sortCode, String accountNumber) { 24 | Optional account = accountRepository 25 | .findBySortCodeAndAccountNumber(sortCode, accountNumber); 26 | 27 | account.ifPresent(value -> 28 | value.setTransactions(transactionRepository 29 | .findBySourceAccountIdOrderByInitiationDate(value.getId()))); 30 | 31 | return account.orElse(null); 32 | } 33 | 34 | public Account getAccount(String accountNumber) { 35 | Optional account = accountRepository 36 | .findByAccountNumber(accountNumber); 37 | 38 | return account.orElse(null); 39 | } 40 | 41 | public Account createAccount(String bankName, String ownerName) { 42 | CodeGenerator codeGenerator = new CodeGenerator(); 43 | Account newAccount = new Account(bankName, ownerName, codeGenerator.generateSortCode(), codeGenerator.generateAccountNumber(), 0.00); 44 | return accountRepository.save(newAccount); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.0 9 | 10 | 11 | com.example.paul 12 | Banking 13 | 0.0.1 14 | Banking 15 | Demo banking project for Spring Boot 16 | 17 | 18 | 17 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-jpa 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-validation 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | com.github.mifmif 37 | generex 38 | 1.0.2 39 | 40 | 41 | com.h2database 42 | h2 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/java/com/example/paul/unit/TransactionRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.unit; 2 | 3 | import com.example.paul.controllers.TransactionRestController; 4 | import com.example.paul.services.TransactionService; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.context.junit.jupiter.SpringExtension; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 14 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 15 | 16 | @ExtendWith(SpringExtension.class) 17 | @WebMvcTest(TransactionRestController.class) 18 | class TransactionRestControllerTest { 19 | 20 | @Autowired 21 | private MockMvc mvc; 22 | 23 | @MockBean 24 | private TransactionService transactionService; 25 | 26 | @Test 27 | void givenMissingInput_whenMakeTransfer_thenVerifyBadRequest() throws Exception { 28 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/transactions") 29 | .contentType(MediaType.APPLICATION_JSON)) 30 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 31 | } 32 | 33 | @Test 34 | void givenInvalidInput_whenMakeTransfer_thenVerifyBadRequest() throws Exception { 35 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/transactions") 36 | .content("{ \"sourceAccount\": {\"sortCode\": \"53-68-92\", \"accountNumber\": \"73084635\" }, \"targetAccount\": {\"sortCode\": \"65-93-37\", \"accountNumber\": \"21956204\"}, \"amount\": -10}") 37 | .contentType(MediaType.APPLICATION_JSON)) 38 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 39 | } 40 | 41 | @Test 42 | void givenNoAccountForInput_whenMakeTransfer_thenVerifyOk() throws Exception { 43 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/transactions") 44 | .content("{\"sourceAccount\": {\"sortCode\": \"53-68-92\", \"accountNumber\": \"73084635\"}, \"targetAccount\": {\"sortCode\": \"65-93-37\", \"accountNumber\": \"21956204\"}, \"amount\": 105.0, \"reference\": \"My ref\", \"latitude\": 66.23423423, \"longitude\": 105.234234}") 45 | .contentType(MediaType.APPLICATION_JSON)) 46 | .andExpect(MockMvcResultMatchers.status().isOk()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/utils/TransactionInput.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.utils; 2 | 3 | import javax.validation.constraints.Max; 4 | import javax.validation.constraints.Min; 5 | import javax.validation.constraints.Positive; 6 | 7 | public class TransactionInput { 8 | 9 | private AccountInput sourceAccount; 10 | 11 | private AccountInput targetAccount; 12 | 13 | @Positive(message = "Transfer amount must be positive") 14 | // Prevent fraudulent transfers attempting to abuse currency conversion errors 15 | @Min(value = 1, message = "Amount must be larger than 1") 16 | private double amount; 17 | 18 | private String reference; 19 | 20 | @Min(value = -90, message = "Latitude must be between -90 and 90") 21 | @Max(value = 90, message = "Latitude must be between -90 and 90") 22 | private Double latitude; 23 | 24 | @Min(value = -180, message = "Longitude must be between -180 and 180") 25 | @Max(value = 180, message = "Longitude must be between -180 and 180") 26 | private Double longitude; 27 | 28 | public TransactionInput() {} 29 | 30 | public AccountInput getSourceAccount() { 31 | return sourceAccount; 32 | } 33 | public void setSourceAccount(AccountInput sourceAccount) { 34 | this.sourceAccount = sourceAccount; 35 | } 36 | public AccountInput getTargetAccount() { 37 | return targetAccount; 38 | } 39 | public void setTargetAccount(AccountInput targetAccount) { 40 | this.targetAccount = targetAccount; 41 | } 42 | public double getAmount() { 43 | return amount; 44 | } 45 | public void setAmount(double amount) { 46 | this.amount = amount; 47 | } 48 | public String getReference() { 49 | return reference; 50 | } 51 | public void setReference(String reference) { 52 | this.reference = reference; 53 | } 54 | public Double getLatitude() { 55 | return latitude; 56 | } 57 | public void setLatitude(Double latitude) { 58 | this.latitude = latitude; 59 | } 60 | public Double getLongitude() { 61 | return longitude; 62 | } 63 | public void setLongitude(Double longitude) { 64 | this.longitude = longitude; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "TransactionInput{" + 70 | "sourceAccount=" + sourceAccount + 71 | ", targetAccount=" + targetAccount + 72 | ", amount=" + amount + 73 | ", reference='" + reference + '\'' + 74 | ", latitude=" + latitude + 75 | ", longitude=" + longitude + 76 | '}'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/example/paul/unit/AccountRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.unit; 2 | 3 | import com.example.paul.controllers.AccountRestController; 4 | import com.example.paul.models.Account; 5 | import com.example.paul.services.AccountService; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit.jupiter.SpringExtension; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 15 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 16 | 17 | import static org.mockito.BDDMockito.given; 18 | 19 | @ExtendWith(SpringExtension.class) 20 | @WebMvcTest(AccountRestController.class) 21 | class AccountRestControllerTest { 22 | 23 | @Autowired 24 | private MockMvc mvc; 25 | 26 | @MockBean 27 | private AccountService accountService; 28 | 29 | @Test 30 | void givenMissingInput_whenCheckingBalance_thenVerifyBadRequest() throws Exception { 31 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/accounts") 32 | .contentType(MediaType.APPLICATION_JSON)) 33 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 34 | } 35 | 36 | @Test 37 | void givenInvalidInput_whenCheckingBalance_thenVerifyBadRequest() throws Exception { 38 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/accounts") 39 | .content("{\"sortCode\": \"53-68\",\"accountNumber\": \"78934\"}") 40 | .contentType(MediaType.APPLICATION_JSON)) 41 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 42 | } 43 | 44 | @Test 45 | void givenNoAccountForInput_whenCheckingBalance_thenVerifyNoContent() throws Exception { 46 | given(accountService.getAccount(null, null)).willReturn(null); 47 | 48 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/accounts") 49 | .content("{\"sortCode\": \"53-68-92\",\"accountNumber\": \"78901234\"}") 50 | .contentType(MediaType.APPLICATION_JSON)) 51 | .andExpect(MockMvcResultMatchers.status().isNoContent()); 52 | } 53 | 54 | @Test 55 | void givenAccountDetails_whenCheckingBalance_thenVerifyOk() throws Exception { 56 | given(accountService.getAccount(null, null)).willReturn( 57 | new Account(1L, "53-68-92", "78901234", 10.1, "Some Bank", "John")); 58 | 59 | mvc.perform(MockMvcRequestBuilders.post("/api/v1/accounts") 60 | .content("{\"sortCode\": \"53-68-92\",\"accountNumber\": \"78901234\"}") 61 | .contentType(MediaType.APPLICATION_JSON)) 62 | .andExpect(MockMvcResultMatchers.status().isNoContent()) 63 | .andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/example/paul/unit/AccountServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.unit; 2 | 3 | import com.example.paul.models.Account; 4 | import com.example.paul.models.Transaction; 5 | import com.example.paul.repositories.AccountRepository; 6 | import com.example.paul.repositories.TransactionRepository; 7 | import com.example.paul.services.AccountService; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class AccountServiceTest { 22 | 23 | @Mock 24 | private AccountRepository accountRepository; 25 | @Mock 26 | private TransactionRepository transactionRepository; 27 | 28 | public AccountService underTest; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | underTest = new AccountService(accountRepository, transactionRepository); 33 | } 34 | 35 | @Test 36 | void shouldReturnAccountBySortCodeAndAccountNumberWhenPresent() { 37 | var account = new Account(1L, "53-68-92", "78901234", 10.1, "Some Bank", "John"); 38 | when(accountRepository.findBySortCodeAndAccountNumber("53-68-92", "78901234")) 39 | .thenReturn(Optional.of(account)); 40 | 41 | var result = underTest.getAccount("53-68-92", "78901234"); 42 | 43 | assertThat(result.getOwnerName()).isEqualTo(account.getOwnerName()); 44 | assertThat(result.getSortCode()).isEqualTo(account.getSortCode()); 45 | assertThat(result.getAccountNumber()).isEqualTo(account.getAccountNumber()); 46 | } 47 | 48 | @Test 49 | void shouldReturnTransactionsForAccount() { 50 | var account = new Account(1L, "53-68-92", "78901234", 10.1, "Some Bank", "John"); 51 | when(accountRepository.findBySortCodeAndAccountNumber("53-68-92", "78901234")) 52 | .thenReturn(Optional.of(account)); 53 | var transaction1 = new Transaction(); 54 | var transaction2 = new Transaction(); 55 | transaction1.setReference("a"); 56 | transaction2.setReference("b"); 57 | when(transactionRepository.findBySourceAccountIdOrderByInitiationDate(account.getId())) 58 | .thenReturn(List.of(transaction1, transaction2)); 59 | 60 | var result = underTest.getAccount("53-68-92", "78901234"); 61 | 62 | assertThat(result.getTransactions()).hasSize(2); 63 | assertThat(result.getTransactions()).extracting("reference").containsExactly("a", "b"); 64 | } 65 | 66 | @Test 67 | void shouldReturnNullWhenAccountBySortCodeAndAccountNotFound() { 68 | when(accountRepository.findBySortCodeAndAccountNumber("53-68-92", "78901234")) 69 | .thenReturn(Optional.empty()); 70 | 71 | var result = underTest.getAccount("53-68-92", "78901234"); 72 | 73 | assertThat(result).isNull(); 74 | } 75 | 76 | @Test 77 | void shouldReturnAccountByAccountNumberWhenPresent() { 78 | } 79 | 80 | @Test 81 | void shouldReturnNullWhenAccountByAccountNotFound() { 82 | } 83 | 84 | @Test 85 | void shouldCreateAccount() { 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/services/TransactionService.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.services; 2 | 3 | import com.example.paul.constants.ACTION; 4 | import com.example.paul.models.Account; 5 | import com.example.paul.models.Transaction; 6 | import com.example.paul.repositories.AccountRepository; 7 | import com.example.paul.repositories.TransactionRepository; 8 | import com.example.paul.utils.TransactionInput; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.Optional; 14 | 15 | @Service 16 | public class TransactionService { 17 | @Autowired 18 | private AccountRepository accountRepository; 19 | 20 | @Autowired 21 | private TransactionRepository transactionRepository; 22 | 23 | public boolean makeTransfer(TransactionInput transactionInput) { 24 | // TODO refactor synchronous implementation with messaging queue 25 | String sourceSortCode = transactionInput.getSourceAccount().getSortCode(); 26 | String sourceAccountNumber = transactionInput.getSourceAccount().getAccountNumber(); 27 | Optional sourceAccount = accountRepository 28 | .findBySortCodeAndAccountNumber(sourceSortCode, sourceAccountNumber); 29 | 30 | String targetSortCode = transactionInput.getTargetAccount().getSortCode(); 31 | String targetAccountNumber = transactionInput.getTargetAccount().getAccountNumber(); 32 | Optional targetAccount = accountRepository 33 | .findBySortCodeAndAccountNumber(targetSortCode, targetAccountNumber); 34 | 35 | if (sourceAccount.isPresent() && targetAccount.isPresent()) { 36 | if (isAmountAvailable(transactionInput.getAmount(), sourceAccount.get().getCurrentBalance())) { 37 | var transaction = new Transaction(); 38 | 39 | transaction.setAmount(transactionInput.getAmount()); 40 | transaction.setSourceAccountId(sourceAccount.get().getId()); 41 | transaction.setTargetAccountId(targetAccount.get().getId()); 42 | transaction.setTargetOwnerName(targetAccount.get().getOwnerName()); 43 | transaction.setInitiationDate(LocalDateTime.now()); 44 | transaction.setCompletionDate(LocalDateTime.now()); 45 | transaction.setReference(transactionInput.getReference()); 46 | transaction.setLatitude(transactionInput.getLatitude()); 47 | transaction.setLongitude(transactionInput.getLongitude()); 48 | 49 | updateAccountBalance(sourceAccount.get(), transactionInput.getAmount(), ACTION.WITHDRAW); 50 | transactionRepository.save(transaction); 51 | 52 | return true; 53 | } 54 | } 55 | return false; 56 | } 57 | 58 | public void updateAccountBalance(Account account, double amount, ACTION action) { 59 | if (action == ACTION.WITHDRAW) { 60 | account.setCurrentBalance((account.getCurrentBalance() - amount)); 61 | } else if (action == ACTION.DEPOSIT) { 62 | account.setCurrentBalance((account.getCurrentBalance() + amount)); 63 | } 64 | accountRepository.save(account); 65 | } 66 | 67 | // TODO support overdrafts or credit account 68 | public boolean isAmountAvailable(double amount, double accountBalance) { 69 | return (accountBalance - amount) > 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/models/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.models; 2 | 3 | import javax.persistence.*; 4 | import java.time.LocalDateTime; 5 | 6 | // TODO Add support for Bank charges, currency conversion, setup repeat payment/ standing order 7 | @Entity 8 | @Table(name = "transaction", schema = "online_bank") 9 | 10 | @SequenceGenerator(name = "transaction_seq", sequenceName = "transaction_sequence", schema = "online_bank", initialValue = 5) 11 | public class Transaction { 12 | 13 | @Id 14 | 15 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "transaction_seq") 16 | private long id; 17 | 18 | private long sourceAccountId; 19 | 20 | private long targetAccountId; 21 | 22 | private String targetOwnerName; 23 | 24 | private double amount; 25 | 26 | private LocalDateTime initiationDate; 27 | 28 | private LocalDateTime completionDate; 29 | 30 | private String reference; 31 | 32 | private Double latitude; 33 | 34 | private Double longitude; 35 | 36 | public Transaction() {} 37 | 38 | public long getId() { 39 | return id; 40 | } 41 | public void setId(long id) { 42 | this.id = id; 43 | } 44 | public long getSourceAccountId() { 45 | return sourceAccountId; 46 | } 47 | public void setSourceAccountId(long sourceAccountId) { 48 | this.sourceAccountId = sourceAccountId; 49 | } 50 | public long getTargetAccountId() { 51 | return targetAccountId; 52 | } 53 | public void setTargetAccountId(long targetAccountId) { 54 | this.targetAccountId = targetAccountId; 55 | } 56 | public String getTargetOwnerName() { 57 | return targetOwnerName; 58 | } 59 | public void setTargetOwnerName(String targetOwnerName) { 60 | this.targetOwnerName = targetOwnerName; 61 | } 62 | public double getAmount() { 63 | return amount; 64 | } 65 | public void setAmount(double amount) { 66 | this.amount = amount; 67 | } 68 | public LocalDateTime getInitiationDate() { 69 | return initiationDate; 70 | } 71 | public void setInitiationDate(LocalDateTime initiationDate) { 72 | this.initiationDate = initiationDate; 73 | } 74 | public LocalDateTime getCompletionDate() { 75 | return completionDate; 76 | } 77 | public void setCompletionDate(LocalDateTime completionDate) { 78 | this.completionDate = completionDate; 79 | } 80 | public String getReference() { 81 | return reference; 82 | } 83 | public void setReference(String reference) { 84 | this.reference = reference; 85 | } 86 | public Double getLatitude() { 87 | return latitude; 88 | } 89 | public void setLatitude(Double latitude) { 90 | this.latitude = latitude; 91 | } 92 | public Double getLongitude() { 93 | return longitude; 94 | } 95 | public void setLongitude(Double longitude) { 96 | this.longitude = longitude; 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | return "Transaction{" + 102 | "sourceAccountId=" + sourceAccountId + 103 | ", targetAccountId=" + targetAccountId + 104 | ", targetOwnerName='" + targetOwnerName + '\'' + 105 | ", amount=" + amount + 106 | ", initiationDate=" + initiationDate + 107 | ", completionDate=" + completionDate + 108 | ", reference='" + reference + '\'' + 109 | '}'; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/com/example/paul/unit/TransactionServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.unit; 2 | 3 | import com.example.paul.models.Account; 4 | import com.example.paul.repositories.AccountRepository; 5 | import com.example.paul.repositories.TransactionRepository; 6 | import com.example.paul.services.TransactionService; 7 | import com.example.paul.utils.AccountInput; 8 | import com.example.paul.utils.TransactionInput; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.TestConfiguration; 14 | import org.springframework.boot.test.mock.mockito.MockBean; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.test.context.junit.jupiter.SpringExtension; 17 | 18 | import java.util.Optional; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.mockito.Mockito.when; 22 | 23 | @ExtendWith(SpringExtension.class) 24 | class TransactionServiceTest { 25 | 26 | @TestConfiguration 27 | static class TransactionServiceTestContextConfiguration { 28 | 29 | @Bean 30 | public TransactionService transactionService() { 31 | return new TransactionService(); 32 | } 33 | } 34 | 35 | @Autowired 36 | private TransactionService transactionService; 37 | 38 | @MockBean 39 | private AccountRepository accountRepository; 40 | 41 | @MockBean 42 | private TransactionRepository transactionRepository; 43 | 44 | @BeforeEach 45 | void setUp() { 46 | var sourceAccount = new Account(1L, "53-68-92", "78901234", 458.1, "Some Bank", "John"); 47 | var targetAccount = new Account(2L, "67-41-18", "48573590", 64.9, "Some Other Bank", "Major"); 48 | 49 | when(accountRepository.findBySortCodeAndAccountNumber("53-68-92", "78901234")) 50 | .thenReturn(Optional.of(sourceAccount)); 51 | when(accountRepository.findBySortCodeAndAccountNumber("67-41-18", "48573590")) 52 | .thenReturn(Optional.of(targetAccount)); 53 | } 54 | 55 | @Test 56 | void whenTransactionDetails_thenTransferShouldBeDenied() { 57 | var sourceAccount = new AccountInput(); 58 | sourceAccount.setSortCode("53-68-92"); 59 | sourceAccount.setAccountNumber("78901234"); 60 | 61 | var targetAccount = new AccountInput(); 62 | targetAccount.setSortCode("67-41-18"); 63 | targetAccount.setAccountNumber("48573590"); 64 | 65 | var input = new TransactionInput(); 66 | input.setSourceAccount(sourceAccount); 67 | input.setTargetAccount(targetAccount); 68 | input.setAmount(50); 69 | input.setReference("My reference"); 70 | 71 | boolean isComplete = transactionService.makeTransfer(input); 72 | 73 | assertThat(isComplete).isTrue(); 74 | } 75 | 76 | @Test 77 | void whenTransactionDetailsAndAmountTooLarge_thenTransferShouldBeDenied() { 78 | var sourceAccount = new AccountInput(); 79 | sourceAccount.setSortCode("53-68-92"); 80 | sourceAccount.setAccountNumber("78901234"); 81 | 82 | var targetAccount = new AccountInput(); 83 | targetAccount.setSortCode("67-41-18"); 84 | targetAccount.setAccountNumber("48573590"); 85 | 86 | var input = new TransactionInput(); 87 | input.setSourceAccount(sourceAccount); 88 | input.setTargetAccount(targetAccount); 89 | input.setAmount(10000); 90 | input.setReference("My reference"); 91 | 92 | boolean isComplete = transactionService.makeTransfer(input); 93 | 94 | assertThat(isComplete).isFalse(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/models/Account.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.models; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | import java.util.List; 8 | 9 | // TODO Add support for multiple account types (business, savings, etc.) 10 | // TODO Add support for foreign currency accounts 11 | @Entity 12 | @Table(name = "account", schema = "online_bank") 13 | public class Account { 14 | 15 | @Id @GeneratedValue 16 | private long id; 17 | 18 | private String sortCode; 19 | 20 | private String accountNumber; 21 | 22 | private double currentBalance; 23 | 24 | private String bankName; 25 | 26 | private String ownerName; 27 | 28 | private transient List transactions; 29 | 30 | protected Account() {} 31 | public Account(String bankName, String ownerName, String generateSortCode, String generateAccountNumber, double currentBalance) { 32 | this.sortCode = generateSortCode; 33 | this.accountNumber = generateAccountNumber; 34 | this.currentBalance = currentBalance; 35 | this.bankName = bankName; 36 | this.ownerName = ownerName; 37 | } 38 | public Account(long id, String sortCode, String accountNumber, double currentBalance, String bankName, String ownerName) { 39 | this.id = id; 40 | this.sortCode = sortCode; 41 | this.accountNumber = accountNumber; 42 | this.currentBalance = currentBalance; 43 | this.bankName = bankName; 44 | this.ownerName = ownerName; 45 | } 46 | 47 | public Account(long id, String sortCode, String accountNumber, double currentBalance, String bankName, String ownerName, List transactions) { 48 | this.id = id; 49 | this.sortCode = sortCode; 50 | this.accountNumber = accountNumber; 51 | this.currentBalance = currentBalance; 52 | this.bankName = bankName; 53 | this.ownerName = ownerName; 54 | this.transactions = transactions; 55 | } 56 | 57 | public long getId() { 58 | return id; 59 | } 60 | public void setId(long id) { 61 | this.id = id; 62 | } 63 | public String getSortCode() { 64 | return sortCode; 65 | } 66 | public void setSortCode(String sortCode) { 67 | this.sortCode = sortCode; 68 | } 69 | public String getAccountNumber() { 70 | return accountNumber; 71 | } 72 | public void setAccountNumber(String accountNumber) { 73 | this.accountNumber = accountNumber; 74 | } 75 | public double getCurrentBalance() { 76 | return currentBalance; 77 | } 78 | public void setCurrentBalance(double currentBalance) { 79 | this.currentBalance = currentBalance; 80 | } 81 | public String getOwnerName() { 82 | return ownerName; 83 | } 84 | public void setOwnerName(String ownerName) { 85 | this.ownerName = ownerName; 86 | } 87 | public String getBankName() { 88 | return bankName; 89 | } 90 | public void setBankName(String bankName) { 91 | this.bankName = bankName; 92 | } 93 | public List getTransactions() { 94 | return transactions; 95 | } 96 | public void setTransactions(List transactions) { 97 | this.transactions = transactions; 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return "Account{" + 103 | "id=" + id + 104 | ", sortCode='" + sortCode + '\'' + 105 | ", accountNumber='" + accountNumber + '\'' + 106 | ", currentBalance=" + currentBalance + 107 | ", bankName='" + bankName + '\'' + 108 | ", ownerName='" + ownerName + '\'' + 109 | '}'; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Banking.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "485cda16-0d43-4aa6-ac55-116fb41d8f17", 4 | "name": "Banking", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Create Account", 10 | "request": { 11 | "method": "PUT", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "name": "Content-Type", 16 | "value": "application/json", 17 | "type": "text" 18 | } 19 | ], 20 | "body": { 21 | "mode": "raw", 22 | "raw": "{\n\t\"bankName\": \"Bank of Ceyloan\",\n\t\"ownerName\": \"Nayananga Muhandiram\"\n}\n" 23 | }, 24 | "url": { 25 | "raw": "localhost:8080/api/v1/accounts", 26 | "host": [ 27 | "localhost" 28 | ], 29 | "port": "8080", 30 | "path": [ 31 | "api", 32 | "v1", 33 | "accounts" 34 | ] 35 | } 36 | }, 37 | "response": [] 38 | }, 39 | { 40 | "name": "Make Transaction", 41 | "request": { 42 | "method": "POST", 43 | "header": [ 44 | { 45 | "key": "Content-Type", 46 | "name": "Content-Type", 47 | "value": "application/json", 48 | "type": "text" 49 | } 50 | ], 51 | "body": { 52 | "mode": "raw", 53 | "raw": "{\n\t\"sourceAccount\": {\n\t\t\"sortCode\": \"53-68-92\",\n\t\t\"accountNumber\": \"73084635\"\n\t},\n\t\"targetAccount\": {\n\t\t\"sortCode\": \"65-93-37\",\n\t\t\"accountNumber\": \"21956204\"\n\t},\n\t\"amount\": 105.0,\n\t\"reference\": \"My ref\",\n\t\"latitude\": 66.23423423,\n\t\"longitude\": 105.234234\n}" 54 | }, 55 | "url": { 56 | "raw": "localhost:8080/api/v1/transactions", 57 | "host": [ 58 | "localhost" 59 | ], 60 | "port": "8080", 61 | "path": [ 62 | "api", 63 | "v1", 64 | "transactions" 65 | ] 66 | } 67 | }, 68 | "response": [] 69 | }, 70 | { 71 | "name": "Check Balance", 72 | "request": { 73 | "method": "POST", 74 | "header": [ 75 | { 76 | "key": "Content-Type", 77 | "name": "Content-Type", 78 | "value": "application/json", 79 | "type": "text" 80 | } 81 | ], 82 | "body": { 83 | "mode": "raw", 84 | "raw": "{\n\t\"sortCode\": \"35-16-67\",\n\t\"accountNumber\": \"95753174\"\n}\n" 85 | }, 86 | "url": { 87 | "raw": "localhost:8080/api/v1/accounts", 88 | "host": [ 89 | "localhost" 90 | ], 91 | "port": "8080", 92 | "path": [ 93 | "api", 94 | "v1", 95 | "accounts" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | }, 101 | { 102 | "name": "Withdraw", 103 | "request": { 104 | "method": "POST", 105 | "header": [ 106 | { 107 | "key": "Content-Type", 108 | "name": "Content-Type", 109 | "type": "text", 110 | "value": "application/json" 111 | } 112 | ], 113 | "body": { 114 | "mode": "raw", 115 | "raw": "{\n\t\"sortCode\": \"35-16-67\",\n\t\"accountNumber\": \"95753174\",\n \"amount\": 100.00\n}\n" 116 | }, 117 | "url": { 118 | "raw": "localhost:8080/api/v1/withdraw", 119 | "host": [ 120 | "localhost" 121 | ], 122 | "port": "8080", 123 | "path": [ 124 | "api", 125 | "v1", 126 | "withdraw" 127 | ] 128 | } 129 | }, 130 | "response": [] 131 | }, 132 | { 133 | "name": "Deposit", 134 | "request": { 135 | "method": "POST", 136 | "header": [ 137 | { 138 | "key": "Content-Type", 139 | "name": "Content-Type", 140 | "type": "text", 141 | "value": "application/json" 142 | } 143 | ], 144 | "body": { 145 | "mode": "raw", 146 | "raw": "{\n\t\"targetAccountNo\": \"95753174\",\n \"amount\": 1000.00\n}\n" 147 | }, 148 | "url": { 149 | "raw": "localhost:8080/api/v1/deposit", 150 | "host": [ 151 | "localhost" 152 | ], 153 | "port": "8080", 154 | "path": [ 155 | "api", 156 | "v1", 157 | "deposit" 158 | ] 159 | } 160 | }, 161 | "response": [] 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /src/main/java/com/example/paul/controllers/AccountRestController.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.controllers; 2 | 3 | import com.example.paul.constants.constants; 4 | import com.example.paul.models.Account; 5 | import com.example.paul.services.AccountService; 6 | import com.example.paul.utils.AccountInput; 7 | import com.example.paul.utils.CreateAccountInput; 8 | import com.example.paul.utils.InputValidator; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.validation.FieldError; 16 | import org.springframework.web.bind.MethodArgumentNotValidException; 17 | import org.springframework.web.bind.annotation.*; 18 | 19 | import javax.validation.Valid; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | @RestController 24 | @RequestMapping("api/v1") 25 | public class AccountRestController { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(AccountRestController.class); 28 | 29 | private final AccountService accountService; 30 | 31 | @Autowired 32 | public AccountRestController(AccountService accountService) { 33 | this.accountService = accountService; 34 | } 35 | 36 | @PostMapping(value = "/accounts", 37 | consumes = MediaType.APPLICATION_JSON_VALUE, 38 | produces = MediaType.APPLICATION_JSON_VALUE) 39 | public ResponseEntity checkAccountBalance( 40 | // TODO In the future support searching by card number in addition to sort code and account number 41 | @Valid @RequestBody AccountInput accountInput) { 42 | LOGGER.debug("Triggered AccountRestController.accountInput"); 43 | 44 | // Validate input 45 | if (InputValidator.isSearchCriteriaValid(accountInput)) { 46 | // Attempt to retrieve the account information 47 | Account account = accountService.getAccount( 48 | accountInput.getSortCode(), accountInput.getAccountNumber()); 49 | 50 | // Return the account details, or warn that no account was found for given input 51 | if (account == null) { 52 | return new ResponseEntity<>(constants.NO_ACCOUNT_FOUND, HttpStatus.OK); 53 | } else { 54 | return new ResponseEntity<>(account, HttpStatus.OK); 55 | } 56 | } else { 57 | return new ResponseEntity<>(constants.INVALID_SEARCH_CRITERIA, HttpStatus.BAD_REQUEST); 58 | } 59 | } 60 | 61 | 62 | @PutMapping(value = "/accounts", 63 | consumes = MediaType.APPLICATION_JSON_VALUE, 64 | produces = MediaType.APPLICATION_JSON_VALUE) 65 | public ResponseEntity createAccount( 66 | @Valid @RequestBody CreateAccountInput createAccountInput) { 67 | LOGGER.debug("Triggered AccountRestController.createAccountInput"); 68 | 69 | // Validate input 70 | if (InputValidator.isCreateAccountCriteriaValid(createAccountInput)) { 71 | // Attempt to retrieve the account information 72 | Account account = accountService.createAccount( 73 | createAccountInput.getBankName(), createAccountInput.getOwnerName()); 74 | 75 | // Return the account details, or warn that no account was found for given input 76 | if (account == null) { 77 | return new ResponseEntity<>(constants.CREATE_ACCOUNT_FAILED, HttpStatus.OK); 78 | } else { 79 | return new ResponseEntity<>(account, HttpStatus.OK); 80 | } 81 | } else { 82 | return new ResponseEntity<>(constants.INVALID_SEARCH_CRITERIA, HttpStatus.BAD_REQUEST); 83 | } 84 | } 85 | 86 | @ResponseStatus(HttpStatus.BAD_REQUEST) 87 | @ExceptionHandler(MethodArgumentNotValidException.class) 88 | public Map handleValidationExceptions( 89 | MethodArgumentNotValidException ex) { 90 | Map errors = new HashMap<>(); 91 | 92 | ex.getBindingResult().getAllErrors().forEach((error) -> { 93 | String fieldName = ((FieldError) error).getField(); 94 | String errorMessage = error.getDefaultMessage(); 95 | errors.put(fieldName, errorMessage); 96 | }); 97 | 98 | return errors; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/example/paul/controllers/TransactionRestController.java: -------------------------------------------------------------------------------- 1 | package com.example.paul.controllers; 2 | 3 | import com.example.paul.constants.ACTION; 4 | import com.example.paul.models.Account; 5 | import com.example.paul.services.AccountService; 6 | import com.example.paul.services.TransactionService; 7 | import com.example.paul.utils.*; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.validation.FieldError; 15 | import org.springframework.web.bind.MethodArgumentNotValidException; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import javax.validation.Valid; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import static com.example.paul.constants.constants.*; 23 | 24 | @RestController 25 | @RequestMapping("api/v1") 26 | public class TransactionRestController { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(TransactionRestController.class); 29 | 30 | private final AccountService accountService; 31 | private final TransactionService transactionService; 32 | 33 | @Autowired 34 | public TransactionRestController(AccountService accountService, TransactionService transactionService) { 35 | this.accountService = accountService; 36 | this.transactionService = transactionService; 37 | } 38 | 39 | @PostMapping(value = "/transactions", 40 | consumes = MediaType.APPLICATION_JSON_VALUE, 41 | produces = MediaType.APPLICATION_JSON_VALUE) 42 | public ResponseEntity makeTransfer( 43 | @Valid @RequestBody TransactionInput transactionInput) { 44 | if (InputValidator.isSearchTransactionValid(transactionInput)) { 45 | // new Thread(() -> transactionService.makeTransfer(transactionInput)); 46 | boolean isComplete = transactionService.makeTransfer(transactionInput); 47 | return new ResponseEntity<>(isComplete, HttpStatus.OK); 48 | } else { 49 | return new ResponseEntity<>(INVALID_TRANSACTION, HttpStatus.BAD_REQUEST); 50 | } 51 | } 52 | 53 | @PostMapping(value = "/withdraw", 54 | consumes = MediaType.APPLICATION_JSON_VALUE, 55 | produces = MediaType.APPLICATION_JSON_VALUE) 56 | public ResponseEntity withdraw( 57 | @Valid @RequestBody WithdrawInput withdrawInput) { 58 | LOGGER.debug("Triggered AccountRestController.withdrawInput"); 59 | 60 | // Validate input 61 | if (InputValidator.isSearchCriteriaValid(withdrawInput)) { 62 | // Attempt to retrieve the account information 63 | Account account = accountService.getAccount( 64 | withdrawInput.getSortCode(), withdrawInput.getAccountNumber()); 65 | 66 | // Return the account details, or warn that no account was found for given input 67 | if (account == null) { 68 | return new ResponseEntity<>(NO_ACCOUNT_FOUND, HttpStatus.OK); 69 | } else { 70 | if (transactionService.isAmountAvailable(withdrawInput.getAmount(), account.getCurrentBalance())) { 71 | transactionService.updateAccountBalance(account, withdrawInput.getAmount(), ACTION.WITHDRAW); 72 | return new ResponseEntity<>(SUCCESS, HttpStatus.OK); 73 | } 74 | return new ResponseEntity<>(INSUFFICIENT_ACCOUNT_BALANCE, HttpStatus.OK); 75 | } 76 | } else { 77 | return new ResponseEntity<>(INVALID_SEARCH_CRITERIA, HttpStatus.BAD_REQUEST); 78 | } 79 | } 80 | 81 | 82 | @PostMapping(value = "/deposit", 83 | consumes = MediaType.APPLICATION_JSON_VALUE, 84 | produces = MediaType.APPLICATION_JSON_VALUE) 85 | public ResponseEntity deposit( 86 | @Valid @RequestBody DepositInput depositInput) { 87 | LOGGER.debug("Triggered AccountRestController.depositInput"); 88 | 89 | // Validate input 90 | if (InputValidator.isAccountNoValid(depositInput.getTargetAccountNo())) { 91 | // Attempt to retrieve the account information 92 | Account account = accountService.getAccount(depositInput.getTargetAccountNo()); 93 | 94 | // Return the account details, or warn that no account was found for given input 95 | if (account == null) { 96 | return new ResponseEntity<>(NO_ACCOUNT_FOUND, HttpStatus.OK); 97 | } else { 98 | transactionService.updateAccountBalance(account, depositInput.getAmount(), ACTION.DEPOSIT); 99 | return new ResponseEntity<>(SUCCESS, HttpStatus.OK); 100 | } 101 | } else { 102 | return new ResponseEntity<>(INVALID_SEARCH_CRITERIA, HttpStatus.BAD_REQUEST); 103 | } 104 | } 105 | 106 | @ResponseStatus(HttpStatus.BAD_REQUEST) 107 | @ExceptionHandler(MethodArgumentNotValidException.class) 108 | public Map handleValidationExceptions( 109 | MethodArgumentNotValidException ex) { 110 | Map errors = new HashMap<>(); 111 | 112 | ex.getBindingResult().getAllErrors().forEach((error) -> { 113 | String fieldName = ((FieldError) error).getField(); 114 | String errorMessage = error.getDefaultMessage(); 115 | errors.put(fieldName, errorMessage); 116 | }); 117 | 118 | return errors; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | --------------------------------------------------------------------------------